diff --git a/.ci/README.md b/.ci/README.md new file mode 100644 index 0000000000..62b9669242 --- /dev/null +++ b/.ci/README.md @@ -0,0 +1,11 @@ +# !! DO NOT MODIFY THE API FILE IN THIS DIRECTORY !! + +The `api-current.txt` file contains a summary of Corda's current public APIs, +as generated by the `api-scanner` Gradle plugin. (See [here](../gradle-plugins/api-scanner/README.md) for a detailed description of this plugin.) It will be regenerated and the copy in this repository updated by the Release Manager with +each new Corda release. It will not be modified otherwise except under special circumstances that will require extra approval. + +Deleting or changing the existing Corda APIs listed in `api-current.txt` may +break developers' CorDapps in the next Corda release! Please remember that we +have committed to API Stability for CorDapps. + +# !! DO NOT MODIFY THE API FILE IN THIS DIRECTORY !! diff --git a/.ci/api-current.txt b/.ci/api-current.txt new file mode 100644 index 0000000000..6e8e8df371 --- /dev/null +++ b/.ci/api-current.txt @@ -0,0 +1,3272 @@ +public class net.corda.core.CordaException extends java.lang.Exception implements net.corda.core.CordaThrowable + public () + public (String) + public (String, String, Throwable) + public (String, Throwable) + public void addSuppressed(Throwable[]) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Throwable getCause() + @org.jetbrains.annotations.Nullable public String getMessage() + @org.jetbrains.annotations.Nullable public String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public String getOriginalMessage() + public int hashCode() + public void setCause(Throwable) + public void setMessage(String) + public void setOriginalExceptionClassName(String) +## +public class net.corda.core.CordaRuntimeException extends java.lang.RuntimeException implements net.corda.core.CordaThrowable + public (String) + public (String, String, Throwable) + public (String, Throwable) + public void addSuppressed(Throwable[]) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Throwable getCause() + @org.jetbrains.annotations.Nullable public String getMessage() + @org.jetbrains.annotations.Nullable public String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public String getOriginalMessage() + public int hashCode() + public void setCause(Throwable) + public void setMessage(String) + public void setOriginalExceptionClassName(String) +## +public interface net.corda.core.CordaThrowable + public abstract void addSuppressed(Throwable[]) + @org.jetbrains.annotations.Nullable public abstract String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public abstract String getOriginalMessage() + public abstract void setCause(Throwable) + public abstract void setMessage(String) + public abstract void setOriginalExceptionClassName(String) +## +public final class net.corda.core.Utils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture toFuture(rx.Observable) + @org.jetbrains.annotations.NotNull public static final rx.Observable toObservable(net.corda.core.concurrent.CordaFuture) +## +public final class net.corda.core.concurrent.ConcurrencyUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture firstOf(net.corda.core.concurrent.CordaFuture[], kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture firstOf(net.corda.core.concurrent.CordaFuture[], org.slf4j.Logger, kotlin.jvm.functions.Function1) + public static final Object match(concurrent.Future, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public static final String shortCircuitedTaskFailedMessage = "Short-circuited task failed:" +## +public interface net.corda.core.concurrent.CordaFuture extends java.util.concurrent.Future + public abstract void then(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public abstract concurrent.CompletableFuture toCompletableFuture() +## +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 +## +public final class net.corda.core.contracts.Amount extends java.lang.Object implements java.lang.Comparable + public (long, Object) + public (long, java.math.BigDecimal, Object) + public int compareTo(net.corda.core.contracts.Amount) + public final long component1() + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal component2() + @org.jetbrains.annotations.NotNull public final Object component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount copy(long, java.math.BigDecimal, Object) + public boolean equals(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal getDisplayTokenSize() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.math.BigDecimal getDisplayTokenSize(Object) + public final long getQuantity() + @org.jetbrains.annotations.NotNull public final Object getToken() + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount minus(net.corda.core.contracts.Amount) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount parseCurrency(String) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount plus(net.corda.core.contracts.Amount) + @org.jetbrains.annotations.NotNull public final List splitEvenly(int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.Nullable public static final net.corda.core.contracts.Amount sumOrNull(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount sumOrThrow(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount sumOrZero(Iterable, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount times(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount times(long) + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal toDecimal() + @org.jetbrains.annotations.NotNull public String toString() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount zero(Object) + public static final net.corda.core.contracts.Amount$Companion Companion +## +public static final class net.corda.core.contracts.Amount$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final java.math.BigDecimal getDisplayTokenSize(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount parseCurrency(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.Amount sumOrNull(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount sumOrThrow(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount sumOrZero(Iterable, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount zero(Object) +## +public final class net.corda.core.contracts.AmountTransfer extends java.lang.Object + public (long, Object, Object, Object) + @org.jetbrains.annotations.NotNull public final List apply(List, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer copy(long, Object, Object, Object) + public boolean equals(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) + @org.jetbrains.annotations.NotNull public final Object getDestination() + public final long getQuantityDelta() + @org.jetbrains.annotations.NotNull public final Object getSource() + @org.jetbrains.annotations.NotNull public final Object getToken() + public int hashCode() + @org.jetbrains.annotations.NotNull public final List novate(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer plus(net.corda.core.contracts.AmountTransfer) + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal toDecimal() + @org.jetbrains.annotations.NotNull public String toString() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer zero(Object, Object, Object) + public static final net.corda.core.contracts.AmountTransfer$Companion Companion +## +public static final class net.corda.core.contracts.AmountTransfer$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer zero(Object, Object, Object) +## +public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash + public abstract void extractFile(String, java.io.OutputStream) + @org.jetbrains.annotations.NotNull public abstract List getSigners() + @org.jetbrains.annotations.NotNull public abstract java.io.InputStream open() + @org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR() +## +public interface net.corda.core.contracts.AttachmentConstraint + public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) +## +public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +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 +## +public final class net.corda.core.contracts.Command extends java.lang.Object + public (net.corda.core.contracts.CommandData, java.security.PublicKey) + public (net.corda.core.contracts.CommandData, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Command copy(net.corda.core.contracts.CommandData, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getSigners() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData getValue() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.contracts.CommandAndState extends java.lang.Object + public (net.corda.core.contracts.CommandData, net.corda.core.contracts.OwnableState) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.OwnableState component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandAndState copy(net.corda.core.contracts.CommandData, net.corda.core.contracts.OwnableState) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData getCommand() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.OwnableState getOwnableState() + public int hashCode() + public String toString() +## +public interface net.corda.core.contracts.CommandData +## +public final class net.corda.core.contracts.CommandWithParties extends java.lang.Object + public (List, List, net.corda.core.contracts.CommandData) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandWithParties copy(List, List, net.corda.core.contracts.CommandData) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getSigners() + @org.jetbrains.annotations.NotNull public final List getSigningParties() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData getValue() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.ComponentGroupEnum extends java.lang.Enum + protected (String, int) + public static net.corda.core.contracts.ComponentGroupEnum valueOf(String) + public static net.corda.core.contracts.ComponentGroupEnum[] values() +## +public interface net.corda.core.contracts.Contract + public abstract void verify(net.corda.core.transactions.LedgerTransaction) +## +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 void extractFile(String, java.io.OutputStream) + @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() + @org.jetbrains.annotations.NotNull public java.io.InputStream open() + @org.jetbrains.annotations.NotNull public jar.JarInputStream openAsJAR() +## +public interface net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract List getParticipants() +## +public final class net.corda.core.contracts.ContractsDSL extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.CommandWithParties requireSingleCommand(Collection, Class) + public static final Object requireThat(kotlin.jvm.functions.Function1) +## +public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.OwnableState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.Amount getAmount() + @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) +## +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) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getAttachmentId() + public int hashCode() + public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) + public String toString() +## +public final class net.corda.core.contracts.InsufficientBalanceException extends net.corda.core.flows.FlowException + public (net.corda.core.contracts.Amount) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount getAmountMissing() +## +public final class net.corda.core.contracts.Issued extends java.lang.Object + public (net.corda.core.contracts.PartyAndReference, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PartyAndReference component1() + @org.jetbrains.annotations.NotNull public final Object component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Issued copy(net.corda.core.contracts.PartyAndReference, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PartyAndReference getIssuer() + @org.jetbrains.annotations.NotNull public final Object getProduct() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public @interface net.corda.core.contracts.LegalProseReference + public abstract String uri() +## +public interface net.corda.core.contracts.LinearState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.UniqueIdentifier getLinearId() +## +public interface net.corda.core.contracts.MoveCommand extends net.corda.core.contracts.CommandData + @org.jetbrains.annotations.Nullable public abstract Class getContract() +## +public interface net.corda.core.contracts.NamedByHash + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash getId() +## +public interface net.corda.core.contracts.OwnableState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.AbstractParty getOwner() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.CommandAndState withNewOwner(net.corda.core.identity.AbstractParty) +## +public final class net.corda.core.contracts.PartyAndReference extends java.lang.Object + public (net.corda.core.identity.AbstractParty, net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.OpaqueBytes component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PartyAndReference copy(net.corda.core.identity.AbstractParty, net.corda.core.utilities.OpaqueBytes) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty getParty() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.OpaqueBytes getReference() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.contracts.PrivacySalt extends net.corda.core.utilities.OpaqueBytes + public () + public (byte[]) +## +public final class net.corda.core.contracts.Requirements extends java.lang.Object + public final void using(String, boolean) + public static final net.corda.core.contracts.Requirements INSTANCE +## +public interface net.corda.core.contracts.SchedulableState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.Nullable public abstract net.corda.core.contracts.ScheduledActivity nextScheduledActivity(net.corda.core.contracts.StateRef, net.corda.core.flows.FlowLogicRefFactory) +## +public interface net.corda.core.contracts.Scheduled + @org.jetbrains.annotations.NotNull public abstract java.time.Instant getScheduledAt() +## +public final class net.corda.core.contracts.ScheduledActivity extends java.lang.Object implements net.corda.core.contracts.Scheduled + public (net.corda.core.flows.FlowLogicRef, java.time.Instant) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowLogicRef component1() + @org.jetbrains.annotations.NotNull public final java.time.Instant component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledActivity copy(net.corda.core.flows.FlowLogicRef, java.time.Instant) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowLogicRef getLogicRef() + @org.jetbrains.annotations.NotNull public java.time.Instant getScheduledAt() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.ScheduledStateRef extends java.lang.Object implements net.corda.core.contracts.Scheduled + public (net.corda.core.contracts.StateRef, java.time.Instant) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component1() + @org.jetbrains.annotations.NotNull public final java.time.Instant component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledStateRef copy(net.corda.core.contracts.StateRef, java.time.Instant) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getRef() + @org.jetbrains.annotations.NotNull public java.time.Instant getScheduledAt() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.SourceAndAmount extends java.lang.Object + public (Object, net.corda.core.contracts.Amount, Object) + @org.jetbrains.annotations.NotNull public final Object component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount component2() + @org.jetbrains.annotations.Nullable public final Object component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.SourceAndAmount copy(Object, net.corda.core.contracts.Amount, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount getAmount() + @org.jetbrains.annotations.Nullable public final Object getRef() + @org.jetbrains.annotations.NotNull public final Object getSource() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.StateAndContract extends java.lang.Object + public (net.corda.core.contracts.ContractState, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndContract copy(net.corda.core.contracts.ContractState, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getContract() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getState() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.StateAndRef extends java.lang.Object + public (net.corda.core.contracts.TransactionState, net.corda.core.contracts.StateRef) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TransactionState component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef copy(net.corda.core.contracts.TransactionState, net.corda.core.contracts.StateRef) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getRef() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TransactionState getState() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.StateRef extends java.lang.Object + public (net.corda.core.crypto.SecureHash, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef copy(net.corda.core.crypto.SecureHash, int) + public boolean equals(Object) + public final int getIndex() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxhash() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.contracts.Structures extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash hash(net.corda.core.contracts.ContractState) + @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount withoutIssuer(net.corda.core.contracts.Amount) +## +public abstract class net.corda.core.contracts.TimeWindow extends java.lang.Object + public () + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow between(java.time.Instant, java.time.Instant) + public abstract boolean contains(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow fromOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow fromStartAndDuration(java.time.Instant, java.time.Duration) + @org.jetbrains.annotations.Nullable public abstract java.time.Instant getFromTime() + @org.jetbrains.annotations.Nullable public abstract java.time.Instant getMidpoint() + @org.jetbrains.annotations.Nullable public abstract java.time.Instant getUntilTime() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow untilOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow withTolerance(java.time.Instant, java.time.Duration) + public static final net.corda.core.contracts.TimeWindow$Companion Companion +## +public static final class net.corda.core.contracts.TimeWindow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow between(java.time.Instant, java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow fromOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow fromStartAndDuration(java.time.Instant, java.time.Duration) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow untilOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow withTolerance(java.time.Instant, java.time.Duration) +## +public interface net.corda.core.contracts.TokenizableAssetInfo + @org.jetbrains.annotations.NotNull public abstract java.math.BigDecimal getDisplayTokenSize() +## +public final class net.corda.core.contracts.TransactionResolutionException extends net.corda.core.flows.FlowException + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +public final class net.corda.core.contracts.TransactionState extends java.lang.Object + public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) + public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) + public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.Nullable public final Integer component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AttachmentConstraint component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TransactionState copy(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AttachmentConstraint getConstraint() + @org.jetbrains.annotations.NotNull public final String getContract() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getData() + @org.jetbrains.annotations.Nullable public final Integer getEncumbrance() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getNotary() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.TransactionStateKt extends java.lang.Object +## +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() +## +public static final class net.corda.core.contracts.TransactionVerificationException$ContractConstraintRejection extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, String) +## +public static final class net.corda.core.contracts.TransactionVerificationException$ContractCreationError extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, String, Throwable) +## +public static final class net.corda.core.contracts.TransactionVerificationException$ContractRejection extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, net.corda.core.contracts.Contract, Throwable) +## +public static final class net.corda.core.contracts.TransactionVerificationException$Direction extends java.lang.Enum + protected (String, int) + public static net.corda.core.contracts.TransactionVerificationException$Direction valueOf(String) + public static net.corda.core.contracts.TransactionVerificationException$Direction[] values() +## +public static final class net.corda.core.contracts.TransactionVerificationException$DuplicateInputStates extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, net.corda.core.utilities.NonEmptySet) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NonEmptySet getDuplicates() +## +public static final class net.corda.core.contracts.TransactionVerificationException$InvalidNotaryChange extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash) +## +public static final class net.corda.core.contracts.TransactionVerificationException$MissingAttachmentRejection extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, String) +## +public static final class net.corda.core.contracts.TransactionVerificationException$MoreThanOneNotary extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash) +## +public static final class net.corda.core.contracts.TransactionVerificationException$NotaryChangeInWrongTransactionType extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.identity.Party) +## +public static final class net.corda.core.contracts.TransactionVerificationException$SignersMissing extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, List) +## +public static final class net.corda.core.contracts.TransactionVerificationException$TransactionMissingEncumbranceException extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, int, net.corda.core.contracts.TransactionVerificationException$Direction) +## +public abstract class net.corda.core.contracts.TypeOnlyCommandData extends java.lang.Object implements net.corda.core.contracts.CommandData + public () + public boolean equals(Object) + public int hashCode() +## +public final class net.corda.core.contracts.UniqueIdentifier extends java.lang.Object implements java.lang.Comparable + public () + public (String, UUID) + public int compareTo(net.corda.core.contracts.UniqueIdentifier) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.NotNull public final UUID component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.UniqueIdentifier copy(String, UUID) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final String getExternalId() + @org.jetbrains.annotations.NotNull public final UUID getId() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.contracts.UniqueIdentifier$Companion Companion +## +public static final class net.corda.core.contracts.UniqueIdentifier$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.UniqueIdentifier fromString(String) +## +public interface net.corda.core.contracts.UpgradedContract extends net.corda.core.contracts.Contract + @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) +## +public interface net.corda.core.cordapp.Cordapp + @org.jetbrains.annotations.NotNull public abstract List getContractClassNames() + @org.jetbrains.annotations.NotNull public abstract List getCordappClasses() + @org.jetbrains.annotations.NotNull public abstract Set getCustomSchemas() + @org.jetbrains.annotations.NotNull public abstract List getInitiatedFlows() + @org.jetbrains.annotations.NotNull public abstract java.net.URL getJarPath() + @org.jetbrains.annotations.NotNull public abstract String getName() + @org.jetbrains.annotations.NotNull public abstract List getRpcFlows() + @org.jetbrains.annotations.NotNull public abstract List getSchedulableFlows() + @org.jetbrains.annotations.NotNull public abstract List getSerializationWhitelists() + @org.jetbrains.annotations.NotNull public abstract List getServiceFlows() + @org.jetbrains.annotations.NotNull public abstract List getServices() +## +public final class net.corda.core.cordapp.CordappContext extends java.lang.Object + public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader) + @org.jetbrains.annotations.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId() + @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() + @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp() +## +public interface net.corda.core.cordapp.CordappProvider + @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappContext getAppContext() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.crypto.SecureHash getContractAttachmentID(String) +## +public class net.corda.core.crypto.AddressFormatException extends java.lang.IllegalArgumentException + public () + public (String) +## +public class net.corda.core.crypto.Base58 extends java.lang.Object + public () + public static byte[] decode(String) + public static byte[] decodeChecked(String) + public static java.math.BigInteger decodeToBigInteger(String) + public static String encode(byte[]) +## +public final class net.corda.core.crypto.CompositeKey extends java.lang.Object implements java.security.PublicKey + public final void checkValidity() + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getAlgorithm() + @org.jetbrains.annotations.NotNull public final List getChildren() + @org.jetbrains.annotations.NotNull public byte[] getEncoded() + @org.jetbrains.annotations.NotNull public String getFormat() + @org.jetbrains.annotations.NotNull public final Set getLeafKeys() + public final int getThreshold() + public int hashCode() + public final boolean isFulfilledBy(Iterable) + public final boolean isFulfilledBy(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.crypto.CompositeKey$Companion Companion + @org.jetbrains.annotations.NotNull public static final String KEY_ALGORITHM = "COMPOSITE" +## +public static final class net.corda.core.crypto.CompositeKey$Builder extends java.lang.Object + public () + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey$Builder addKey(java.security.PublicKey, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey$Builder addKeys(List) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey build(Integer) +## +public static final class net.corda.core.crypto.CompositeKey$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getInstance(org.bouncycastle.asn1.ASN1Primitive) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getInstance(byte[]) +## +public static final class net.corda.core.crypto.CompositeKey$NodeAndWeight extends org.bouncycastle.asn1.ASN1Object implements java.lang.Comparable + public (java.security.PublicKey, int) + public int compareTo(net.corda.core.crypto.CompositeKey$NodeAndWeight) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey$NodeAndWeight copy(java.security.PublicKey, int) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getNode() + public final int getWeight() + public int hashCode() + @org.jetbrains.annotations.NotNull public org.bouncycastle.asn1.ASN1Primitive toASN1Primitive() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.crypto.CompositeKeyFactory extends java.security.KeyFactorySpi + public () + @org.jetbrains.annotations.NotNull protected java.security.PrivateKey engineGeneratePrivate(java.security.spec.KeySpec) + @org.jetbrains.annotations.Nullable protected java.security.PublicKey engineGeneratePublic(java.security.spec.KeySpec) + @org.jetbrains.annotations.NotNull protected java.security.spec.KeySpec engineGetKeySpec(java.security.Key, Class) + @org.jetbrains.annotations.NotNull protected java.security.Key engineTranslateKey(java.security.Key) +## +public final class net.corda.core.crypto.CompositeSignature extends java.security.Signature + public () + @kotlin.Deprecated @org.jetbrains.annotations.NotNull protected Object engineGetParameter(String) + protected void engineInitSign(java.security.PrivateKey) + protected void engineInitVerify(java.security.PublicKey) + @kotlin.Deprecated protected void engineSetParameter(String, Object) + protected void engineSetParameter(java.security.spec.AlgorithmParameterSpec) + @org.jetbrains.annotations.NotNull protected byte[] engineSign() + protected void engineUpdate(byte) + protected void engineUpdate(byte[], int, int) + protected boolean engineVerify(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.Provider$Service getService(java.security.Provider) + public static final net.corda.core.crypto.CompositeSignature$Companion Companion + @org.jetbrains.annotations.NotNull public static final String SIGNATURE_ALGORITHM = "COMPOSITESIG" +## +public static final class net.corda.core.crypto.CompositeSignature$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final java.security.Provider$Service getService(java.security.Provider) +## +public static final class net.corda.core.crypto.CompositeSignature$State extends java.lang.Object + public (java.io.ByteArrayOutputStream, net.corda.core.crypto.CompositeKey) + @org.jetbrains.annotations.NotNull public final java.io.ByteArrayOutputStream component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeSignature$State copy(java.io.ByteArrayOutputStream, net.corda.core.crypto.CompositeKey) + public final boolean engineVerify(byte[]) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.io.ByteArrayOutputStream getBuffer() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey getVerifyKey() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.CompositeSignaturesWithKeys extends java.lang.Object + public (List) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeSignaturesWithKeys copy(List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getSigs() + public int hashCode() + public String toString() + public static final net.corda.core.crypto.CompositeSignaturesWithKeys$Companion Companion +## +public static final class net.corda.core.crypto.CompositeSignaturesWithKeys$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeSignaturesWithKeys getEMPTY() +## +public final class net.corda.core.crypto.CordaObjectIdentifier extends java.lang.Object + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.ASN1ObjectIdentifier COMPOSITE_KEY + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.ASN1ObjectIdentifier COMPOSITE_SIGNATURE + public static final net.corda.core.crypto.CordaObjectIdentifier INSTANCE +## +public final class net.corda.core.crypto.CordaSecurityProvider extends java.security.Provider + public () + public static final net.corda.core.crypto.CordaSecurityProvider$Companion Companion + @org.jetbrains.annotations.NotNull public static final String PROVIDER_NAME = "Corda" +## +public static final class net.corda.core.crypto.CordaSecurityProvider$Companion extends java.lang.Object +## +public final class net.corda.core.crypto.Crypto extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey decodePrivateKey(String, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey decodePrivateKey(net.corda.core.crypto.SignatureScheme, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey decodePrivateKey(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey decodePublicKey(String, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey decodePublicKey(net.corda.core.crypto.SignatureScheme, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey decodePublicKey(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPair(java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPair(net.corda.core.crypto.SignatureScheme, java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPairFromEntropy(java.math.BigInteger) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPairFromEntropy(net.corda.core.crypto.SignatureScheme, java.math.BigInteger) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final byte[] doSign(String, java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.TransactionSignature doSign(java.security.KeyPair, net.corda.core.crypto.SignableData) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final byte[] doSign(java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final byte[] doSign(net.corda.core.crypto.SignatureScheme, java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic public static final boolean doVerify(String, java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean doVerify(java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean doVerify(net.corda.core.crypto.SecureHash, net.corda.core.crypto.TransactionSignature) + @kotlin.jvm.JvmStatic public static final boolean doVerify(net.corda.core.crypto.SignatureScheme, java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.Provider findProvider(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(java.security.PrivateKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(java.security.PublicKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(org.bouncycastle.asn1.x509.AlgorithmIdentifier) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair(net.corda.core.crypto.SignatureScheme) + @kotlin.jvm.JvmStatic public static final boolean isSupportedSignatureScheme(net.corda.core.crypto.SignatureScheme) + @kotlin.jvm.JvmStatic public static final boolean isValid(java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean isValid(net.corda.core.crypto.SecureHash, net.corda.core.crypto.TransactionSignature) + @kotlin.jvm.JvmStatic public static final boolean isValid(net.corda.core.crypto.SignatureScheme, java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean publicKeyOnCurve(net.corda.core.crypto.SignatureScheme, java.security.PublicKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final List supportedSignatureSchemes() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey toSupportedPrivateKey(java.security.PrivateKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey toSupportedPublicKey(java.security.PublicKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey toSupportedPublicKey(org.bouncycastle.asn1.x509.SubjectPublicKeyInfo) + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme COMPOSITE_KEY + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme DEFAULT_SIGNATURE_SCHEME + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme ECDSA_SECP256K1_SHA256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme ECDSA_SECP256R1_SHA256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme EDDSA_ED25519_SHA512 + public static final net.corda.core.crypto.Crypto INSTANCE + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme RSA_SHA256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.DLSequence SHA512_256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme SPHINCS256_SHA256 +## +public final class net.corda.core.crypto.CryptoUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final Set byKeys(Iterable) + @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey component1(java.security.KeyPair) + @org.jetbrains.annotations.NotNull public static final java.security.PublicKey component2(java.security.KeyPair) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash componentHash(net.corda.core.crypto.SecureHash, net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash componentHash(net.corda.core.utilities.OpaqueBytes, net.corda.core.contracts.PrivacySalt, int, int) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 computeNonce(net.corda.core.contracts.PrivacySalt, int, int) + public static final boolean containsAny(java.security.PublicKey, Iterable) + @org.jetbrains.annotations.NotNull public static final java.security.KeyPair entropyToKeyPair(java.math.BigInteger) + @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair() + @org.jetbrains.annotations.NotNull public static final Set getKeys(java.security.PublicKey) + public static final boolean isFulfilledBy(java.security.PublicKey, Iterable) + public static final boolean isFulfilledBy(java.security.PublicKey, java.security.PublicKey) + public static final boolean isValid(java.security.PublicKey, byte[], net.corda.core.crypto.DigitalSignature) + @org.jetbrains.annotations.NotNull public static final java.security.SecureRandom newSecureRandom() + public static final long random63BitValue() + @org.jetbrains.annotations.NotNull public static final byte[] secureRandomBytes(int) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash serializedHash(Object) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.TransactionSignature sign(java.security.KeyPair, net.corda.core.crypto.SignableData) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature$WithKey sign(java.security.KeyPair, net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature$WithKey sign(java.security.KeyPair, byte[]) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature sign(java.security.PrivateKey, byte[]) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature$WithKey sign(java.security.PrivateKey, byte[], java.security.PublicKey) + @org.jetbrains.annotations.NotNull public static final String toStringShort(java.security.PublicKey) + public static final boolean verify(java.security.KeyPair, byte[], byte[]) + public static final boolean verify(java.security.PublicKey, byte[], net.corda.core.crypto.DigitalSignature) + public static final boolean verify(java.security.PublicKey, byte[], byte[]) +## +public class net.corda.core.crypto.DigitalSignature extends net.corda.core.utilities.OpaqueBytes + public (byte[]) +## +public static class net.corda.core.crypto.DigitalSignature$WithKey extends net.corda.core.crypto.DigitalSignature + public (java.security.PublicKey, byte[]) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getBy() + public final boolean isValid(byte[]) + public final boolean verify(net.corda.core.utilities.OpaqueBytes) + public final boolean verify(byte[]) +## +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 +## +public static final class net.corda.core.crypto.MerkleTree$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getMerkleTree(List) +## +public static final class net.corda.core.crypto.MerkleTree$Leaf extends net.corda.core.crypto.MerkleTree + 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.crypto.MerkleTree$Leaf copy(net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getHash() + public int hashCode() + public String toString() +## +public static final class net.corda.core.crypto.MerkleTree$Node extends net.corda.core.crypto.MerkleTree + public (net.corda.core.crypto.SecureHash, net.corda.core.crypto.MerkleTree, net.corda.core.crypto.MerkleTree) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree$Node copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.MerkleTree, net.corda.core.crypto.MerkleTree) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getHash() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getLeft() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getRight() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.MerkleTreeException extends net.corda.core.CordaException + public (String) + @org.jetbrains.annotations.NotNull public final String getReason() +## +public final class net.corda.core.crypto.NullKeys extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AnonymousParty getNULL_PARTY() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.TransactionSignature getNULL_SIGNATURE() + public static final net.corda.core.crypto.NullKeys INSTANCE +## +public static final class net.corda.core.crypto.NullKeys$NullPublicKey extends java.lang.Object implements java.security.PublicKey, java.lang.Comparable + public int compareTo(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public String getAlgorithm() + @org.jetbrains.annotations.NotNull public byte[] getEncoded() + @org.jetbrains.annotations.NotNull public String getFormat() + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.crypto.NullKeys$NullPublicKey INSTANCE +## +public final class net.corda.core.crypto.PartialMerkleTree extends java.lang.Object + public (net.corda.core.crypto.PartialMerkleTree$PartialTree) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree getRoot() + public final boolean verify(net.corda.core.crypto.SecureHash, List) + public static final net.corda.core.crypto.PartialMerkleTree$Companion Companion +## +public static final class net.corda.core.crypto.PartialMerkleTree$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree build(net.corda.core.crypto.MerkleTree, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash rootAndUsedHashes(net.corda.core.crypto.PartialMerkleTree$PartialTree, List) +## +public abstract static class net.corda.core.crypto.PartialMerkleTree$PartialTree extends java.lang.Object +## +public static final class net.corda.core.crypto.PartialMerkleTree$PartialTree$IncludedLeaf extends net.corda.core.crypto.PartialMerkleTree$PartialTree + 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.crypto.PartialMerkleTree$PartialTree$IncludedLeaf copy(net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() + public int hashCode() + public String toString() +## +public static final class net.corda.core.crypto.PartialMerkleTree$PartialTree$Leaf extends net.corda.core.crypto.PartialMerkleTree$PartialTree + 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.crypto.PartialMerkleTree$PartialTree$Leaf copy(net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() + public int hashCode() + public String toString() +## +public static final class net.corda.core.crypto.PartialMerkleTree$PartialTree$Node extends net.corda.core.crypto.PartialMerkleTree$PartialTree + public (net.corda.core.crypto.PartialMerkleTree$PartialTree, net.corda.core.crypto.PartialMerkleTree$PartialTree) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree$Node copy(net.corda.core.crypto.PartialMerkleTree$PartialTree, net.corda.core.crypto.PartialMerkleTree$PartialTree) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree getLeft() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree getRight() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.crypto.SecureHash extends net.corda.core.utilities.OpaqueBytes + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 hashConcat(net.corda.core.crypto.SecureHash) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 parse(String) + @org.jetbrains.annotations.NotNull public final String prefixChars(int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 randomSHA256() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256Twice(byte[]) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.crypto.SecureHash$Companion Companion +## +public static final class net.corda.core.crypto.SecureHash$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 getAllOnesHash() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 getZeroHash() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 parse(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 randomSHA256() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 sha256(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 sha256(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 sha256Twice(byte[]) +## +public static final class net.corda.core.crypto.SecureHash$SHA256 extends net.corda.core.crypto.SecureHash + public (byte[]) +## +public final class net.corda.core.crypto.SecureHashKt extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(byte[]) +## +public final class net.corda.core.crypto.SignableData extends java.lang.Object + public (net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignatureMetadata) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignableData copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignatureMetadata) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata getSignatureMetadata() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.SignatureMetadata extends java.lang.Object + public (int, int) + public final int component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata copy(int, int) + public boolean equals(Object) + public final int getPlatformVersion() + public final int getSchemeNumberID() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.SignatureScheme extends java.lang.Object + public (int, String, org.bouncycastle.asn1.x509.AlgorithmIdentifier, List, String, String, String, java.security.spec.AlgorithmParameterSpec, Integer, String) + public final int component1() + @org.jetbrains.annotations.NotNull public final String component10() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final org.bouncycastle.asn1.x509.AlgorithmIdentifier component3() + @org.jetbrains.annotations.NotNull public final List component4() + @org.jetbrains.annotations.NotNull public final String component5() + @org.jetbrains.annotations.NotNull public final String component6() + @org.jetbrains.annotations.NotNull public final String component7() + @org.jetbrains.annotations.Nullable public final java.security.spec.AlgorithmParameterSpec component8() + @org.jetbrains.annotations.Nullable public final Integer component9() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureScheme copy(int, String, org.bouncycastle.asn1.x509.AlgorithmIdentifier, List, String, String, String, java.security.spec.AlgorithmParameterSpec, Integer, String) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final java.security.spec.AlgorithmParameterSpec getAlgSpec() + @org.jetbrains.annotations.NotNull public final String getAlgorithmName() + @org.jetbrains.annotations.NotNull public final List getAlternativeOIDs() + @org.jetbrains.annotations.NotNull public final String getDesc() + @org.jetbrains.annotations.Nullable public final Integer getKeySize() + @org.jetbrains.annotations.NotNull public final String getProviderName() + @org.jetbrains.annotations.NotNull public final String getSchemeCodeName() + public final int getSchemeNumberID() + @org.jetbrains.annotations.NotNull public final String getSignatureName() + @org.jetbrains.annotations.NotNull public final org.bouncycastle.asn1.x509.AlgorithmIdentifier getSignatureOID() + public int hashCode() + public String toString() +## +public class net.corda.core.crypto.SignedData extends java.lang.Object + public (net.corda.core.serialization.SerializedBytes, net.corda.core.crypto.DigitalSignature$WithKey) + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getRaw() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature$WithKey getSig() + @org.jetbrains.annotations.NotNull public final Object verified() + protected void verifyData(Object) +## +public final class net.corda.core.crypto.TransactionSignature extends net.corda.core.crypto.DigitalSignature + public (byte[], java.security.PublicKey, net.corda.core.crypto.SignatureMetadata) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getBy() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata getSignatureMetadata() + public int hashCode() + public final boolean isValid(net.corda.core.crypto.SecureHash) + public final boolean verify(net.corda.core.crypto.SecureHash) +## +public abstract class net.corda.core.flows.AbstractStateReplacementFlow extends java.lang.Object + public () +## +public abstract static class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession) + public (net.corda.core.flows.FlowSession, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getInitiatingSession() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + protected abstract void verifyProposal(net.corda.core.transactions.SignedTransaction, net.corda.core.flows.AbstractStateReplacementFlow$Proposal) + public static final net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion Companion +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$APPROVING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$APPROVING INSTANCE +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$VERIFYING INSTANCE +## +public abstract static class net.corda.core.flows.AbstractStateReplacementFlow$Instigator extends net.corda.core.flows.FlowLogic + public (net.corda.core.contracts.StateAndRef, Object, net.corda.core.utilities.ProgressTracker) + @org.jetbrains.annotations.NotNull protected abstract net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.contracts.StateAndRef call() + public final Object getModification() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef getOriginalState() + @org.jetbrains.annotations.NotNull public List getParticipantSessions() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + public static final net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion Companion +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$NOTARY extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$NOTARY INSTANCE +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$SIGNING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$SIGNING INSTANCE +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Proposal extends java.lang.Object + public (net.corda.core.contracts.StateRef, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component1() + public final Object component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.AbstractStateReplacementFlow$Proposal copy(net.corda.core.contracts.StateRef, Object) + public boolean equals(Object) + public final Object getModification() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getStateRef() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx extends java.lang.Object + public (net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx copy(net.corda.core.transactions.SignedTransaction) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getStx() + public int hashCode() + public String toString() +## +public final class net.corda.core.flows.CollectSignatureFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.transactions.SignedTransaction, net.corda.core.flows.FlowSession, List) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getPartiallySignedTx() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getSession() + @org.jetbrains.annotations.NotNull public final List getSigningKeys() +## +public final class net.corda.core.flows.CollectSignaturesFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.transactions.SignedTransaction, Collection) + public (net.corda.core.transactions.SignedTransaction, Collection, Iterable) + public (net.corda.core.transactions.SignedTransaction, Collection, Iterable, net.corda.core.utilities.ProgressTracker) + public (net.corda.core.transactions.SignedTransaction, Collection, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() + @org.jetbrains.annotations.Nullable public final Iterable getMyOptionalKeys() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getPartiallySignedTx() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @org.jetbrains.annotations.NotNull public final Collection getSessionsToCollectFrom() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ProgressTracker tracker() + public static final net.corda.core.flows.CollectSignaturesFlow$Companion Companion +## +public static final class net.corda.core.flows.CollectSignaturesFlow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.CollectSignaturesFlow$Companion$COLLECTING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.CollectSignaturesFlow$Companion$COLLECTING INSTANCE +## +public static final class net.corda.core.flows.CollectSignaturesFlow$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.CollectSignaturesFlow$Companion$VERIFYING INSTANCE +## +public final class net.corda.core.flows.ContractUpgradeFlow extends java.lang.Object + public static final net.corda.core.flows.ContractUpgradeFlow INSTANCE +## +public static final class net.corda.core.flows.ContractUpgradeFlow$Authorise extends net.corda.core.flows.FlowLogic + public (net.corda.core.contracts.StateAndRef, Class) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef getStateAndRef() +## +public static final class net.corda.core.flows.ContractUpgradeFlow$Deauthorise extends net.corda.core.flows.FlowLogic + public (net.corda.core.contracts.StateRef) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getStateRef() +## +public static final class net.corda.core.flows.ContractUpgradeFlow$Initiate extends net.corda.core.flows.AbstractStateReplacementFlow$Instigator + public (net.corda.core.contracts.StateAndRef, Class) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() +## +public abstract class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() + @org.jetbrains.annotations.NotNull public final Object getPayload() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendPayloadAndReceiveDataRequest(net.corda.core.flows.FlowSession, Object) + @co.paralleluniverse.fibers.Suspendable protected void verifyDataRequest(net.corda.core.internal.FetchDataFlow$Request$Data) +## +public final class net.corda.core.flows.FinalityFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.transactions.SignedTransaction) + public (net.corda.core.transactions.SignedTransaction, Set) + public (net.corda.core.transactions.SignedTransaction, Set, net.corda.core.utilities.ProgressTracker) + public (net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getTransaction() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ProgressTracker tracker() + public static final net.corda.core.flows.FinalityFlow$Companion Companion +## +public static final class net.corda.core.flows.FinalityFlow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING INSTANCE +## +public static final class net.corda.core.flows.FinalityFlow$Companion$NOTARISING extends net.corda.core.utilities.ProgressTracker$Step + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker childProgressTracker() + public static final net.corda.core.flows.FinalityFlow$Companion$NOTARISING INSTANCE +## +public class net.corda.core.flows.FlowException extends net.corda.core.CordaException + public () + public (String) + public (String, Throwable) + public (Throwable) +## +public final class net.corda.core.flows.FlowInfo extends java.lang.Object + public (int, String) + public final int component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInfo copy(int, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getAppName() + public final int getFlowVersion() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.flows.FlowInitiator extends java.lang.Object implements java.security.Principal +## +public static final class net.corda.core.flows.FlowInitiator$Peer extends net.corda.core.flows.FlowInitiator + public (net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator$Peer copy(net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getName() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getParty() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowInitiator$RPC extends net.corda.core.flows.FlowInitiator + public (String) + @org.jetbrains.annotations.NotNull public final String component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator$RPC copy(String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getName() + @org.jetbrains.annotations.NotNull public final String getUsername() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowInitiator$Scheduled extends net.corda.core.flows.FlowInitiator + public (net.corda.core.contracts.ScheduledStateRef) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledStateRef component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator$Scheduled copy(net.corda.core.contracts.ScheduledStateRef) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getName() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledStateRef getScheduledState() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowInitiator$Shell extends net.corda.core.flows.FlowInitiator + @org.jetbrains.annotations.NotNull public String getName() + public static final net.corda.core.flows.FlowInitiator$Shell INSTANCE +## +public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object + public () + @co.paralleluniverse.fibers.Suspendable public abstract Object call() + public final void checkFlowPermission(String, Map) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public final net.corda.core.flows.FlowStackSnapshot flowStackSnapshot() + @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() + @org.jetbrains.annotations.Nullable public net.corda.core.utilities.ProgressTracker getProgressTracker() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getRunId() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.ServiceHub getServiceHub() + @org.jetbrains.annotations.NotNull public final net.corda.core.internal.FlowStateMachine getStateMachine() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession initiateFlow(net.corda.core.identity.Party) + @co.paralleluniverse.fibers.Suspendable public final void persistFlowStackSnapshot() + @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.utilities.UntrustworthyData receive(Class, net.corda.core.identity.Party) + 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) + public final void setStateMachine(net.corda.core.internal.FlowStateMachine) + @co.paralleluniverse.fibers.Suspendable public Object subFlow(net.corda.core.flows.FlowLogic) + @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed track() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction waitForLedgerCommit(net.corda.core.crypto.SecureHash) +## +public interface net.corda.core.flows.FlowLogicRef +## +public interface net.corda.core.flows.FlowLogicRefFactory +## +public abstract class net.corda.core.flows.FlowSession extends java.lang.Object + public () + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party getCounterparty() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.FlowInfo getCounterpartyFlowInfo() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.UntrustworthyData receive(Class) + @co.paralleluniverse.fibers.Suspendable public abstract void send(Object) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.UntrustworthyData sendAndReceive(Class, Object) +## +public final class net.corda.core.flows.FlowStackSnapshot extends java.lang.Object + public (java.time.Instant, String, List) + @org.jetbrains.annotations.NotNull public final java.time.Instant component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final List component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowStackSnapshot copy(java.time.Instant, String, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getFlowClass() + @org.jetbrains.annotations.NotNull public final List getStackFrames() + @org.jetbrains.annotations.NotNull public final java.time.Instant getTime() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowStackSnapshot$Frame extends java.lang.Object + public (StackTraceElement, List) + @org.jetbrains.annotations.NotNull public final StackTraceElement component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowStackSnapshot$Frame copy(StackTraceElement, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getStackObjects() + @org.jetbrains.annotations.NotNull public final StackTraceElement getStackTraceElement() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.flows.IllegalFlowLogicException extends java.lang.IllegalArgumentException + public (Class, String) +## +public @interface net.corda.core.flows.InitiatedBy + public abstract Class value() +## +public @interface net.corda.core.flows.InitiatingFlow + public abstract int version() +## +public final class net.corda.core.flows.NotaryChangeFlow extends net.corda.core.flows.AbstractStateReplacementFlow$Instigator + public (net.corda.core.contracts.StateAndRef, net.corda.core.identity.Party, net.corda.core.utilities.ProgressTracker) + @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() +## +public abstract class net.corda.core.flows.NotaryError extends java.lang.Object +## +public static final class net.corda.core.flows.NotaryError$Conflict extends net.corda.core.flows.NotaryError + public (net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$Conflict copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData getConflict() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.flows.NotaryError$TimeWindowInvalid extends net.corda.core.flows.NotaryError + public static final net.corda.core.flows.NotaryError$TimeWindowInvalid INSTANCE +## +public static final class net.corda.core.flows.NotaryError$TransactionInvalid extends net.corda.core.flows.NotaryError + public (Throwable) + @org.jetbrains.annotations.NotNull public final Throwable component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$TransactionInvalid copy(Throwable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Throwable getCause() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.flows.NotaryError$WrongNotary extends net.corda.core.flows.NotaryError + public static final net.corda.core.flows.NotaryError$WrongNotary INSTANCE +## +public final class net.corda.core.flows.NotaryException extends net.corda.core.flows.FlowException + public (net.corda.core.flows.NotaryError) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError getError() +## +public final class net.corda.core.flows.NotaryFlow extends java.lang.Object + public () +## +public static class net.corda.core.flows.NotaryFlow$Client extends net.corda.core.flows.FlowLogic + public (net.corda.core.transactions.SignedTransaction) + public (net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + public static final net.corda.core.flows.NotaryFlow$Client$Companion Companion +## +public static final class net.corda.core.flows.NotaryFlow$Client$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.NotaryFlow$Client$Companion$REQUESTING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.NotaryFlow$Client$Companion$REQUESTING INSTANCE +## +public static final class net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING INSTANCE +## +public abstract static class net.corda.core.flows.NotaryFlow$Service extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession, net.corda.core.node.services.TrustedAuthorityNotaryService) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @co.paralleluniverse.fibers.Suspendable protected final void checkNotary(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.TrustedAuthorityNotaryService getService() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.TransactionParts receiveAndVerifyTx() +## +public final class net.corda.core.flows.ReceiveStateAndRefFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() +## +public final class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession) + public (net.corda.core.flows.FlowSession, boolean) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() +## +public @interface net.corda.core.flows.SchedulableFlow +## +public class net.corda.core.flows.SendStateAndRefFlow extends net.corda.core.flows.DataVendingFlow + public (net.corda.core.flows.FlowSession, List) +## +public class net.corda.core.flows.SendTransactionFlow extends net.corda.core.flows.DataVendingFlow + public (net.corda.core.flows.FlowSession, net.corda.core.transactions.SignedTransaction) +## +public abstract class net.corda.core.flows.SignTransactionFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() + @co.paralleluniverse.fibers.Suspendable protected abstract void checkTransaction(net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ProgressTracker tracker() + public static final net.corda.core.flows.SignTransactionFlow$Companion Companion +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion$RECEIVING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.SignTransactionFlow$Companion$RECEIVING INSTANCE +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion$SIGNING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.SignTransactionFlow$Companion$SIGNING INSTANCE +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING INSTANCE +## +public final class net.corda.core.flows.StackFrameDataToken extends java.lang.Object + public (String) + @org.jetbrains.annotations.NotNull public final String component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StackFrameDataToken copy(String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getClassName() + public int hashCode() + public String toString() +## +public @interface net.corda.core.flows.StartableByRPC +## +public final class net.corda.core.flows.StateMachineRunId extends java.lang.Object + public (UUID) + @org.jetbrains.annotations.NotNull public final UUID component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId copy(UUID) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final UUID getUuid() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.flows.StateMachineRunId$Companion Companion +## +public static final class net.corda.core.flows.StateMachineRunId$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId createRandom() +## +public class net.corda.core.flows.StateReplacementException extends net.corda.core.flows.FlowException + public () + public (String) + public (String, Throwable) +## +public final class net.corda.core.flows.TransactionParts extends java.lang.Object + public (net.corda.core.crypto.SecureHash, List, net.corda.core.contracts.TimeWindow, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow component3() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.TransactionParts copy(net.corda.core.crypto.SecureHash, List, net.corda.core.contracts.TimeWindow, net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final List getInputs() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow getTimestamp() + public int hashCode() + public String toString() +## +public final class net.corda.core.flows.UnexpectedFlowEndException extends net.corda.core.CordaRuntimeException + public (String) + public (String, Throwable) +## +public abstract class net.corda.core.identity.AbstractParty extends java.lang.Object + public (java.security.PublicKey) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getOwningKey() + public int hashCode() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.CordaX500Name nameOrNull() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.PartyAndReference ref(net.corda.core.utilities.OpaqueBytes) +## +public final class net.corda.core.identity.AnonymousParty extends net.corda.core.identity.AbstractParty + public (java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.CordaX500Name nameOrNull() + @org.jetbrains.annotations.NotNull public net.corda.core.contracts.PartyAndReference ref(net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.identity.CordaX500Name extends java.lang.Object + public (String, String, String) + public (String, String, String, String) + public (String, String, String, String, String, String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name build(javax.security.auth.x500.X500Principal) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.Nullable public final String component2() + @org.jetbrains.annotations.NotNull public final String component3() + @org.jetbrains.annotations.NotNull public final String component4() + @org.jetbrains.annotations.Nullable public final String component5() + @org.jetbrains.annotations.NotNull public final String component6() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name copy(String, String, String, String, String, String) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final String getCommonName() + @org.jetbrains.annotations.NotNull public final String getCountry() + @org.jetbrains.annotations.NotNull public final String getLocality() + @org.jetbrains.annotations.NotNull public final String getOrganisation() + @org.jetbrains.annotations.Nullable public final String getOrganisationUnit() + @org.jetbrains.annotations.Nullable public final String getState() + @org.jetbrains.annotations.NotNull public final javax.security.auth.x500.X500Principal getX500Principal() + public int hashCode() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name parse(String) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.identity.CordaX500Name$Companion Companion + public static final int LENGTH_COUNTRY = 2 + public static final int MAX_LENGTH_COMMON_NAME = 64 + public static final int MAX_LENGTH_LOCALITY = 64 + public static final int MAX_LENGTH_ORGANISATION = 128 + public static final int MAX_LENGTH_ORGANISATION_UNIT = 64 + public static final int MAX_LENGTH_STATE = 64 +## +public static final class net.corda.core.identity.CordaX500Name$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name build(javax.security.auth.x500.X500Principal) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name parse(String) +## +public final class net.corda.core.identity.IdentityUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final Map excludeHostNode(net.corda.core.node.ServiceHub, Map) + @org.jetbrains.annotations.NotNull public static final Map excludeNotary(Map, net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public static final Map groupAbstractPartyByWellKnownParty(net.corda.core.node.ServiceHub, Collection) + @org.jetbrains.annotations.NotNull public static final Map groupAbstractPartyByWellKnownParty(net.corda.core.node.ServiceHub, Collection, boolean) + @org.jetbrains.annotations.NotNull public static final Map groupPublicKeysByWellKnownParty(net.corda.core.node.ServiceHub, Collection) + @org.jetbrains.annotations.NotNull public static final Map groupPublicKeysByWellKnownParty(net.corda.core.node.ServiceHub, Collection, boolean) +## +public final class net.corda.core.identity.Party extends net.corda.core.identity.AbstractParty + public (java.security.cert.X509Certificate) + public (net.corda.core.identity.CordaX500Name, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AnonymousParty anonymise() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name getName() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.CordaX500Name nameOrNull() + @org.jetbrains.annotations.NotNull public net.corda.core.contracts.PartyAndReference ref(net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.identity.PartyAndCertificate extends java.lang.Object + public (java.security.cert.CertPath) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final java.security.cert.X509Certificate component2() + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.cert.CertPath getCertPath() + @org.jetbrains.annotations.NotNull public final java.security.cert.X509Certificate getCertificate() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name getName() + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getOwningKey() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getParty() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() + @org.jetbrains.annotations.NotNull public final java.security.cert.PKIXCertPathValidatorResult verify(java.security.cert.TrustAnchor) +## +public interface net.corda.core.messaging.AllPossibleRecipients extends net.corda.core.messaging.MessageRecipients +## +public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.messaging.RPCOps + public abstract void addVaultTransactionNote(net.corda.core.crypto.SecureHash, String) + public abstract boolean attachmentExists(net.corda.core.crypto.SecureHash) + public abstract void clearNetworkMapCache() + @org.jetbrains.annotations.NotNull public abstract java.time.Instant currentNodeTime() + public abstract int getProtocolVersion() + @org.jetbrains.annotations.NotNull public abstract Iterable getVaultTransactionNotes(net.corda.core.crypto.SecureHash) + @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() + @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() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo nodeInfo() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo nodeInfoFromParty(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.NotNull public abstract List notaryIdentities() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party notaryPartyFromX500Name(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public abstract java.io.InputStream openAttachment(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String, boolean) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract List registeredFlows() + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed stateMachineRecordedTransactionMappingFeed() + @org.jetbrains.annotations.NotNull public abstract List stateMachineRecordedTransactionMappingSnapshot() + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed stateMachinesFeed() + @org.jetbrains.annotations.NotNull public abstract List stateMachinesSnapshot() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash uploadAttachment(java.io.InputStream) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQuery(Class) + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryByCriteria(net.corda.core.node.services.vault.QueryCriteria, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryByWithPagingSpec(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryByWithSorting(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrack(Class) + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackByCriteria(Class, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackByWithPagingSpec(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackByWithSorting(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture waitUntilNetworkReady() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromAnonymous(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public final class net.corda.core.messaging.CordaRPCOpsKt extends java.lang.Object +## +public final class net.corda.core.messaging.DataFeed extends java.lang.Object + public (Object, rx.Observable) + public final Object component1() + @org.jetbrains.annotations.NotNull public final rx.Observable component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.DataFeed copy(Object, rx.Observable) + public boolean equals(Object) + public final Object getSnapshot() + @org.jetbrains.annotations.NotNull public final rx.Observable getUpdates() + public int hashCode() + public String toString() +## +public interface net.corda.core.messaging.FlowHandle extends java.lang.AutoCloseable + public abstract void close() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getReturnValue() +## +public final class net.corda.core.messaging.FlowHandleImpl extends java.lang.Object implements net.corda.core.messaging.FlowHandle + public (net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture) + public void close() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.concurrent.CordaFuture component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.FlowHandleImpl copy(net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public net.corda.core.concurrent.CordaFuture getReturnValue() + public int hashCode() + public String toString() +## +public interface net.corda.core.messaging.FlowProgressHandle extends net.corda.core.messaging.FlowHandle + public abstract void close() + @org.jetbrains.annotations.NotNull public abstract rx.Observable getProgress() +## +public final class net.corda.core.messaging.FlowProgressHandleImpl extends java.lang.Object implements net.corda.core.messaging.FlowProgressHandle + public (net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture, rx.Observable) + public void close() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.concurrent.CordaFuture component2() + @org.jetbrains.annotations.NotNull public final rx.Observable component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.FlowProgressHandleImpl copy(net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture, rx.Observable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public rx.Observable getProgress() + @org.jetbrains.annotations.NotNull public net.corda.core.concurrent.CordaFuture getReturnValue() + public int hashCode() + public String toString() +## +public interface net.corda.core.messaging.MessageRecipientGroup extends net.corda.core.messaging.MessageRecipients +## +public interface net.corda.core.messaging.MessageRecipients +## +public interface net.corda.core.messaging.RPCOps + public abstract int getProtocolVersion() +## +public @interface net.corda.core.messaging.RPCReturnsObservables +## +public interface net.corda.core.messaging.SingleMessageRecipient extends net.corda.core.messaging.MessageRecipients +## +public final class net.corda.core.messaging.StateMachineInfo extends java.lang.Object + public (net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.FlowInitiator, net.corda.core.messaging.DataFeed) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator component3() + @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineInfo copy(net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.FlowInitiator, net.corda.core.messaging.DataFeed) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getFlowLogicClassName() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator getInitiator() + @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed getProgressTrackerStepAndUpdates() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.messaging.StateMachineTransactionMapping extends java.lang.Object + public (net.corda.core.flows.StateMachineRunId, net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineTransactionMapping copy(net.corda.core.flows.StateMachineRunId, net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getStateMachineRunId() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTransactionId() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.messaging.StateMachineUpdate extends java.lang.Object + @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.StateMachineRunId getId() +## +public static final class net.corda.core.messaging.StateMachineUpdate$Added extends net.corda.core.messaging.StateMachineUpdate + public (net.corda.core.messaging.StateMachineInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineUpdate$Added copy(net.corda.core.messaging.StateMachineInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineInfo getStateMachineInfo() + public int hashCode() + public String toString() +## +public static final class net.corda.core.messaging.StateMachineUpdate$Removed extends net.corda.core.messaging.StateMachineUpdate + public (net.corda.core.flows.StateMachineRunId, net.corda.core.utilities.Try) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineUpdate$Removed copy(net.corda.core.flows.StateMachineRunId, net.corda.core.utilities.Try) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try getResult() + public int hashCode() + public String toString() +## +public interface net.corda.core.node.AppServiceHub extends net.corda.core.node.ServiceHub + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.FlowHandle startFlow(net.corda.core.flows.FlowLogic) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.FlowProgressHandle startTrackedFlow(net.corda.core.flows.FlowLogic) +## +public final class net.corda.core.node.NodeInfo extends java.lang.Object + public (List, List, int, long) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + public final int component3() + public final long component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo copy(List, List, int, long) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getAddresses() + @org.jetbrains.annotations.NotNull public final List getLegalIdentities() + @org.jetbrains.annotations.NotNull public final List getLegalIdentitiesAndCerts() + public final int getPlatformVersion() + public final long getSerial() + public int hashCode() + public final boolean isLegalIdentity(net.corda.core.identity.Party) + public String toString() +## +public interface net.corda.core.node.ServiceHub extends net.corda.core.node.ServicesForResolution + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializeAsToken cordaService(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract java.time.Clock getClock() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.KeyManagementService getKeyManagementService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo getMyInfo() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.NetworkMapCache getNetworkMapCache() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.TransactionStorage getValidatedTransactions() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.VaultService getVaultService() + @org.jetbrains.annotations.NotNull public abstract java.sql.Connection jdbcSession() + public abstract void recordTransactions(Iterable) + public abstract void recordTransactions(boolean, Iterable) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, Iterable) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.StateAndRef toStateAndRef(net.corda.core.contracts.StateRef) +## +public interface net.corda.core.node.ServicesForResolution extends net.corda.core.node.StateLoader + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.AttachmentStorage getAttachments() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappProvider getCordappProvider() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.IdentityService getIdentityService() +## +public interface net.corda.core.node.StateLoader + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) +## +public interface net.corda.core.node.services.AttachmentStorage + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.contracts.Attachment openAttachment(net.corda.core.crypto.SecureHash) +## +public final class net.corda.core.node.services.AttachmentStorageKt extends java.lang.Object +## +public interface net.corda.core.node.services.ContractUpgradeService + @org.jetbrains.annotations.Nullable public abstract String getAuthorisedContractUpgrade(net.corda.core.contracts.StateRef) + public abstract void removeAuthorisedContractUpgrade(net.corda.core.contracts.StateRef) + public abstract void storeAuthorisedContractUpgrade(net.corda.core.contracts.StateRef, Class) +## +public @interface net.corda.core.node.services.CordaService +## +public interface net.corda.core.node.services.IdentityService + public abstract void assertOwnership(net.corda.core.identity.Party, net.corda.core.identity.AnonymousParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate certificateFromKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract Iterable getAllIdentities() + @org.jetbrains.annotations.NotNull public abstract java.security.cert.CertStore getCaCertStore() + @org.jetbrains.annotations.NotNull public abstract java.security.cert.TrustAnchor getTrustAnchor() + @org.jetbrains.annotations.NotNull public abstract java.security.cert.X509Certificate getTrustRoot() + @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String, boolean) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party requireWellKnownPartyFromAnonymous(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate verifyAndRegisterIdentity(net.corda.core.identity.PartyAndCertificate) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromAnonymous(net.corda.core.contracts.PartyAndReference) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromAnonymous(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public interface net.corda.core.node.services.KeyManagementService + @org.jetbrains.annotations.NotNull public abstract Iterable filterMyKeys(Iterable) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract java.security.PublicKey freshKey() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.PartyAndCertificate freshKeyAndCert(net.corda.core.identity.PartyAndCertificate, boolean) + @org.jetbrains.annotations.NotNull public abstract Set getKeys() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature sign(net.corda.core.crypto.SignableData, java.security.PublicKey) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.DigitalSignature$WithKey sign(byte[], java.security.PublicKey) +## +public interface net.corda.core.node.services.NetworkMapCache + public abstract void clearNetworkMapCache() + @org.jetbrains.annotations.NotNull public abstract List getAllNodes() + @org.jetbrains.annotations.NotNull public abstract rx.Observable getChanged() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByAddress(net.corda.core.utilities.NetworkHostAndPort) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalIdentity(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getNodeReady() + @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalIdentityKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public abstract List getNotaryIdentities() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.services.PartyInfo getPartyInfo(net.corda.core.identity.Party) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getPeerByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate getPeerCertificateByLegalName(net.corda.core.identity.CordaX500Name) + public abstract boolean isNotary(net.corda.core.identity.Party) + public abstract boolean isValidatingNotary(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() +## +public abstract static class net.corda.core.node.services.NetworkMapCache$MapChange extends java.lang.Object + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo getNode() +## +public static final class net.corda.core.node.services.NetworkMapCache$MapChange$Added extends net.corda.core.node.services.NetworkMapCache$MapChange + public (net.corda.core.node.NodeInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.NetworkMapCache$MapChange$Added copy(net.corda.core.node.NodeInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getNode() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.NetworkMapCache$MapChange$Modified extends net.corda.core.node.services.NetworkMapCache$MapChange + public (net.corda.core.node.NodeInfo, net.corda.core.node.NodeInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.NetworkMapCache$MapChange$Modified copy(net.corda.core.node.NodeInfo, net.corda.core.node.NodeInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getNode() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo getPreviousNode() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.NetworkMapCache$MapChange$Removed extends net.corda.core.node.services.NetworkMapCache$MapChange + public (net.corda.core.node.NodeInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.NetworkMapCache$MapChange$Removed copy(net.corda.core.node.NodeInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getNode() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.node.services.NotaryService extends net.corda.core.serialization.SingletonSerializeAsToken + public () + @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.FlowLogic createServiceFlow(net.corda.core.flows.FlowSession) + @org.jetbrains.annotations.NotNull public abstract java.security.PublicKey getNotaryIdentityKey() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.ServiceHub getServices() + public abstract void start() + public abstract void stop() +## +public abstract class net.corda.core.node.services.PartyInfo extends java.lang.Object + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party getParty() +## +public static final class net.corda.core.node.services.PartyInfo$DistributedNode extends net.corda.core.node.services.PartyInfo + public (net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.PartyInfo$DistributedNode copy(net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getParty() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.PartyInfo$SingleNode extends net.corda.core.node.services.PartyInfo + public (net.corda.core.identity.Party, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.PartyInfo$SingleNode copy(net.corda.core.identity.Party, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getAddresses() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getParty() + public int hashCode() + public String toString() +## +public final class net.corda.core.node.services.StatesNotAvailableException extends net.corda.core.flows.FlowException + public (String, Throwable) + @org.jetbrains.annotations.Nullable public Throwable getCause() + @org.jetbrains.annotations.Nullable public String getMessage() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.node.services.TimeWindowChecker extends java.lang.Object + public () + public (java.time.Clock) + @org.jetbrains.annotations.NotNull public final java.time.Clock getClock() + public final boolean isValid(net.corda.core.contracts.TimeWindow) +## +public interface net.corda.core.node.services.TransactionStorage + @org.jetbrains.annotations.Nullable public abstract net.corda.core.transactions.SignedTransaction getTransaction(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public abstract rx.Observable getUpdates() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() +## +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) +## +public abstract class net.corda.core.node.services.TrustedAuthorityNotaryService extends net.corda.core.node.services.NotaryService + public () + public final void commitInputStates(List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull protected org.slf4j.Logger getLog() + @org.jetbrains.annotations.NotNull protected abstract net.corda.core.node.services.TimeWindowChecker getTimeWindowChecker() + @org.jetbrains.annotations.NotNull protected abstract net.corda.core.node.services.UniquenessProvider getUniquenessProvider() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.TransactionSignature sign(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature$WithKey sign(byte[]) + public final void validateTimeWindow(net.corda.core.contracts.TimeWindow) +## +public final class net.corda.core.node.services.UniquenessException extends net.corda.core.CordaException + public (net.corda.core.node.services.UniquenessProvider$Conflict) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$Conflict getError() +## +public interface net.corda.core.node.services.UniquenessProvider + public abstract void commit(List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party) +## +public static final class net.corda.core.node.services.UniquenessProvider$Conflict extends java.lang.Object + public (Map) + @org.jetbrains.annotations.NotNull public final Map component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$Conflict copy(Map) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Map getStateHistory() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.UniquenessProvider$ConsumingTx extends java.lang.Object + public (net.corda.core.crypto.SecureHash, int, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$ConsumingTx copy(net.corda.core.crypto.SecureHash, int, net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + public final int getInputIndex() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getRequestingParty() + public int hashCode() + public String toString() +## +public final class net.corda.core.node.services.UnknownAnonymousPartyException extends net.corda.core.CordaException + public (String) +## +public final class net.corda.core.node.services.Vault extends java.lang.Object + public (Iterable) + @org.jetbrains.annotations.NotNull public final Iterable getStates() + public static final net.corda.core.node.services.Vault$Companion Companion +## +public static final class net.corda.core.node.services.Vault$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update getNoNotaryUpdate() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update getNoUpdate() +## +public static final class net.corda.core.node.services.Vault$Page extends java.lang.Object + public (List, List, long, net.corda.core.node.services.Vault$StateStatus, List) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + public final long component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component4() + @org.jetbrains.annotations.NotNull public final List component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Page copy(List, List, long, net.corda.core.node.services.Vault$StateStatus, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getOtherResults() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus getStateTypes() + @org.jetbrains.annotations.NotNull public final List getStates() + @org.jetbrains.annotations.NotNull public final List getStatesMetadata() + public final long getTotalStatesAvailable() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.Vault$StateMetadata extends java.lang.Object + public (net.corda.core.contracts.StateRef, String, java.time.Instant, java.time.Instant, net.corda.core.node.services.Vault$StateStatus, net.corda.core.identity.AbstractParty, String, java.time.Instant) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final java.time.Instant component3() + @org.jetbrains.annotations.Nullable public final java.time.Instant component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component5() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.AbstractParty component6() + @org.jetbrains.annotations.Nullable public final String component7() + @org.jetbrains.annotations.Nullable public final java.time.Instant component8() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateMetadata copy(net.corda.core.contracts.StateRef, String, java.time.Instant, java.time.Instant, net.corda.core.node.services.Vault$StateStatus, net.corda.core.identity.AbstractParty, String, java.time.Instant) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final java.time.Instant getConsumedTime() + @org.jetbrains.annotations.NotNull public final String getContractStateClassName() + @org.jetbrains.annotations.Nullable public final String getLockId() + @org.jetbrains.annotations.Nullable public final java.time.Instant getLockUpdateTime() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.AbstractParty getNotary() + @org.jetbrains.annotations.NotNull public final java.time.Instant getRecordedTime() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getRef() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus getStatus() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.Vault$StateStatus extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.Vault$StateStatus valueOf(String) + public static net.corda.core.node.services.Vault$StateStatus[] values() +## +public static final class net.corda.core.node.services.Vault$Update extends java.lang.Object + public (Set, Set, UUID, net.corda.core.node.services.Vault$UpdateType) + @org.jetbrains.annotations.NotNull public final Set component1() + @org.jetbrains.annotations.NotNull public final Set component2() + @org.jetbrains.annotations.Nullable public final UUID component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$UpdateType component4() + public final boolean containsType(Class, net.corda.core.node.services.Vault$StateStatus) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update copy(Set, Set, UUID, net.corda.core.node.services.Vault$UpdateType) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Set getConsumed() + @org.jetbrains.annotations.Nullable public final UUID getFlowId() + @org.jetbrains.annotations.NotNull public final Set getProduced() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$UpdateType getType() + public int hashCode() + public final boolean isEmpty() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update plus(net.corda.core.node.services.Vault$Update) + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.node.services.Vault$UpdateType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.Vault$UpdateType valueOf(String) + public static net.corda.core.node.services.Vault$UpdateType[] values() +## +public final class net.corda.core.node.services.VaultQueryException extends net.corda.core.flows.FlowException + public (String) +## +public interface net.corda.core.node.services.VaultService + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page _queryBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed _trackBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + public abstract void addNoteToTransaction(net.corda.core.crypto.SecureHash, String) + @org.jetbrains.annotations.NotNull public abstract rx.Observable getRawUpdates() + @org.jetbrains.annotations.NotNull public abstract Iterable getTransactionNotes(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public abstract rx.Observable getUpdates() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + public abstract void softLockRelease(UUID, net.corda.core.utilities.NonEmptySet) + public abstract void softLockReserve(UUID, net.corda.core.utilities.NonEmptySet) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract List tryLockFungibleStatesForSpending(UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture whenConsumed(net.corda.core.contracts.StateRef) +## +public final class net.corda.core.node.services.VaultServiceKt extends java.lang.Object +## +public final class net.corda.core.node.services.vault.AggregateFunctionType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.AggregateFunctionType valueOf(String) + public static net.corda.core.node.services.vault.AggregateFunctionType[] values() +## +public final class net.corda.core.node.services.vault.BinaryComparisonOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.BinaryComparisonOperator valueOf(String) + public static net.corda.core.node.services.vault.BinaryComparisonOperator[] values() +## +public final class net.corda.core.node.services.vault.BinaryLogicalOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.BinaryLogicalOperator valueOf(String) + public static net.corda.core.node.services.vault.BinaryLogicalOperator[] values() +## +public final class net.corda.core.node.services.vault.Builder extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$Between between(Comparable, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression between(reflect.Field, Comparable, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression between(kotlin.reflect.KProperty1, Comparable, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison compare(net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression comparePredicate(reflect.Field, net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression comparePredicate(kotlin.reflect.KProperty1, net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression count(reflect.Field) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression count(kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison equal(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression equal(reflect.Field, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression equal(kotlin.reflect.KProperty1, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression functionPredicate(reflect.Field, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression functionPredicate(kotlin.reflect.KProperty1, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison greaterThan(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThan(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThan(kotlin.reflect.KProperty1, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison greaterThanOrEqual(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThanOrEqual(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThanOrEqual(kotlin.reflect.KProperty1, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression in(reflect.Field, Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression in(Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression in(kotlin.reflect.KProperty1, Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression isNull(reflect.Field) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression isNull(kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison lessThan(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThan(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThan(kotlin.reflect.KProperty1, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison lessThanOrEqual(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThanOrEqual(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThanOrEqual(kotlin.reflect.KProperty1, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression like(reflect.Field, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression like(kotlin.reflect.KProperty1, String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison notEqual(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notEqual(reflect.Field, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notEqual(kotlin.reflect.KProperty1, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notIn(reflect.Field, Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression notIn(Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notIn(kotlin.reflect.KProperty1, Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notLike(reflect.Field, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notLike(kotlin.reflect.KProperty1, String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notNull(reflect.Field) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notNull(kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression predicate(reflect.Field, net.corda.core.node.services.vault.ColumnPredicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression predicate(kotlin.reflect.KProperty1, net.corda.core.node.services.vault.ColumnPredicate) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + public static final net.corda.core.node.services.vault.Builder INSTANCE +## +public final class net.corda.core.node.services.vault.CollectionOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.CollectionOperator valueOf(String) + public static net.corda.core.node.services.vault.CollectionOperator[] values() +## +public final class net.corda.core.node.services.vault.Column extends java.lang.Object + public (String, Class) + public (reflect.Field) + public (kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final Class getDeclaringClass() + @org.jetbrains.annotations.NotNull public final String getName() +## +public abstract class net.corda.core.node.services.vault.ColumnPredicate extends java.lang.Object +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$AggregateFunction extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.AggregateFunctionType) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AggregateFunctionType component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$AggregateFunction copy(net.corda.core.node.services.vault.AggregateFunctionType) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AggregateFunctionType getType() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$Between extends net.corda.core.node.services.vault.ColumnPredicate + public (Comparable, Comparable) + @org.jetbrains.annotations.NotNull public final Comparable component1() + @org.jetbrains.annotations.NotNull public final Comparable component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$Between copy(Comparable, Comparable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Comparable getRightFromLiteral() + @org.jetbrains.annotations.NotNull public final Comparable getRightToLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryComparisonOperator component1() + @org.jetbrains.annotations.NotNull public final Comparable component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison copy(net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryComparisonOperator getOperator() + @org.jetbrains.annotations.NotNull public final Comparable getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.CollectionOperator, Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CollectionOperator component1() + @org.jetbrains.annotations.NotNull public final Collection component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression copy(net.corda.core.node.services.vault.CollectionOperator, Collection) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CollectionOperator getOperator() + @org.jetbrains.annotations.NotNull public final Collection getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.EqualityComparisonOperator, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.EqualityComparisonOperator component1() + public final Object component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison copy(net.corda.core.node.services.vault.EqualityComparisonOperator, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.EqualityComparisonOperator getOperator() + public final Object getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$Likeness extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.LikenessOperator, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.LikenessOperator component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$Likeness copy(net.corda.core.node.services.vault.LikenessOperator, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.LikenessOperator getOperator() + @org.jetbrains.annotations.NotNull public final String getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$NullExpression extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.NullOperator) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.NullOperator component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$NullExpression copy(net.corda.core.node.services.vault.NullOperator) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.NullOperator getOperator() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.node.services.vault.CriteriaExpression extends java.lang.Object +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate component2() + @org.jetbrains.annotations.Nullable public final List component3() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.Sort$Direction component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression copy(net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column getColumn() + @org.jetbrains.annotations.Nullable public final List getGroupByColumns() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.Sort$Direction getOrderBy() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate getPredicate() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$BinaryLogical extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.BinaryLogicalOperator) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryLogicalOperator component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$BinaryLogical copy(net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.BinaryLogicalOperator) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getLeft() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryLogicalOperator getOperator() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getRight() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression copy(net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column getColumn() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate getPredicate() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$Not extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.CriteriaExpression) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$Not copy(net.corda.core.node.services.vault.CriteriaExpression) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getExpression() + public int hashCode() + public String toString() +## +public final class net.corda.core.node.services.vault.EqualityComparisonOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.EqualityComparisonOperator valueOf(String) + public static net.corda.core.node.services.vault.EqualityComparisonOperator[] values() +## +public interface net.corda.core.node.services.vault.IQueryCriteriaParser + @org.jetbrains.annotations.NotNull public abstract Collection parse(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract Collection parseAnd(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseOr(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) +## +public final class net.corda.core.node.services.vault.LikenessOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.LikenessOperator valueOf(String) + public static net.corda.core.node.services.vault.LikenessOperator[] values() +## +public final class net.corda.core.node.services.vault.NullOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.NullOperator valueOf(String) + public static net.corda.core.node.services.vault.NullOperator[] values() +## +public interface net.corda.core.node.services.vault.Operator +## +public final class net.corda.core.node.services.vault.PageSpecification extends java.lang.Object + public () + public (int, int) + public final int component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.PageSpecification copy(int, int) + public boolean equals(Object) + public final int getPageNumber() + public final int getPageSize() + public int hashCode() + public final boolean isDefault() + public String toString() +## +public abstract class net.corda.core.node.services.vault.QueryCriteria extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria and(net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria or(net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public abstract static class net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria + public () + @org.jetbrains.annotations.Nullable public abstract Set getContractStateTypes() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$StateStatus getStatus() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + public () + public (List) + public (List, List) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + @org.jetbrains.annotations.Nullable public final List component1() + @org.jetbrains.annotations.Nullable public final List component2() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate component3() + @org.jetbrains.annotations.Nullable public final List component4() + @org.jetbrains.annotations.Nullable public final List component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component6() + @org.jetbrains.annotations.Nullable public final Set component7() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria copy(List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.Nullable public final List getIssuer() + @org.jetbrains.annotations.Nullable public final List getIssuerRef() + @org.jetbrains.annotations.Nullable public final List getOwner() + @org.jetbrains.annotations.Nullable public final List getParticipants() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate getQuantity() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + public () + public (List) + public (List, List) + public (List, List, List) + public (List, List, List, net.corda.core.node.services.Vault$StateStatus) + public (List, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public (List, List, net.corda.core.node.services.Vault$StateStatus, Set) + @org.jetbrains.annotations.Nullable public final List component1() + @org.jetbrains.annotations.Nullable public final List component2() + @org.jetbrains.annotations.Nullable public final List component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component4() + @org.jetbrains.annotations.Nullable public final Set component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria copy(List, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.Nullable public final List getExternalId() + @org.jetbrains.annotations.Nullable public final List getParticipants() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + @org.jetbrains.annotations.Nullable public final List getUuid() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition extends java.lang.Object + public (net.corda.core.node.services.vault.QueryCriteria$SoftLockingType, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingType component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition copy(net.corda.core.node.services.vault.QueryCriteria$SoftLockingType, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getLockIds() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingType getType() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$SoftLockingType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.QueryCriteria$SoftLockingType valueOf(String) + public static net.corda.core.node.services.vault.QueryCriteria$SoftLockingType[] values() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$TimeCondition extends java.lang.Object + public (net.corda.core.node.services.vault.QueryCriteria$TimeInstantType, net.corda.core.node.services.vault.ColumnPredicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$TimeInstantType component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$TimeCondition copy(net.corda.core.node.services.vault.QueryCriteria$TimeInstantType, net.corda.core.node.services.vault.ColumnPredicate) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate getPredicate() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$TimeInstantType getType() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$TimeInstantType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.QueryCriteria$TimeInstantType valueOf(String) + public static net.corda.core.node.services.vault.QueryCriteria$TimeInstantType[] values() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + public (net.corda.core.node.services.vault.CriteriaExpression) + public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus) + public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus, Set) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component2() + @org.jetbrains.annotations.Nullable public final Set component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria copy(net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus, Set) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getExpression() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + public () + public (net.corda.core.node.services.Vault$StateStatus) + public (net.corda.core.node.services.Vault$StateStatus, Set) + public (net.corda.core.node.services.Vault$StateStatus, Set, List) + public (net.corda.core.node.services.Vault$StateStatus, Set, List, List) + public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition) + public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition, net.corda.core.node.services.vault.QueryCriteria$TimeCondition) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component1() + @org.jetbrains.annotations.Nullable public final Set component2() + @org.jetbrains.annotations.Nullable public final List component3() + @org.jetbrains.annotations.Nullable public final List component4() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition component5() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$TimeCondition component6() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria copy(net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition, net.corda.core.node.services.vault.QueryCriteria$TimeCondition) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.Nullable public final List getNotary() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition getSoftLockingCondition() + @org.jetbrains.annotations.Nullable public final List getStateRefs() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$TimeCondition getTimeCondition() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public final class net.corda.core.node.services.vault.QueryCriteriaUtils extends java.lang.Object + public static final Object builder(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public static final String getColumnName(net.corda.core.node.services.vault.Column) + @org.jetbrains.annotations.NotNull public static final Class resolveEnclosingObjectFromColumn(net.corda.core.node.services.vault.Column) + @org.jetbrains.annotations.NotNull public static final Class resolveEnclosingObjectFromExpression(net.corda.core.node.services.vault.CriteriaExpression) + public static final int DEFAULT_PAGE_NUM = 1 + public static final int DEFAULT_PAGE_SIZE = 200 + public static final int MAX_PAGE_SIZE = 2147483647 +## +public final class net.corda.core.node.services.vault.Sort extends java.lang.Object + public (Collection) + @org.jetbrains.annotations.NotNull public final Collection component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort copy(Collection) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Collection getColumns() + public int hashCode() + public String toString() +## +public static interface net.corda.core.node.services.vault.Sort$Attribute +## +public static final class net.corda.core.node.services.vault.Sort$CommonStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String, String) + @org.jetbrains.annotations.Nullable public final String getAttributeChild() + @org.jetbrains.annotations.NotNull public final String getAttributeParent() + public static net.corda.core.node.services.vault.Sort$CommonStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$CommonStateAttribute[] values() +## +public static final class net.corda.core.node.services.vault.Sort$Direction extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.Sort$Direction valueOf(String) + public static net.corda.core.node.services.vault.Sort$Direction[] values() +## +public static final class net.corda.core.node.services.vault.Sort$FungibleStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String) + @org.jetbrains.annotations.NotNull public final String getAttributeName() + public static net.corda.core.node.services.vault.Sort$FungibleStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$FungibleStateAttribute[] values() +## +public static final class net.corda.core.node.services.vault.Sort$LinearStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String) + @org.jetbrains.annotations.NotNull public final String getAttributeName() + public static net.corda.core.node.services.vault.Sort$LinearStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$LinearStateAttribute[] values() +## +public static final class net.corda.core.node.services.vault.Sort$SortColumn extends java.lang.Object + public (net.corda.core.node.services.vault.SortAttribute, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Direction component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$SortColumn copy(net.corda.core.node.services.vault.SortAttribute, net.corda.core.node.services.vault.Sort$Direction) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Direction getDirection() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute getSortAttribute() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.Sort$VaultStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String) + @org.jetbrains.annotations.NotNull public final String getAttributeName() + public static net.corda.core.node.services.vault.Sort$VaultStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$VaultStateAttribute[] values() +## +public abstract class net.corda.core.node.services.vault.SortAttribute extends java.lang.Object +## +public static final class net.corda.core.node.services.vault.SortAttribute$Custom extends net.corda.core.node.services.vault.SortAttribute + public (Class, String) + @org.jetbrains.annotations.NotNull public final Class component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Class getEntityStateClass() + @org.jetbrains.annotations.NotNull public final String getEntityStateColumnName() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.SortAttribute$Standard extends net.corda.core.node.services.vault.SortAttribute + public (net.corda.core.node.services.vault.Sort$Attribute) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Attribute component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute$Standard copy(net.corda.core.node.services.vault.Sort$Attribute) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Attribute getAttribute() + public int hashCode() + public String toString() +## +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 + public static final net.corda.core.schemas.CommonSchemaV1 INSTANCE +## +public static class net.corda.core.schemas.CommonSchemaV1$FungibleState extends net.corda.core.schemas.PersistentState + public (Set, net.corda.core.identity.AbstractParty, long, net.corda.core.identity.AbstractParty, byte[]) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty getIssuer() + @org.jetbrains.annotations.NotNull public final byte[] getIssuerRef() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty getOwner() + @org.jetbrains.annotations.Nullable public final Set getParticipants() + public final long getQuantity() + public final void setIssuer(net.corda.core.identity.AbstractParty) + public final void setIssuerRef(byte[]) + public final void setOwner(net.corda.core.identity.AbstractParty) + public final void setParticipants(Set) + public final void setQuantity(long) +## +public static class net.corda.core.schemas.CommonSchemaV1$LinearState extends net.corda.core.schemas.PersistentState + public (Set, String, UUID) + public (net.corda.core.contracts.UniqueIdentifier, Set) + @org.jetbrains.annotations.Nullable public final String getExternalId() + @org.jetbrains.annotations.Nullable public final Set getParticipants() + @org.jetbrains.annotations.NotNull public final UUID getUuid() + public final void setExternalId(String) + public final void setParticipants(Set) + public final void setUuid(UUID) +## +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.NotNull public final String getName() + public final int getVersion() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.schemas.NodeInfoSchema extends java.lang.Object + public static final net.corda.core.schemas.NodeInfoSchema INSTANCE +## +public final class net.corda.core.schemas.NodeInfoSchemaV1 extends net.corda.core.schemas.MappedSchema + public static final net.corda.core.schemas.NodeInfoSchemaV1 INSTANCE +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort extends java.lang.Object + public () + public (net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort) + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort copy(net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort) + public boolean equals(Object) + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NetworkHostAndPort toHostAndPort() + public String toString() + public static final net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort$Companion Companion +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort fromHostAndPort(net.corda.core.utilities.NetworkHostAndPort) +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$DBPartyAndCertificate extends java.lang.Object + public () + public (String, String, byte[], boolean, Set) + public (net.corda.core.identity.PartyAndCertificate, boolean) + @org.jetbrains.annotations.NotNull public final String component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final byte[] component3() + public final boolean component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$DBPartyAndCertificate copy(String, String, byte[], boolean, Set) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getName() + @org.jetbrains.annotations.NotNull public final String getOwningKey() + @org.jetbrains.annotations.NotNull public final byte[] getPartyCertBinary() + public int hashCode() + public final boolean isMain() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.PartyAndCertificate toLegalIdentityAndCert() + public String toString() +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort extends java.lang.Object implements java.io.Serializable + public () + public (String, Integer) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.Nullable public final Integer component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort copy(String, Integer) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final String getHost() + @org.jetbrains.annotations.Nullable public final Integer getPort() + public int hashCode() + public String toString() +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$PersistentNodeInfo extends java.lang.Object + public () + public (int, List, List, int, long) + @org.jetbrains.annotations.NotNull public final List getAddresses() + public final int getId() + @org.jetbrains.annotations.NotNull public final List getLegalIdentitiesAndCerts() + public final int getPlatformVersion() + public final long getSerial() + public final void setId(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo toNodeInfo() +## +public class net.corda.core.schemas.PersistentState extends java.lang.Object implements net.corda.core.schemas.StatePersistable + public () + public (net.corda.core.schemas.PersistentStateRef) + @org.jetbrains.annotations.Nullable public final net.corda.core.schemas.PersistentStateRef getStateRef() + public final void setStateRef(net.corda.core.schemas.PersistentStateRef) +## +public final class net.corda.core.schemas.PersistentStateRef extends java.lang.Object implements java.io.Serializable + public () + public (String, Integer) + public (net.corda.core.contracts.StateRef) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.Nullable public final Integer component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.PersistentStateRef copy(String, Integer) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final Integer getIndex() + @org.jetbrains.annotations.Nullable public final String getTxId() + public int hashCode() + public final void setIndex(Integer) + public final void setTxId(String) + public String toString() +## +public interface net.corda.core.schemas.QueryableState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.schemas.PersistentState generateMappedObject(net.corda.core.schemas.MappedSchema) + @org.jetbrains.annotations.NotNull public abstract Iterable supportedSchemas() +## +public interface net.corda.core.schemas.StatePersistable +## +public interface net.corda.core.serialization.ClassWhitelist + public abstract boolean hasListed(Class) +## +public @interface net.corda.core.serialization.CordaSerializable +## +public @interface net.corda.core.serialization.DeprecatedConstructorForDeserialization + public abstract int version() +## +public final class net.corda.core.serialization.MissingAttachmentsException extends net.corda.core.CordaException + public (List) + @org.jetbrains.annotations.NotNull public final List getIds() +## +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 + @org.jetbrains.annotations.NotNull public abstract ClassLoader getDeserializationClassLoader() + public abstract boolean getObjectReferencesEnabled() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.ByteSequence getPreferredSerializationVersion() + @org.jetbrains.annotations.NotNull public abstract Map getProperties() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext$UseCase getUseCase() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.ClassWhitelist getWhitelist() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withAttachmentsClassLoader(List) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withClassLoader(ClassLoader) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withPreferredSerializationVersion(net.corda.core.utilities.ByteSequence) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withProperty(Object, Object) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withWhitelisted(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withoutReferences() +## +public static final class net.corda.core.serialization.SerializationContext$UseCase extends java.lang.Enum + protected (String, int) + public static net.corda.core.serialization.SerializationContext$UseCase valueOf(String) + public static net.corda.core.serialization.SerializationContext$UseCase[] values() +## +public final class net.corda.core.serialization.SerializationDefaults extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getCHECKPOINT_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getP2P_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getRPC_CLIENT_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getRPC_SERVER_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationFactory getSERIALIZATION_FACTORY() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getSTORAGE_CONTEXT() + public final void setCHECKPOINT_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setP2P_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setRPC_CLIENT_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setRPC_SERVER_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setSERIALIZATION_FACTORY(net.corda.core.serialization.SerializationFactory) + public final void setSTORAGE_CONTEXT(net.corda.core.serialization.SerializationContext) + public static final net.corda.core.serialization.SerializationDefaults INSTANCE +## +public abstract class net.corda.core.serialization.SerializationFactory extends java.lang.Object + public () + public final Object asCurrent(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public abstract Object deserialize(net.corda.core.utilities.ByteSequence, Class, net.corda.core.serialization.SerializationContext) + @org.jetbrains.annotations.Nullable public final net.corda.core.serialization.SerializationContext getCurrentContext() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getDefaultContext() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationContext) + public final Object withCurrentContext(net.corda.core.serialization.SerializationContext, kotlin.jvm.functions.Function0) + public static final net.corda.core.serialization.SerializationFactory$Companion Companion +## +public static final class net.corda.core.serialization.SerializationFactory$Companion extends java.lang.Object + @org.jetbrains.annotations.Nullable public final net.corda.core.serialization.SerializationFactory getCurrentFactory() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationFactory getDefaultFactory() +## +public interface net.corda.core.serialization.SerializationToken + @org.jetbrains.annotations.NotNull public abstract Object fromToken(net.corda.core.serialization.SerializeAsTokenContext) +## +public interface net.corda.core.serialization.SerializationWhitelist + @org.jetbrains.annotations.NotNull public abstract List getWhitelist() +## +public interface net.corda.core.serialization.SerializeAsToken + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationToken toToken(net.corda.core.serialization.SerializeAsTokenContext) +## +public interface net.corda.core.serialization.SerializeAsTokenContext + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.ServiceHub getServiceHub() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializeAsToken getSingleton(String) + public abstract void putSingleton(net.corda.core.serialization.SerializeAsToken) +## +public final class net.corda.core.serialization.SerializedBytes extends net.corda.core.utilities.OpaqueBytes + public (byte[]) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +public final class net.corda.core.serialization.SingletonSerializationToken extends java.lang.Object implements net.corda.core.serialization.SerializationToken + @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializeAsToken fromToken(net.corda.core.serialization.SerializeAsTokenContext) + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SingletonSerializationToken registerWithContext(net.corda.core.serialization.SerializeAsTokenContext, net.corda.core.serialization.SerializeAsToken) + public static final net.corda.core.serialization.SingletonSerializationToken$Companion Companion +## +public static final class net.corda.core.serialization.SingletonSerializationToken$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SingletonSerializationToken singletonSerializationToken(Class) +## +public abstract class net.corda.core.serialization.SingletonSerializeAsToken extends java.lang.Object implements net.corda.core.serialization.SerializeAsToken + public () + @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SingletonSerializationToken toToken(net.corda.core.serialization.SerializeAsTokenContext) +## +public abstract class net.corda.core.transactions.BaseTransaction extends java.lang.Object implements net.corda.core.contracts.NamedByHash + public () + protected void checkBaseInvariants() + @org.jetbrains.annotations.NotNull public final List filterOutRefs(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final List filterOutputs(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef findOutRef(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState findOutput(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public abstract List getInputs() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getOutput(int) + @org.jetbrains.annotations.NotNull public final List getOutputStates() + @org.jetbrains.annotations.NotNull public abstract List getOutputs() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef outRef(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef outRef(net.corda.core.contracts.ContractState) + @org.jetbrains.annotations.NotNull public final List outRefsOfType(Class) + @org.jetbrains.annotations.NotNull public final List outputsOfType(Class) + @org.jetbrains.annotations.NotNull public String toString() +## +public class net.corda.core.transactions.ComponentGroup extends java.lang.Object + public (int, List) + @org.jetbrains.annotations.NotNull public List getComponents() + public int getGroupIndex() +## +public final class net.corda.core.transactions.ComponentVisibilityException extends net.corda.core.CordaException + public (net.corda.core.crypto.SecureHash, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final String getReason() +## +public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction + public () + @org.jetbrains.annotations.NotNull public abstract List getInputs() +## +public final class net.corda.core.transactions.FilteredComponentGroup extends net.corda.core.transactions.ComponentGroup + public (int, List, List, net.corda.core.crypto.PartialMerkleTree) + public final int component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final List component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredComponentGroup copy(int, List, List, net.corda.core.crypto.PartialMerkleTree) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public List getComponents() + public int getGroupIndex() + @org.jetbrains.annotations.NotNull public final List getNonces() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree getPartialMerkleTree() + public int hashCode() + public String toString() +## +public final class net.corda.core.transactions.FilteredTransaction extends net.corda.core.transactions.TraversableTransaction + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(net.corda.core.transactions.WireTransaction, function.Predicate) + public final void checkAllComponentsVisible(net.corda.core.contracts.ComponentGroupEnum) + public final boolean checkWithFun(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final List getFilteredComponentGroups() + @org.jetbrains.annotations.NotNull public final List getGroupHashes() + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + public final void verify() + public static final net.corda.core.transactions.FilteredTransaction$Companion Companion +## +public static final class net.corda.core.transactions.FilteredTransaction$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(net.corda.core.transactions.WireTransaction, function.Predicate) +## +public final class net.corda.core.transactions.FilteredTransactionVerificationException extends net.corda.core.CordaException + public (net.corda.core.crypto.SecureHash, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final String getReason() +## +public abstract class net.corda.core.transactions.FullTransaction extends net.corda.core.transactions.BaseTransaction + public () + protected void checkBaseInvariants() + @org.jetbrains.annotations.NotNull public abstract List getInputs() +## +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) + @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() + @org.jetbrains.annotations.NotNull public final List component3() + @org.jetbrains.annotations.NotNull public final List component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component5() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party component6() + @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) + 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) + @org.jetbrains.annotations.NotNull public final List filterInputs(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Command findCommand(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef findInRef(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState findInput(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getAttachment(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getAttachment(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final List getAttachments() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Command getCommand(int) + @org.jetbrains.annotations.NotNull public final List getCommands() + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getInput(int) + @org.jetbrains.annotations.NotNull public final List getInputStates() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow getTimeWindow() + @org.jetbrains.annotations.NotNull public final List groupStates(Class, kotlin.jvm.functions.Function1) + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef inRef(int) + @org.jetbrains.annotations.NotNull public final List inRefsOfType(Class) + @org.jetbrains.annotations.NotNull public final List inputsOfType(Class) + public String toString() + public final void verify() +## +public static final class net.corda.core.transactions.LedgerTransaction$InOutGroup extends java.lang.Object + public (List, List, Object) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final Object component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction$InOutGroup copy(List, List, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Object getGroupingKey() + @org.jetbrains.annotations.NotNull public final List getInputs() + @org.jetbrains.annotations.NotNull public final List getOutputs() + public int hashCode() + public String toString() +## +public final class net.corda.core.transactions.MissingContractAttachments extends net.corda.core.flows.FlowException + public (List) + @org.jetbrains.annotations.NotNull public final List getStates() +## +public final class net.corda.core.transactions.NotaryChangeLedgerTransaction extends net.corda.core.transactions.FullTransaction implements net.corda.core.transactions.TransactionWithSignatures + public (List, net.corda.core.identity.Party, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, List) + public void checkSignaturesAreValid() + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component4() + @org.jetbrains.annotations.NotNull public final List component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction copy(List, net.corda.core.identity.Party, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.NotNull public List getKeyDescriptions(Set) + @org.jetbrains.annotations.NotNull public Set getMissingSigners() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getNewNotary() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + @org.jetbrains.annotations.NotNull public Set getRequiredSigningKeys() + @org.jetbrains.annotations.NotNull public List getSigs() + public int hashCode() + public String toString() + public void verifyRequiredSignatures() +## +public final class net.corda.core.transactions.NotaryChangeWireTransaction extends net.corda.core.transactions.CoreTransaction + public (List, net.corda.core.identity.Party, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction copy(List, net.corda.core.identity.Party, net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getNewNotary() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolve(net.corda.core.node.ServiceHub, List) + public String toString() +## +public final class net.corda.core.transactions.SignedTransaction extends java.lang.Object implements net.corda.core.transactions.TransactionWithSignatures + public (net.corda.core.serialization.SerializedBytes, List) + public (net.corda.core.transactions.CoreTransaction, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(function.Predicate) + public void checkSignaturesAreValid() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction copy(net.corda.core.serialization.SerializedBytes, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final List getInputs() + @org.jetbrains.annotations.NotNull public ArrayList getKeyDescriptions(Set) + @org.jetbrains.annotations.NotNull public Set getMissingSigners() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction getNotaryChangeTx() + @org.jetbrains.annotations.NotNull public Set getRequiredSigningKeys() + @org.jetbrains.annotations.NotNull public List getSigs() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction getTx() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getTxBits() + public int hashCode() + public final boolean isNotaryChangeTransaction() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(net.corda.core.crypto.TransactionSignature) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolveNotaryChangeTransaction(net.corda.core.node.ServiceHub) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub, boolean) + @org.jetbrains.annotations.NotNull public String toString() + public final void verify(net.corda.core.node.ServiceHub) + public final void verify(net.corda.core.node.ServiceHub, boolean) + public void verifyRequiredSignatures() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(java.security.KeyPair, net.corda.core.crypto.SignatureMetadata) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(net.corda.core.crypto.TransactionSignature) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignatures(Iterable) + public static final net.corda.core.transactions.SignedTransaction$Companion Companion +## +public static final class net.corda.core.transactions.SignedTransaction$SignaturesMissingException extends java.security.SignatureException implements net.corda.core.CordaThrowable, net.corda.core.contracts.NamedByHash + public (Set, List, net.corda.core.crypto.SecureHash) + public void addSuppressed(Throwable[]) + @org.jetbrains.annotations.NotNull public final List getDescriptions() + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final Set getMissing() + @org.jetbrains.annotations.Nullable public String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public String getOriginalMessage() + public void setCause(Throwable) + public void setMessage(String) + public void setOriginalExceptionClassName(String) +## +public class net.corda.core.transactions.TransactionBuilder extends java.lang.Object + public () + public (net.corda.core.identity.Party) + public (net.corda.core.identity.Party, UUID, List, List, List, List, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addAttachment(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addCommand(net.corda.core.contracts.Command) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addCommand(net.corda.core.contracts.CommandData, List) + @org.jetbrains.annotations.NotNull public net.corda.core.transactions.TransactionBuilder addInputState(net.corda.core.contracts.StateAndRef) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.contracts.AttachmentConstraint) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.TransactionState) + @org.jetbrains.annotations.NotNull public final List attachments() + @org.jetbrains.annotations.NotNull public final List commands() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder copy() + @org.jetbrains.annotations.NotNull protected final List getAttachments() + @org.jetbrains.annotations.NotNull protected final List getCommands() + @org.jetbrains.annotations.NotNull protected final List getInputs() + @org.jetbrains.annotations.NotNull public final UUID getLockId() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull protected final List getOutputs() + @org.jetbrains.annotations.NotNull protected final net.corda.core.contracts.PrivacySalt getPrivacySalt() + @org.jetbrains.annotations.Nullable protected final net.corda.core.contracts.TimeWindow getWindow() + @org.jetbrains.annotations.NotNull public final List inputStates() + @org.jetbrains.annotations.NotNull public final List outputStates() + public final void setLockId(UUID) + public final void setNotary(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder setPrivacySalt(net.corda.core.contracts.PrivacySalt) + protected final void setPrivacySalt(net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder setTimeWindow(java.time.Instant, java.time.Duration) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder setTimeWindow(net.corda.core.contracts.TimeWindow) + protected final void setWindow(net.corda.core.contracts.TimeWindow) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction toSignedTransaction(net.corda.core.node.services.KeyManagementService, java.security.PublicKey, net.corda.core.crypto.SignatureMetadata, net.corda.core.node.ServicesForResolution) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction toWireTransaction(net.corda.core.node.ServicesForResolution) + public final void verify(net.corda.core.node.ServiceHub) +## +public interface net.corda.core.transactions.TransactionWithSignatures extends net.corda.core.contracts.NamedByHash + public abstract void checkSignaturesAreValid() + @org.jetbrains.annotations.NotNull public abstract List getKeyDescriptions(Set) + @org.jetbrains.annotations.NotNull public abstract Set getMissingSigners() + @org.jetbrains.annotations.NotNull public abstract Set getRequiredSigningKeys() + @org.jetbrains.annotations.NotNull public abstract List getSigs() + public abstract void verifyRequiredSignatures() +## +public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction + public (List) + @org.jetbrains.annotations.NotNull public final List getAttachments() + @org.jetbrains.annotations.NotNull public final List getAvailableComponentGroups() + @org.jetbrains.annotations.NotNull public final List getCommands() + @org.jetbrains.annotations.NotNull public List getComponentGroups() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow getTimeWindow() +## +public final class net.corda.core.transactions.WireTransaction extends net.corda.core.transactions.TraversableTransaction + @kotlin.Deprecated public (List, List, List, List, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + public (List, net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(function.Predicate) + public final void checkSignature(net.corda.core.crypto.TransactionSignature) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getMerkleTree() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt() + @org.jetbrains.annotations.NotNull public final Set getRequiredSigningKeys() + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.transactions.WireTransaction$Companion Companion +## +public static final class net.corda.core.transactions.WireTransaction$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final List createComponentGroups(List, List, List, List, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow) +## +public final class net.corda.core.utilities.ByteArrays extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final byte[] parseAsHex(String) + @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence sequence(byte[], int, int) + @org.jetbrains.annotations.NotNull public static final String toHexString(byte[]) +## +public abstract class net.corda.core.utilities.ByteSequence extends java.lang.Object implements java.lang.Comparable + public int compareTo(net.corda.core.utilities.ByteSequence) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence copy() + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public abstract byte[] getBytes() + public abstract int getOffset() + public abstract int getSize() + public int hashCode() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int, int) + @org.jetbrains.annotations.NotNull public final java.io.ByteArrayInputStream open() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence subSequence(int, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence take(int) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.utilities.ByteSequence$Companion Companion +## +public static final class net.corda.core.utilities.ByteSequence$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int, int) +## +public final class net.corda.core.utilities.EncodingUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final byte[] base58ToByteArray(String) + @org.jetbrains.annotations.NotNull public static final String base58ToRealString(String) + @org.jetbrains.annotations.NotNull public static final String base58toBase64(String) + @org.jetbrains.annotations.NotNull public static final String base58toHex(String) + @org.jetbrains.annotations.NotNull public static final byte[] base64ToByteArray(String) + @org.jetbrains.annotations.NotNull public static final String base64ToRealString(String) + @org.jetbrains.annotations.NotNull public static final String base64toBase58(String) + @org.jetbrains.annotations.NotNull public static final String base64toHex(String) + @org.jetbrains.annotations.NotNull public static final String hexToBase58(String) + @org.jetbrains.annotations.NotNull public static final String hexToBase64(String) + @org.jetbrains.annotations.NotNull public static final byte[] hexToByteArray(String) + @org.jetbrains.annotations.NotNull public static final String hexToRealString(String) + @org.jetbrains.annotations.NotNull public static final java.security.PublicKey parsePublicKeyBase58(String) + @org.jetbrains.annotations.NotNull public static final String toBase58(byte[]) + @org.jetbrains.annotations.NotNull public static final String toBase58String(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public static final String toBase64(byte[]) + @org.jetbrains.annotations.NotNull public static final String toHex(byte[]) + @org.jetbrains.annotations.NotNull public static final byte[] toSHA256Bytes(java.security.PublicKey) +## +public final class net.corda.core.utilities.KotlinUtilsKt extends java.lang.Object + public static final void debug(org.slf4j.Logger, kotlin.jvm.functions.Function0) + public static final int exactAdd(int, int) + public static final long exactAdd(long, long) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getDays(int) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getHours(int) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getMillis(int) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getMinutes(int) + public static final Object getOrThrow(concurrent.Future, java.time.Duration) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getSeconds(int) + @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NonEmptySet toNonEmptySet(Collection) + public static final void trace(org.slf4j.Logger, kotlin.jvm.functions.Function0) + @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.PropertyDelegate transient(kotlin.jvm.functions.Function0) +## +public final class net.corda.core.utilities.NetworkHostAndPort extends java.lang.Object + public (String, int) + @org.jetbrains.annotations.NotNull public final String component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NetworkHostAndPort copy(String, int) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getHost() + public final int getPort() + public int hashCode() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NetworkHostAndPort parse(String) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.utilities.NetworkHostAndPort$Companion Companion + @org.jetbrains.annotations.NotNull public static final String INVALID_PORT_FORMAT = "Invalid port: %s" + @org.jetbrains.annotations.NotNull public static final String MISSING_PORT_FORMAT = "Missing port: %s" + @org.jetbrains.annotations.NotNull public static final String UNPARSEABLE_ADDRESS_FORMAT = "Unparseable address: %s" +## +public static final class net.corda.core.utilities.NetworkHostAndPort$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NetworkHostAndPort parse(String) +## +public final class net.corda.core.utilities.NonEmptySet extends java.lang.Object implements kotlin.jvm.internal.markers.KMappedMarker, java.util.Set + public boolean add(Object) + public boolean addAll(Collection) + public void clear() + public boolean contains(Object) + public boolean containsAll(Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NonEmptySet copyOf(Collection) + public boolean equals(Object) + public void forEach(function.Consumer) + public int getSize() + public int hashCode() + public final Object head() + public boolean isEmpty() + @org.jetbrains.annotations.NotNull public Iterator iterator() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NonEmptySet of(Object) + @org.jetbrains.annotations.NotNull public stream.Stream parallelStream() + public boolean remove(Object) + public boolean removeAll(Collection) + public boolean retainAll(Collection) + @org.jetbrains.annotations.NotNull public Spliterator spliterator() + @org.jetbrains.annotations.NotNull public stream.Stream stream() + public Object[] toArray() + public Object[] toArray(Object[]) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.utilities.NonEmptySet$Companion Companion +## +public static final class net.corda.core.utilities.NonEmptySet$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NonEmptySet copyOf(Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NonEmptySet of(Object) +## +public static final class net.corda.core.utilities.NonEmptySet$iterator$1 extends java.lang.Object implements java.util.Iterator, kotlin.jvm.internal.markers.KMappedMarker + public boolean hasNext() + public Object next() + public void remove() +## +public class net.corda.core.utilities.OpaqueBytes extends net.corda.core.utilities.ByteSequence + public (byte[]) + @org.jetbrains.annotations.NotNull public final byte[] getBytes() + public int getOffset() + public int getSize() + public static final net.corda.core.utilities.OpaqueBytes$Companion Companion +## +public static final class net.corda.core.utilities.OpaqueBytes$Companion extends java.lang.Object +## +public final class net.corda.core.utilities.OpaqueBytesSubSequence extends net.corda.core.utilities.ByteSequence + public (byte[], int, int) + @org.jetbrains.annotations.NotNull public byte[] getBytes() + public int getOffset() + public int getSize() +## +public final class net.corda.core.utilities.ProgressTracker extends java.lang.Object + public final void endWithError(Throwable) + @org.jetbrains.annotations.NotNull public final List getAllSteps() + @org.jetbrains.annotations.NotNull public final rx.Observable getChanges() + @org.jetbrains.annotations.Nullable public final net.corda.core.utilities.ProgressTracker getChildProgressTracker(net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getCurrentStep() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getCurrentStepRecursive() + public final boolean getHasEnded() + @org.jetbrains.annotations.Nullable public final net.corda.core.utilities.ProgressTracker getParent() + public final int getStepIndex() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step[] getSteps() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTopLevelTracker() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step nextStep() + public final void setChildProgressTracker(net.corda.core.utilities.ProgressTracker$Step, net.corda.core.utilities.ProgressTracker) + public final void setCurrentStep(net.corda.core.utilities.ProgressTracker$Step) +## +public abstract static class net.corda.core.utilities.ProgressTracker$Change extends java.lang.Object +## +public static final class net.corda.core.utilities.ProgressTracker$Change$Position extends net.corda.core.utilities.ProgressTracker$Change + public (net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Change$Position copy(net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getNewStep() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTracker() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.ProgressTracker$Change$Rendering extends net.corda.core.utilities.ProgressTracker$Change + public (net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Change$Rendering copy(net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getOfStep() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTracker() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.ProgressTracker$Change$Structural extends net.corda.core.utilities.ProgressTracker$Change + public (net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Change$Structural copy(net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getParent() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTracker() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.ProgressTracker$DONE extends net.corda.core.utilities.ProgressTracker$Step + public boolean equals(Object) + public static final net.corda.core.utilities.ProgressTracker$DONE INSTANCE +## +public static class net.corda.core.utilities.ProgressTracker$Step extends java.lang.Object + public (String) + @org.jetbrains.annotations.Nullable public net.corda.core.utilities.ProgressTracker childProgressTracker() + @org.jetbrains.annotations.NotNull public rx.Observable getChanges() + @org.jetbrains.annotations.NotNull public Map getExtraAuditData() + @org.jetbrains.annotations.NotNull public String getLabel() +## +public static final class net.corda.core.utilities.ProgressTracker$UNSTARTED extends net.corda.core.utilities.ProgressTracker$Step + public boolean equals(Object) + public static final net.corda.core.utilities.ProgressTracker$UNSTARTED INSTANCE +## +public interface net.corda.core.utilities.PropertyDelegate + public abstract Object getValue(Object, kotlin.reflect.KProperty) +## +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) + public abstract Object getOrThrow() + public abstract boolean isFailure() + public abstract boolean isSuccess() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try map(kotlin.jvm.functions.Function1) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.Try on(kotlin.jvm.functions.Function0) + public static final net.corda.core.utilities.Try$Companion Companion +## +public static final class net.corda.core.utilities.Try$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try on(kotlin.jvm.functions.Function0) +## +public static final class net.corda.core.utilities.Try$Failure extends net.corda.core.utilities.Try + public (Throwable) + @org.jetbrains.annotations.NotNull public final Throwable component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try$Failure copy(Throwable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Throwable getException() + public Object getOrThrow() + public int hashCode() + public boolean isFailure() + public boolean isSuccess() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.Try$Success extends net.corda.core.utilities.Try + public (Object) + public final Object component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try$Success copy(Object) + public boolean equals(Object) + public Object getOrThrow() + public final Object getValue() + public int hashCode() + public boolean isFailure() + public boolean isSuccess() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.utilities.UntrustworthyData extends java.lang.Object + public (Object) + public final Object getFromUntrustedWorld() + @co.paralleluniverse.fibers.Suspendable public final Object unwrap(net.corda.core.utilities.UntrustworthyData$Validator) +## +public static interface net.corda.core.utilities.UntrustworthyData$Validator extends java.io.Serializable + @co.paralleluniverse.fibers.Suspendable public abstract Object validate(Object) +## +public final class net.corda.core.utilities.UntrustworthyDataKt extends java.lang.Object + public static final Object unwrap(net.corda.core.utilities.UntrustworthyData, kotlin.jvm.functions.Function1) +## +public interface net.corda.core.utilities.VariablePropertyDelegate extends net.corda.core.utilities.PropertyDelegate + public abstract void setValue(Object, kotlin.reflect.KProperty, Object) +## +public final class net.corda.client.jackson.JacksonSupport extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper(com.fasterxml.jackson.core.JsonFactory) + @org.jetbrains.annotations.NotNull public final com.fasterxml.jackson.databind.Module getCordaModule() + public static final net.corda.client.jackson.JacksonSupport INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AmountDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.contracts.Amount deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$AmountDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AmountSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.contracts.Amount, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$AmountSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AnonymousPartyDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.identity.AnonymousParty deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$AnonymousPartyDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AnonymousPartySerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.identity.AnonymousParty, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$AnonymousPartySerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$CordaX500NameDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.identity.CordaX500Name deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$CordaX500NameDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$CordaX500NameSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.identity.CordaX500Name, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$CordaX500NameSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$IdentityObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper + public (net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) + public final boolean getFuzzyIdentityMatch() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.IdentityService getIdentityService() + @org.jetbrains.annotations.NotNull public Set partiesFromName(String) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$NoPartyObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper + public (com.fasterxml.jackson.core.JsonFactory) + @org.jetbrains.annotations.NotNull public Void partiesFromName(String) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$NodeInfoDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$NodeInfoDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$NodeInfoSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.node.NodeInfo, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$NodeInfoSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$OpaqueBytesDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.OpaqueBytes deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$OpaqueBytesDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$OpaqueBytesSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.utilities.OpaqueBytes, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$OpaqueBytesSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$PartyDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$PartyDeserializer INSTANCE +## +public static interface net.corda.client.jackson.JacksonSupport$PartyObjectMapper + @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$PartySerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.identity.Party, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$PartySerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$PublicKeyDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public java.security.PublicKey deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$PublicKeyDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$PublicKeySerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(java.security.PublicKey, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$PublicKeySerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$RpcObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper + public (net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) + public final boolean getFuzzyIdentityMatch() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.CordaRPCOps getRpc() + @org.jetbrains.annotations.NotNull public Set partiesFromName(String) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$SecureHashDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + public () + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) +## +public static final class net.corda.client.jackson.JacksonSupport$SecureHashSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.crypto.SecureHash, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$SecureHashSerializer INSTANCE +## +public abstract static class net.corda.client.jackson.JacksonSupport$SignedTransactionMixin extends java.lang.Object + public () + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash getId() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getInputs() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.NotaryChangeWireTransaction getNotaryChangeTx() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract Set getRequiredSigningKeys() + @com.fasterxml.jackson.annotation.JsonProperty @org.jetbrains.annotations.NotNull protected abstract List getSigs() + @com.fasterxml.jackson.annotation.JsonProperty @org.jetbrains.annotations.NotNull protected abstract net.corda.core.transactions.CoreTransaction getTransaction() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.WireTransaction getTx() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializedBytes getTxBits() +## +public static final class net.corda.client.jackson.JacksonSupport$ToStringSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$ToStringSerializer INSTANCE +## +public abstract static class net.corda.client.jackson.JacksonSupport$WireTransactionMixin extends java.lang.Object + public () + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getAvailableComponentHashes() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getAvailableComponents() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.MerkleTree getMerkleTree() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getOutputStates() +## +public class net.corda.client.jackson.StringToMethodCallParser extends java.lang.Object + public (Class) + public (Class, com.fasterxml.jackson.databind.ObjectMapper) + public (kotlin.reflect.KClass) + @org.jetbrains.annotations.NotNull public final Map getAvailableCommands() + @org.jetbrains.annotations.NotNull protected final com.google.common.collect.Multimap getMethodMap() + @org.jetbrains.annotations.NotNull public final Map getMethodParamNames() + @org.jetbrains.annotations.NotNull public List paramNamesFromConstructor(reflect.Constructor) + @org.jetbrains.annotations.NotNull public List paramNamesFromMethod(reflect.Method) + @org.jetbrains.annotations.NotNull public final net.corda.client.jackson.StringToMethodCallParser$ParsedMethodCall parse(Object, String) + @org.jetbrains.annotations.NotNull public final Object[] parseArguments(String, List, String) + public static final net.corda.client.jackson.StringToMethodCallParser$Companion Companion +## +public static final class net.corda.client.jackson.StringToMethodCallParser$Companion extends java.lang.Object +## +public final class net.corda.client.jackson.StringToMethodCallParser$ParsedMethodCall extends java.lang.Object implements java.util.concurrent.Callable + public (net.corda.client.jackson.StringToMethodCallParser, Object, reflect.Method, Object[]) + @org.jetbrains.annotations.Nullable public Object call() + @org.jetbrains.annotations.NotNull public final Object[] getArgs() + @org.jetbrains.annotations.NotNull public final reflect.Method getMethod() + @org.jetbrains.annotations.Nullable public final Object invoke() +## +public static class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException extends net.corda.core.CordaException + public (String, Throwable) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$FailedParse extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (Exception) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$MissingParameter extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String, String, String) + @org.jetbrains.annotations.NotNull public final String getParamName() +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$ReflectionDataMissing extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String, int) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$TooManyParameters extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String, String) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$UnknownMethod extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String) + @org.jetbrains.annotations.NotNull public final String getMethodName() +## +public final class net.corda.client.rpc.CordaRPCClient extends java.lang.Object + public (net.corda.core.utilities.NetworkHostAndPort) + public (net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration) + @org.jetbrains.annotations.NotNull public final net.corda.client.rpc.CordaRPCConnection start(String, String) + public final Object use(String, String, kotlin.jvm.functions.Function1) +## +public final class net.corda.client.rpc.CordaRPCClientConfiguration extends java.lang.Object + public (java.time.Duration) + @org.jetbrains.annotations.NotNull public final java.time.Duration component1() + @org.jetbrains.annotations.NotNull public final net.corda.client.rpc.CordaRPCClientConfiguration copy(java.time.Duration) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.time.Duration getConnectionMaxRetryInterval() + public int hashCode() + public String toString() + public static final net.corda.client.rpc.CordaRPCClientConfiguration$Companion Companion + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.client.rpc.CordaRPCClientConfiguration DEFAULT +## +public static final class net.corda.client.rpc.CordaRPCClientConfiguration$Companion extends java.lang.Object +## +public final class net.corda.client.rpc.CordaRPCConnection extends java.lang.Object implements net.corda.client.rpc.RPCConnection + public (net.corda.client.rpc.RPCConnection) + public void close() + public void forceClose() + @org.jetbrains.annotations.NotNull public net.corda.core.messaging.CordaRPCOps getProxy() + public int getServerProtocolVersion() + public void notifyServerAndClose() +## +public final class net.corda.client.rpc.PermissionException extends net.corda.core.CordaRuntimeException + public (String) +## +public interface net.corda.client.rpc.RPCConnection extends java.io.Closeable + public abstract void forceClose() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.RPCOps getProxy() + public abstract int getServerProtocolVersion() + public abstract void notifyServerAndClose() +## +public class net.corda.client.rpc.RPCException extends net.corda.core.CordaRuntimeException + public (String) + public (String, Throwable) +## +public @interface net.corda.client.rpc.RPCSinceVersion + public abstract int version() +## +public final class net.corda.client.rpc.UtilsKt extends java.lang.Object + public static final void notUsed(rx.Observable) +## diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh new file mode 100755 index 0000000000..e9ac25cf6d --- /dev/null +++ b/.ci/check-api-changes.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +echo "Starting API Diff" + +APIHOME=$(dirname $0) + +apiCurrent=$APIHOME/api-current.txt +if [ ! -f $apiCurrent ]; then + echo "Missing $apiCurrent file - cannot check API diff. Please rebase or add it to this release" + exit -1 +fi + +# Remove the two header lines from the diff output. +diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt | tail --lines=+3` +echo "Diff contents:" +echo "$diffContents" +echo + +# A removed line means that an API was either deleted or modified. +removals=$(echo "$diffContents" | grep "^-") +removalCount=`grep -v "^$" < + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 25abc91c93..4ecdc4bfee 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,6 +2,8 @@ + + @@ -19,6 +21,8 @@ + + @@ -42,8 +46,11 @@ + + + @@ -58,6 +65,8 @@ + + diff --git a/.idea/scopes/Corda_API.xml b/.idea/scopes/Corda_API.xml new file mode 100644 index 0000000000..fa137ebcf0 --- /dev/null +++ b/.idea/scopes/Corda_API.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/README.md b/README.md index 790b967bb5..3408574289 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,23 @@ ![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + + # Corda Enterprise Corda Enterprise is R3's closed source patch set on top of Corda Open Source. It adds features and improvements that we plan to charge for. +Corda is a decentralised database system in which nodes trust each other as little as possible. + +## Features + +* Smart contracts that can be written in Java and other JVM languages +* Flow framework to manage communication and negotiation between participants +* Peer-to-peer network of nodes +* "Notary" infrastructure to validate uniqueness and sequencing of transactions without global broadcast +* Enables the development and deployment of distributed apps called CorDapps +* Written in [Kotlin](https://kotlinlang.org), targeting the JVM + ## Extra features * Doorman @@ -12,3 +25,36 @@ plan to charge for. * Flow triage screen in Explorer * No stupid jokes at startup * SGX + +## Getting started + +1. Read the [Getting Started](https://docs.corda.net/getting-set-up.html) documentation +2. Run the [Example CorDapp](https://docs.corda.net/tutorial-cordapp.html) +3. Read about Corda's [Key Concepts](https://docs.corda.net/key-concepts.html) +3. Follow the [Hello, World! tutorial](https://docs.corda.net/hello-world-index.html) + +## Useful links + +* [Project Website](https://corda.net) +* [Documentation](https://docs.corda.net) +* [Slack Channel](https://slack.corda.net/) +* [Stack Overflow tag](https://stackoverflow.com/questions/tagged/corda) +* [Forum](https://discourse.corda.net) +* [Meetups](https://www.meetup.com/pro/corda/) +* [Training Courses](https://www.corda.net/corda-training/) + +## Contributing + +Please read [here](./CONTRIBUTING.md). + +## License + +[Apache 2.0](./LICENSE) + +## Acknowledgements + +![YourKit](https://www.yourkit.com/images/yklogo.png) + +YourKit supports open source projects with its full-featured Java Profiler. + +YourKit, LLC is the creator of [YourKit Java Profiler](https://www.yourkit.com/java/profiler/) and [YourKit .NET Profiler](https://www.yourkit.com/.net/profiler/), innovative and intelligent tools for profiling Java and .NET applications. diff --git a/build.gradle b/build.gradle index 468c1363a0..84695d932a 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ buildscript { ext.jopt_simple_version = '5.0.2' ext.jansi_version = '1.14' ext.hibernate_version = '5.2.6.Final' - ext.h2_version = '1.4.194' + ext.h2_version = '1.4.194' // Update docs if renamed or removed. ext.rxjava_version = '1.2.4' ext.dokka_version = '0.9.14' ext.eddsa_version = '0.2.0' @@ -59,7 +59,9 @@ buildscript { classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version" classpath "net.corda.plugins:cordformation:$gradle_plugins_version" - classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0' + classpath "net.corda.plugins:cordapp:$gradle_plugins_version" + classpath "net.corda.plugins:api-scanner:$gradle_plugins_version" + classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0' classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" classpath "org.ajoberstar:grgit:1.1.0" @@ -115,12 +117,14 @@ allprojects { } } - tasks.withType(Jar) { // Includes War and Ear + tasks.withType(Jar) { task -> + // Includes War and Ear manifest { attributes('Corda-Release-Version': corda_release_version) attributes('Corda-Platform-Version': corda_platform_version) attributes('Corda-Revision': corda_revision) attributes('Corda-Vendor': 'Corda Enterprise Edition') + attributes('Automatic-Module-Name': "net.corda.${task.project.name.replaceAll('-', '.')}") } } @@ -175,27 +179,31 @@ repositories { } } -// TODO: Corda root project currently produces a dummy cordapp when it shouldn't. // Required for building out the fat JAR. dependencies { - cordaCompile project(':node') + compile project(':node') compile "com.google.guava:guava:$guava_version" // Set to corda compile to ensure it exists now deploy nodes no longer relies on build - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') - cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') // For the buildCordappDependenciesJar task - cordaRuntime project(':client:jfx') - cordaRuntime project(':client:mock') - cordaRuntime project(':client:rpc') - cordaRuntime project(':core') - cordaRuntime project(':confidential-identities') - cordaRuntime project(':finance') - cordaRuntime project(':webserver') + runtime project(':client:jfx') + runtime project(':client:mock') + runtime project(':client:rpc') + runtime project(':core') + runtime project(':confidential-identities') + runtime project(':finance') + runtime project(':webserver') testCompile project(':test-utils') } +jar { + // Prevent the root project from building an unwanted dummy CorDapp. + enabled = false +} + task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { dependsOn = subprojects.test additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs) @@ -223,16 +231,14 @@ tasks.withType(Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=GB" node { name "O=Controller,OU=corda,L=London,C=GB" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] p2pPort 10002 cordapps = [] } node { name "O=Bank A,OU=corda,L=London,C=GB" - advertisedServices = [] p2pPort 10012 rpcPort 10013 webPort 10014 @@ -240,7 +246,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,OU=corda,L=London,C=GB" - advertisedServices = [] p2pPort 10007 rpcPort 10008 webPort 10009 @@ -289,12 +294,20 @@ artifactory { publish { contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' repository { - repoKey = 'corda-releases' + repoKey = 'enterprise-dev' username = 'teamcity' password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') } + defaults { - publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities') + // Root project applies the plugin (for this block) but does not need to be published + if(project != rootProject) { + publications(project.extensions.publish.name()) + } } } } + +task generateApi(type: net.corda.plugins.GenerateApi){ + baseName = "api-corda" +} diff --git a/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy b/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy index 6b6b87b483..85034e7bbd 100644 --- a/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy +++ b/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy @@ -28,7 +28,7 @@ class CanonicalizerPlugin implements Plugin { output.setMethod(ZipOutputStream.DEFLATED) entries.each { - def newEntry = new ZipEntry( it.name ) + def newEntry = new ZipEntry(it.name) newEntry.setLastModifiedTime(zeroTime) newEntry.setCreationTime(zeroTime) diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index 4e6c9b1f69..fadfdf9975 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'com.jfrog.artifactory' dependencies { @@ -24,6 +25,9 @@ dependencies { jar { baseName 'corda-jackson' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.jackson' + } } publish { diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index a9935f12fc..65465e04bb 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -17,6 +17,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService @@ -28,8 +29,9 @@ import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.parsePublicKeyBase58 -import net.corda.core.utilities.toBase58String +import net.corda.core.utilities.base58ToByteArray +import net.corda.core.utilities.base64ToByteArray +import net.corda.core.utilities.toBase64 import net.i2p.crypto.eddsa.EdDSAPublicKey import java.math.BigDecimal import java.security.PublicKey @@ -83,13 +85,9 @@ object JacksonSupport { addDeserializer(SecureHash::class.java, SecureHashDeserializer()) addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) - // For ed25519 pubkeys - addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer) - addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer) - - // For composite keys - addSerializer(CompositeKey::class.java, CompositeKeySerializer) - addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer) + // Public key types + addSerializer(PublicKey::class.java, PublicKeySerializer) + addDeserializer(PublicKey::class.java, PublicKeyDeserializer) // For NodeInfo // TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this. @@ -121,12 +119,14 @@ object JacksonSupport { * match an identity known from the network map. If true, the name is matched more leniently but if the match * is ambiguous a [JsonParseException] is thrown. */ - @JvmStatic @JvmOverloads + @JvmStatic + @JvmOverloads fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory(), fuzzyIdentityMatch: Boolean = false): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch)) /** For testing or situations where deserialising parties is not required */ - @JvmStatic @JvmOverloads + @JvmStatic + @JvmOverloads fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory)) /** @@ -136,7 +136,8 @@ object JacksonSupport { * match an identity known from the network map. If true, the name is matched more leniently but if the match * is ambiguous a [JsonParseException] is thrown. */ - @JvmStatic @JvmOverloads + @JvmStatic + @JvmOverloads fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory(), fuzzyIdentityMatch: Boolean = false) = configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch)) @@ -158,7 +159,7 @@ object JacksonSupport { object AnonymousPartySerializer : JsonSerializer() { override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.owningKey.toBase58String()) + PublicKeySerializer.serialize(obj.owningKey, generator, provider) } } @@ -168,8 +169,7 @@ object JacksonSupport { parser.nextToken() } - // TODO this needs to use some industry identifier(s) instead of these keys - val key = parsePublicKeyBase58(parser.text) + val key = PublicKeyDeserializer.deserialize(parser, context) return AnonymousParty(key) } } @@ -187,19 +187,24 @@ object JacksonSupport { } val mapper = parser.codec as PartyObjectMapper - // TODO: We should probably have a better specified way of identifying X.500 names vs keys - // Base58 keys never include an equals character, while X.500 names always will, so we use that to determine - // how to parse the content - return if (parser.text.contains("=")) { + // The comma character is invalid in base64, and required as a separator for X.500 names. As Corda + // X.500 names all involve at least three attributes (organisation, locality, country), they must + // include a comma. As such we can use it as a distinguisher between the two types. + return if (parser.text.contains(",")) { val principal = CordaX500Name.parse(parser.text) mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal") } else { val nameMatches = mapper.partiesFromName(parser.text) if (nameMatches.isEmpty()) { + val derBytes = try { + parser.text.base64ToByteArray() + } catch (e: AddressFormatException) { + throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base64 encoded public key: " + e.message) + } val key = try { - parsePublicKeyBase58(parser.text) + Crypto.decodePublicKey(derBytes) } catch (e: Exception) { - throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key") + throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a valid public key: " + e.message) } mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") } else if (nameMatches.size == 1) { @@ -225,7 +230,7 @@ object JacksonSupport { return try { CordaX500Name.parse(parser.text) - } catch(ex: IllegalArgumentException) { + } catch (ex: IllegalArgumentException) { throw JsonParseException(parser, "Invalid Corda X.500 name ${parser.text}: ${ex.message}", ex) } } @@ -265,47 +270,30 @@ object JacksonSupport { parser.nextToken() } try { - @Suppress("UNCHECKED_CAST") - return SecureHash.parse(parser.text) as T + return uncheckedCast(SecureHash.parse(parser.text)) } catch (e: Exception) { throw JsonParseException(parser, "Invalid hash ${parser.text}: ${e.message}") } } } - object PublicKeySerializer : JsonSerializer() { - override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) { - check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) - generator.writeString(obj.toBase58String()) + object PublicKeySerializer : JsonSerializer() { + override fun serialize(obj: PublicKey, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(obj.encoded.toBase64()) } } - object PublicKeyDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey { + object PublicKeyDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey { return try { - parsePublicKeyBase58(parser.text) as EdDSAPublicKey + val derBytes = parser.text.base64ToByteArray() + Crypto.decodePublicKey(derBytes) } catch (e: Exception) { throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}") } } } - object CompositeKeySerializer : JsonSerializer() { - override fun serialize(obj: CompositeKey, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.toBase58String()) - } - } - - object CompositeKeyDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey { - return try { - parsePublicKeyBase58(parser.text) as CompositeKey - } catch (e: Exception) { - throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}") - } - } - } - object AmountSerializer : JsonSerializer>() { override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeString(value.toString()) @@ -325,7 +313,7 @@ object JacksonSupport { // Attempt parsing as a currency token. TODO: This needs thought about how to extend to other token types. val currency = Currency.getInstance(token) return Amount(quantity, currency) - } catch(e2: Exception) { + } catch (e2: Exception) { throw JsonParseException(parser, "Invalid amount ${parser.text}", e2) } } diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt index ddbf924c27..a79d14f591 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.google.common.collect.HashMultimap import com.google.common.collect.Multimap import net.corda.client.jackson.StringToMethodCallParser.ParsedMethodCall +import net.corda.core.CordaException import org.slf4j.LoggerFactory import java.lang.reflect.Constructor import java.lang.reflect.Method @@ -99,7 +100,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( val methodParamNames: Map> = targetType.declaredMethods.mapNotNull { try { it.name to paramNamesFromMethod(it) - } catch(e: KotlinReflectionInternalError) { + } catch (e: KotlinReflectionInternalError) { // Kotlin reflection doesn't support every method that can exist on an object (in particular, reified // inline methods) so we just ignore those here. null @@ -146,7 +147,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( } } - open class UnparseableCallException(command: String, cause: Throwable? = null) : Exception("Could not parse as a command: $command", cause) { + open class UnparseableCallException(command: String, cause: Throwable? = null) : CordaException("Could not parse as a command: $command", cause) { class UnknownMethod(val methodName: String) : UnparseableCallException("Unknown command name: $methodName") class MissingParameter(methodName: String, val paramName: String, command: String) : UnparseableCallException("Parameter $paramName missing from attempt to invoke $methodName in command: $command") class TooManyParameters(methodName: String, command: String) : UnparseableCallException("Too many parameters provided for $methodName: $command") @@ -174,7 +175,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( try { val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr) return ParsedMethodCall(target, method, args) - } catch(e: UnparseableCallException) { + } catch (e: UnparseableCallException) { if (index == methods.size - 1) throw e } @@ -197,7 +198,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args) try { om.readValue(entry.traverse(om), argType) - } catch(e: Exception) { + } catch (e: Exception) { throw UnparseableCallException.FailedParse(e) } } @@ -211,16 +212,17 @@ open class StringToMethodCallParser @JvmOverloads constructor( } /** Returns a string-to-string map of commands to a string describing available parameter types. */ - val availableCommands: Map get() { - return methodMap.entries().map { entry -> - val (name, args) = entry // TODO: Kotlin 1.1 - val argStr = if (args.parameterCount == 0) "" else { - val paramNames = methodParamNames[name]!! - val typeNames = args.parameters.map { it.type.simpleName } - val paramTypes = paramNames.zip(typeNames) - paramTypes.map { "${it.first}: ${it.second}" }.joinToString(", ") - } - Pair(name, argStr) - }.toMap() - } + val availableCommands: Map + get() { + return methodMap.entries().map { entry -> + val (name, args) = entry // TODO: Kotlin 1.1 + val argStr = if (args.parameterCount == 0) "" else { + val paramNames = methodParamNames[name]!! + val typeNames = args.parameters.map { it.type.simpleName } + val paramTypes = paramNames.zip(typeNames) + paramTypes.map { "${it.first}: ${it.second}" }.joinToString(", ") + } + Pair(name, argStr) + }.toMap() + } } 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 961e2978ac..d8595e6bd9 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 @@ -14,15 +14,16 @@ import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MINI_CORP import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.contracts.DummyContract -import net.i2p.crypto.eddsa.EdDSAPublicKey import org.junit.Before import org.junit.Test +import java.math.BigInteger +import java.security.PublicKey import java.util.* -import kotlin.reflect.jvm.jvmName import kotlin.test.assertEquals class JacksonSupportTest : TestDependencyInjectionBase() { companion object { + private val SEED = BigInteger.valueOf(20170922L) val mapper = JacksonSupport.createNonRpcMapper() } @@ -37,15 +38,34 @@ class JacksonSupportTest : TestDependencyInjectionBase() { } @Test - fun publicKeySerializingWorks() { - val publicKey = generateKeyPair().public + fun `should serialize Composite keys`() { + val expected = "\"MIHAMBUGE2mtoq+J1bjir/ONk6yd5pab0FoDgaYAMIGiAgECMIGcMDIDLQAwKjAFBgMrZXADIQAgIX1QlJRgaLlD0ttLlJF5kNqT/7P7QwCvrWc9+/248gIBATAyAy0AMCowBQYDK2VwAyEAqS0JPGlzdviBZjB9FaNY+w6cVs3/CQ2A5EimE9Lyng4CAQEwMgMtADAqMAUGAytlcAMhALq4GG0gBQZIlaKE6ucooZsuoKUbH4MtGSmA6cwj136+AgEB\"" + val innerKeys = (1..3).map { i -> + Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED.plus(BigInteger.valueOf(i.toLong()))).public + } + // Build a 2 of 3 composite key + val publicKey = CompositeKey.Builder().let { + innerKeys.forEach { key -> it.addKey(key, 1) } + it.build(2) + } val serialized = mapper.writeValueAsString(publicKey) - val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java) + assertEquals(expected, serialized) + val parsedKey = mapper.readValue(serialized, PublicKey::class.java) assertEquals(publicKey, parsedKey) } private class Dummy(val notional: Amount) + @Test + fun `should serialize EdDSA keys`() { + val expected = "\"MCowBQYDK2VwAyEACFTgLk1NOqYXAfxLoR7ctSbZcl9KMXu58Mq31Kv1Dwk=\"" + val publicKey = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED).public + val serialized = mapper.writeValueAsString(publicKey) + assertEquals(expected, serialized) + val parsedKey = mapper.readValue(serialized, PublicKey::class.java) + assertEquals(publicKey, parsedKey) + } + @Test fun readAmount() { val oldJson = """ @@ -72,10 +92,10 @@ class JacksonSupportTest : TestDependencyInjectionBase() { fun writeTransaction() { val attachmentRef = SecureHash.randomSHA256() whenever(cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID)) - .thenReturn(attachmentRef) + .thenReturn(attachmentRef) fun makeDummyTx(): SignedTransaction { val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)) - .toWireTransaction(services) + .toWireTransaction(services) val signatures = TransactionSignature( ByteArray(1), ALICE_PUBKEY, diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index 7dd7878175..aaf1d5fd14 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -57,8 +57,11 @@ task integrationTest(type: Test) { jar { baseName 'corda-jfx' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.jfx' + } } publish { name jar.baseName -} \ No newline at end of file +} diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index bf5140fc36..e0131e6e99 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -27,8 +27,6 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.driver @@ -59,7 +57,7 @@ class NodeMonitorModelTest : DriverBasedTest() { startFlowPermission()) ) val aliceNodeFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(cashUser)) - val notaryHandle = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() + val notaryHandle = startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() val aliceNodeHandle = aliceNodeFuture.getOrThrow() aliceNode = aliceNodeHandle.nodeInfo newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo } @@ -71,7 +69,7 @@ class NodeMonitorModelTest : DriverBasedTest() { vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed() networkMapUpdates = monitor.networkMap.bufferUntilSubscribed() - monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false) + monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password) rpc = monitor.proxyObservable.value!! notaryParty = notaryHandle.nodeInfo.legalIdentities[1] @@ -79,7 +77,7 @@ class NodeMonitorModelTest : DriverBasedTest() { bobNode = bobNodeHandle.nodeInfo val monitorBob = NodeMonitorModel() stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed() - monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false) + monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password) rpcBob = monitorBob.proxyObservable.value!! runTest() } diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt index 6a81eb0dc9..e62f531dcf 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt @@ -6,6 +6,7 @@ import net.corda.client.jfx.utils.fold import net.corda.client.jfx.utils.map import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef +import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.Vault import net.corda.finance.contracts.asset.Cash import rx.Observable @@ -37,10 +38,9 @@ class ContractStateModel { companion object { private fun Collection>.filterCashStateAndRefs(): List> { return this.map { stateAndRef -> - @Suppress("UNCHECKED_CAST") if (stateAndRef.state.data is Cash.State) { // Kotlin doesn't unify here for some reason - stateAndRef as StateAndRef + uncheckedCast(stateAndRef) } else { null } diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt index 71a6b66733..feb3548ed2 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt @@ -4,6 +4,7 @@ import javafx.beans.property.ObjectProperty import javafx.beans.value.ObservableValue import javafx.beans.value.WritableValue import javafx.collections.ObservableList +import net.corda.core.internal.uncheckedCast import org.reactfx.EventSink import org.reactfx.EventStream import rx.Observable @@ -78,9 +79,7 @@ object Models { if (model.javaClass != klass.java) { throw IllegalStateException("Model stored as ${klass.qualifiedName} has type ${model.javaClass}") } - - @Suppress("UNCHECKED_CAST") - return model as M + return uncheckedCast(model) } inline fun get(origin: KClass<*>): M = get(M::class, origin) diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt index 836e046887..868849d56b 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt @@ -1,4 +1,5 @@ @file:JvmName("ModelsUtils") + package net.corda.client.jfx.model import javafx.beans.property.ObjectProperty diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt index 15cf636622..2213d40bbc 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt @@ -38,11 +38,14 @@ class NetworkIdentityModel { }) val notaries: ObservableList = networkIdentities.map { - it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } ?: false } + it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } == true } }.map { it?.party }.filterNotNull() val notaryNodes: ObservableList = notaries.map { rpcProxy.value?.nodeInfoFromParty(it) }.filterNotNull() - val parties: ObservableList = networkIdentities.filtered { it.legalIdentities.all { it !in notaries } } + val parties: ObservableList = networkIdentities + .filtered { it.legalIdentities.all { it !in notaries } } + // TODO: REMOVE THIS HACK WHEN NETWORK MAP REDESIGN WORK IS COMPLETED. + .filtered { it.legalIdentities.all { it.name.organisation != "Network Map Service" } } val myIdentity = rpcProxy.map { it?.nodeInfo()?.legalIdentitiesAndCerts?.first()?.party } fun partyFromPublicKey(publicKey: PublicKey): ObservableValue = identityCache[publicKey] diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 3cde49536e..0b866332f9 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -55,13 +55,12 @@ class NodeMonitorModel { * Register for updates to/from a given vault. * TODO provide an unsubscribe mechanism */ - fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, initialiseSerialization: Boolean = true) { + fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) { val client = CordaRPCClient( - hostAndPort = nodeHostAndPort, - configuration = CordaRPCClientConfiguration.default.copy( + nodeHostAndPort, + CordaRPCClientConfiguration.DEFAULT.copy( connectionMaxRetryInterval = 10.seconds - ), - initialiseSerialization = initialiseSerialization + ) ) val connection = client.start(username, password) val proxy = connection.proxy diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt index 9b8a5f45e5..58b602b834 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt @@ -253,14 +253,15 @@ class ConcatenatedList(sourceList: ObservableList>) : Trans } } - override val size: Int get() { - recalculateOffsets() - if (nestedIndexOffsets.size > 0) { - return nestedIndexOffsets.last() - } else { - return 0 + override val size: Int + get() { + recalculateOffsets() + if (nestedIndexOffsets.size > 0) { + return nestedIndexOffsets.last() + } else { + return 0 + } } - } override fun getSourceIndex(index: Int): Int { throw UnsupportedOperationException("Source index not supported in concatenation") diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt index 17c68ac20f..76c75c38de 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt @@ -1,4 +1,5 @@ @file:JvmName("ObservableFold") + package net.corda.client.jfx.utils import javafx.application.Platform diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt index 10ae4aeff6..a863af5ade 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt @@ -1,4 +1,5 @@ @file:JvmName("ObservableUtilities") + package net.corda.client.jfx.utils import javafx.application.Platform @@ -14,6 +15,7 @@ import javafx.collections.ObservableMap import javafx.collections.transformation.FilteredList import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import org.fxmisc.easybind.EasyBind @@ -92,8 +94,7 @@ fun ObservableValue.bind(function: (A) -> ObservableValue): Obs * propagate variance constraints and type inference fails. */ fun ObservableValue.bindOut(function: (A) -> ObservableValue): ObservableValue = - @Suppress("UNCHECKED_CAST") - EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue) + EasyBind.monadic(this).flatMap(uncheckedCast(function)) /** * enum class FilterCriterion { HEIGHT, NAME } @@ -105,8 +106,7 @@ fun ObservableValue.bindOut(function: (A) -> ObservableValue ObservableList.filter(predicate: ObservableValue<(A) -> Boolean>): ObservableList { // We cast here to enforce variance, FilteredList should be covariant - @Suppress("UNCHECKED_CAST") - return FilteredList(this as ObservableList).apply { + return FilteredList(uncheckedCast(this)).apply { predicateProperty().bind(predicate.map { predicateFunction -> Predicate { predicateFunction(it) } }) @@ -120,13 +120,11 @@ fun ObservableList.filter(predicate: ObservableValue<(A) -> Boolean>) */ fun ObservableList.filterNotNull(): ObservableList { //TODO This is a tactical work round for an issue with SAM conversion (https://youtrack.jetbrains.com/issue/ALL-1552) so that the M10 explorer works. - @Suppress("UNCHECKED_CAST") - return (this as ObservableList).filtered(object : Predicate { + return uncheckedCast(uncheckedCast>(this).filtered(object : Predicate { override fun test(t: A?): Boolean { return t != null - } - }) as ObservableList + })) } /** diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt index a751e3ac99..4ee03afc56 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt @@ -50,18 +50,19 @@ open class ReadOnlyBackedObservableMapBase : ObservableMap { override fun isEmpty() = backingMap.isEmpty() - override val entries: MutableSet> get() = backingMap.entries.fold(mutableSetOf()) { set, entry -> - set.add(object : MutableMap.MutableEntry { - override var value: A = entry.value.first - override val key = entry.key - override fun setValue(newValue: A): A { - val old = value - value = newValue - return old - } - }) - set - } + override val entries: MutableSet> + get() = backingMap.entries.fold(mutableSetOf()) { set, entry -> + set.add(object : MutableMap.MutableEntry { + override var value: A = entry.value.first + override val key = entry.key + override fun setValue(newValue: A): A { + val old = value + value = newValue + return old + } + }) + set + } override val keys: MutableSet get() = backingMap.keys override val values: MutableCollection get() = ArrayList(backingMap.values.map { it.first }) diff --git a/client/mock/build.gradle b/client/mock/build.gradle index 7432fdd312..b492455b49 100644 --- a/client/mock/build.gradle +++ b/client/mock/build.gradle @@ -21,8 +21,11 @@ dependencies { jar { baseName 'corda-mock' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.mock' + } } publish { name jar.baseName -} \ No newline at end of file +} diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index 1955dff5d2..28e938c843 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -50,7 +50,7 @@ open class EventGenerator(val parties: List, val currencies: List, currencies: List, notary: Party): EventGenerator(parties, currencies, notary) { +class ErrorFlowsEventGenerator(parties: List, currencies: List, notary: Party) : EventGenerator(parties, currencies, notary) { enum class IssuerEvents { NORMAL_EXIT, EXIT_ERROR @@ -62,7 +62,7 @@ class ErrorFlowsEventGenerator(parties: List, currencies: List, when (errorType) { IssuerEvents.NORMAL_EXIT -> { println("Normal exit") - if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount) + if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount) ExitRequest(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care. } IssuerEvents.EXIT_ERROR -> { diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt index 3993383a1a..e01c39d6f9 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt @@ -1,6 +1,7 @@ package net.corda.client.mock import net.corda.client.mock.Generator.Companion.choice +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.Try import java.util.* @@ -115,14 +116,13 @@ class Generator(val generate: (SplittableRandom) -> Try) { fun frequency(vararg generators: Pair>) = frequency(generators.toList()) - fun sequence(generators: List>) = Generator { + fun sequence(generators: List>) = Generator> { val result = mutableListOf() for (generator in generators) { val element = generator.generate(it) - @Suppress("UNCHECKED_CAST") when (element) { is Try.Success -> result.add(element.value) - is Try.Failure -> return@Generator element as Try> + is Try.Failure -> return@Generator uncheckedCast(element) } } Try.Success(result) @@ -175,7 +175,7 @@ class Generator(val generate: (SplittableRandom) -> Try) { } - fun replicatePoisson(meanSize: Double, generator: Generator, atLeastOne: Boolean = false) = Generator { + fun replicatePoisson(meanSize: Double, generator: Generator, atLeastOne: Boolean = false) = Generator> { val chance = (meanSize - 1) / meanSize val result = mutableListOf() var finish = false @@ -191,8 +191,7 @@ class Generator(val generate: (SplittableRandom) -> Try) { } } if (res is Try.Failure) { - @Suppress("UNCHECKED_CAST") - return@Generator res as Try> + return@Generator uncheckedCast(res) } } Try.Success(result) diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt index dc1315b175..ffd110fbeb 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt @@ -1,4 +1,5 @@ @file:JvmName("Generators") + package net.corda.client.mock import net.corda.core.contracts.Amount diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 2f36d1c315..bd234e8ee1 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'com.jfrog.artifactory' description 'Corda client RPC modules' @@ -89,6 +90,9 @@ task smokeTest(type: Test) { jar { baseName 'corda-rpc' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.rpc' + } } publish { diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index 90e4b8faca..4c0245462d 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -1,6 +1,5 @@ package net.corda.client.rpc; -import net.corda.client.rpc.internal.RPCClient; import net.corda.core.concurrent.CordaFuture; import net.corda.core.contracts.Amount; import net.corda.core.messaging.CordaRPCOps; @@ -9,11 +8,9 @@ import net.corda.core.utilities.OpaqueBytes; import net.corda.finance.flows.AbstractCashFlow; import net.corda.finance.flows.CashIssueFlow; import net.corda.finance.flows.CashPaymentFlow; -import net.corda.finance.schemas.*; +import net.corda.finance.schemas.CashSchemaV1; import net.corda.node.internal.Node; import net.corda.node.internal.StartedNode; -import net.corda.node.services.transactions.ValidatingNotaryService; -import net.corda.nodeapi.internal.ServiceInfo; import net.corda.nodeapi.User; import net.corda.testing.CoreTestUtils; import net.corda.testing.node.NodeBasedTest; @@ -25,24 +22,26 @@ import java.io.IOException; import java.util.*; import java.util.concurrent.ExecutionException; -import static java.util.Collections.*; +import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static kotlin.test.AssertionsKt.assertEquals; -import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault; import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.contracts.GetBalances.getCashBalance; import static net.corda.node.services.FlowPermissions.startFlowPermission; -import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.TestConstants.getALICE; public class CordaRPCJavaClientTest extends NodeBasedTest { + public CordaRPCJavaClientTest() { + super(Arrays.asList("net.corda.finance.contracts", CashSchemaV1.class.getPackage().getName())); + } + private List perms = Arrays.asList(startFlowPermission(CashPaymentFlow.class), startFlowPermission(CashIssueFlow.class)); private Set permSet = new HashSet<>(perms); private User rpcUser = new User("user1", "test", permSet); private StartedNode node; private CordaRPCClient client; - private RPCClient.RPCConnection connection = null; + private RPCConnection connection = null; private CordaRPCOps rpcProxy; private void login(String username, String password) { @@ -52,18 +51,14 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws ExecutionException, InterruptedException { - setCordappPackages("net.corda.finance.contracts"); - Set services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null))); - CordaFuture> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap()); + CordaFuture> nodeFuture = startNotaryNode(getALICE().getName(), singletonList(rpcUser), true); node = nodeFuture.get(); - node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE)); - client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()), getDefault(), false); + client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress())); } @After public void done() throws IOException { connection.close(); - unsetCordappPackages(); } @Test diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt new file mode 100644 index 0000000000..89264f2e05 --- /dev/null +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt @@ -0,0 +1,90 @@ +package net.corda.client.rpc + +import co.paralleluniverse.fibers.Suspendable +import com.esotericsoftware.kryo.KryoException +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.unwrap +import net.corda.node.internal.Node +import net.corda.node.internal.StartedNode +import net.corda.nodeapi.User +import net.corda.testing.* +import net.corda.testing.node.NodeBasedTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException + +@CordaSerializable +data class Packet(val x: () -> Long) + +class BlacklistKotlinClosureTest : NodeBasedTest(listOf("net.corda.client.rpc")) { + companion object { + @Suppress("UNUSED") val logger = loggerFor() + const val EVIL: Long = 666 + } + + @StartableByRPC + @InitiatingFlow + class FlowC(private val remoteParty: Party, private val data: Packet) : FlowLogic() { + @Suspendable + override fun call() { + val session = initiateFlow(remoteParty) + val x = session.sendAndReceive(data).unwrap { x -> x } + logger.info("FlowC: ${x.x()}") + } + } + + @InitiatedBy(FlowC::class) + class RemoteFlowC(private val session: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val packet = session.receive().unwrap { x -> x } + logger.info("RemoteFlowC: ${packet.x() + 1}") + session.send(Packet({ packet.x() + 1 })) + } + } + + @JvmField + @Rule + val expectedEx: ExpectedException = ExpectedException.none() + + private val rpcUser = User("user1", "test", permissions = setOf("ALL")) + private lateinit var aliceNode: StartedNode + private lateinit var bobNode: StartedNode + private lateinit var aliceClient: CordaRPCClient + private var connection: CordaRPCConnection? = null + + private fun login(username: String, password: String) { + connection = aliceClient.start(username, password) + } + + @Before + fun setUp() { + aliceNode = startNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() + bobNode = startNode(BOB.name, rpcUsers = listOf(rpcUser)).getOrThrow() + bobNode.registerInitiatedFlow(RemoteFlowC::class.java) + aliceClient = CordaRPCClient(aliceNode.internals.configuration.rpcAddress!!) + } + + @After + fun done() { + connection?.close() + bobNode.internals.stop() + aliceNode.internals.stop() + } + + @Test + fun `closure sent via RPC`() { + login(rpcUser.username, rpcUser.password) + val proxy = connection!!.proxy + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + proxy.startFlow(::FlowC, bobNode.info.chooseIdentity(), Packet{ EVIL }).returnValue.getOrThrow() + } +} \ No newline at end of file diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index b4c10fa808..277769354c 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -2,6 +2,7 @@ package net.corda.client.rpc import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowInitiator +import net.corda.core.internal.packageName import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow @@ -19,14 +20,10 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.chooseIdentity import net.corda.testing.node.NodeBasedTest -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.After @@ -36,7 +33,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class CordaRPCClientTest : NodeBasedTest() { +class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) { private val rpcUser = User("user1", "test", permissions = setOf( startFlowPermission(), startFlowPermission() @@ -51,16 +48,13 @@ class CordaRPCClientTest : NodeBasedTest() { @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts") - node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow() - node.internals.registerCustomSchemas(setOf(CashSchemaV1)) - client = CordaRPCClient(node.internals.configuration.rpcAddress!!, initialiseSerialization = false) + node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() + client = CordaRPCClient(node.internals.configuration.rpcAddress!!) } @After fun done() { connection?.close() - unsetCordappPackages() } @Test diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 4433ba03be..7ccf1913d5 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -7,6 +7,7 @@ import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize import net.corda.core.utilities.* import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi @@ -75,10 +76,12 @@ class RPCStabilityTests { rpcDriver { Try.on { startRpcClient(NetworkHostAndPort("localhost", 9999)).get() } val server = startRpcServer(ops = DummyOps) - Try.on { startRpcClient( - server.get().broker.hostAndPort!!, - configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) - ).get() } + Try.on { + startRpcClient( + server.get().broker.hostAndPort!!, + configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) + ).get() + } } } repeat(5) { @@ -172,7 +175,7 @@ class RPCStabilityTests { } } - interface LeakObservableOps: RPCOps { + interface LeakObservableOps : RPCOps { fun leakObservable(): Observable } @@ -248,6 +251,7 @@ class RPCStabilityTests { val trackSubscriberCountObservable = UnicastSubject.create().share(). doOnSubscribe { subscriberCount.incrementAndGet() }. doOnUnsubscribe { subscriberCount.decrementAndGet() } + override fun subscribe(): Observable { return trackSubscriberCountObservable } @@ -260,7 +264,7 @@ class RPCStabilityTests { ).get() val numberOfClients = 4 - val clients = (1 .. numberOfClients).map { + val clients = (1..numberOfClients).map { startRandomRpcClient(server.broker.hostAndPort!!) }.transpose().get() @@ -271,7 +275,7 @@ class RPCStabilityTests { clients[0].destroyForcibly() pollUntilClientNumber(server, numberOfClients - 1) // Kill the rest - (1 .. numberOfClients - 1).forEach { + (1..numberOfClients - 1).forEach { clients[it].destroyForcibly() } pollUntilClientNumber(server, 0) @@ -283,6 +287,7 @@ class RPCStabilityTests { interface SlowConsumerRPCOps : RPCOps { fun streamAtInterval(interval: Duration, size: Int): Observable } + class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps { override val protocolVersion = 0 @@ -291,6 +296,7 @@ class RPCStabilityTests { return Observable.interval(interval.toMillis(), TimeUnit.MILLISECONDS).map { chunk } } } + @Test fun `slow consumers are kicked`() { rpcDriver { @@ -315,9 +321,9 @@ class RPCStabilityTests { clientAddress = SimpleString(myQueue), id = RPCApi.RpcRequestId(random63BitValue()), methodName = SlowConsumerRPCOps::streamAtInterval.name, - arguments = listOf(10.millis, 123456) + serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT).bytes ) - request.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message) + request.writeToClientMessage(message) producer.send(message) session.commit() diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index e5ac356d76..2def3fbf20 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -10,42 +10,68 @@ import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT import java.time.Duration -/** @see RPCClient.RPCConnection */ -class CordaRPCConnection internal constructor( - connection: RPCClient.RPCConnection -) : RPCClient.RPCConnection by connection +/** + * This class is essentially just a wrapper for an RPCConnection and can be treated identically. + * + * @see RPCConnection + */ +class CordaRPCConnection internal constructor(connection: RPCConnection) : RPCConnection by connection -/** @see RPCClientConfiguration */ -data class CordaRPCClientConfiguration( - val connectionMaxRetryInterval: Duration -) { +/** + * Can be used to configure the RPC client connection. + * + * @property connectionMaxRetryInterval How much time to wait between connection retries if the server goes down. This + * time will be reached via exponential backoff. + */ +data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration) { internal fun toRpcClientConfiguration(): RPCClientConfiguration { return RPCClientConfiguration.default.copy( connectionMaxRetryInterval = connectionMaxRetryInterval ) } + companion object { - @JvmStatic - val default = CordaRPCClientConfiguration( - connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval - ) + /** + * Returns the default configuration we recommend you use. + */ + @JvmField + val DEFAULT = CordaRPCClientConfiguration(connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval) } } -/** @see RPCClient */ -//TODO Add SSL support -class CordaRPCClient( +/** + * An RPC client connects to the specified server and allows you to make calls to the server that perform various + * useful tasks. Please see the Client RPC section of docs.corda.net to learn more about how this API works. A brief + * description is provided here. + * + * Calling [start] returns an [RPCConnection] containing a proxy that lets you invoke RPCs on the server. Calls on + * it block, and if the server throws an exception then it will be rethrown on the client. Proxies are thread safe and + * may be used to invoke multiple RPCs in parallel. + * + * RPC sends and receives are logged on the net.corda.rpc logger. + * + * The [CordaRPCOps] defines what client RPCs are available. If an RPC returns an [rx.Observable] anywhere in the object + * graph returned then the server-side observable is transparently forwarded to the client side here. + * *You are expected to use it*. The server will begin sending messages immediately that will be buffered on the + * client, you are expected to drain by subscribing to the returned observer. You can opt-out of this by simply + * calling the [net.corda.client.rpc.notUsed] method on it. + * + * You don't have to explicitly close the observable if you actually subscribe to it: it will close itself and free up + * the server-side resources either when the client or JVM itself is shutdown, or when there are no more subscribers to + * it. Once all the subscribers to a returned observable are unsubscribed or the observable completes successfully or + * with an error, the observable is closed and you can't then re-subscribe again: you'll have to re-request a fresh + * observable with another RPC. + * + * @param hostAndPort The network address to connect to. + * @param configuration An optional configuration used to tweak client behaviour. + */ +class CordaRPCClient @JvmOverloads constructor( hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default, - initialiseSerialization: Boolean = true + configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT ) { init { - // Init serialization. It's plausible there are multiple clients in a single JVM, so be tolerant of - // others having registered first. // TODO: allow clients to have serialization factory etc injected and align with RPC protocol version? - if (initialiseSerialization) { - KryoClientSerializationScheme.initialiseSerialization() - } + KryoClientSerializationScheme.initialiseSerialization() } private val rpcClient = RPCClient( @@ -54,10 +80,24 @@ class CordaRPCClient( KRYO_RPC_CLIENT_CONTEXT ) + /** + * Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable] + * and can be used with a try-with-resources statement. If you don't use that, you should use the + * [RPCConnection.notifyServerAndClose] or [RPCConnection.forceClose] methods to dispose of the connection object + * when done. + * + * @param username The username to authenticate with. + * @param password The password to authenticate with. + * @throws RPCException if the server version is too low or if the server isn't reachable within a reasonable timeout. + */ fun start(username: String, password: String): CordaRPCConnection { return CordaRPCConnection(rpcClient.start(CordaRPCOps::class.java, username, password)) } + /** + * A helper for Kotlin users that simply closes the connection after the block has executed. Be careful not to + * over-use this, as setting up and closing connections takes time. + */ inline fun use(username: String, password: String, block: (CordaRPCConnection) -> A): A { return start(username, password).use(block) } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt index 0498801989..71596c6e5e 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt @@ -1,10 +1,10 @@ package net.corda.client.rpc +import net.corda.core.CordaRuntimeException import net.corda.core.serialization.CordaSerializable /** * Thrown to indicate that the calling user does not have permission for something they have requested (for example * calling a method). */ -@CordaSerializable -class PermissionException(msg: String) : RuntimeException(msg) +class PermissionException(msg: String) : CordaRuntimeException(msg) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCConnection.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCConnection.kt new file mode 100644 index 0000000000..ae53adee53 --- /dev/null +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCConnection.kt @@ -0,0 +1,38 @@ +package net.corda.client.rpc + +import net.corda.core.messaging.RPCOps +import java.io.Closeable + +/** + * Holds a [proxy] object implementing [I] that forwards requests to the RPC server. The server version can be queried + * via this interface. + * + * [Closeable.close] may be used to shut down the connection and release associated resources. It is an + * alias for [notifyServerAndClose]. + */ +interface RPCConnection : Closeable { + /** + * Holds a synthetic class that automatically forwards method calls to the server, and returns the response. + */ + val proxy: I + + /** The RPC protocol version reported by the server. */ + val serverProtocolVersion: Int + + /** + * Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources. + * If the server is not available this method may block for a short period until it's clear the server is not + * coming back. + */ + fun notifyServerAndClose() + + /** + * Closes this client without notifying the server. + * + * The server will eventually clear out the RPC message queue and disconnect subscribed observers, + * but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose]. + * This method is helpful when the node may be shutting down or have already shut down and you don't want to + * block waiting for it to come back, which typically happens in integration tests and demos rather than production. + */ + fun forceClose() +} \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt index 66ddb33b1d..2275cd0d3f 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt @@ -5,6 +5,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.* +import java.util.concurrent.atomic.AtomicBoolean class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { @@ -23,7 +24,9 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException() companion object { + val isInitialised = AtomicBoolean(false) fun initialiseSerialization() { + if (!isInitialised.compareAndSet(false, true)) return try { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { registerScheme(KryoClientSerializationScheme()) @@ -31,10 +34,14 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { // Check that it's registered as we expect - check(SerializationDefaults.SERIALIZATION_FACTORY is SerializationFactoryImpl) { "RPC client encountered conflicting configuration of serialization subsystem." } - check((SerializationDefaults.SERIALIZATION_FACTORY as SerializationFactoryImpl).alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { "RPC client encountered conflicting configuration of serialization subsystem." } + val factory = SerializationDefaults.SERIALIZATION_FACTORY + val checkedFactory = factory as? SerializationFactoryImpl + ?: throw IllegalStateException("RPC client encountered conflicting configuration of serialization subsystem: $factory") + check(checkedFactory.alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { + "RPC client encountered conflicting configuration of serialization subsystem." + } } } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index f0fc941668..773c511da9 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -1,8 +1,10 @@ package net.corda.client.rpc.internal +import net.corda.client.rpc.RPCConnection import net.corda.client.rpc.RPCException import net.corda.core.crypto.random63BitValue import net.corda.core.internal.logElapsedTime +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults @@ -17,7 +19,6 @@ import net.corda.nodeapi.config.SSLConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient -import java.io.Closeable import java.lang.reflect.Proxy import java.time.Duration @@ -79,12 +80,6 @@ data class RPCClientConfiguration( } } -/** - * An RPC client that may be used to create connections to an RPC server. - * - * @param transport The Artemis transport to use to connect to the server. - * @param rpcConfiguration Configuration used to tweak client behaviour. - */ class RPCClient( val transport: TransportConfiguration, val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default, @@ -101,54 +96,6 @@ class RPCClient( private val log = loggerFor>() } - /** - * Holds a proxy object implementing [I] that forwards requests to the RPC server. - * - * [Closeable.close] may be used to shut down the connection and release associated resources. - */ - interface RPCConnection : Closeable { - val proxy: I - /** The RPC protocol version reported by the server */ - val serverProtocolVersion: Int - - /** - * Closes this client without notifying the server. - * The server will eventually clear out the RPC message queue and disconnect subscribed observers, - * but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose]. - * This method is helpful when the node may be shutting down or - * have already shut down and you don't want to block waiting for it to come back. - */ - fun forceClose() - - /** - * Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources. - * If the server is not available this method may block for a short period until it's clear the server is not coming back. - */ - fun notifyServerAndClose() - } - - /** - * Returns an [RPCConnection] containing a proxy that lets you invoke RPCs on the server. Calls on it block, and if - * the server throws an exception then it will be rethrown on the client. Proxies are thread safe and may be used to - * invoke multiple RPCs in parallel. - * - * RPC sends and receives are logged on the net.corda.rpc logger. - * - * The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object - * graph returned then the server-side observable is transparently forwarded to the client side here. - * *You are expected to use it*. The server will begin sending messages immediately that will be buffered on the - * client, you are expected to drain by subscribing to the returned observer. You can opt-out of this by simply - * calling the [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually - * subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself - * is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are - * unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't - * then re-subscribe again: you'll have to re-request a fresh observable with another RPC. - * - * @param rpcOpsClass The [Class] of the RPC interface. - * @param username The username to authenticate with. - * @param password The password to authenticate with. - * @throws RPCException if the server version is too low or if the server isn't reachable within the given time. - */ fun start( rpcOpsClass: Class, username: String, @@ -168,10 +115,7 @@ class RPCClient( val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass, serializationContext) try { proxyHandler.start() - - @Suppress("UNCHECKED_CAST") - val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I - + val ops: I = uncheckedCast(Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler)) val serverProtocolVersion = ops.protocolVersion if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) { throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" + diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 10e901eb70..0a89aecc92 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -19,6 +19,7 @@ import net.corda.core.internal.LifeCycle import net.corda.core.internal.ThreadBox import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.serialize import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow @@ -78,6 +79,7 @@ class RPCClientProxyHandler( STARTED, FINISHED } + private val lifeCycle = LifeCycle(State.UNSTARTED) private companion object { @@ -208,11 +210,12 @@ class RPCClientProxyHandler( val rpcId = RPCApi.RpcRequestId(random63BitValue()) callSiteMap?.set(rpcId.toLong, Throwable("")) try { - val request = RPCApi.ClientToServer.RpcRequest(clientAddress, rpcId, method.name, arguments?.toList() ?: emptyList()) + val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext) + val request = RPCApi.ClientToServer.RpcRequest(clientAddress, rpcId, method.name, serialisedArguments.bytes) val replyFuture = SettableFuture.create() sessionAndProducerPool.run { val message = it.session.createMessage(false) - request.writeToClientMessage(serializationContextWithObservableContext, message) + request.writeToClientMessage(message) log.debug { val argumentsString = arguments?.joinToString() ?: "" diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index bc522e42f0..31daccc9c2 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -6,7 +6,6 @@ import net.corda.core.identity.CordaX500Name; import net.corda.core.identity.Party; import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.FlowHandle; -import net.corda.core.node.NodeInfo; import net.corda.core.utilities.OpaqueBytes; import net.corda.finance.flows.AbstractCashFlow; import net.corda.finance.flows.CashIssueFlow; @@ -41,7 +40,6 @@ public class StandaloneCordaRPCJavaClientTest { private NodeProcess notary; private CordaRPCOps rpcProxy; private CordaRPCConnection connection; - private NodeInfo notaryNode; private Party notaryNodeIdentity; private NodeConfig notaryConfig = new NodeConfig( @@ -49,8 +47,8 @@ public class StandaloneCordaRPCJavaClientTest { port.getAndIncrement(), port.getAndIncrement(), port.getAndIncrement(), - Collections.singletonList("corda.notary.validating"), - Arrays.asList(rpcUser), + true, + Collections.singletonList(rpcUser), null ); @@ -61,7 +59,6 @@ public class StandaloneCordaRPCJavaClientTest { notary = factory.create(notaryConfig); connection = notary.connect(); rpcProxy = connection.getProxy(); - notaryNode = fetchNotaryIdentity(); notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0); } @@ -70,16 +67,16 @@ public class StandaloneCordaRPCJavaClientTest { try { connection.close(); } finally { - if(notary != null) { + if (notary != null) { notary.close(); } } } private void copyFinanceCordapp() { - Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins")); + Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps")); try { - Files.createDirectories(pluginsDir); + Files.createDirectories(cordappsDir); } catch (IOException ex) { fail("Failed to create directories"); } @@ -87,7 +84,7 @@ public class StandaloneCordaRPCJavaClientTest { paths.forEach(file -> { if (file.toString().contains("corda-finance")) { try { - Files.copy(file, pluginsDir.resolve(file.getFileName())); + Files.copy(file, cordappsDir.resolve(file.getFileName())); } catch (IOException ex) { fail("Failed to copy finance jar"); } @@ -98,11 +95,6 @@ public class StandaloneCordaRPCJavaClientTest { } } - private NodeInfo fetchNotaryIdentity() { - List nodeDataSnapshot = rpcProxy.networkMapSnapshot(); - return nodeDataSnapshot.get(0); - } - @Test public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException { Amount dollars123 = new Amount<>(123, Currency.getInstance("USD")); diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 6da6aa3dc4..d6d305f542 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -64,7 +64,7 @@ class StandaloneCordaRPClientTest { p2pPort = port.andIncrement, rpcPort = port.andIncrement, webPort = port.andIncrement, - extraServices = listOf("corda.notary.validating"), + isNotary = true, users = listOf(user) ) @@ -89,12 +89,12 @@ class StandaloneCordaRPClientTest { } private fun copyFinanceCordapp() { - val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").createDirectories() + val cordappsDir = (factory.baseDirectory(notaryConfig) / "cordapps").createDirectories() // Find the finance jar file for the smoke tests of this module val financeJar = Paths.get("build", "resources", "smokeTest").list { it.filter { "corda-finance" in it.toString() }.toList().single() } - financeJar.copyToDirectory(pluginsDir) + financeJar.copyToDirectory(cordappsDir) } @Test @@ -114,14 +114,14 @@ class StandaloneCordaRPClientTest { @Test fun `test starting flow`() { rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity) - .returnValue.getOrThrow(timeout) + .returnValue.getOrThrow(timeout) } @Test fun `test starting tracked flow`() { var trackCount = 0 val handle = rpcProxy.startTrackedFlow( - ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity + ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity ) val updateLatch = CountDownLatch(1) handle.progress.subscribe { msg -> @@ -156,7 +156,7 @@ class StandaloneCordaRPClientTest { // Now issue some cash rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNodeIdentity) - .returnValue.getOrThrow(timeout) + .returnValue.getOrThrow(timeout) updateLatch.await() assertEquals(1, updateCount.get()) } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt index c6b5329d8c..35eaa9dfd8 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt @@ -20,10 +20,13 @@ open class AbstractRPCTest { } companion object { - @JvmStatic @Parameterized.Parameters(name = "Mode = {0}") + @JvmStatic + @Parameterized.Parameters(name = "Mode = {0}") fun defaultModes() = modes(RPCTestMode.InVm, RPCTestMode.Netty) + fun modes(vararg modes: RPCTestMode) = listOf(*modes).map { arrayOf(it) } } + @Parameterized.Parameter lateinit var mode: RPCTestMode diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index b9d64a3cab..2923040034 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -26,9 +26,11 @@ import java.util.concurrent.TimeUnit @RunWith(Parameterized::class) class RPCPerformanceTests : AbstractRPCTest() { companion object { - @JvmStatic @Parameterized.Parameters(name = "Mode = {0}") + @JvmStatic + @Parameterized.Parameters(name = "Mode = {0}") fun modes() = modes(RPCTestMode.Netty) } + private interface TestOps : RPCOps { fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray } @@ -60,7 +62,7 @@ class RPCPerformanceTests : AbstractRPCTest() { val executor = Executors.newFixedThreadPool(4) val N = 10000 val latch = CountDownLatch(N) - for (i in 1 .. N) { + for (i in 1..N) { executor.submit { proxy.ops.simpleReply(ByteArray(1024), 1024) latch.countDown() @@ -155,10 +157,12 @@ class RPCPerformanceTests : AbstractRPCTest() { data class BigMessagesResult( val Mbps: Double ) + @Test fun `big messages`() { warmup() - measure(listOf(1)) { clientParallelism -> // TODO this hangs with more parallelism + measure(listOf(1)) { clientParallelism -> + // TODO this hangs with more parallelism rpcDriver { val proxy = testProxy( RPCClientConfiguration.default, diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt index 4411bbfd07..cee75881aa 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt @@ -79,7 +79,7 @@ class RPCPermissionsTests : AbstractRPCTest() { } @Test - fun `check ALL is implemented the correct way round` () { + fun `check ALL is implemented the correct way round`() { rpcDriver { val joeUser = userOf("joe", setOf(DUMMY_FLOW)) val proxy = testProxyFor(joeUser) diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt index 8887aa2476..b22ed8b506 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt @@ -13,6 +13,7 @@ class RepeatingBytesInputStream(val bytesToRepeat: ByteArray, val numberOfBytes: return bytesToRepeat[(numberOfBytes - bytesLeft) % bytesToRepeat.size].toInt() } } + override fun read(byteArray: ByteArray, offset: Int, length: Int): Int { val lastIdx = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft) for (i in offset until lastIdx) { diff --git a/confidential-identities/build.gradle b/confidential-identities/build.gradle index 04212b3df1..c6a29c0d2e 100644 --- a/confidential-identities/build.gradle +++ b/confidential-identities/build.gradle @@ -6,15 +6,12 @@ apply plugin: 'kotlin' apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'com.jfrog.artifactory' description 'Corda Experimental Confidential Identities' dependencies { - // Note the :confidential-identities module is a CorDapp in its own right - // and CorDapps using :confidential-identities features should use 'cordapp' not 'compile' linkage. - cordaCompile project(':core') + compile project(':core') // Quasar, for suspendable fibres. compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt index 01f520afda..b454556bf3 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt @@ -12,14 +12,12 @@ import net.corda.core.utilities.unwrap object IdentitySyncFlow { /** - * Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential - * identities used in the transaction. This is intended for use as a subflow of another flow, typically between + * Flow for ensuring that our counterparties in a transaction have the full certificate paths for *our* confidential + * identities used in states present in the transaction. This is intended for use as a subflow of another flow, typically between * transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants * to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that * party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that * identity. - * - * @return a mapping of well known identities to the confidential identities used in the transaction. */ // TODO: Can this be triggered automatically from [SendTransactionFlow] class Send(val otherSideSessions: Set, @@ -36,17 +34,10 @@ object IdentitySyncFlow { @Suspendable override fun call() { progressTracker.currentStep = SYNCING_IDENTITIES - val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) - val identities: Set = states.flatMap { it.participants }.toSet() - // Filter participants down to the set of those not in the network map (are not well known) - val confidentialIdentities = identities - .filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() } - .toList() - val identityCertificates: Map = identities - .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap() + val identityCertificates: Map = extractOurConfidentialIdentities() otherSideSessions.forEach { otherSideSession -> - val requestedIdentities: List = otherSideSession.sendAndReceive>(confidentialIdentities).unwrap { req -> + val requestedIdentities: List = otherSideSession.sendAndReceive>(identityCertificates.keys.toList()).unwrap { req -> require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" } req } @@ -61,6 +52,20 @@ object IdentitySyncFlow { } } + private fun extractOurConfidentialIdentities(): Map { + val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) + val identities: Set = states.flatMap(ContractState::participants).toSet() + // Filter participants down to the set of those not in the network map (are not well known) + val confidentialIdentities = identities + .filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() } + .toList() + return confidentialIdentities + .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) } + // Filter down to confidential identities of our well known identity + // TODO: Consider if this too restrictive - we perhaps should be checking the name on the signing certificate in the certificate path instead + .filter { it.second?.name == ourIdentity.name } + .toMap() + } } /** diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt index 7257620d19..575c1f8d52 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt @@ -1,15 +1,32 @@ package net.corda.confidential import co.paralleluniverse.fibers.Suspendable +import net.corda.core.crypto.DigitalSignature +import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.toX509CertHolder import net.corda.core.node.services.IdentityService +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap +import org.bouncycastle.asn1.DERSet +import org.bouncycastle.asn1.pkcs.CertificationRequestInfo +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset +import java.security.PublicKey +import java.security.SignatureException +import java.security.cert.CertPath +import java.util.* /** * Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction @@ -27,8 +44,32 @@ class SwapIdentitiesFlow(private val otherParty: Party, object AWAITING_KEY : ProgressTracker.Step("Awaiting key") fun tracker() = ProgressTracker(AWAITING_KEY) - fun validateAndRegisterIdentity(identityService: IdentityService, otherSide: Party, anonymousOtherSide: PartyAndCertificate): PartyAndCertificate { - require(anonymousOtherSide.name == otherSide.name) + /** + * Generate the determinstic data blob the confidential identity's key holder signs to indicate they want to + * represent the subject named in the X.509 certificate. Note that this is never actually sent between nodes, + * but only the signature is sent. The blob is built independently on each node and the received signature + * verified against the expected blob, rather than exchanging the blob. + */ + fun buildDataToSign(confidentialIdentity: PartyAndCertificate): ByteArray { + val certOwnerAssert = CertificateOwnershipAssertion(confidentialIdentity.name, confidentialIdentity.owningKey) + return certOwnerAssert.serialize().bytes + } + + @Throws(SwapIdentitiesException::class) + fun validateAndRegisterIdentity(identityService: IdentityService, + otherSide: Party, + anonymousOtherSideBytes: PartyAndCertificate, + sigBytes: DigitalSignature): PartyAndCertificate { + val anonymousOtherSide: PartyAndCertificate = anonymousOtherSideBytes + if (anonymousOtherSide.name != otherSide.name) { + throw SwapIdentitiesException("Certificate subject must match counterparty's well known identity.") + } + val signature = DigitalSignature.WithKey(anonymousOtherSide.owningKey, sigBytes.bytes) + try { + signature.verify(buildDataToSign(anonymousOtherSideBytes)) + } catch(ex: SignatureException) { + throw SwapIdentitiesException("Signature does not match the expected identity ownership assertion.", ex) + } // Validate then store their identity so that we can prove the key in the transaction is owned by the // counterparty. identityService.verifyAndRegisterIdentity(anonymousOtherSide) @@ -40,6 +81,7 @@ class SwapIdentitiesFlow(private val otherParty: Party, override fun call(): LinkedHashMap { progressTracker.currentStep = AWAITING_KEY val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) + val serializedIdentity = SerializedBytes(legalIdentityAnonymous.serialize().bytes) // Special case that if we're both parties, a single identity is generated val identities = LinkedHashMap() @@ -47,13 +89,33 @@ class SwapIdentitiesFlow(private val otherParty: Party, identities.put(otherParty, legalIdentityAnonymous.party.anonymise()) } else { val otherSession = initiateFlow(otherParty) - val anonymousOtherSide = otherSession.sendAndReceive(legalIdentityAnonymous).unwrap { confidentialIdentity -> - validateAndRegisterIdentity(serviceHub.identityService, otherSession.counterparty, confidentialIdentity) - } + val data = buildDataToSign(legalIdentityAnonymous) + val ourSig: DigitalSignature.WithKey = serviceHub.keyManagementService.sign(data, legalIdentityAnonymous.owningKey) + val ourIdentWithSig = IdentityWithSignature(serializedIdentity, ourSig.withoutKey()) + val anonymousOtherSide = otherSession.sendAndReceive(ourIdentWithSig) + .unwrap { (confidentialIdentityBytes, theirSigBytes) -> + val confidentialIdentity: PartyAndCertificate = confidentialIdentityBytes.bytes.deserialize() + validateAndRegisterIdentity(serviceHub.identityService, otherParty, confidentialIdentity, theirSigBytes) + } identities.put(ourIdentity, legalIdentityAnonymous.party.anonymise()) - identities.put(otherSession.counterparty, anonymousOtherSide.party.anonymise()) + identities.put(otherParty, anonymousOtherSide.party.anonymise()) } return identities } + @CordaSerializable + data class IdentityWithSignature(val identity: SerializedBytes, val signature: DigitalSignature) } + +/** + * Data class used only in the context of asserting the owner of the private key for the listed key wants to use it + * to represent the named entity. This is pairs with an X.509 certificate (which asserts the signing identity says + * the key represents the named entity), but protects against a certificate authority incorrectly claiming others' + * keys. + */ +@CordaSerializable +data class CertificateOwnershipAssertion(val x500Name: CordaX500Name, + val publicKey: PublicKey) + +open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null) + : FlowException(message, cause) \ No newline at end of file diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt index 753d9a3927..a5a17ccdf8 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt @@ -4,6 +4,9 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.identity.PartyAndCertificate +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap @@ -20,9 +23,14 @@ class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEna override fun call() { val revocationEnabled = false progressTracker.currentStep = SENDING_KEY - val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) - otherSideSession.sendAndReceive(legalIdentityAnonymous).unwrap { confidentialIdentity -> - SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, confidentialIdentity) - } + val ourConfidentialIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) + val serializedIdentity = SerializedBytes(ourConfidentialIdentity.serialize().bytes) + val data = SwapIdentitiesFlow.buildDataToSign(ourConfidentialIdentity) + val ourSig = serviceHub.keyManagementService.sign(data, ourConfidentialIdentity.owningKey) + otherSideSession.sendAndReceive(SwapIdentitiesFlow.IdentityWithSignature(serializedIdentity, ourSig.withoutKey())) + .unwrap { (theirConfidentialIdentityBytes, theirSigBytes) -> + val theirConfidentialIdentity = theirConfidentialIdentityBytes.deserialize() + SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, theirConfidentialIdentity, theirSigBytes) + } } } \ No newline at end of file diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index f2907c7c7c..b760817afe 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -13,12 +13,14 @@ import net.corda.core.utilities.unwrap import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull class IdentitySyncFlowTests { @@ -26,31 +28,29 @@ class IdentitySyncFlowTests { @Before fun before() { - setCordappPackages("net.corda.finance.contracts.asset") // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true) + mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset")) } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `sync confidential identities`() { // Set up values we'll need - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val alice: Party = aliceNode.services.myInfo.chooseIdentity() - val bob: Party = bobNode.services.myInfo.chooseIdentity() + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val alice: Party = aliceNode.info.singleIdentity() + val bob: Party = bobNode.info.singleIdentity() + val notary = notaryNode.services.getDefaultNotary() bobNode.internals.registerInitiatedFlow(Receive::class.java) // Alice issues then pays some cash to a new confidential identity that Bob doesn't know about val anonymous = true val ref = OpaqueBytes.of(0x01) - val notary = aliceNode.services.getDefaultNotary() val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notary)) val issueTx = issueFlow.resultFuture.getOrThrow().stx val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner @@ -67,6 +67,40 @@ class IdentitySyncFlowTests { assertEquals(expected, actual) } + @Test + fun `don't offer other's identities confidential identities`() { + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val charlieNode = mockNet.createPartyNode(CHARLIE_NAME) + val alice: Party = aliceNode.info.singleIdentity() + val bob: Party = bobNode.info.singleIdentity() + val charlie: Party = charlieNode.info.singleIdentity() + val notary = notaryNode.services.getDefaultNotary() + bobNode.internals.registerInitiatedFlow(Receive::class.java) + + // Charlie issues then pays some cash to a new confidential identity + val anonymous = true + val ref = OpaqueBytes.of(0x01) + val issueFlow = charlieNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, charlie, anonymous, notary)) + val issueTx = issueFlow.resultFuture.getOrThrow().stx + val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner + val confidentialIdentCert = charlieNode.services.identityService.certificateFromKey(confidentialIdentity.owningKey)!! + + // Manually inject this identity into Alice's database so the node could leak it, but we prove won't + aliceNode.database.transaction { aliceNode.services.identityService.verifyAndRegisterIdentity(confidentialIdentCert) } + assertNotNull(aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + + // Generate a payment from Charlie to Alice, including the confidential state + val payTx = charlieNode.services.startFlow(CashPaymentFlow(1000.DOLLARS, alice, anonymous)).resultFuture.getOrThrow().stx + + // Run the flow to sync up the identities, and confirm Charlie's confidential identity doesn't leak + assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + aliceNode.services.startFlow(Initiator(bob, payTx.tx)).resultFuture.getOrThrow() + assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + } + /** * Very lightweight wrapping flow to trigger the counterparty flow that receives the identities. */ diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 684cd92c82..8030469fe5 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -4,16 +4,10 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.utilities.getOrThrow -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.chooseIdentity +import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue +import kotlin.test.* class SwapIdentitiesFlowTests { @Test @@ -22,12 +16,11 @@ class SwapIdentitiesFlowTests { val mockNet = MockNetwork(false, true) // Set up values we'll need - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val alice: Party = aliceNode.services.myInfo.chooseIdentity() - val bob: Party = bobNode.services.myInfo.chooseIdentity() - mockNet.registerIdentities() + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val alice = aliceNode.info.singleIdentity() + val bob = bobNode.services.myInfo.singleIdentity() // Run the flows val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)) @@ -53,4 +46,69 @@ class SwapIdentitiesFlowTests { mockNet.stopNodes() } + + /** + * Check that flow is actually validating the name on the certificate presented by the counterparty. + */ + @Test + fun `verifies identity name`() { + // We run this in parallel threads to help catch any race conditions that may exist. + val mockNet = MockNetwork(false, true) + + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val bob: Party = bobNode.services.myInfo.singleIdentity() + val notBob = notaryNode.database.transaction { + notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false) + } + val sigData = SwapIdentitiesFlow.buildDataToSign(notBob) + val signature = notaryNode.services.keyManagementService.sign(sigData, notBob.owningKey) + assertFailsWith("Certificate subject must match counterparty's well known identity.") { + SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, notBob, signature.withoutKey()) + } + + mockNet.stopNodes() + } + + /** + * Check that flow is actually validating its the signature presented by the counterparty. + */ + @Test + fun `verifies signature`() { + // We run this in parallel threads to help catch any race conditions that may exist. + val mockNet = MockNetwork(false, true) + + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val bob: Party = bobNode.services.myInfo.singleIdentity() + // Check that the wrong signature is rejected + notaryNode.database.transaction { + notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false) + }.let { anonymousNotary -> + val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousNotary) + val signature = notaryNode.services.keyManagementService.sign(sigData, anonymousNotary.owningKey) + assertFailsWith("Signature does not match the given identity and nonce") { + SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousNotary, signature.withoutKey()) + } + } + // Check that the right signing key, but wrong identity is rejected + val anonymousAlice = aliceNode.database.transaction { + aliceNode.services.keyManagementService.freshKeyAndCert(aliceNode.services.myInfo.chooseIdentityAndCert(), false) + } + bobNode.database.transaction { + bobNode.services.keyManagementService.freshKeyAndCert(bobNode.services.myInfo.chooseIdentityAndCert(), false) + }.let { anonymousBob -> + val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousAlice) + val signature = bobNode.services.keyManagementService.sign(sigData, anonymousBob.owningKey) + assertFailsWith("Signature does not match the given identity and nonce.") { + SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousBob, signature.withoutKey()) + } + } + + mockNet.stopNodes() + } } diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf index 16eb90f526..0089d1cb24 100644 --- a/config/dev/generalnodea.conf +++ b/config/dev/generalnodea.conf @@ -4,7 +4,6 @@ trustStorePassword : "trustpass" p2pAddress : "localhost:10002" rpcAddress : "localhost:10003" webAddress : "localhost:10004" -extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" legalName : "O=Network Map Service,OU=corda,L=London,C=GB" diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf index 1eb839aece..af4e26cc27 100644 --- a/config/dev/generalnodeb.conf +++ b/config/dev/generalnodeb.conf @@ -4,7 +4,6 @@ trustStorePassword : "trustpass" p2pAddress : "localhost:10005" rpcAddress : "localhost:10006" webAddress : "localhost:10007" -extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" legalName : "O=Network Map Service,OU=corda,L=London,C=GB" diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf index 664f2b6816..0519e56872 100644 --- a/config/dev/nameservernode.conf +++ b/config/dev/nameservernode.conf @@ -3,5 +3,7 @@ keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10000" webAddress : "localhost:10001" -extraAdvertisedServiceIds : [ "corda.notary.validating" ] +notary : { + validating : true +} useHTTPS : false diff --git a/constants.properties b/constants.properties index 93971c05df..48f942803c 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=1.0.0 +gradlePluginsVersion=2.0.4 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/core/build.gradle b/core/build.gradle index 655067d680..d821ec7d37 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'kotlin' apply plugin: 'kotlin-jpa' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'com.jfrog.artifactory' description 'Corda core' @@ -94,6 +95,13 @@ jar { baseName 'corda-core' } +scanApi { + excludeClasses = [ + // Kotlin should probably have declared this class as "synthetic". + "net.corda.core.Utils\$toFuture\$1\$subscription\$1" + ] +} + publish { name jar.baseName } diff --git a/core/src/main/kotlin/net/corda/core/CordaException.kt b/core/src/main/kotlin/net/corda/core/CordaException.kt index 4e5353fa77..52bbd82172 100644 --- a/core/src/main/kotlin/net/corda/core/CordaException.kt +++ b/core/src/main/kotlin/net/corda/core/CordaException.kt @@ -15,10 +15,11 @@ interface CordaThrowable { open class CordaException internal constructor(override var originalExceptionClassName: String? = null, private var _message: String? = null, private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable { - constructor(message: String?, cause: Throwable?) : this(null, message, cause) + constructor(message: String?) : this(null, message, null) + override val message: String? get() = if (originalExceptionClassName == null) originalMessage else { if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage" @@ -59,10 +60,12 @@ open class CordaException internal constructor(override var originalExceptionCla } open class CordaRuntimeException(override var originalExceptionClassName: String?, - private var _message: String? = null, - private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable { + private var _message: String?, + private var _cause: Throwable?) : RuntimeException(null, null, true, true), CordaThrowable { constructor(message: String?, cause: Throwable?) : this(null, message, cause) + constructor(message: String?) : this(null, message, null) + override val message: String? get() = if (originalExceptionClassName == null) originalMessage else { if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage" diff --git a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt index 548521a2e8..f86ca21fce 100644 --- a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -1,4 +1,5 @@ @file:JvmName("ConcurrencyUtils") + package net.corda.core.concurrent import net.corda.core.internal.concurrent.openFuture @@ -28,7 +29,7 @@ fun firstOf(vararg futures: CordaFuture, handler: (CordaFuture firstOf(futures: Array>, log: Logger, handler: (CordaFuture) -> W): CordaFuture { val resultFuture = openFuture() diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index 43101b2a3e..629461a3d7 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -21,7 +21,8 @@ interface TokenizableAssetInfo { * Amount represents a positive quantity of some token (currency, asset, etc.), measured in quantity of the smallest * representable units. The nominal quantity represented by each individual token is equal to the [displayTokenSize]. * The scale property of the [displayTokenSize] should correctly reflect the displayed decimal places and is used - * when rounding conversions from indicative/displayed amounts in [BigDecimal] to Amount occur via the Amount.fromDecimal method. + * when rounding conversions from indicative/displayed amounts in [BigDecimal] to Amount occur via the + * [Amount.fromDecimal] method. * * Amounts of different tokens *do not mix* and attempting to add or subtract two amounts of different currencies * will throw [IllegalArgumentException]. Amounts may not be negative. Amounts are represented internally using a signed @@ -29,10 +30,11 @@ interface TokenizableAssetInfo { * multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer * overflow. * - * @property quantity the number of tokens as a Long value. - * @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. - * @property token an instance of type T, usually a singleton. - * @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required. + * @property quantity the number of tokens as a long value. + * @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display + * places if the scale parameter is non-zero. + * @property token the type of token this is an amount of. This is usually a singleton. + * @param T the type of the token, for example [Currency]. T should implement [TokenizableAssetInfo] if automatic conversion to/from a display format is required. */ @CordaSerializable data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable> { @@ -40,11 +42,10 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, companion object { /** * Build an Amount from a decimal representation. For example, with an input of "12.34 GBP", - * returns an amount with a quantity of "1234" tokens. The displayTokenSize as determined via - * getDisplayTokenSize is used to determine the conversion scaling. - * e.g. Bonds might be in nominal amounts of 100, currencies in 0.01 penny units. + * returns an amount with a quantity of "1234" tokens. The function [getDisplayTokenSize] is used to determine the + * conversion scaling, for example bonds might be in nominal amounts of 100, currencies in 0.01 penny units. * - * @see Amount.toDecimal + * @see Amount.toDecimal * @throws ArithmeticException if the intermediate calculations cannot be converted to an unsigned 63-bit token amount. */ @JvmStatic @@ -166,7 +167,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, } } } - } catch(e: Exception) { + } catch (e: Exception) { throw IllegalArgumentException("Could not parse $input as a currency", e) } throw IllegalArgumentException("Did not recognise the currency in $input or could not parse") @@ -195,6 +196,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * Mixing non-identical token types will throw [IllegalArgumentException]. * * @throws ArithmeticException if there is overflow of Amount tokens during the summation + * @throws IllegalArgumentException if mixing non-identical token types. */ operator fun plus(other: Amount): Amount { checkToken(other) @@ -202,11 +204,11 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, } /** - * A checked addition operator is supported to simplify netting of Amounts. - * If this leads to the Amount going negative this will throw [IllegalArgumentException]. - * Mixing non-identical token types will throw [IllegalArgumentException]. + * A checked subtraction operator is supported to simplify netting of Amounts. * - * @throws ArithmeticException if there is Numeric underflow + * @throws ArithmeticException if there is numeric underflow. + * @throws IllegalArgumentException if this leads to the amount going negative, or would mix non-identical token + * types. */ operator fun minus(other: Amount): Amount { checkToken(other) @@ -222,6 +224,8 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount. * Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour. * N.B. Division is not supported as fractional tokens are not representable by an Amount. + * + * @throws ArithmeticException if there is overflow of Amount tokens during the multiplication. */ operator fun times(other: Long): Amount = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token) @@ -229,13 +233,15 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount. * Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour. * N.B. Division is not supported as fractional tokens are not representable by an Amount. + * + * @throws ArithmeticException if there is overflow of Amount tokens during the multiplication. */ operator fun times(other: Int): Amount = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token) /** * This method provides a token conserving divide mechanism. * @param partitions the number of amounts to divide the current quantity into. - * @result Returns [partitions] separate Amount objects which sum to the same quantity as this Amount + * @return 'partitions' separate Amount objects which sum to the same quantity as this Amount * and differ by no more than a single token in size. */ fun splitEvenly(partitions: Int): List> { @@ -249,8 +255,10 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, /** * Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity - * of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize, - * which determines the size of a single token and controls the trailing decimal places via it's scale property. + * of "1234" GBP, returns "12.34". The precise representation is controlled by the display token size ( + * from [getDisplayTokenSize]), which determines the size of a single token and controls the trailing decimal + * places via its scale property. *Note* that currencies such as the Bahraini Dinar use 3 decimal places, + * and it must not be presumed that this converts amounts to 2 decimal places. * * @see Amount.fromDecimal */ diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index 51036f8dfe..82a1d348a0 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -4,6 +4,7 @@ package net.corda.core.contracts import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import java.security.PublicKey import java.util.* @@ -33,7 +34,7 @@ inline fun requireThat(body: Requirements.() -> R) = Requirements.body() /** Filters the command list by type, party and public key all at once. */ inline fun Collection>.select(signer: PublicKey? = null, - party: AbstractParty? = null) = + party: AbstractParty? = null) = filter { it.value is T }. filter { if (signer == null) true else signer in it.signers }. filter { if (party == null) true else party in it.signingParties }. @@ -43,7 +44,7 @@ inline fun Collection> /** Filters the command list by type, parties and public keys all at once. */ inline fun Collection>.select(signers: Collection?, - parties: Collection?) = + parties: Collection?) = filter { it.value is T }. filter { if (signers == null) true else it.signers.containsAll(signers) }. filter { if (parties == null) true else it.signingParties.containsAll(parties) }. @@ -58,7 +59,7 @@ inline fun Collection> /** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */ fun Collection>.requireSingleCommand(klass: Class) = - mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as CommandWithParties else null }.single() + mapNotNull { if (klass.isInstance(it.value)) uncheckedCast, CommandWithParties>(it) else null }.single() /** * Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key. diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 9ce12b7293..e509b7da14 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -199,9 +199,6 @@ interface MoveCommand : CommandData { val contract: Class? } -/** Indicates that this transaction replaces the inputs contract state to another contract state */ -data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData - // DOCSTART 6 /** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */ @CordaSerializable diff --git a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt index e26ea50cd0..8126292138 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt @@ -64,6 +64,17 @@ abstract class TimeWindow { */ abstract val midpoint: Instant? + /** + * Returns the duration between [fromTime] and [untilTime] if both are non-null. Otherwise returns null. + */ + val length: Duration? get() { + return if (fromTime == null || untilTime == null) { + null + } else { + Duration.between(fromTime, untilTime) + } + } + /** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */ abstract operator fun contains(instant: Instant): Boolean @@ -85,6 +96,7 @@ abstract class TimeWindow { init { require(fromTime < untilTime) { "fromTime must be earlier than untilTime" } } + override val midpoint: Instant get() = fromTime + (fromTime until untilTime) / 2 override fun contains(instant: Instant): Boolean = instant >= fromTime && instant < untilTime override fun toString(): String = "[$fromTime, $untilTime)" diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index ff9426ec90..8f7f5f8d56 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -1,8 +1,8 @@ package net.corda.core.cordapp import net.corda.core.flows.FlowLogic -import net.corda.core.node.CordaPluginRegistry import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import java.net.URL @@ -13,20 +13,26 @@ import java.net.URL * * This will only need to be constructed manually for certain kinds of tests. * + * @property name Cordapp name - derived from the base name of the Cordapp JAR (therefore may not be unique) * @property contractClassNames List of contracts * @property initiatedFlows List of initiatable flow classes * @property rpcFlows List of RPC initiable flows classes + * @property serviceFlows List of [CordaService] initiable flows classes + * @property schedulableFlows List of flows startable by the scheduler * @property servies List of RPC services - * @property plugins List of Corda plugin registries + * @property serializationWhitelists List of Corda plugin registries * @property customSchemas List of custom schemas * @property jarPath The path to the JAR for this CorDapp */ interface Cordapp { + val name: String val contractClassNames: List val initiatedFlows: List>> val rpcFlows: List>> + val serviceFlows: List>> + val schedulableFlows: List>> val services: List> - val plugins: List + val serializationWhitelists: List val customSchemas: Set val jarPath: URL val cordappClasses: List diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt index 8317a11a64..b359e31991 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.crypto.composite.CompositeSignaturesWithKeys import net.corda.core.serialization.deserialize import java.io.ByteArrayOutputStream import java.security.* diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt similarity index 81% rename from core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt rename to core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt index f9c8da8ab3..0ea62b1f0e 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt @@ -1,6 +1,5 @@ -package net.corda.core.crypto.composite +package net.corda.core.crypto -import net.corda.core.crypto.TransactionSignature import net.corda.core.serialization.CordaSerializable /** diff --git a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt index eaae5e2ffb..b9bdd52afb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt @@ -31,6 +31,8 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur object CordaObjectIdentifier { // UUID-based OID // TODO: Register for an OID space and issue our own shorter OID. - @JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002") - @JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") + @JvmField + val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002") + @JvmField + val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") } diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 605b95466c..f2c0a45918 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -2,7 +2,10 @@ package net.corda.core.crypto import net.corda.core.internal.X509EdDSAEngine import net.corda.core.serialization.serialize -import net.i2p.crypto.eddsa.* +import net.i2p.crypto.eddsa.EdDSAEngine +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.EdDSASecurityProvider import net.i2p.crypto.eddsa.math.GroupElement import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable @@ -39,8 +42,6 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec import java.math.BigInteger import java.security.* -import java.security.KeyFactory -import java.security.KeyPairGenerator import java.security.spec.InvalidKeySpecException import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec @@ -148,7 +149,7 @@ object Crypto { "at the cost of larger key sizes and loss of compatibility." ) - /** Corda composite key type */ + /** Corda composite key type. */ @JvmField val COMPOSITE_KEY = SignatureScheme( 6, @@ -774,9 +775,10 @@ object Crypto { // it forms, by itself, the new private key, which in turn is used to compute the new public key. val pointQ = FixedPointCombMultiplier().multiply(parameterSpec.g, deterministicD) // This is unlikely to happen, but we should check for point at infinity. - if (pointQ.isInfinity) + if (pointQ.isInfinity) { // Instead of throwing an exception, we retry with SHA256(seed). return deriveKeyPairECDSA(parameterSpec, privateKey, seed.sha256().bytes) + } val publicKeySpec = ECPublicKeySpec(pointQ, parameterSpec) val publicKeyD = BCECPublicKey(privateKey.algorithm, publicKeySpec, BouncyCastleProvider.CONFIGURATION) @@ -822,7 +824,7 @@ object Crypto { @JvmStatic fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) - // custom key pair generator from entropy. + // Custom key pair generator from entropy. private fun deriveEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair { val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length. @@ -849,6 +851,7 @@ object Crypto { override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? { return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } } + override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? { return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } } @@ -880,7 +883,7 @@ object Crypto { } } - // return true if EdDSA publicKey is point at infinity. + // Return true if EdDSA publicKey is point at infinity. // For EdDSA a custom function is required as it is not supported by the I2P implementation. private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean { return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) @@ -892,7 +895,7 @@ object Crypto { return signatureScheme.schemeCodeName in signatureSchemeMap } - // validate a key, by checking its algorithmic params. + // Validate a key, by checking its algorithmic params. private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { return when (key) { is PublicKey -> validatePublicKey(signatureScheme, key) @@ -901,7 +904,7 @@ object Crypto { } } - // check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). + // Check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean { return when (key) { is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key) @@ -910,7 +913,7 @@ object Crypto { } } - // check if a private key satisfies algorithm specs. + // Check if a private key satisfies algorithm specs. private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { return when (key) { is BCECPrivateKey -> key.parameters == signatureScheme.algSpec @@ -922,7 +925,6 @@ object Crypto { /** * Convert a public key to a supported implementation. - * * @param key a public key. * @return a supported implementation of the input public key. * @throws IllegalArgumentException on not supported scheme or if the given key specification @@ -941,7 +943,16 @@ object Crypto { * is inappropriate for a supported key factory to produce a private key. */ @JvmStatic - fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded) + fun toSupportedPublicKey(key: PublicKey): PublicKey { + return when (key) { + is BCECPublicKey -> key + is BCRSAPublicKey -> key + is BCSphincs256PublicKey -> key + is EdDSAPublicKey -> key + is CompositeKey -> key + else -> decodePublicKey(key.encoded) + } + } /** * Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. @@ -952,5 +963,13 @@ object Crypto { * is inappropriate for a supported key factory to produce a private key. */ @JvmStatic - fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded) + fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { + return when (key) { + is BCECPrivateKey -> key + is BCRSAPrivateKey -> key + is BCSphincs256PrivateKey -> key + is EdDSAPrivateKey -> key + else -> decodePrivateKey(key.encoded) + } + } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 7592e71c5d..5bc47ed6fb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -21,9 +21,19 @@ import java.security.* * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign)) +/** + * Utility to simplify the act of signing a byte array and return a [DigitalSignature.WithKey] object. + * Note that there is no check if the public key matches with the signing private key. + * @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the [DigitalSignature.WithKey] object on the input message [bytesToSign] and [publicKey]. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ +@Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes) /** @@ -34,9 +44,13 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSigna * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public) + +/** Helper function to sign the bytes of [bytesToSign] with a key pair. */ +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes) + /** * Helper function for signing a [SignableData] object. * @param signableData the object to be signed. @@ -56,8 +70,8 @@ fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSi * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ -// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`, -@Throws(SignatureException::class, IllegalArgumentException::class, InvalidKeyException::class) +// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`. +@Throws(SignatureException::class, InvalidKeyException::class) fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.doVerify(this, signature.bytes, content) /** @@ -70,21 +84,25 @@ fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.d * signature). * @throws SignatureException if the signature is invalid (i.e. damaged). * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. + * @throws IllegalStateException if this is a [CompositeKey], because verification of composite key signatures is not supported. * @return whether the signature is correct for this key. */ -@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class) -fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature) : Boolean { +@Throws(SignatureException::class, InvalidKeyException::class) +fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean { if (this is CompositeKey) throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification. return Crypto.isValid(this, signature.bytes, content) } /** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */ -fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58() +fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58() +/** Return a [Set] of the contained keys if this is a [CompositeKey]; otherwise, return a [Set] with a single element (this [PublicKey]). */ val PublicKey.keys: Set get() = (this as? CompositeKey)?.leafKeys ?: setOf(this) -fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey)) +/** Return true if [otherKey] fulfils the requirements of this [PublicKey]. */ +fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey)) +/** Return true if [otherKeys] fulfil the requirements of this [PublicKey]. */ fun PublicKey.isFulfilledBy(otherKeys: Iterable): Boolean = (this as? CompositeKey)?.isFulfilledBy(otherKeys) ?: (this in otherKeys) /** Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. */ @@ -98,8 +116,9 @@ fun Iterable.byKeys() = map { it.by }.toSet() // Allow Kotlin destructuring: // val (private, public) = keyPair +/* The [PrivateKey] of this [KeyPair] .*/ operator fun KeyPair.component1(): PrivateKey = this.private - +/* The [PublicKey] of this [KeyPair] .*/ operator fun KeyPair.component2(): PublicKey = this.public /** A simple wrapper that will make it easier to swap out the EC algorithm we use in future. */ @@ -122,7 +141,7 @@ fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEnt * if this signatureData algorithm is unable to process the input data provided, etc. * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. */ -@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData) /** @@ -135,7 +154,7 @@ fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = * if this signatureData algorithm is unable to process the input data provided, etc. * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. */ -@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this.public, signatureData, clearData) /** diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 9e28be8a3a..0db44ba841 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -23,6 +23,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { */ @Throws(InvalidKeyException::class, SignatureException::class) fun verify(content: ByteArray) = by.verify(content, this) + /** * Utility to simplify the act of verifying a signature. * @@ -32,6 +33,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { */ @Throws(InvalidKeyException::class, SignatureException::class) fun verify(content: OpaqueBytes) = by.verify(content.bytes, this) + /** * Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an * exception, making it more suitable where a boolean is required, but normally you should use the function @@ -44,5 +46,6 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { */ @Throws(InvalidKeyException::class, SignatureException::class) fun isValid(content: ByteArray) = by.isValid(content, this) + fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes) } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index 0011198d75..77e06bf1c2 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -1,11 +1,12 @@ package net.corda.core.crypto +import net.corda.core.CordaException import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.serialization.CordaSerializable import java.util.* @CordaSerializable -class MerkleTreeException(val reason: String) : Exception("Partial Merkle Tree exception. Reason: $reason") +class MerkleTreeException(val reason: String) : CordaException("Partial Merkle Tree exception. Reason: $reason") /** * Building and verification of Partial Merkle Tree. diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index f0773a7bac..6555ac6af7 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -19,32 +19,87 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { } } + /** + * Convert the hash value to an uppercase hexadecimal [String]. + */ override fun toString(): String = bytes.toHexString() + /** + * Returns the first [prefixLen] hexadecimal digits of the [SecureHash] value. + * @param prefixLen The number of characters in the prefix. + */ fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen) + + /** + * Append a second hash value to this hash value, and then compute the SHA-256 hash of the result. + * @param other The hash to append to this one. + */ fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256() // Like static methods in Java, except the 'companion' is a singleton that can have state. companion object { + /** + * Converts a SHA-256 hash value represented as a hexadecimal [String] into a [SecureHash]. + * @param str A sequence of 64 hexadecimal digits that represents a SHA-256 hash value. + * @throws IllegalArgumentException The input string does not contain 64 hexadecimal digits, or it contains incorrectly-encoded characters. + */ @JvmStatic - fun parse(str: String) = str.toUpperCase().parseAsHex().let { - when (it.size) { - 32 -> SHA256(it) - else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str") + fun parse(str: String): SHA256 { + return str.toUpperCase().parseAsHex().let { + when (it.size) { + 32 -> SHA256(it) + else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str") + } } } - @JvmStatic fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes)) - @JvmStatic fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes) - @JvmStatic fun sha256(str: String) = sha256(str.toByteArray()) + /** + * Computes the SHA-256 hash value of the [ByteArray]. + * @param bytes The [ByteArray] to hash. + */ + @JvmStatic + fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes)) - @JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) + /** + * Computes the SHA-256 hash of the [ByteArray], and then computes the SHA-256 hash of the hash. + * @param bytes The [ByteArray] to hash. + */ + @JvmStatic + fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes) + + /** + * Computes the SHA-256 hash of the [String]'s UTF-8 byte contents. + * @param str [String] whose UTF-8 contents will be hashed. + */ + @JvmStatic + fun sha256(str: String) = sha256(str.toByteArray()) + + /** + * Generates a random SHA-256 value. + */ + @JvmStatic + fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) + + /** + * A SHA-256 hash value consisting of 32 0x00 bytes. + */ val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) + + /** + * A SHA-256 hash value consisting of 32 0xFF bytes. + */ val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() })) } // In future, maybe SHA3, truncated hashes etc. } +/** + * Compute the SHA-256 hash for the contents of the [ByteArray]. + */ fun ByteArray.sha256(): SecureHash.SHA256 = SecureHash.sha256(this) + +/** + * Compute the SHA-256 hash for the contents of the [OpaqueBytes]. + */ fun OpaqueBytes.sha256(): SecureHash.SHA256 = SecureHash.sha256(this.bytes) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt index 472a8a1024..bc39699f55 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -23,8 +24,7 @@ open class SignedData(val raw: SerializedBytes, val sig: DigitalSign @Throws(SignatureException::class) fun verified(): T { sig.by.verify(raw.bytes, sig) - @Suppress("UNCHECKED_CAST") - val data = raw.deserialize() as T + val data: T = uncheckedCast(raw.deserialize()) verifyData(data) return data } diff --git a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt index 28e843a82d..4f5f7eb207 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt @@ -11,7 +11,7 @@ import java.util.* * This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction signatures. */ @CordaSerializable -class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata): DigitalSignature(bytes) { +class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata) : DigitalSignature(bytes) { /** * Function to verify a [SignableData] object's signature. * Note that [SignableData] contains the id of the transaction and extra metadata, such as DLT's platform version. diff --git a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt index bcc712df71..af0c406410 100644 --- a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt @@ -70,15 +70,7 @@ abstract class AbstractStateReplacementFlow { val finalTx = stx + signatures serviceHub.recordTransactions(finalTx) - val newOutput = run { - if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub).outRef(0) - } else { - stx.tx.outRef(0) - } - } - - return newOutput + return stx.resolveBaseTransaction(serviceHub).outRef(0) } /** @@ -136,7 +128,8 @@ abstract class AbstractStateReplacementFlow { // We use Void? instead of Unit? as that's what you'd use in Java. abstract class Acceptor(val initiatingSession: FlowSession, override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic() { - constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker()) + constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker()) + companion object { object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal") object APPROVING : ProgressTracker.Step("State replacement approved") @@ -173,11 +166,7 @@ abstract class AbstractStateReplacementFlow { } val finalTx = stx + allSignatures - if (finalTx.isNotaryChangeTransaction()) { - finalTx.resolveNotaryChangeTransaction(serviceHub).verifyRequiredSignatures() - } else { - finalTx.verifyRequiredSignatures() - } + finalTx.resolveTransactionWithSignatures(serviceHub).verifyRequiredSignatures() serviceHub.recordTransactions(finalTx) } @@ -194,11 +183,7 @@ abstract class AbstractStateReplacementFlow { // TODO Check the set of multiple identities? val myKey = ourIdentity.owningKey - val requiredKeys = if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub).requiredSigningKeys - } else { - stx.tx.requiredSigningKeys - } + val requiredKeys = stx.resolveTransactionWithSignatures(serviceHub).requiredSigningKeys require(myKey in requiredKeys) { "Party is not a participant for any of the input states of transaction ${stx.id}" } } diff --git a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt index cfb232a7ec..8dfe4a69ff 100644 --- a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt @@ -61,11 +61,12 @@ import java.security.PublicKey * just in the states. If null, the default well known identity of the node is used. */ // TODO: AbstractStateReplacementFlow needs updating to use this flow. -class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction, - val sessionsToCollectFrom: Collection, - val myOptionalKeys: Iterable?, - override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { +class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: SignedTransaction, + val sessionsToCollectFrom: Collection, + val myOptionalKeys: Iterable?, + override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { @JvmOverloads constructor(partiallySignedTx: SignedTransaction, sessionsToCollectFrom: Collection, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker) + companion object { object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") @@ -134,6 +135,7 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session: FlowSession, val signingKeys: List) : FlowLogic>() { constructor(partiallySignedTx: SignedTransaction, session: FlowSession, vararg signingKeys: PublicKey) : this(partiallySignedTx, session, listOf(*signingKeys)) + @Suspendable override fun call(): List { // SendTransactionFlow allows counterparty to access our data to resolve the transaction. @@ -224,7 +226,7 @@ abstract class SignTransactionFlow(val otherSideSession: FlowSession, // Perform some custom verification over the transaction. try { checkTransaction(stx) - } catch(e: Exception) { + } catch (e: Exception) { if (e is IllegalStateException || e is IllegalArgumentException || e is AssertionError) throw FlowException(e) else diff --git a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt index 0612d5d9aa..423b6c1fa6 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt @@ -3,9 +3,6 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.internal.ContractUpgradeUtils -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.TransactionBuilder -import java.security.PublicKey /** * A flow to be used for authorising and upgrading state objects of an old contract to a new contract. @@ -16,30 +13,6 @@ import java.security.PublicKey * use the new updated state for future transactions. */ object ContractUpgradeFlow { - - @JvmStatic - fun verify(tx: LedgerTransaction) { - // Contract Upgrade transaction should have 1 input, 1 output and 1 command. - verify(tx.inputs.single().state, - tx.outputs.single(), - tx.commandsOfType().single()) - } - - @JvmStatic - fun verify(input: TransactionState, output: TransactionState, commandData: Command) { - val command = commandData.value - val participantKeys: Set = input.data.participants.map { it.owningKey }.toSet() - val keysThatSigned: Set = commandData.signers.toSet() - @Suppress("UNCHECKED_CAST") - val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract - requireThat { - "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) - "Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract) - "Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass) - "Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data)) - } - } - /** * Authorise a contract state upgrade. * @@ -49,11 +22,13 @@ object ContractUpgradeFlow { * * This flow will NOT initiate the upgrade process. To start the upgrade process, see [Initiate]. */ + // DOCSTART 1 @StartableByRPC class Authorise( val stateAndRef: StateAndRef<*>, private val upgradedContractClass: Class> - ) : FlowLogic() { + ) : FlowLogic() { + // DOCEND 1 @Suspendable override fun call(): Void? { val upgrade = upgradedContractClass.newInstance() @@ -70,10 +45,12 @@ object ContractUpgradeFlow { * Deauthorise a contract state upgrade. * This will remove the upgrade authorisation from persistent store (and prevent any further upgrade) */ + // DOCSTART 2 @StartableByRPC class Deauthorise(val stateRef: StateRef) : FlowLogic() { @Suspendable override fun call(): Void? { + //DOCEND 2 serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef) return null } @@ -89,23 +66,6 @@ object ContractUpgradeFlow { newContractClass: Class> ) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { - companion object { - fun assembleBareTx( - stateRef: StateAndRef, - upgradedContractClass: Class>, - privacySalt: PrivacySalt - ): TransactionBuilder { - val contractUpgrade = upgradedContractClass.newInstance() - return TransactionBuilder(stateRef.state.notary) - .withItems( - stateRef, - StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name), - Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }), - privacySalt - ) - } - } - @Suspendable override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt()) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 7b45eb6a73..a08654acb9 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -26,8 +26,8 @@ import net.corda.core.utilities.ProgressTracker */ @InitiatingFlow class FinalityFlow(val transaction: SignedTransaction, - private val extraRecipients: Set, - override val progressTracker: ProgressTracker) : FlowLogic() { + private val extraRecipients: Set, + override val progressTracker: ProgressTracker) : FlowLogic() { constructor(transaction: SignedTransaction, extraParticipants: Set) : this(transaction, extraParticipants, tracker()) constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker()) constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker) @@ -88,7 +88,7 @@ class FinalityFlow(val transaction: SignedTransaction, private fun hasNoNotarySignature(stx: SignedTransaction): Boolean { val notaryKey = stx.tx.notary?.owningKey val signers = stx.sigs.map { it.by }.toSet() - return !(notaryKey?.isFulfilledBy(signers) ?: false) + return notaryKey?.isFulfilledBy(signers) != true } private fun getPartiesToSend(ltx: LedgerTransaction): Set { diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt index 3860e73a06..6bc488c162 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt @@ -16,14 +16,22 @@ sealed class FlowInitiator : Principal { data class RPC(val username: String) : FlowInitiator() { override fun getName(): String = username } + /** Started when we get new session initiation request. */ data class Peer(val party: Party) : FlowInitiator() { override fun getName(): String = party.name.toString() } + + /** Started by a CordaService. */ + data class Service(val serviceClassName: String) : FlowInitiator() { + override fun getName(): String = serviceClassName + } + /** Started as scheduled activity. */ data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() { override fun getName(): String = "Scheduler" } + // TODO When proper ssh access enabled, add username/use RPC? object Shell : FlowInitiator() { override fun getName(): String = "Shell User" 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 2137b5ee03..0239c81f20 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -1,11 +1,13 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable +import co.paralleluniverse.strands.Strand import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.abbreviate +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub @@ -15,6 +17,8 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.debug import org.slf4j.Logger +import java.time.Duration +import java.time.Instant /** * A sub-class of [FlowLogic] implements a flow using direct, straight line blocking code. Thus you @@ -42,6 +46,34 @@ abstract class FlowLogic { /** This is where you should log things to. */ val logger: Logger get() = stateMachine.logger + companion object { + /** + * Return the outermost [FlowLogic] instance, or null if not in a flow. + */ + @JvmStatic + val currentTopLevel: FlowLogic<*>? get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic + + /** + * If on a flow, suspends the flow and only wakes it up after at least [duration] time has passed. Otherwise, + * just sleep for [duration]. This sleep function is not designed to aid scheduling, for which you should + * consider using [SchedulableState]. It is designed to aid with managing contention for which you have not + * managed via another means. + * + * Warning: long sleeps and in general long running flows are highly discouraged, as there is currently no + * support for flow migration! This method will throw an exception if you attempt to sleep for longer than + * 5 minutes. + */ + @Suspendable + @JvmStatic + @Throws(FlowException::class) + fun sleep(duration: Duration) { + if (duration.compareTo(Duration.ofMinutes(5)) > 0) { + throw FlowException("Attempt to sleep for longer than 5 minutes is not supported. Consider using SchedulableState.") + } + (Strand.currentStrand() as? FlowStateMachine<*>)?.sleepUntil(Instant.now() + duration) ?: Strand.sleep(duration.toMillis()) + } + } + /** * Returns a wrapped [java.util.UUID] object that identifies this state machine run (i.e. subflows have the same * identifier as their parents). @@ -55,6 +87,10 @@ abstract class FlowLogic { */ val serviceHub: ServiceHub get() = stateMachine.serviceHub + /** + * Creates a communication session with [party]. Subsequently you may send/receive using this session object. Note + * that this function does not communicate in itself, the counter-flow will be kicked off by the first send/receive. + */ @Suspendable fun initiateFlow(party: Party): FlowSession = stateMachine.initiateFlow(party, flowUsedForSessions) @@ -100,7 +136,7 @@ abstract class FlowLogic { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING) inline fun sendAndReceive(otherParty: Party, payload: Any): UntrustworthyData { @@ -116,7 +152,7 @@ abstract class FlowLogic { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING) @Suspendable @@ -165,7 +201,7 @@ abstract class FlowLogic { * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly * corrupted data in order to exploit your code. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Deprecated("Use FlowSession.receive()", level = DeprecationLevel.WARNING) @Suspendable @@ -173,6 +209,38 @@ abstract class FlowLogic { return stateMachine.receive(receiveType, otherParty, flowUsedForSessions) } + /** Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [receiveAll(receiveType: Class, sessions: List): List>] when the same type is expected from all sessions. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them. + */ + @Suspendable + open fun receiveAll(sessions: Map>): Map> { + return stateMachine.receiveAll(sessions, this) + } + + /** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [sessions: Map>): Map>] when sessions are expected to receive different types. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions]. + */ + @Suspendable + open fun receiveAll(receiveType: Class, sessions: List): List> { + enforceNoDuplicates(sessions) + return castMapValuesToKnownType(receiveAll(associateSessionsToReceiveType(receiveType, sessions))) + } + /** * Queues the given [payload] for sending to the [otherParty] and continues without suspending. * @@ -227,7 +295,6 @@ abstract class FlowLogic { stateMachine.checkFlowPermission(permissionName, extraAuditData) } - /** * Flows can call this method to record application level flow audit events * @param eventType is a string representing the type of event. Each flow is given a distinct namespace for these names. @@ -330,6 +397,18 @@ abstract class FlowLogic { ours.setChildProgressTracker(ours.currentStep, theirs) } } + + private fun enforceNoDuplicates(sessions: List) { + require(sessions.size == sessions.toSet().size) { "A flow session can only appear once as argument." } + } + + private fun associateSessionsToReceiveType(receiveType: Class, sessions: List): Map> { + return sessions.associateByTo(LinkedHashMap(), { it }, { receiveType }) + } + + private fun castMapValuesToKnownType(map: Map>): List> { + return map.values.map { uncheckedCast>(it) } + } } /** @@ -347,4 +426,4 @@ data class FlowInfo( * to deduplicate it from other releases of the same CorDapp, typically a version string. See the * [CorDapp JAR format](https://docs.corda.net/cordapp-build-systems.html#cordapp-jar-format) for more details. */ - val appName: String) + val appName: String) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index 3f9996c081..0b90909180 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -24,16 +24,4 @@ class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentEx */ // TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes) @CordaSerializable -interface FlowLogicRef - - -/** - * This is just some way to track what attachments need to be in the class loader, but may later include some app - * properties loaded from the attachments. And perhaps the authenticated user for an API call? - */ -@CordaSerializable -data class AppContext(val attachments: List) { - // TODO: build a real [AttachmentsClassLoader] etc - val classLoader: ClassLoader - get() = this.javaClass.classLoader -} +interface FlowLogicRef \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt index 9c2e5425d6..f5589f96c4 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt @@ -5,7 +5,18 @@ import net.corda.core.identity.Party import net.corda.core.utilities.UntrustworthyData /** - * To port existing flows: + * + * A [FlowSession] is a handle on a communication sequence between two paired flows, possibly running on separate nodes. + * It is used to send and receive messages between the flows as well as to query information about the counter-flow. + * + * There are two ways of obtaining such a session: + * + * 1. Calling [FlowLogic.initiateFlow]. This will create a [FlowSession] object on which the first send/receive + * operation will attempt to kick off a corresponding [InitiatedBy] flow on the counterparty's node. + * 2. As constructor parameter to [InitiatedBy] flows. This session is the one corresponding to the initiating flow and + * may be used for replies. + * + * To port flows using the old Party-based API: * * Look for [Deprecated] usages of send/receive/sendAndReceive/getFlowInfo. * @@ -31,6 +42,10 @@ import net.corda.core.utilities.UntrustworthyData * otherSideSession.send(something) */ abstract class FlowSession { + /** + * The [Party] on the other side of this session. In the case of a session created by [FlowLogic.initiateFlow] + * [counterparty] is the same Party as the one passed to that function. + */ abstract val counterparty: Party /** @@ -54,12 +69,13 @@ abstract class FlowSession { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Suspendable inline fun sendAndReceive(payload: Any): UntrustworthyData { return sendAndReceive(R::class.java, payload) } + /** * Serializes and queues the given [payload] object for sending to the [counterparty]. Suspends until a response * is received, which must be of the given [receiveType]. Remember that when receiving data from other parties the data @@ -69,7 +85,7 @@ abstract class FlowSession { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Suspendable abstract fun sendAndReceive(receiveType: Class, payload: Any): UntrustworthyData @@ -85,6 +101,7 @@ abstract class FlowSession { inline fun receive(): UntrustworthyData { return receive(R::class.java) } + /** * Suspends until [counterparty] sends us a message of type [receiveType]. * @@ -92,7 +109,7 @@ abstract class FlowSession { * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly * corrupted data in order to exploit your code. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Suspendable abstract fun receive(receiveType: Class): UntrustworthyData diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index f738df62be..c96070fba7 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -13,7 +13,6 @@ import net.corda.core.node.services.NotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.UniquenessProvider import net.corda.core.serialization.CordaSerializable -import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData @@ -55,11 +54,7 @@ class NotaryFlow { } try { - if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub).verifySignaturesExcept(notaryParty.owningKey) - } else { - stx.verifySignaturesExcept(notaryParty.owningKey) - } + stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey) } catch (ex: SignatureException) { throw NotaryException(NotaryError.TransactionInvalid(ex)) } @@ -138,8 +133,11 @@ class NotaryFlow { // Check if transaction is intended to be signed by this notary. @Suspendable protected fun checkNotary(notary: Party?) { - if (notary !in serviceHub.myInfo.legalIdentities) + // TODO This check implies that it's OK to use the node's main identity. Shouldn't it be just limited to the + // notary identities? + if (notary !in serviceHub.myInfo.legalIdentities) { throw NotaryException(NotaryError.WrongNotary) + } } @Suspendable @@ -171,5 +169,5 @@ sealed class NotaryError { override fun toString() = cause.toString() } - object WrongNotary: NotaryError() + object WrongNotary : NotaryError() } diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 35f17c64ea..2e686e8746 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -13,7 +13,7 @@ import java.security.SignatureException * This flow is a combination of [FlowSession.receive], resolve and [SignedTransaction.verify]. This flow will receive the * [SignedTransaction] and perform the resolution back-and-forth required to check the dependencies and download any missing * attachments. The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify]. - * + * * @param otherSideSession session to the other side which is calling [SendTransactionFlow]. * @param checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify]. */ diff --git a/core/src/main/kotlin/net/corda/core/flows/StartableByService.kt b/core/src/main/kotlin/net/corda/core/flows/StartableByService.kt new file mode 100644 index 0000000000..3674c53e04 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/StartableByService.kt @@ -0,0 +1,12 @@ +package net.corda.core.flows + +import kotlin.annotation.AnnotationTarget.CLASS + +/** + * Any [FlowLogic] which is to be started by the [AppServiceHub] interface from + * within a [CordaService] must have this annotation. If it's missing the + * flow will not be allowed to start and an exception will be thrown. + */ +@Target(CLASS) +@MustBeDocumented +annotation class StartableByService \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt index 6f6f1a8109..8f03ed640b 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -13,6 +13,7 @@ import java.security.PublicKey abstract class AbstractParty(val owningKey: PublicKey) { /** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */ override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey + override fun hashCode(): Int = owningKey.hashCode() abstract fun nameOrNull(): CordaX500Name? diff --git a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt index a563ebb1ca..37e872d562 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -81,7 +81,7 @@ data class CordaX500Name(val commonName: String?, private val countryCodes: Set = ImmutableSet.copyOf(Locale.getISOCountries()) @JvmStatic - fun build(principal: X500Principal) : CordaX500Name { + fun build(principal: X500Principal): CordaX500Name { val x500Name = X500Name.getInstance(principal.encoded) val attrsMap: Map = x500Name.rdNs .flatMap { it.typesAndValues.asList() } @@ -109,7 +109,7 @@ data class CordaX500Name(val commonName: String?, } @JvmStatic - fun parse(name: String) : CordaX500Name = build(X500Principal(name)) + fun parse(name: String): CordaX500Name = build(X500Principal(name)) } @Transient diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index 78f36ca543..9316391ae7 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -29,6 +29,7 @@ import java.security.cert.X509Certificate class Party(val name: CordaX500Name, owningKey: PublicKey) : AbstractParty(owningKey) { constructor(certificate: X509Certificate) : this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey)) + override fun nameOrNull(): CordaX500Name = name fun anonymise(): AnonymousParty = AnonymousParty(owningKey) override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt index f158138b84..12661d2881 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -11,7 +11,9 @@ import java.security.cert.* */ @CordaSerializable class PartyAndCertificate(val certPath: CertPath) { - @Transient val certificate: X509Certificate + @Transient + val certificate: X509Certificate + init { require(certPath.type == "X.509") { "Only X.509 certificates supported" } val certs = certPath.certificates @@ -19,7 +21,8 @@ class PartyAndCertificate(val certPath: CertPath) { certificate = certs[0] as X509Certificate } - @Transient val party: Party = Party(certificate) + @Transient + val party: Party = Party(certificate) val owningKey: PublicKey get() = party.owningKey val name: CordaX500Name get() = party.name diff --git a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt index 7c7e5202f5..eb057cfe30 100644 --- a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt @@ -13,22 +13,38 @@ object Emoji { (System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o") } - @JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385) - @JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537) - @JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0) - @JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0) - @JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F) - @JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F) - @JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705) - @JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE) - @JvmStatic val CODE_COOL_GUY: String = codePointsString(0x1F60E) - @JvmStatic val CODE_NO_ENTRY: String = codePointsString(0x1F6AB) - @JvmStatic val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620) - @JvmStatic val CODE_BOOKS: String = codePointsString(0x1F4DA) - @JvmStatic val CODE_SLEEPING_FACE: String = codePointsString(0x1F634) - @JvmStatic val CODE_LIGHTBULB: String = codePointsString(0x1F4A1) - @JvmStatic val CODE_FREE: String = codePointsString(0x1F193) - @JvmStatic val CODE_SOON: String = codePointsString(0x1F51C) + @JvmStatic + val CODE_SANTA_CLAUS: String = codePointsString(0x1F385) + @JvmStatic + val CODE_DIAMOND: String = codePointsString(0x1F537) + @JvmStatic + val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0) + @JvmStatic + val CODE_NEWSPAPER: String = codePointsString(0x1F4F0) + @JvmStatic + val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F) + @JvmStatic + val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F) + @JvmStatic + val CODE_GREEN_TICK: String = codePointsString(0x2705) + @JvmStatic + val CODE_PAPERCLIP: String = codePointsString(0x1F4CE) + @JvmStatic + val CODE_COOL_GUY: String = codePointsString(0x1F60E) + @JvmStatic + val CODE_NO_ENTRY: String = codePointsString(0x1F6AB) + @JvmStatic + val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620) + @JvmStatic + val CODE_BOOKS: String = codePointsString(0x1F4DA) + @JvmStatic + val CODE_SLEEPING_FACE: String = codePointsString(0x1F634) + @JvmStatic + val CODE_LIGHTBULB: String = codePointsString(0x1F4A1) + @JvmStatic + val CODE_FREE: String = codePointsString(0x1F193) + @JvmStatic + val CODE_SOON: String = codePointsString(0x1F51C) /** 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 0789c535ba..28b44dfa4e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -114,8 +114,7 @@ sealed class FetchDataFlow( protected abstract fun load(txid: SecureHash): T? - @Suppress("UNCHECKED_CAST") - protected open fun convert(wire: W): T = wire as T + protected open fun convert(wire: W): T = uncheckedCast(wire) private fun validateFetchResponse(maybeItems: UntrustworthyData>, requests: List): List { diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index dc09d6da3f..a2b0e2fd15 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -10,6 +10,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.UntrustworthyData import org.slf4j.Logger +import java.time.Instant /** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */ interface FlowStateMachine { @@ -30,25 +31,32 @@ interface FlowStateMachine { fun receive(receiveType: Class, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData @Suspendable - fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>): Unit + fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>) @Suspendable fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction - fun checkFlowPermission(permissionName: String, extraAuditData: Map): Unit + @Suspendable + fun sleepUntil(until: Instant) - fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map): Unit + fun checkFlowPermission(permissionName: String, extraAuditData: Map) + + fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) @Suspendable fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? @Suspendable - fun persistFlowStackSnapshot(flowClass: Class>): Unit + fun persistFlowStackSnapshot(flowClass: Class>) + val logic: FlowLogic val serviceHub: ServiceHub val logger: Logger val id: StateMachineRunId val resultFuture: CordaFuture val flowInitiator: FlowInitiator val ourIdentityAndCert: PartyAndCertificate + + @Suspendable + fun receiveAll(sessions: Map>, sessionFlow: FlowLogic<*>): Map> } 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 cbb80eb2ae..2f101b869f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -1,7 +1,14 @@ +@file:JvmName("InternalUtils") + package net.corda.core.internal import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 +import net.corda.core.node.ServiceHub +import net.corda.core.node.ServicesForResolution +import net.corda.core.serialization.SerializationContext +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.transactions.WireTransaction import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.slf4j.Logger @@ -44,6 +51,7 @@ operator fun Duration.times(multiplicand: Long): Duration = multipliedBy(multipl * separator problems. */ operator fun Path.div(other: String): Path = resolve(other) + operator fun String.div(other: String): Path = Paths.get(this) / other /** @@ -99,6 +107,7 @@ fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path { Files.copy(this, targetFile, *options) return targetFile } + fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options) fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options) fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options) @@ -226,25 +235,29 @@ private fun IntProgression.toSpliterator(): Spliterator.OfInt { fun IntProgression.stream(parallel: Boolean = false): IntStream = StreamSupport.intStream(toSpliterator(), parallel) -@Suppress("UNCHECKED_CAST") // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable). -inline fun Stream.toTypedArray() = toArray { size -> arrayOfNulls(size) } as Array +// When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable): +inline fun Stream.toTypedArray(): Array = uncheckedCast(toArray { size -> arrayOfNulls(size) }) fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */ fun Class<*>.staticField(name: String): DeclaredField = DeclaredField(this, name, null) + /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [KClass]. */ fun KClass<*>.staticField(name: String): DeclaredField = DeclaredField(java, name, null) -/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */ + +/** @suppress Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */ fun Any.declaredField(name: String): DeclaredField = DeclaredField(javaClass, name, this) + /** * Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared * in its superclass [clazz]. + * @suppress */ fun Any.declaredField(clazz: KClass<*>, name: String): DeclaredField = DeclaredField(clazz.java, name, this) /** creates a new instance if not a Kotlin object */ -fun KClass.objectOrNewInstance(): T { +fun KClass.objectOrNewInstance(): T { return this.objectInstance ?: this.createInstance() } @@ -255,8 +268,7 @@ fun KClass.objectOrNewInstance(): T { class DeclaredField(clazz: Class<*>, name: String, private val receiver: Any?) { private val javaField = clazz.getDeclaredField(name).apply { isAccessible = true } var value: T - @Suppress("UNCHECKED_CAST") - get() = javaField.get(receiver) as T + get() = uncheckedCast(javaField.get(receiver)) set(value) = javaField.set(receiver, value) } @@ -273,4 +285,21 @@ annotation class VisibleForTesting @Suppress("UNCHECKED_CAST") fun uncheckedCast(obj: T) = obj as U -fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } \ No newline at end of file +fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } + +/** + * Provide access to internal method for AttachmentClassLoaderTests + * @suppress + */ +fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { + return toWireTransactionWithContext(services, serializationContext) +} + +/** + * Provide access to internal method for AttachmentClassLoaderTests + * @suppress + */ +fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext) + +/** Convenience method to get the package name of a class literal. */ +val KClass<*>.packageName get() = java.`package`.name diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt index 3e4e3a526d..edbb545d20 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt @@ -27,6 +27,7 @@ class LazyPool( STARTED, FINISHED } + private val lifeCycle = LifeCycle(State.STARTED) private fun clearIfNeeded(instance: A): A { diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt index 6746989291..5974f97f8e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt @@ -18,6 +18,7 @@ class LazyStickyPool( private class InstanceBox { var instance: LinkedBlockingQueue? = null } + private val random = Random() private val boxes = Array(size) { InstanceBox() } diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index f297fc7c5b..e0fec0f7ac 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.serialization.CordaSerializable @@ -19,14 +20,15 @@ import java.util.* class ResolveTransactionsFlow(private val txHashes: Set, private val otherSide: FlowSession) : FlowLogic>() { /** - * Resolves and validates the dependencies of the specified [signedTransaction]. Fetches the attachments, but does - * *not* validate or store the [signedTransaction] itself. + * Resolves and validates the dependencies of the specified [SignedTransaction]. Fetches the attachments, but does + * *not* validate or store the [SignedTransaction] itself. * * @return a list of verified [SignedTransaction] objects, in a depth-first order. */ constructor(signedTransaction: SignedTransaction, otherSide: FlowSession) : this(dependencyIDs(signedTransaction), otherSide) { this.signedTransaction = signedTransaction } + companion object { private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() @@ -63,7 +65,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, } @CordaSerializable - class ExcessivelyLargeTransactionGraph : Exception() + class ExcessivelyLargeTransactionGraph : FlowException() /** Transaction for fetch attachments for */ private var signedTransaction: SignedTransaction? = null diff --git a/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt b/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt new file mode 100644 index 0000000000..9c4e3abb7f --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt @@ -0,0 +1,7 @@ +package net.corda.core.internal + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractClassName + +/** Indicates that this transaction replaces the inputs contract state to another contract state */ +data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt b/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt index ad0ae9bc39..ae815be6ca 100644 --- a/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt +++ b/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt @@ -6,7 +6,7 @@ import kotlin.reflect.KProperty * A write-once property to be used as delegate for Kotlin var properties. The expectation is that this is initialised * prior to the spawning of any threads that may access it and so there's no need for it to be volatile. */ -class WriteOnceProperty(private val defaultValue:T? = null) { +class WriteOnceProperty(private val defaultValue: T? = null) { private var v: T? = defaultValue operator fun getValue(thisRef: Any?, property: KProperty<*>) = v ?: throw IllegalStateException("Write-once property $property not set.") diff --git a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt index 0bebc73130..cd5fac1ee1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt @@ -46,6 +46,7 @@ class X509EdDSAEngine : Signature { override fun engineSetParameter(params: AlgorithmParameterSpec) = engine.setParameter(params) @Suppress("DEPRECATION") override fun engineGetParameter(param: String): Any = engine.getParameter(param) + @Suppress("DEPRECATION") override fun engineSetParameter(param: String, value: Any?) = engine.setParameter(param, value) } diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 4f7aecd6fe..02410db37a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -2,23 +2,28 @@ package net.corda.core.internal.cordapp import net.corda.core.cordapp.Cordapp import net.corda.core.flows.FlowLogic -import net.corda.core.node.CordaPluginRegistry import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken +import java.io.File import java.net.URL data class CordappImpl( override val contractClassNames: List, override val initiatedFlows: List>>, override val rpcFlows: List>>, + override val serviceFlows: List>>, + override val schedulableFlows: List>>, override val services: List>, - override val plugins: List, + override val serializationWhitelists: List, override val customSchemas: Set, override val jarPath: URL) : Cordapp { + override val name: String = File(jarPath.toURI()).name.removeSuffix(".jar") + /** * An exhaustive list of all classes relevant to the node within this CorDapp * * TODO: Also add [SchedulableFlow] as a Cordapp class */ - override val cordappClasses = ((rpcFlows + initiatedFlows + services + plugins.map { javaClass }).map { it.name } + contractClassNames) + override val cordappClasses = ((rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames) } diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 50664c52e3..d29cc7a193 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -104,12 +104,15 @@ interface CordaRPCOps : RPCOps { fun vaultQuery(contractStateType: Class): Vault.Page { return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class): Vault.Page { return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultQueryByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { return vaultQueryBy(criteria, paging, Sort(emptySet()), contractStateType) } + fun vaultQueryByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { return vaultQueryBy(criteria, PageSpecification(), sorting, contractStateType) } @@ -142,12 +145,15 @@ interface CordaRPCOps : RPCOps { fun vaultTrack(contractStateType: Class): DataFeed, Vault.Update> { return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultTrackByCriteria(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultTrackByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return vaultTrackBy(criteria, paging, Sort(emptySet()), contractStateType) } + fun vaultTrackByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType) } @@ -250,12 +256,21 @@ interface CordaRPCOps : RPCOps { * @return well known identity, if found. */ fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? + /** Returns the [Party] corresponding to the given key, if found. */ fun partyFromKey(key: PublicKey): Party? /** Returns the [Party] with the X.500 principal as it's [Party.name]. */ fun wellKnownPartyFromX500Name(x500Name: CordaX500Name): Party? + /** + * Get a notary identity by name. + * + * @return the notary identity, or null if there is no notary by that name. Note that this will return null if there + * is a peer with that name but they are not a recognised notary service. + */ + fun notaryPartyFromX500Name(x500Name: CordaX500Name): Party? + /** * Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may * get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results @@ -271,6 +286,8 @@ interface CordaRPCOps : RPCOps { /** * Returns a node's info from the network map cache, where known. + * Notice that when there are more than one node for a given name (in case of distributed services) first service node + * found will be returned. * * @return the node info if available. */ @@ -300,7 +317,7 @@ inline fun > CordaRPCOps.startFlow( flowConstructor: () -> R ): FlowHandle = startFlowDynamic(R::class.java) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A) -> R, arg0: A diff --git a/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt b/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt index 9cd9776061..166825de0c 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt @@ -43,7 +43,7 @@ data class FlowHandleImpl( override val id: StateMachineRunId, override val returnValue: CordaFuture) : FlowHandle { - // Remember to add @Throws to FlowHandle.close() if this throws an exception. + // Remember to add @Throws to FlowHandle.close() if this throws an exception. override fun close() { returnValue.cancel(false) } diff --git a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt new file mode 100644 index 0000000000..94ab6da2e9 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt @@ -0,0 +1,30 @@ +package net.corda.core.node + +import net.corda.core.flows.FlowLogic +import net.corda.core.messaging.FlowHandle +import net.corda.core.messaging.FlowProgressHandle +import rx.Observable + +/** + * A [CordaService] annotated class requires a constructor taking a + * single parameter of type [AppServiceHub]. + * With the [AppServiceHub] parameter a [CordaService] is able to access to privileged operations. + * In particular such a [CordaService] can initiate and track flows marked with [net.corda.core.flows.StartableByService]. + */ +interface AppServiceHub : ServiceHub { + + /** + * Start the given flow with the given arguments. [flow] must be annotated + * with [net.corda.core.flows.StartableByService]. + * TODO it is assumed here that the flow object has an appropriate classloader. + */ + fun startFlow(flow: FlowLogic): FlowHandle + + /** + * Start the given flow with the given arguments, returning an [Observable] with a single observation of the + * result of running the flow. [flow] must be annotated with [net.corda.core.flows.StartableByService]. + * TODO it is assumed here that the flow object has an appropriate classloader. + */ + fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle + +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt deleted file mode 100644 index a74d36d3de..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.core.node - -import net.corda.core.contracts.ContractState -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.node.services.VaultQueryService -import net.corda.core.schemas.MappedSchema -import net.corda.core.schemas.QueryableState -import net.corda.core.serialization.SerializationCustomization -import java.util.function.Function - -/** - * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file - * to extend a Corda node with additional application services. - */ -abstract class CordaPluginRegistry { - /** - * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. - * - * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that - * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method. - ** - * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. - */ - open fun customizeSerialization(custom: SerializationCustomization): Boolean = false -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 4abfca9b92..07e6c550be 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -1,30 +1,51 @@ package net.corda.core.node +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.NetworkHostAndPort /** - * Info about a network node that acts on behalf of some form of contract party. - * @param legalIdentitiesAndCerts is a non-empty list, where the first identity is assumed to be the default identity of the node. + * Information about a network node that acts on behalf of some party. NodeInfos can be found via the network map + * cache, accessible from a [net.corda.core.node.services.NetworkMapCache]. They are also available via RPC + * using the [net.corda.core.messaging.CordaRPCOps.networkMapSnapshot] method. + * + * @property addresses An ordered list of IP addresses/hostnames where the node can be contacted. + * @property legalIdentitiesAndCerts A non-empty list, where the first identity is assumed to be the default identity of the node. + * @property platformVersion An integer representing the set of protocol features the node supports. See the docsite + * for information on how the platform is versioned. + * @property serial An arbitrary number incremented each time the NodeInfo is changed. This is analogous to the same + * concept in DNS. */ -// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. @CordaSerializable data class NodeInfo(val addresses: List, val legalIdentitiesAndCerts: List, val platformVersion: Int, val serial: Long ) { + // TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. init { require(legalIdentitiesAndCerts.isNotEmpty()) { "Node should have at least one legal identity" } } @Transient private var _legalIdentities: List? = null - val legalIdentities: List get() { - return _legalIdentities ?: legalIdentitiesAndCerts.map { it.party }.also { _legalIdentities = it } - } + + /** + * An ordered list of legal identities supported by this node. The node will always have at least one, so if you + * are porting code from earlier versions of Corda that expected a single party per node, just use the first item + * in the returned list. + */ + val legalIdentities: List + get() { + return _legalIdentities ?: legalIdentitiesAndCerts.map { it.party }.also { _legalIdentities = it } + } /** Returns true if [party] is one of the identities of this node, else false. */ fun isLegalIdentity(party: Party): Boolean = party in legalIdentities + + fun identityFromX500Name(name: CordaX500Name): Party { + val identity = legalIdentitiesAndCerts.singleOrNull { it.name == name } ?: throw IllegalArgumentException("Node does not have an identity \"$name\"") + return identity.party + } } diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index cf484b6b24..7114e6c843 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -6,10 +6,7 @@ import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party -import net.corda.core.internal.toMultiMap +import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.FilteredTransaction @@ -19,13 +16,29 @@ import java.security.PublicKey import java.sql.Connection import java.time.Clock +/** + * Part of [ServiceHub]. + */ +interface StateLoader { + /** + * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. + * + * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. + */ + @Throws(TransactionResolutionException::class) + fun loadState(stateRef: StateRef): TransactionState<*> +} + /** * Subset of node services that are used for loading transactions from the wire into fully resolved, looked up * forms ready for verification. - * - * @see ServiceHub */ -interface ServicesForResolution { +interface ServicesForResolution : StateLoader { + /** + * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus + * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential + * identities back to the well known identity (i.e. the identity in the network map) of a party. + */ val identityService: IdentityService /** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */ @@ -33,28 +46,44 @@ interface ServicesForResolution { /** Provides access to anything relating to cordapps including contract attachment resolution and app context */ val cordappProvider: CordappProvider - - /** - * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. - * - * @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction. - */ - @Throws(TransactionResolutionException::class) - fun loadState(stateRef: StateRef): TransactionState<*> } /** - * A service hub simply vends references to the other services a node has. Some of those services may be missing or - * mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of - * functionality and you don't want to hard-code which types in the interface. + * A service hub is the starting point for most operations you can do inside the node. You are provided with one + * when a class annotated with [CordaService] is constructed, and you have access to one inside flows. Most RPCs + * simply forward to the services found here after some access checking. * - * Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid their internal - * state from being serialized in checkpoints. + * The APIs are organised roughly by category, with a few very important top level APIs available on the ServiceHub + * itself. Inside a flow, it's safe to keep a reference to services found here on the stack: checkpointing will do the + * right thing (it won't try to serialise the internals of the service). + * + * In unit test environments, some of those services may be missing or mocked out. */ interface ServiceHub : ServicesForResolution { + // NOTE: Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid + // their internal state from being serialized in checkpoints. + + /** + * The vault service lets you observe, soft lock and add notes to states that involve you or are relevant to your + * node in some way. Additionally you may query and track states that correspond to various criteria. + */ val vaultService: VaultService - val vaultQueryService: VaultQueryService + + /** + * The key management service is responsible for storing and using private keys to sign things. An + * implementation of this may, for example, call out to a hardware security module that enforces various + * auditing and frequency-of-use requirements. + * + * You don't normally need to use this directly. If you have a [TransactionBuilder] and wish to sign it to + * get a [SignedTransaction], look at [signInitialTransaction]. + */ val keyManagementService: KeyManagementService + /** + * The [ContractUpgradeService] is responsible for securely upgrading contract state objects according to + * a specified and mutually agreed (amongst participants) contract version. + * + * @see ContractUpgradeFlow to understand the workflow associated with contract upgrades. + */ val contractUpgradeService: ContractUpgradeService /** @@ -64,9 +93,27 @@ interface ServiceHub : ServicesForResolution { */ val validatedTransactions: TransactionStorage + /** + * A network map contains lists of nodes on the network along with information about their identity keys, services + * they provide and host names or IP addresses where they can be connected to. The cache wraps around a map fetched + * from an authoritative service, and adds easy lookup of the data stored within it. Generally it would be initialised + * with a specified network map service, which it fetches data from and then subscribes to updates of. + */ val networkMapCache: NetworkMapCache + + /** + * INTERNAL. DO NOT USE. + * @suppress + */ val transactionVerifierService: TransactionVerifierService + + /** + * A [Clock] representing the node's current time. This should be used in preference to directly accessing the + * clock so the current time can be controlled during unit testing. + */ val clock: Clock + + /** The [NodeInfo] object corresponding to our own entry in the network map. */ val myInfo: NodeInfo /** @@ -109,19 +156,6 @@ interface ServiceHub : ServicesForResolution { recordTransactions(true, txs) } - /** - * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. - * - * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. - */ - @Throws(TransactionResolutionException::class) - override fun loadState(stateRef: StateRef): TransactionState<*> { - val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index] - } else stx.tx.outputs[stateRef.index] - } - /** * Converts the given [StateRef] into a [StateAndRef] object. * @@ -130,11 +164,7 @@ interface ServiceHub : ServicesForResolution { @Throws(TransactionResolutionException::class) fun toStateAndRef(stateRef: StateRef): StateAndRef { val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(this).outRef(stateRef.index) - } else { - stx.tx.outRef(stateRef.index) - } + return stx.resolveBaseTransaction(this).outRef(stateRef.index) } private val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentitiesAndCerts.first().owningKey @@ -268,9 +298,12 @@ interface ServiceHub : ServicesForResolution { /** * Exposes a JDBC connection (session) object using the currently configured database. * Applications can use this to execute arbitrary SQL queries (native, direct, prepared, callable) - * against its Node database tables (including custom contract tables defined by extending [Queryable]). + * against its Node database tables (including custom contract tables defined by extending + * [net.corda.core.schemas.QueryableState]). + * * When used within a flow, this session automatically forms part of the enclosing flow transaction boundary, * and thus queryable data will include everything committed as of the last checkpoint. + * * @throws IllegalStateException if called outside of a transaction. * @return A new [Connection] */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt index 0e40e3acce..cdefa92037 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt @@ -7,7 +7,7 @@ import kotlin.annotation.AnnotationTarget.CLASS /** * Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation. - * Such a class needs to have a constructor with a single parameter of type [ServiceHub]. This constructor will be invoked + * Such a class needs to have a constructor with a single parameter of type [AppServiceHub]. This constructor will be invoked * during node start to initialise the service. The service hub provided can be used to get information about the node * that may be necessary for the service. Corda services are created as singletons within the node and are available * to flows via [ServiceHub.cordaService]. diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 41a60bb30d..489e316e01 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -1,5 +1,6 @@ package net.corda.core.node.services +import net.corda.core.CordaException import net.corda.core.contracts.PartyAndReference import net.corda.core.identity.* import java.security.InvalidAlgorithmParameterException @@ -9,7 +10,11 @@ import java.security.cert.* /** * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential - * identities back to the well known identity (i.e. the identity in the network map) of a party. + * identities back to the well known identity. + * + * Well known identities in Corda are the public identity of a party, registered with the network map directory, + * whereas confidential identities are distributed only on a need to know basis (typically between parties in + * a transaction being built). See [NetworkMapCache] for retrieving well known identities from the network map. */ interface IdentityService { val trustRoot: X509Certificate @@ -42,8 +47,9 @@ interface IdentityService { fun getAllIdentities(): Iterable /** - * Get the certificate and path for a known identity's owning key. + * Resolves a public key to the well known identity [PartyAndCertificate] instance which is owned by the key. * + * @param owningKey The [PublicKey] to determine well known identity for. * @return the party and certificate, or null if unknown. */ fun certificateFromKey(owningKey: PublicKey): PartyAndCertificate? @@ -58,17 +64,18 @@ interface IdentityService { fun partyFromKey(key: PublicKey): Party? /** - * Resolves a party name to the well known identity [Party] instance for this name. - * @param name The [CordaX500Name] to search for. + * Resolves a party name to the well known identity [Party] instance for this name. Where possible well known identity + * lookup from name should be done from the network map (via [NetworkMapCache]) instead, as it is the authoritative + * source of well known identities. + * + * @param name The [CordaX500Name] to determine well known identity for. * @return If known the canonical [Party] with that name, else null. */ fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? /** - * Returns the well known identity from an [AbstractParty]. This is intended to resolve the well known identity, - * as visible in the [NetworkMapCache] from a confidential identity. - * It transparently handles returning the well known identity back if - * a well known identity is passed in. + * Resolves a (optionally) confidential identity to the corresponding well known identity [Party]. + * It transparently handles returning the well known identity back if a well known identity is passed in. * * @param party identity to determine well known identity for. * @return well known identity, if found. @@ -76,11 +83,12 @@ interface IdentityService { fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? /** - * Returns the well known identity from a PartyAndReference. This is intended to resolve the well known identity, - * as visible in the [NetworkMapCache] from a confidential identity. - * It transparently handles returning the well known identity back if - * a well known identity is passed in. + * Resolves a (optionally) confidential identity to the corresponding well known identity [Party]. + * Convenience method which unwraps the [Party] from the [PartyAndReference] and then resolves the + * well known identity as normal. + * It transparently handles returning the well known identity back if a well known identity is passed in. * + * @param partyRef identity (and reference, which is unused) to determine well known identity for. * @return the well known identity, or null if unknown. */ fun wellKnownPartyFromAnonymous(partyRef: PartyAndReference) = wellKnownPartyFromAnonymous(partyRef.party) @@ -105,4 +113,4 @@ interface IdentityService { fun partiesFromName(query: String, exactMatch: Boolean): Set } -class UnknownAnonymousPartyException(msg: String) : Exception(msg) +class UnknownAnonymousPartyException(msg: String) : CordaException(msg) diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 8eb0a52a07..792baadb39 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -4,6 +4,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.serialization.CordaSerializable @@ -28,6 +29,7 @@ interface NetworkMapCache { data class Modified(override val node: NodeInfo, val previousNode: NodeInfo) : MapChange() } + // DOCSTART 1 /** * A list of notary services available on the network. * @@ -35,6 +37,8 @@ interface NetworkMapCache { */ // TODO this list will be taken from NetworkParameters distributed by NetworkMap. val notaryIdentities: List + // DOCEND 1 + /** Tracks changes to the network map cache. */ val changed: Observable /** Future to track completion of the NetworkMapService registration. */ @@ -50,6 +54,8 @@ interface NetworkMapCache { * Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party * is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this * returns null. + * Notice that when there are more than one node for a given party (in case of distributed services) first service node + * found will be returned. See also: [getNodesByLegalIdentityKey]. * * @param party party to retrieve node information for. * @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is @@ -57,41 +63,61 @@ interface NetworkMapCache { */ fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? - /** Look up the node info for a legal name. */ + /** + * Look up the node info for a legal name. + * Notice that when there are more than one node for a given name (in case of distributed services) first service node + * found will be returned. + */ fun getNodeByLegalName(name: CordaX500Name): NodeInfo? /** Look up the node info for a host and port. */ fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? - fun getPeerByLegalName(name: CordaX500Name): Party? = getNodeByLegalName(name)?.let { - it.legalIdentitiesAndCerts.singleOrNull { it.name == name }?.party - } + /** + * Look up a well known identity (including certificate path) of a legal name. This should be used in preference + * to well known identity lookup in the identity service where possible, as the network map is the authoritative + * source of well known identities. + */ + fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? + + /** + * Look up the well known identity of a legal name. This should be used in preference + * to well known identity lookup in the identity service where possible, as the network map is the authoritative + * source of well known identities. + */ + fun getPeerByLegalName(name: CordaX500Name): Party? = getPeerCertificateByLegalName(name)?.party /** Return all [NodeInfo]s the node currently is aware of (including ourselves). */ val allNodes: List /** - * Look up the node infos for a specific peer key. - * In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of - * the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises - * the identity of the *whole group*. + * Look up the node information entries for a specific identity key. + * Note that normally there will be only one node for a key, but for clusters of nodes or distributed services there + * can be multiple nodes. */ fun getNodesByLegalIdentityKey(identityKey: PublicKey): List + /** + * Look up the node information entries for a legal name. + * Note that normally there will be only one node for a legal name, but for clusters of nodes or distributed services there + * can be multiple nodes. + */ + fun getNodesByLegalName(name: CordaX500Name): List + /** Returns information about the party, which may be a specific node or a service */ fun getPartyInfo(party: Party): PartyInfo? - /** Gets a notary identity by the given name. */ + // DOCSTART 2 + /** Look up a well known identity of notary by legal name. */ fun getNotary(name: CordaX500Name): Party? = notaryIdentities.firstOrNull { it.name == name } + // DOCEND 2 /** Checks whether a given party is an advertised notary identity. */ fun isNotary(party: Party): Boolean = party in notaryIdentities /** Checks whether a given party is an validating notary identity. */ - fun isValidatingNotary(party: Party): Boolean { - require(isNotary(party)) { "No notary found with identity $party." } - return !party.name.toString().contains("corda.notary.simple", true) // TODO This implementation will change after introducing of NetworkParameters. - } + // TODO This implementation will change after introducing of NetworkParameters. + fun isValidatingNotary(party: Party): Boolean = isNotary(party) && "validating" in party.name.commonName!! /** Clear all network map data from local node cache. */ fun clearNetworkMapCache() diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 71e24c506d..8241fcd13c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -1,5 +1,6 @@ package net.corda.core.node.services +import com.google.common.primitives.Booleans import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.* @@ -13,6 +14,19 @@ import org.slf4j.Logger import java.security.PublicKey abstract class NotaryService : SingletonSerializeAsToken() { + companion object { + const val ID_PREFIX = "corda.notary." + fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String { + require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" } + return StringBuffer(ID_PREFIX).apply { + append(if (validating) "validating" else "simple") + if (raft) append(".raft") + if (bft) append(".bft") + if (custom) append(".custom") + }.toString() + } + } + abstract val services: ServiceHub abstract val notaryIdentityKey: PublicKey diff --git a/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt index 2db0543618..998c846c20 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt @@ -8,6 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort */ sealed class PartyInfo { abstract val party: Party - data class SingleNode(override val party: Party, val addresses: List): PartyInfo() - data class DistributedNode(override val party: Party): PartyInfo() + + data class SingleNode(override val party: Party, val addresses: List) : PartyInfo() + data class DistributedNode(override val party: Party) : PartyInfo() } diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt index 99f40a0492..8cc5c7af65 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt @@ -5,6 +5,7 @@ import net.corda.core.transactions.LedgerTransaction /** * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. + * @suppress */ interface TransactionVerifierService { /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt deleted file mode 100644 index 2cfacd51f5..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt +++ /dev/null @@ -1,140 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.contracts.ContractState -import net.corda.core.flows.FlowException -import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort - -interface VaultQueryService { - - // DOCSTART VaultQueryAPI - /** - * Generic vault query function which takes a [QueryCriteria] object to define filters, - * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), - * and returns a [Vault.Page] object containing the following: - * 1. states as a List of (page number and size defined by [PageSpecification]) - * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. - * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1) - * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL - * 5. other results (aggregate functions with/without using value groups) - * - * @throws VaultQueryException if the query cannot be executed for any reason - * (missing criteria or parsing error, paging errors, unsupported query, underlying database error) - * - * Notes - * If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned. - * API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results, - * otherwise a [VaultQueryException] will be thrown alerting to this condition. - * It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification]. - */ - @Throws(VaultQueryException::class) - fun _queryBy(criteria: QueryCriteria, - paging: PageSpecification, - sorting: Sort, - contractStateType: Class): Vault.Page - - /** - * Generic vault query function which takes a [QueryCriteria] object to define filters, - * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), - * and returns a [Vault.PageAndUpdates] object containing - * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) - * 2) an [Observable] of [Vault.Update] - * - * @throws VaultQueryException if the query cannot be executed for any reason - * - * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. - * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). - */ - @Throws(VaultQueryException::class) - fun _trackBy(criteria: QueryCriteria, - paging: PageSpecification, - sorting: Sort, - contractStateType: Class): DataFeed, Vault.Update> - // DOCEND VaultQueryAPI - - // Note: cannot apply @JvmOverloads to interfaces nor interface implementations - // Java Helpers - fun queryBy(contractStateType: Class): Vault.Page { - return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria): Vault.Page { - return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { - return _queryBy(criteria, paging, Sort(emptySet()), contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { - return _queryBy(criteria, PageSpecification(), sorting, contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { - return _queryBy(criteria, paging, sorting, contractStateType) - } - - fun trackBy(contractStateType: Class): DataFeed, Vault.Update> { - return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, Sort(emptySet()), contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), sorting, contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, sorting, contractStateType) - } -} - -inline fun VaultQueryService.queryBy(): Vault.Page { - return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria): Vault.Page { - return _queryBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page { - return _queryBy(criteria, paging, Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page { - return _queryBy(criteria, PageSpecification(), sorting, T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { - return _queryBy(criteria, paging, sorting, T::class.java) -} - -inline fun VaultQueryService.trackBy(): DataFeed, Vault.Update> { - return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), sorting, T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, sorting, T::class.java) -} - -class VaultQueryException(description: String) : FlowException(description) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 754bbd69fd..38545ac754 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -6,7 +6,10 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty +import net.corda.core.messaging.DataFeed +import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.utilities.NonEmptySet @@ -96,7 +99,7 @@ class Vault(val states: Iterable>) { companion object { val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL) - val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE) + val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE) } @CordaSerializable @@ -149,7 +152,6 @@ class Vault(val states: Iterable>) { * Note that transactions we've seen are held by the storage service, not the vault. */ interface VaultService { - /** * Prefer the use of [updates] unless you know why you want to use this instead. * @@ -167,11 +169,6 @@ interface VaultService { */ val updates: Observable> - /** - * Enable creation of observables of updates. - */ - val updatesPublisher: PublishSubject> - /** * Provide a [CordaFuture] for when a [StateRef] is consumed, which can be very useful in building tests. */ @@ -222,7 +219,7 @@ interface VaultService { // DOCEND SoftLockAPI /** - * Helper function to combine using [VaultQueryService] calls to determine spendable states and soft locking them. + * Helper function to determine spendable states and soft locking them. * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending` * However, this is fully generic and can operate with custom [FungibleAsset] states. * @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states. @@ -242,8 +239,135 @@ interface VaultService { amount: Amount, contractStateType: Class): List> + // DOCSTART VaultQueryAPI + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.Page] object containing the following: + * 1. states as a List of (page number and size defined by [PageSpecification]) + * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. + * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1) + * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL + * 5. other results (aggregate functions with/without using value groups) + * + * @throws VaultQueryException if the query cannot be executed for any reason + * (missing criteria or parsing error, paging errors, unsupported query, underlying database error) + * + * Notes + * If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned. + * API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results, + * otherwise a [VaultQueryException] will be thrown alerting to this condition. + * It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification]. + */ + @Throws(VaultQueryException::class) + fun _queryBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractStateType: Class): Vault.Page + + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.PageAndUpdates] object containing + * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) + * 2) an [Observable] of [Vault.Update] + * + * @throws VaultQueryException if the query cannot be executed for any reason + * + * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. + * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). + */ + @Throws(VaultQueryException::class) + fun _trackBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractStateType: Class): DataFeed, Vault.Update> + // DOCEND VaultQueryAPI + + // Note: cannot apply @JvmOverloads to interfaces nor interface implementations + // Java Helpers + fun queryBy(contractStateType: Class): Vault.Page { + return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria): Vault.Page { + return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + return _queryBy(criteria, paging, Sort(emptySet()), contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { + return _queryBy(criteria, PageSpecification(), sorting, contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { + return _queryBy(criteria, paging, sorting, contractStateType) + } + + fun trackBy(contractStateType: Class): DataFeed, Vault.Update> { + return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, Sort(emptySet()), contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), sorting, contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, sorting, contractStateType) + } } +inline fun VaultService.queryBy(): Vault.Page { + return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria): Vault.Page { + return _queryBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + return _queryBy(criteria, paging, Sort(emptySet()), T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page { + return _queryBy(criteria, PageSpecification(), sorting, T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { + return _queryBy(criteria, paging, sorting, T::class.java) +} + +inline fun VaultService.trackBy(): DataFeed, Vault.Update> { + return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), sorting, T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, sorting, T::class.java) +} + +class VaultQueryException(description: String) : FlowException(description) class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) { override fun toString() = "Soft locking error: $message" diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 6f15271057..1e5eeb858f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -49,12 +49,12 @@ sealed class QueryCriteria { /** * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] */ - data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - override val contractStateTypes: Set>? = null, - val stateRefs: List? = null, - val notary: List? = null, - val softLockingCondition: SoftLockingCondition? = null, - val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { + data class VaultQueryCriteria @JvmOverloads constructor(override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + val stateRefs: List? = null, + val notary: List? = null, + val softLockingCondition: SoftLockingCondition? = null, + val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) @@ -69,10 +69,10 @@ sealed class QueryCriteria { val externalId: List? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { - constructor(participants: List? = null, - linearId: List? = null, - status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - contractStateTypes: Set>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes) + constructor(participants: List? = null, + linearId: List? = null, + status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + contractStateTypes: Set>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes) override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -80,13 +80,13 @@ sealed class QueryCriteria { } } - /** - * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState] - * - * Valid TokenType implementations defined by Amount are - * [Currency] as used in [Cash] contract state - * [Commodity] as used in [CommodityContract] state - */ + /** + * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState] + * + * Valid TokenType implementations defined by Amount are + * [Currency] as used in [Cash] contract state + * [Commodity] as used in [CommodityContract] state + */ data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, val owner: List? = null, val quantity: ColumnPredicate? = null, @@ -94,11 +94,11 @@ sealed class QueryCriteria { val issuerRef: List? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { - override fun visit(parser: IQueryCriteriaParser): Collection { - super.visit(parser) - return parser.parseCriteria(this) - } - } + override fun visit(parser: IQueryCriteriaParser): Collection { + super.visit(parser) + return parser.parseCriteria(this) + } + } /** * VaultCustomQueryCriteria: provides query by custom attributes defined in a contracts @@ -111,9 +111,9 @@ sealed class QueryCriteria { * Refer to [CommercialPaper.State] for a concrete example. */ data class VaultCustomQueryCriteria @JvmOverloads constructor - (val expression: CriteriaExpression, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { + (val expression: CriteriaExpression, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) @@ -121,13 +121,13 @@ sealed class QueryCriteria { } // enable composition of [QueryCriteria] - private data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + private data class AndComposition(val a: QueryCriteria, val b: QueryCriteria) : QueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseAnd(this.a, this.b) } } - private data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + private data class OrComposition(val a: QueryCriteria, val b: QueryCriteria) : QueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseOr(this.a, this.b) } @@ -148,9 +148,9 @@ interface IQueryCriteriaParser { fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection - fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection + fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection - fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection + fun parse(criteria: QueryCriteria, sorting: Sort? = null): Collection } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 811bc1ee2e..85d7292061 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -2,6 +2,7 @@ package net.corda.core.node.services.vault +import net.corda.core.internal.uncheckedCast import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Field @@ -88,8 +89,7 @@ fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression resolveEnclosingObjectFromColumn(column: Column): Class = column.declaringClass as Class +fun resolveEnclosingObjectFromColumn(column: Column): Class = uncheckedCast(column.declaringClass) fun getColumnName(column: Column): String = column.name /** @@ -170,8 +170,8 @@ data class Sort(val columns: Collection) { @CordaSerializable data class SortColumn( - val sortAttribute: SortAttribute, - val direction: Sort.Direction = Sort.Direction.ASC) + val sortAttribute: SortAttribute, + val direction: Sort.Direction = Sort.Direction.ASC) } @CordaSerializable @@ -199,8 +199,9 @@ object Builder { fun Field.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column(this), predicate) - fun KProperty1.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) + fun KProperty1.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = CriteriaExpression.AggregateFunctionExpression(Column(this), predicate, groupByColumns, orderBy) + fun Field.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = CriteriaExpression.AggregateFunctionExpression(Column(this), predicate, groupByColumns, orderBy) @@ -217,15 +218,32 @@ object Builder { fun > KProperty1.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) fun > KProperty1.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) - @JvmStatic fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) - @JvmStatic fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) - @JvmStatic fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) - @JvmStatic fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) - @JvmStatic fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) - @JvmStatic fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) - @JvmStatic fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) - @JvmStatic fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) - @JvmStatic fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + @JvmStatic + fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + + @JvmStatic + fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + + @JvmStatic + fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + + @JvmStatic + fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + + @JvmStatic + fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + + @JvmStatic + fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + + @JvmStatic + fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) + + @JvmStatic + fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + + @JvmStatic + fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) fun equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) fun notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) @@ -238,45 +256,57 @@ object Builder { fun > notIn(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) fun KProperty1.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) - @JvmStatic fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + @JvmStatic + fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + fun KProperty1.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) - @JvmStatic fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + @JvmStatic + fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) fun KProperty1.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) - @JvmStatic fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + @JvmStatic + fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + fun KProperty1.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) - @JvmStatic fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) + @JvmStatic + fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) /** aggregate functions */ fun KProperty1.sum(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column(it) }, orderBy) - @JvmStatic @JvmOverloads + + @JvmStatic + @JvmOverloads fun Field.sum(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column(it) }, orderBy) fun KProperty1.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) - @JvmStatic fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) + @JvmStatic + fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) fun KProperty1.avg(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column(it) }, orderBy) + @JvmStatic @JvmOverloads fun Field.avg(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column(it) }, orderBy) fun KProperty1.min(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column(it) }, orderBy) + @JvmStatic @JvmOverloads fun Field.min(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column(it) }, orderBy) fun KProperty1.max(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column(it) }, orderBy) + @JvmStatic @JvmOverloads fun Field.max(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column(it) }, orderBy) } inline fun builder(block: Builder.() -> A) = block(Builder) diff --git a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt index 0f9d985791..1d5559d45d 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -38,9 +38,9 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers ) : PersistentState() { constructor(uid: UniqueIdentifier, _participants: Set) - : this(participants = _participants.toMutableSet(), - externalId = uid.externalId, - uuid = uid.id) + : this(participants = _participants.toMutableSet(), + externalId = uid.externalId, + uuid = uid.id) } @MappedSuperclass diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt index 8e019d7b5b..a0e6c80f49 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -31,8 +31,8 @@ object NodeInfoSchemaV1 : MappedSchema( @Column(name = "legal_identities_certs") @ManyToMany(cascade = arrayOf(CascadeType.ALL)) @JoinTable(name = "link_nodeinfo_party", - joinColumns = arrayOf(JoinColumn(name="node_info_id")), - inverseJoinColumns = arrayOf(JoinColumn(name="party_name"))) + joinColumns = arrayOf(JoinColumn(name = "node_info_id")), + inverseJoinColumns = arrayOf(JoinColumn(name = "party_name"))) val legalIdentitiesAndCerts: List, @Column(name = "platform_version") @@ -64,14 +64,15 @@ object NodeInfoSchemaV1 : MappedSchema( @Entity data class DBHostAndPort( - @EmbeddedId - private val pk: PKHostAndPort + @EmbeddedId + private val pk: PKHostAndPort ) { companion object { fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort( PKHostAndPort(hostAndPort.host, hostAndPort.port) ) } + fun toHostAndPort(): NetworkHostAndPort { return NetworkHostAndPort(this.pk.host!!, this.pk.port!!) } diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index a02241aae2..cd55da4982 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -49,7 +49,8 @@ open class MappedSchema(schemaFamily: Class<*>, * A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The * [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself). */ -@MappedSuperclass @CordaSerializable open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : StatePersistable +@MappedSuperclass +@CordaSerializable open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : StatePersistable /** * Embedded [StateRef] representation used in state mapping. diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt index ff90f2a462..ce80444256 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt @@ -11,6 +11,9 @@ import java.lang.annotation.Inherited * * It also makes it possible for a code reviewer to clearly identify the classes that can be passed on the wire. * + * Do NOT include [AnnotationTarget.EXPRESSION] as one of the @Target parameters, as this would allow any Lambda to + * be serialised. This would be a security hole. + * * TODO: As we approach a long term wire format, this annotation will only be permitted on classes that meet certain criteria. */ @Target(AnnotationTarget.CLASS) diff --git a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt index 08a920ab63..a5934be42d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt @@ -1,7 +1,8 @@ package net.corda.core.serialization +import net.corda.core.CordaException import net.corda.core.crypto.SecureHash /** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found. */ @CordaSerializable -class MissingAttachmentsException(val ids: List) : Exception() \ No newline at end of file +class MissingAttachmentsException(val ids: List) : CordaException() \ No newline at end of file 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 131578482e..a91636ddc7 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.WriteOnceProperty import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.sequence +import java.sql.Blob /** * An abstraction for serializing and deserializing objects, with support for versioning of the wire format via @@ -115,6 +116,7 @@ interface SerializationContext { * The use case we are serializing or deserializing for. See [UseCase]. */ val useCase: UseCase + /** * Helper method to return a new context based on this context with the property added. */ @@ -184,6 +186,11 @@ inline fun SerializedBytes.deserialize(serializationFactory */ inline fun ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.sequence().deserialize(serializationFactory, context) +/** + * Convenience extension method for deserializing a JDBC Blob, utilising the defaults. + */ +inline fun Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context) + /** * Convenience extension method for serializing an object of type T, utilising the defaults. */ @@ -195,7 +202,6 @@ fun T.serialize(serializationFactory: SerializationFactory = Serializa * A type safe wrapper around a byte array that contains a serialised object. You can call [SerializedBytes.deserialize] * to get the original object back. */ -@Suppress("unused") // Type parameter is just for documentation purposes. class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { // It's OK to use lazy here because SerializedBytes is configured to use the ImmutableClassSerializer. val hash: SecureHash by lazy { bytes.sha256() } diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt deleted file mode 100644 index 91c5dea265..0000000000 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.corda.core.serialization - -interface SerializationCustomization { - fun addToWhitelist(vararg types: Class<*>) -} - diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt new file mode 100644 index 0000000000..0c2be0c16b --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt @@ -0,0 +1,16 @@ +package net.corda.core.serialization + +/** + * Provide a subclass of this via the [java.util.ServiceLoader] mechanism to be able to whitelist types for + * serialisation that you cannot otherwise annotate. The name of the class must appear in a text file on the + * classpath under the path META-INF/services/net.corda.core.serialization.SerializationWhitelist + */ +interface SerializationWhitelist { + /** + * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. + * + * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that + * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method. + */ + val whitelist: List> +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index da0d98dbe7..0d5a798d9f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -2,8 +2,9 @@ package net.corda.core.transactions import net.corda.core.contracts.* import net.corda.core.identity.Party -import net.corda.core.internal.indexOfOrThrow import net.corda.core.internal.castIfPossible +import net.corda.core.internal.indexOfOrThrow +import net.corda.core.internal.uncheckedCast import java.util.function.Predicate /** @@ -33,15 +34,13 @@ abstract class BaseTransaction : NamedByHash { } private fun checkNoDuplicateInputs() { - val duplicates = inputs.groupBy { it }.filter { it.value.size > 1 }.keys - check(duplicates.isEmpty()) { "Duplicate input states detected" } + check(inputs.size == inputs.toSet().size) { "Duplicate input states detected" } } /** * Returns a [StateAndRef] for the given output index. */ - @Suppress("UNCHECKED_CAST") - fun outRef(index: Int): StateAndRef = StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) + fun outRef(index: Int): StateAndRef = StateAndRef(uncheckedCast(outputs[index]), StateRef(id, index)) /** * Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. @@ -111,8 +110,7 @@ abstract class BaseTransaction : NamedByHash { */ fun outRefsOfType(clazz: Class): List> { return outputs.mapIndexedNotNull { index, state -> - @Suppress("UNCHECKED_CAST") - clazz.castIfPossible(state.data)?.let { StateAndRef(state as TransactionState, StateRef(id, index)) } + clazz.castIfPossible(state.data)?.let { StateAndRef(uncheckedCast(state), StateRef(id, index)) } } } 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 4fa5778071..1881431114 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -3,8 +3,12 @@ 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.UpgradeCommand import net.corda.core.internal.castIfPossible +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.Try +import java.security.PublicKey import java.util.* import java.util.function.Predicate @@ -45,6 +49,16 @@ data class LedgerTransaction( checkEncumbrancesValid() } + private companion object { + @JvmStatic + private fun createContractFor(className: ContractClassName): Try { + return Try.on { this::class.java.classLoader.loadClass(className).asSubclass(Contract::class.java).getConstructor().newInstance() } + } + } + + private val contracts: Map> = (inputs.map { it.state.contract } + outputs.map { it.contract }) + .toSet().map { it to createContractFor(it) }.toMap() + val inputStates: List get() = inputs.map { it.state.data } /** @@ -52,8 +66,7 @@ data class LedgerTransaction( * @param index The index into the inputs. * @return The [StateAndRef] */ - @Suppress("UNCHECKED_CAST") - fun inRef(index: Int): StateAndRef = inputs[index] as StateAndRef + fun inRef(index: Int): StateAndRef = uncheckedCast(inputs[index]) /** * Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified. @@ -63,7 +76,12 @@ data class LedgerTransaction( @Throws(TransactionVerificationException::class) fun verify() { verifyConstraints() - verifyContracts() + // TODO: make contract upgrade transactions have a separate type + if (commands.any { it.value is UpgradeCommand }) { + verifyContractUpgrade() + } else { + verifyContracts() + } } /** @@ -90,19 +108,18 @@ data class LedgerTransaction( * If any contract fails to verify, the whole transaction is considered to be invalid. */ private fun verifyContracts() { - val contracts = (inputs.map { it.state.contract } + outputs.map { it.contract }).toSet() - for (contractClassName in contracts) { - val contract = try { - assert(javaClass.classLoader == ClassLoader.getSystemClassLoader()) - javaClass.classLoader.loadClass(contractClassName).asSubclass(Contract::class.java).getConstructor().newInstance() - } catch (e: ClassNotFoundException) { - throw TransactionVerificationException.ContractCreationError(id, contractClassName, e) - } - - try { - contract.verify(this) - } catch (e: Throwable) { - throw TransactionVerificationException.ContractRejection(id, contract, e) + for (contractEntry in contracts.entries) { + val result = contractEntry.value + when (result) { + is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, contractEntry.key, result.exception) + is Try.Success -> { + val contract = result.value + try { + contract.verify(this) + } catch (e: Throwable) { + throw TransactionVerificationException.ContractRejection(id, contract, e) + } + } } } } @@ -153,6 +170,25 @@ data class LedgerTransaction( } } + private fun verifyContractUpgrade() { + // Contract Upgrade transaction should have 1 input, 1 output and 1 command. + val input = inputs.single().state + val output = outputs.single() + val commandData = commandsOfType().single() + + val command = commandData.value + val participantKeys: Set = input.data.participants.map { it.owningKey }.toSet() + val keysThatSigned: Set = commandData.signers.toSet() + @Suppress("UNCHECKED_CAST") + val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract + requireThat { + "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) + "Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract) + "Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass) + "Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data)) + } + } + /** * Given a type and a function that returns a grouping key, associates inputs and outputs together so that they * can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement @@ -230,8 +266,7 @@ data class LedgerTransaction( * @return the possibly empty list of inputs [StateAndRef] matching the clazz restriction. */ fun inRefsOfType(clazz: Class): List> { - @Suppress("UNCHECKED_CAST") - return inputs.mapNotNull { if (clazz.isInstance(it.state.data)) it as StateAndRef else null } + return inputs.mapNotNull { if (clazz.isInstance(it.state.data)) uncheckedCast, StateAndRef>(it) else null } } inline fun inRefsOfType(): List> = inRefsOfType(T::class.java) @@ -307,8 +342,7 @@ data class LedgerTransaction( * @param index the position of the item in the commands. * @return The Command at the requested index */ - @Suppress("UNCHECKED_CAST") - fun getCommand(index: Int): Command = Command(commands[index].value as T, commands[index].signers) + fun getCommand(index: Int): Command = Command(uncheckedCast(commands[index].value), commands[index].signers) /** * Helper to simplify getting all [Command] items with a [CommandData] of a particular class, interface, or base class. diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index ea99e30811..8f2c476004 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -1,5 +1,6 @@ package net.corda.core.transactions +import net.corda.core.CordaException import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party @@ -47,7 +48,7 @@ abstract class TraversableTransaction(open val componentGroups: List> get() { val result = mutableListOf(inputs, outputs, commands, attachments) @@ -104,7 +105,7 @@ class FilteredTransaction private constructor( * Construction of partial transaction from [WireTransaction] based on filtering. * Note that list of nonces to be sent is updated on the fly, based on the index of the filtered tx component. * @param filtering filtering over the whole WireTransaction. - * @returns a list of [FilteredComponentGroup] used in PartialMerkleTree calculation and verification. + * @return a list of [FilteredComponentGroup] used in PartialMerkleTree calculation and verification. */ private fun filterWithFun(wtx: WireTransaction, filtering: Predicate): List { val filteredSerialisedComponents: MutableMap> = hashMapOf() @@ -137,7 +138,7 @@ class FilteredTransaction private constructor( fun updateFilteredComponents() { wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.INPUTS_GROUP.ordinal, internalIndex) } wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.OUTPUTS_GROUP.ordinal, internalIndex) } - wtx.commands.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.COMMANDS_GROUP.ordinal, internalIndex) } + wtx.commands.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.COMMANDS_GROUP.ordinal, internalIndex) } wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) } if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0) if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0) @@ -146,7 +147,7 @@ class FilteredTransaction private constructor( // we decide to filter and attach this field to a FilteredTransaction. // An example would be to redact certain contract state types, but otherwise leave a transaction alone, // including the unknown new components. - wtx.componentGroups.filter { it.groupIndex >= ComponentGroupEnum.values().size }.forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component-> filter(component, componentGroup.groupIndex, internalIndex) }} + wtx.componentGroups.filter { it.groupIndex >= ComponentGroupEnum.values().size }.forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component -> filter(component, componentGroup.groupIndex, internalIndex) } } } fun createPartialMerkleTree(componentGroupIndex: Int) = PartialMerkleTree.build(MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), filteredComponentHashes[componentGroupIndex]!!) @@ -155,7 +156,7 @@ class FilteredTransaction private constructor( updateFilteredComponents() val filteredComponentGroups: MutableList = mutableListOf() filteredSerialisedComponents.forEach { (groupIndex, value) -> - filteredComponentGroups.add(FilteredComponentGroup(groupIndex, value, filteredComponentNonces[groupIndex]!!, createPartialMerkleTree(groupIndex) )) + filteredComponentGroups.add(FilteredComponentGroup(groupIndex, value, filteredComponentNonces[groupIndex]!!, createPartialMerkleTree(groupIndex))) } return filteredComponentGroups } @@ -182,7 +183,7 @@ class FilteredTransaction private constructor( // Compute partial Merkle roots for each filtered component and verify each of the partial Merkle trees. filteredComponentGroups.forEach { (groupIndex, components, nonces, groupPartialTree) -> - verificationCheck(groupIndex < groupHashes.size ) { "There is no matching component group hash for group $groupIndex" } + verificationCheck(groupIndex < groupHashes.size) { "There is no matching component group hash for group $groupIndex" } val groupMerkleRoot = groupHashes[groupIndex] verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) { "Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match" } verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) { "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" } @@ -196,7 +197,7 @@ class FilteredTransaction private constructor( * over a transaction with the attachment that wasn't verified. Of course it depends on how you implement it, but else -> false * should solve a problem with possible later extensions to WireTransaction. * @param checkingFun function that performs type checking on the structure fields and provides verification logic accordingly. - * @returns false if no elements were matched on a structure or checkingFun returned false. + * @return false if no elements were matched on a structure or checkingFun returned false. */ fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean { val checkList = availableComponentGroups.flatten().map { checkingFun(it) } @@ -225,7 +226,7 @@ class FilteredTransaction private constructor( "Did not receive components for group ${componentGroupEnum.ordinal} and cannot verify they didn't exist in the original wire transaction" } } else { - visibilityCheck(group.groupIndex < groupHashes.size ) { "There is no matching component group hash for group ${group.groupIndex}" } + visibilityCheck(group.groupIndex < groupHashes.size) { "There is no matching component group hash for group ${group.groupIndex}" } val groupPartialRoot = groupHashes[group.groupIndex] val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash visibilityCheck(groupPartialRoot == groupFullRoot) { "The partial Merkle tree root does not match with the received root for group ${group.groupIndex}" } @@ -252,7 +253,7 @@ class FilteredTransaction private constructor( * This is similar to [ComponentGroup], but it also includes the corresponding nonce per component. */ @CordaSerializable -data class FilteredComponentGroup(override val groupIndex: Int, override val components: List, val nonces: List, val partialMerkleTree: PartialMerkleTree): ComponentGroup(groupIndex, components) { +data class FilteredComponentGroup(override val groupIndex: Int, override val components: List, val nonces: List, val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { init { check(components.size == nonces.size) { "Size of transaction components and nonces do not match" } } @@ -263,11 +264,11 @@ data class FilteredComponentGroup(override val groupIndex: Int, override val com * @param reason information about the exception. */ @CordaSerializable -class ComponentVisibilityException(val id: SecureHash, val reason: String) : Exception("Component visibility error for transaction with id:$id. Reason: $reason") +class ComponentVisibilityException(val id: SecureHash, val reason: String) : CordaException("Component visibility error for transaction with id:$id. Reason: $reason") /** Thrown when [FilteredTransaction.verify] fails. * @param id transaction's id. * @param reason information about the exception. */ @CordaSerializable -class FilteredTransactionVerificationException(val id: SecureHash, val reason: String) : Exception("Transaction with id:$id cannot be verified. Reason: $reason") +class FilteredTransactionVerificationException(val id: SecureHash, val reason: String) : CordaException("Transaction with id:$id cannot be verified. Reason: $reason") diff --git a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt index ac75722df9..725e75da0f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt @@ -2,6 +2,7 @@ package net.corda.core.transactions import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionState +import net.corda.core.flows.FlowException import net.corda.core.serialization.CordaSerializable /** @@ -11,4 +12,4 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable class MissingContractAttachments(val states: List>) - : Exception("Cannot find contract attachments for ${states.map { it.contract }.distinct() }") \ No newline at end of file + : FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}") \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index f592f998b7..332718fd94 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -7,6 +7,7 @@ import net.corda.core.crypto.serializedHash import net.corda.core.utilities.toBase58String import net.corda.core.identity.Party import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.serialization.CordaSerializable import java.security.PublicKey @@ -39,9 +40,10 @@ data class NotaryChangeWireTransaction( */ override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) } - fun resolve(services: ServiceHub, sigs: List): NotaryChangeLedgerTransaction { + fun resolve(services: ServiceHub, sigs: List) = resolve(services as StateLoader, sigs) + fun resolve(stateLoader: StateLoader, sigs: List): NotaryChangeLedgerTransaction { val resolvedInputs = inputs.map { ref -> - services.loadState(ref).let { StateAndRef(it, ref) } + stateLoader.loadState(ref).let { StateAndRef(it, ref) } } return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 31857af7e9..c34fd26065 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -1,10 +1,13 @@ package net.corda.core.transactions +import net.corda.core.CordaException +import net.corda.core.CordaThrowable import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.internal.VisibleForTesting import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -44,7 +47,8 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** Cache the deserialized form of the transaction. This is useful when building a transaction or collecting signatures. */ - @Volatile @Transient private var cachedTransaction: CoreTransaction? = null + @Volatile + @Transient private var cachedTransaction: CoreTransaction? = null /** Lazily calculated access to the deserialised/hashed transaction data. */ private val transaction: CoreTransaction get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this } @@ -175,19 +179,52 @@ data class SignedTransaction(val txBits: SerializedBytes, fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction + /** + * Resolves the underlying base transaction and then returns it, handling any special case transactions such as + * [NotaryChangeWireTransaction]. + */ + fun resolveBaseTransaction(services: StateLoader): BaseTransaction { + return when (transaction) { + is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) + is WireTransaction -> this.tx + is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") + else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}") + } + } + + /** + * Resolves the underlying transaction with signatures and then returns it, handling any special case transactions + * such as [NotaryChangeWireTransaction]. + */ + fun resolveTransactionWithSignatures(services: ServiceHub): TransactionWithSignatures { + return when (transaction) { + is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) + is WireTransaction -> this + is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") + else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}") + } + } + /** * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ - fun resolveNotaryChangeTransaction(services: ServiceHub): NotaryChangeLedgerTransaction { + fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as StateLoader) + + fun resolveNotaryChangeTransaction(stateLoader: StateLoader): NotaryChangeLedgerTransaction { val ntx = transaction as? NotaryChangeWireTransaction ?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${transaction::class.simpleName}") - return ntx.resolve(services, sigs) + return ntx.resolve(stateLoader, sigs) } override fun toString(): String = "${javaClass.simpleName}(id=$id)" + private companion object { + private fun missingSignatureMsg(missing: Set, descriptions: List, id: SecureHash): String = + "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}" + } + @CordaSerializable class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) - : NamedByHash, SignatureException("Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}") + : NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) } 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 9f972ee155..b0149dce8d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -38,8 +38,8 @@ open class TransactionBuilder( protected val commands: MutableList> = arrayListOf(), protected var window: TimeWindow? = null, protected var privacySalt: PrivacySalt = PrivacySalt() - ) { - constructor(notary: Party) : this (notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) +) { + constructor(notary: Party) : this(notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) /** * Creates a copy of the builder. @@ -81,9 +81,10 @@ open class TransactionBuilder( * * @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder]. */ - @JvmOverloads @Throws(MissingContractAttachments::class) - fun toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction { + fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services) + + internal fun toWireTransactionWithContext(services: ServicesForResolution, 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 // will be available when building the transaction. In exceptional cases the TransactionStates must be created @@ -103,7 +104,9 @@ open class TransactionBuilder( } @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) - fun toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext? = null) = toWireTransaction(services, serializationContext).toLedgerTransaction(services) + fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services) + + internal fun toLedgerTransactionWithContext(services: ServiceHub, serializationContext: SerializationContext) = toWireTransactionWithContext(services, serializationContext).toLedgerTransaction(services) @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub) { @@ -122,7 +125,6 @@ open class TransactionBuilder( return this } - @JvmOverloads fun addOutputState(state: TransactionState<*>): TransactionBuilder { outputs.add(state) return this @@ -176,6 +178,7 @@ open class TransactionBuilder( // Accessors that yield immutable snapshots. fun inputStates(): List = ArrayList(inputs) + fun attachments(): List = ArrayList(attachments) fun outputStates(): List> = ArrayList(outputs) fun commands(): List> = ArrayList(commands) 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 39de4816a9..2325344286 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -63,15 +63,16 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr override val id: SecureHash get() = merkleTree.hash /** Public keys that need to be fulfilled by signatures in order for the transaction to be valid. */ - val requiredSigningKeys: Set get() { - val commandKeys = commands.flatMap { it.signers }.toSet() - // TODO: prevent notary field from being set if there are no inputs and no timestamp. - return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) { - commandKeys + notary.owningKey - } else { - commandKeys + val requiredSigningKeys: Set + get() { + val commandKeys = commands.flatMap { it.signers }.toSet() + // TODO: prevent notary field from being set if there are no inputs and no timestamp. + return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) { + commandKeys + notary.owningKey + } else { + commandKeys + } } - } /** * Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to @@ -84,7 +85,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { return toLedgerTransaction( resolveIdentity = { services.identityService.partyFromKey(it) }, - resolveAttachment = { services.attachments.openAttachment(it)}, + resolveAttachment = { services.attachments.openAttachment(it) }, resolveStateRef = { services.loadState(it) }, resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) } ) @@ -114,7 +115,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } // Open attachments specified in this transaction. If we haven't downloaded them, we fail. val contractAttachments = findAttachmentContracts(resolvedInputs, resolveContractAttachment, resolveAttachment) - val attachments = contractAttachments + (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) }).distinct() + // 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() return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt) } @@ -122,7 +124,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * Build filtered transaction using provided filtering functions. */ fun buildFilteredTransaction(filtering: Predicate): FilteredTransaction = - FilteredTransaction.buildFilteredTransaction(this, filtering) + FilteredTransaction.buildFilteredTransaction(this, filtering) /** * Builds whole Merkle tree for a transaction. @@ -235,7 +237,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr ): List { val contractAttachments = (outputs + resolvedInputs.map { it.state }).map { Pair(it, resolveContractAttachment(it)) } val missingAttachments = contractAttachments.filter { it.second == null } - return if(missingAttachments.isEmpty()) { + return if (missingAttachments.isEmpty()) { contractAttachments.map { ContractAttachment(resolveAttachment(it.second!!) ?: throw AttachmentResolutionException(it.second!!), it.first.contract) } } else { throw MissingContractAttachments(missingAttachments.map { it.first }) diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 23eb6d86e5..cf8b1f915d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -25,6 +25,7 @@ sealed class ByteSequence : Comparable { * The start position of the sequence within the byte array. */ abstract val offset: Int + /** Returns a [ByteArrayInputStream] of the bytes */ fun open() = ByteArrayInputStream(bytes, offset, size) @@ -117,8 +118,11 @@ sealed class ByteSequence : Comparable { * In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such * functionality to Java, but it won't arrive for a few years yet! */ -open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() { +open class OpaqueBytes(bytes: ByteArray) : ByteSequence() { companion object { + /** + * Create [OpaqueBytes] from a sequence of [Byte] values. + */ @JvmStatic fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b)) } @@ -127,13 +131,35 @@ open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() { require(bytes.isNotEmpty()) } - override val size: Int get() = bytes.size - override val offset: Int get() = 0 + /** + * The bytes are always cloned so that this object becomes immutable. This has been done + * to prevent tampering with entities such as [SecureHash] and [PrivacySalt], as well as + * preserve the integrity of our hash constants [zeroHash] and [allOnesHash]. + * + * Cloning like this may become a performance issue, depending on whether or not the JIT + * compiler is ever able to optimise away the clone. In which case we may need to revisit + * this later. + */ + override final val bytes: ByteArray = bytes + get() = field.clone() + override val size: Int = bytes.size + override val offset: Int = 0 } +/** + * Copy [size] bytes from this [ByteArray] starting from [offset] into a new [ByteArray]. + */ fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of(this, offset, size) +/** + * Converts this [ByteArray] into a [String] of hexadecimal digits. + */ fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this) + +/** + * Converts this [String] of hexadecimal digits into a [ByteArray]. + * @throws IllegalArgumentException if the [String] contains incorrectly-encoded characters. + */ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this) /** diff --git a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index 1cc4e53807..2fd31c9590 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -3,9 +3,8 @@ package net.corda.core.utilities import net.corda.core.crypto.Base58 +import net.corda.core.crypto.Crypto import net.corda.core.crypto.sha256 -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize import java.nio.charset.Charset import java.security.PublicKey import java.util.* @@ -15,11 +14,13 @@ import javax.xml.bind.DatatypeConverter // [ByteArray] encoders +/** Convert a byte array to a Base58 encoded [String]. */ fun ByteArray.toBase58(): String = Base58.encode(this) +/** Convert a byte array to a Base64 encoded [String]. */ fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) -/** Convert a byte array to a hex (base 16) capitalized encoded string.*/ +/** Convert a byte array to a hex (Base16) capitalized encoded [String]. */ fun ByteArray.toHex(): String = DatatypeConverter.printHexBinary(this) @@ -65,6 +66,15 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64() // TODO We use for both CompositeKeys and EdDSAPublicKey custom serializers and deserializers. We need to specify encoding. // TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition // structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519). -fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize() -fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58() -fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes // TODO: decide on the format of hashed key (encoded Vs serialised). +/** + * Method to return the [PublicKey] object given its Base58-[String] representation. + * @param base58String the Base58 encoded format of the serialised [PublicKey]. + * @return the resulted [PublicKey] after decoding the [base58String] input and then deserialising to a [PublicKey] object. + */ +fun parsePublicKeyBase58(base58String: String): PublicKey = Crypto.decodePublicKey(base58String.base58ToByteArray()) + +/** Return the Base58 representation of the serialised public key. */ +fun PublicKey.toBase58String(): String = this.encoded.toBase58() + +/** Return the bytes of the SHA-256 output for this public key. */ +fun PublicKey.toSHA256Bytes(): ByteArray = this.encoded.sha256().bytes diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index a726dbf7fb..b12ea8353d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -1,6 +1,7 @@ package net.corda.core.utilities import net.corda.core.internal.concurrent.get +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -101,14 +102,13 @@ private class TransientProperty internal constructor(private val initiali @Transient private var initialised = false @Transient private var value: T? = null - @Suppress("UNCHECKED_CAST") @Synchronized override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { if (!initialised) { value = initialiser() initialised = true } - return value as T + return uncheckedCast(value) } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt index 8866e79e1b..6153c1c5d2 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt @@ -27,7 +27,7 @@ data class NetworkHostAndPort(val host: String, val port: Int) { fun parse(str: String): NetworkHostAndPort { val uri = try { URI(null, str, null, null, null) - } catch(ex: URISyntaxException) { + } catch (ex: URISyntaxException) { throw IllegalArgumentException("Host and port syntax is invalid, expected host:port") } require(uri.host != null) { NetworkHostAndPort.UNPARSEABLE_ADDRESS_FORMAT.format(str) } diff --git a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt index 9e364769e2..ab5f0b86d7 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt @@ -48,11 +48,13 @@ class NonEmptySet private constructor(private val elements: Set) : Set /** Returns the first element of the set. */ fun head(): T = elements.iterator().next() + override fun isEmpty(): Boolean = false override fun iterator() = object : Iterator by elements.iterator() {} // Following methods are not delegated by Kotlin's Class delegation override fun forEach(action: Consumer) = elements.forEach(action) + override fun stream(): Stream = elements.stream() override fun parallelStream(): Stream = elements.parallelStream() override fun spliterator(): Spliterator = elements.spliterator() diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt index 4ff2224ade..cfe471c1b1 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Try.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -1,5 +1,6 @@ package net.corda.core.utilities +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Try.Failure import net.corda.core.utilities.Try.Success @@ -35,30 +36,27 @@ sealed class Try { abstract fun getOrThrow(): A /** Maps the given function to the value from this [Success], or returns `this` if this is a [Failure]. */ - @Suppress("UNCHECKED_CAST") inline fun map(function: (A) -> B): Try = when (this) { is Success -> Success(function(value)) - is Failure -> this as Try + is Failure -> uncheckedCast(this) } /** Returns the given function applied to the value from this [Success], or returns `this` if this is a [Failure]. */ - @Suppress("UNCHECKED_CAST") inline fun flatMap(function: (A) -> Try): Try = when (this) { is Success -> function(value) - is Failure -> this as Try + is Failure -> uncheckedCast(this) } /** * Maps the given function to the values from this [Success] and [other], or returns `this` if this is a [Failure] * or [other] if [other] is a [Failure]. */ - @Suppress("UNCHECKED_CAST") inline fun combine(other: Try, function: (A, B) -> C): Try = when (this) { is Success -> when (other) { is Success -> Success(function(value, other.value)) - is Failure -> other as Try + is Failure -> uncheckedCast(other) } - is Failure -> this as Try + is Failure -> uncheckedCast(this) } data class Success(val value: A) : Try() { diff --git a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java index 01855fb5e3..6e836dbaba 100644 --- a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java @@ -5,6 +5,7 @@ import com.google.common.primitives.Primitives; import net.corda.core.identity.Party; import net.corda.node.internal.StartedNode; import net.corda.testing.node.MockNetwork; +import net.corda.testing.TestConstants; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -19,17 +20,17 @@ import static org.junit.Assert.fail; public class FlowsInJavaTest { private final MockNetwork mockNet = new MockNetwork(); - private StartedNode node1; - private StartedNode node2; + private StartedNode aliceNode; + private StartedNode bobNode; @Before public void setUp() throws Exception { - MockNetwork.BasketOfNodes someNodes = mockNet.createSomeNodes(2); - node1 = someNodes.getPartyNodes().get(0); - node2 = someNodes.getPartyNodes().get(1); + mockNet.createNotaryNode(); + aliceNode = mockNet.createPartyNode(TestConstants.getALICE().getName()); + bobNode = mockNet.createPartyNode(TestConstants.getBOB().getName()); mockNet.runNetwork(); // Ensure registration was successful - node1.getInternals().getNodeReadyFuture().get(); + aliceNode.getInternals().getNodeReadyFuture().get(); } @After @@ -39,8 +40,8 @@ public class FlowsInJavaTest { @Test public void suspendableActionInsideUnwrap() throws Exception { - node2.getInternals().registerInitiatedFlow(SendHelloAndThenReceive.class); - Future result = node1.getServices().startFlow(new SendInUnwrapFlow(chooseIdentity(node2.getInfo()))).getResultFuture(); + bobNode.getInternals().registerInitiatedFlow(SendHelloAndThenReceive.class); + Future result = aliceNode.getServices().startFlow(new SendInUnwrapFlow(chooseIdentity(bobNode.getInfo()))).getResultFuture(); mockNet.runNetwork(); assertThat(result.get()).isEqualTo("Hello"); } @@ -55,8 +56,8 @@ public class FlowsInJavaTest { } private void primitiveReceiveTypeTest(Class receiveType) throws InterruptedException { - PrimitiveReceiveFlow flow = new PrimitiveReceiveFlow(chooseIdentity(node2.getInfo()), receiveType); - Future result = node1.getServices().startFlow(flow).getResultFuture(); + PrimitiveReceiveFlow flow = new PrimitiveReceiveFlow(chooseIdentity(bobNode.getInfo()), receiveType); + Future result = aliceNode.getServices().startFlow(flow).getResultFuture(); mockNet.runNetwork(); try { result.get(); diff --git a/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt index 79d06b3ab4..7ed31d3d38 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt @@ -45,11 +45,11 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { // Do not add attachments (empty list). private val componentGroupsA by lazy { listOf( - inputGroup, - outputGroup, - commandGroup, - notaryGroup, - timeWindowGroup + inputGroup, + outputGroup, + commandGroup, + notaryGroup, + timeWindowGroup ) } private val wireTransactionA by lazy { WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt) } @@ -199,6 +199,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { else -> false } } + val ftxInputs = wireTransactionA.buildFilteredTransaction(Predicate(::filtering)) // Inputs only filtered. ftxInputs.verify() ftxInputs.checkAllComponentsVisible(INPUTS_GROUP) @@ -210,6 +211,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { // Filter one input only. fun filteringOneInput(elem: Any) = elem == inputs[0] + val ftxOneInput = wireTransactionA.buildFilteredTransaction(Predicate(::filteringOneInput)) // First input only filtered. ftxOneInput.verify() assertFailsWith { ftxOneInput.checkAllComponentsVisible(INPUTS_GROUP) } 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 153ce6cc47..24509c3c52 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,6 +1,7 @@ package net.corda.core.contracts import net.corda.core.crypto.SecureHash +import net.corda.core.internal.UpgradeCommand import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase diff --git a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt index 8273cd9a80..aa8b071299 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt @@ -22,7 +22,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() { @Before fun setup() { - services.mockCordappProvider.addMockCordapp(DummyContract.PROGRAM_ID, services) + services.mockCordappProvider.addMockCordapp(DummyContract.PROGRAM_ID, services.attachments) } interface Commands { diff --git a/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt b/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt index 453b01eb65..f0da2499b9 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt @@ -1,9 +1,12 @@ package net.corda.core.contracts +import net.corda.core.internal.div +import net.corda.core.internal.times import net.corda.core.utilities.millis import net.corda.core.utilities.minutes import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset.UTC @@ -17,6 +20,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isEqualTo(now) assertThat(timeWindow.untilTime).isNull() assertThat(timeWindow.midpoint).isNull() + assertThat(timeWindow.length).isNull() assertThat(timeWindow.contains(now - 1.millis)).isFalse() assertThat(timeWindow.contains(now)).isTrue() assertThat(timeWindow.contains(now + 1.millis)).isTrue() @@ -28,6 +32,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isNull() assertThat(timeWindow.untilTime).isEqualTo(now) assertThat(timeWindow.midpoint).isNull() + assertThat(timeWindow.length).isNull() assertThat(timeWindow.contains(now - 1.millis)).isTrue() assertThat(timeWindow.contains(now)).isFalse() assertThat(timeWindow.contains(now + 1.millis)).isFalse() @@ -42,6 +47,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isEqualTo(fromTime) assertThat(timeWindow.untilTime).isEqualTo(untilTime) assertThat(timeWindow.midpoint).isEqualTo(today.atTime(12, 15).toInstant(UTC)) + assertThat(timeWindow.length).isEqualTo(Duration.between(fromTime, untilTime)) assertThat(timeWindow.contains(fromTime - 1.millis)).isFalse() assertThat(timeWindow.contains(fromTime)).isTrue() assertThat(timeWindow.contains(fromTime + 1.millis)).isTrue() @@ -51,17 +57,21 @@ class TimeWindowTest { @Test fun fromStartAndDuration() { - val timeWindow = TimeWindow.fromStartAndDuration(now, 10.minutes) + val duration = 10.minutes + val timeWindow = TimeWindow.fromStartAndDuration(now, duration) assertThat(timeWindow.fromTime).isEqualTo(now) - assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes) - assertThat(timeWindow.midpoint).isEqualTo(now + 5.minutes) + assertThat(timeWindow.untilTime).isEqualTo(now + duration) + assertThat(timeWindow.midpoint).isEqualTo(now + duration / 2) + assertThat(timeWindow.length).isEqualTo(duration) } @Test fun withTolerance() { - val timeWindow = TimeWindow.withTolerance(now, 10.minutes) - assertThat(timeWindow.fromTime).isEqualTo(now - 10.minutes) - assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes) + val tolerance = 10.minutes + val timeWindow = TimeWindow.withTolerance(now, tolerance) + assertThat(timeWindow.fromTime).isEqualTo(now - tolerance) + assertThat(timeWindow.untilTime).isEqualTo(now + tolerance) assertThat(timeWindow.midpoint).isEqualTo(now) + assertThat(timeWindow.length).isEqualTo(tolerance * 2) } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 0b7c6187e9..0c65317b44 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,13 +1,12 @@ package net.corda.core.crypto import net.corda.core.crypto.CompositeKey.NodeAndWeight -import net.corda.core.crypto.composite.CompositeSignaturesWithKeys import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.cert import net.corda.core.internal.declaredField import net.corda.core.internal.div import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes -import net.corda.core.internal.cert import net.corda.core.utilities.toBase58String import net.corda.node.utilities.* import net.corda.testing.TestDependencyInjectionBase @@ -217,7 +216,7 @@ class CompositeKeyTests : TestDependencyInjectionBase() { } @Test() - fun `composite key validation with graph cycle detection`() = kryoSpecific("Cycle exists in the object graph which is not currently supported in AMQP mode") { + fun `composite key validation with graph cycle detection`() = kryoSpecific("Cycle exists in the object graph which is not currently supported in AMQP mode") { val key1 = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() as CompositeKey val key2 = CompositeKey.Builder().addKeys(alicePublicKey, key1).build() as CompositeKey val key3 = CompositeKey.Builder().addKeys(alicePublicKey, key2).build() as CompositeKey diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index 33e060453f..88854550a4 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -20,7 +20,7 @@ class X509NameConstraintsTest { private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality= "London", country = "GB"), rootKeys) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootKeys) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", locality = "London", country = "GB"), intermediateCAKeyPair.public) diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 5a61e01605..aae7f6fc0d 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -10,14 +10,14 @@ import net.corda.core.internal.FetchDataFlow import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.node.utilities.DatabaseTransactionManager -import net.corda.testing.chooseIdentity +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.ALICE +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB import net.corda.testing.node.MockNetwork +import net.corda.testing.singleIdentity import org.junit.After import org.junit.Before import org.junit.Test @@ -43,7 +43,7 @@ class AttachmentTests { mockNet.stopNodes() } - fun fakeAttachment(): ByteArray { + private fun fakeAttachment(): ByteArray { val bs = ByteArrayOutputStream() val js = JarOutputStream(bs) js.putNextEntry(ZipEntry("file1.txt")) @@ -55,90 +55,90 @@ class AttachmentTests { @Test fun `download and store`() { - val nodes = mockNet.createSomeNodes(2) - val n0 = nodes.partyNodes[0] - val n1 = nodes.partyNodes[1] + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() - n0.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() + val alice = aliceNode.info.singleIdentity() - n0.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) - n1.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) // Insert an attachment into node zero's store directly. - val id = n0.database.transaction { - n0.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) + val id = aliceNode.database.transaction { + aliceNode.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) } // Get node one to run a flow to fetch it and insert it. mockNet.runNetwork() - val f1 = n1.startAttachmentFlow(setOf(id), n0.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) mockNet.runNetwork() - assertEquals(0, f1.resultFuture.getOrThrow().fromDisk.size) + assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size) // Verify it was inserted into node one's store. - val attachment = n1.database.transaction { - n1.attachments.openAttachment(id)!! + val attachment = bobNode.database.transaction { + bobNode.attachments.openAttachment(id)!! } assertEquals(id, attachment.open().readBytes().sha256()) // Shut down node zero and ensure node one can still resolve the attachment. - n0.dispose() + aliceNode.dispose() - val response: FetchDataFlow.Result = n1.startAttachmentFlow(setOf(id), n0.info.chooseIdentity()).resultFuture.getOrThrow() + val response: FetchDataFlow.Result = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow() assertEquals(attachment, response.fromDisk[0]) } @Test fun `missing`() { - val nodes = mockNet.createSomeNodes(2) - val n0 = nodes.partyNodes[0] - val n1 = nodes.partyNodes[1] + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() - n0.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() - n0.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) - n1.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) // Get node one to fetch a non-existent attachment. val hash = SecureHash.randomSHA256() mockNet.runNetwork() - val f1 = n1.startAttachmentFlow(setOf(hash), n0.info.chooseIdentity()) + val alice = aliceNode.info.singleIdentity() + val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice) mockNet.runNetwork() - val e = assertFailsWith { f1.resultFuture.getOrThrow() } + val e = assertFailsWith { bobFlow.resultFuture.getOrThrow() } assertEquals(hash, e.requested) } @Test fun `malicious response`() { // Make a node that doesn't do sanity checking at load time. - val n0 = mockNet.createNode(nodeFactory = object : MockNetwork.Factory { + val aliceNode = mockNet.createNotaryNode(legalName = ALICE.name, nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, - overrideServices: Map?, + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } } } - }, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - val n1 = mockNet.createNode(n0.network.myAddress) + }, validating = false) + val bobNode = mockNet.createNode(legalName = BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() - n0.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() + val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) - n0.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) - n1.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) val attachment = fakeAttachment() // Insert an attachment into node zero's store directly. - val id = n0.database.transaction { - n0.attachments.importAttachment(ByteArrayInputStream(attachment)) + val id = aliceNode.database.transaction { + aliceNode.attachments.importAttachment(ByteArrayInputStream(attachment)) } // Corrupt its store. @@ -146,15 +146,15 @@ class AttachmentTests { System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size) val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment) - n0.database.transaction { - DatabaseTransactionManager.current().session.update(corruptAttachment) + aliceNode.database.transaction { + session.update(corruptAttachment) } // Get n1 to fetch the attachment. Should receive corrupted bytes. mockNet.runNetwork() - val f1 = n1.startAttachmentFlow(setOf(id), n0.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) mockNet.runNetwork() - assertFailsWith { f1.resultFuture.getOrThrow() } + assertFailsWith { bobFlow.resultFuture.getOrThrow() } } private fun StartedNode<*>.startAttachmentFlow(hashes: Set, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes)) diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 6ea86cd6a3..a0b2c0020c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -23,35 +23,41 @@ import kotlin.reflect.KClass import kotlin.test.assertFailsWith class CollectSignaturesFlowTests { + companion object { + private val cordappPackages = listOf("net.corda.testing.contracts") + } + lateinit var mockNet: MockNetwork - lateinit var a: StartedNode - lateinit var b: StartedNode - lateinit var c: StartedNode + lateinit var aliceNode: StartedNode + lateinit var bobNode: StartedNode + lateinit var charlieNode: StartedNode + lateinit var alice: Party + lateinit var bob: Party + lateinit var charlie: Party lateinit var notary: Party - lateinit var services: MockServices @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - services = MockServices() - mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes(3) - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - c = nodes.partyNodes[2] + mockNet = MockNetwork(cordappPackages = cordappPackages) + val notaryNode = mockNet.createNotaryNode() + aliceNode = mockNet.createPartyNode(ALICE.name) + bobNode = mockNet.createPartyNode(BOB.name) + charlieNode = mockNet.createPartyNode(CHARLIE.name) mockNet.runNetwork() - notary = a.services.getDefaultNotary() - a.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + charlie = charlieNode.info.singleIdentity() + notary = notaryNode.services.getDefaultNotary() } @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } private fun registerFlowOnAllNodes(flowClass: KClass>) { - listOf(a, b, c).forEach { + listOf(aliceNode, bobNode, charlieNode).forEach { it.internals.registerInitiatedFlow(flowClass.java) } } @@ -142,18 +148,19 @@ class CollectSignaturesFlowTests { @Test fun `successfully collects two signatures`() { - val bConfidentialIdentity = b.database.transaction { - b.services.keyManagementService.freshKeyAndCert(b.info.chooseIdentityAndCert(), false) + val bConfidentialIdentity = bobNode.database.transaction { + val bobCert = bobNode.services.myInfo.legalIdentitiesAndCerts.single { it.name == bob.name } + bobNode.services.keyManagementService.freshKeyAndCert(bobCert, false) } - a.database.transaction { + aliceNode.database.transaction { // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity - a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) + aliceNode.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) } registerFlowOnAllNodes(TestFlowTwo.Responder::class) val magicNumber = 1337 - val parties = listOf(a.info.chooseIdentity(), bConfidentialIdentity.party, c.info.chooseIdentity()) + val parties = listOf(alice, bConfidentialIdentity.party, charlie) val state = DummyContract.MultiOwnerState(magicNumber, parties) - val flow = a.services.startFlow(TestFlowTwo.Initiator(state)) + val flow = aliceNode.services.startFlow(TestFlowTwo.Initiator(state)) mockNet.runNetwork() val result = flow.resultFuture.getOrThrow() result.verifyRequiredSignatures() @@ -163,9 +170,9 @@ class CollectSignaturesFlowTests { @Test fun `no need to collect any signatures`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.chooseIdentity().ref(1)) - val ptx = a.services.signInitialTransaction(onePartyDummyContract) - val flow = a.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) + val ptx = aliceNode.services.signInitialTransaction(onePartyDummyContract) + val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() val result = flow.resultFuture.getOrThrow() result.verifyRequiredSignatures() @@ -175,10 +182,10 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.chooseIdentity().ref(1)) - val miniCorpServices = MockServices(MINI_CORP_KEY) + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) + val miniCorpServices = MockServices(cordappPackages, MINI_CORP_KEY) val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) - val flow = a.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) + val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() assertFailsWith("The Initiator of CollectSignaturesFlow must have signed the transaction.") { flow.resultFuture.getOrThrow() @@ -188,12 +195,12 @@ class CollectSignaturesFlowTests { @Test fun `passes with multiple initial signatures`() { val twoPartyDummyContract = DummyContract.generateInitial(1337, notary, - a.info.chooseIdentity().ref(1), - b.info.chooseIdentity().ref(2), - b.info.chooseIdentity().ref(3)) - val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) - val signedByBoth = b.services.addSignature(signedByA) - val flow = a.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet())) + alice.ref(1), + bob.ref(2), + bob.ref(3)) + val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract) + val signedByBoth = bobNode.services.addSignature(signedByA) + val flow = aliceNode.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet())) mockNet.runNetwork() val result = flow.resultFuture.getOrThrow() println(result.tx) diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 4ef934798b..782d3ee35f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -34,72 +34,63 @@ import kotlin.test.assertTrue class ContractUpgradeFlowTest { lateinit var mockNet: MockNetwork - lateinit var a: StartedNode - lateinit var b: StartedNode + lateinit var aliceNode: StartedNode + lateinit var bobNode: StartedNode lateinit var notary: Party @Before fun setup() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows") - mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows")) + val notaryNode = mockNet.createNotaryNode() + aliceNode = mockNet.createPartyNode(ALICE.name) + bobNode = mockNet.createPartyNode(BOB.name) // Process registration mockNet.runNetwork() - a.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() - notary = a.services.getDefaultNotary() - val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == notary } - a.database.transaction { - a.services.identityService.verifyAndRegisterIdentity(nodeIdentity) - } - b.database.transaction { - b.services.identityService.verifyAndRegisterIdentity(nodeIdentity) - } + notary = notaryNode.services.getDefaultNotary() } @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `2 parties contract upgrade`() { // Create dummy contract. - val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.chooseIdentity().ref(1), b.info.chooseIdentity().ref(1)) - val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) - val stx = b.services.addSignature(signedByA) + val twoPartyDummyContract = DummyContract.generateInitial(0, notary, aliceNode.info.chooseIdentity().ref(1), bobNode.info.chooseIdentity().ref(1)) + val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract) + val stx = bobNode.services.addSignature(signedByA) - a.services.startFlow(FinalityFlow(stx, setOf(b.info.chooseIdentity()))) + aliceNode.services.startFlow(FinalityFlow(stx, setOf(bobNode.info.chooseIdentity()))) mockNet.runNetwork() - val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } + val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) } + val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) // The request is expected to be rejected because party B hasn't authorised the upgrade yet. - val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val rejectedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade, and immediately deauthorise the same. - b.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() - b.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef(0).ref)).resultFuture.getOrThrow() + bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() + bobNode.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef(0).ref)).resultFuture.getOrThrow() // The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade. - val deauthorisedFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val deauthorisedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() } // Party B authorise the contract state upgrade - b.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() + bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() // Party A initiates contract upgrade flow, expected to succeed this time. - val resultFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val resultFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() val result = resultFuture.getOrThrow() @@ -120,8 +111,8 @@ class ContractUpgradeFlowTest { // Verify outputs. assertTrue(nodeStx!!.tx.outputs.single().data is DummyContractV2.State) } - check(a) - check(b) + check(aliceNode) + check(bobNode) } private fun RPCDriverExposedDSLInterface.startProxy(node: StartedNode<*>, user: User): CordaRPCOps { @@ -139,9 +130,9 @@ class ContractUpgradeFlowTest { fun `2 parties contract upgrade using RPC`() { rpcDriver(initialiseSerialization = false) { // Create dummy contract. - val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.chooseIdentity().ref(1), b.info.chooseIdentity().ref(1)) - val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) - val stx = b.services.addSignature(signedByA) + val twoPartyDummyContract = DummyContract.generateInitial(0, notary, aliceNode.info.chooseIdentity().ref(1), bobNode.info.chooseIdentity().ref(1)) + val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract) + val stx = bobNode.services.addSignature(signedByA) val user = rpcTestUser.copy(permissions = setOf( startFlowPermission(), @@ -149,14 +140,14 @@ class ContractUpgradeFlowTest { startFlowPermission(), startFlowPermission() )) - val rpcA = startProxy(a, user) - val rpcB = startProxy(b, user) - val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(b.info.chooseIdentity())) + val rpcA = startProxy(aliceNode, user) + val rpcB = startProxy(bobNode, user) + val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(bobNode.info.chooseIdentity())) mockNet.runNetwork() handle.returnValue.getOrThrow() - val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } + val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) } + val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) @@ -168,14 +159,14 @@ class ContractUpgradeFlowTest { assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade, and immediately deauthorise the same. - rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) }, + rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, btx!!.tx.outRef(0), DummyContractV2::class.java).returnValue - rpcB.startFlow( { stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) }, + rpcB.startFlow({ stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) }, btx.tx.outRef(0).ref).returnValue // The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade. - val deauthorisedFuture = rpcA.startFlow( {stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) }, + val deauthorisedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) }, atx.tx.outRef(0), DummyContractV2::class.java).returnValue @@ -183,7 +174,7 @@ class ContractUpgradeFlowTest { assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() } // Party B authorise the contract state upgrade. - rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) }, + rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, btx.tx.outRef(0), DummyContractV2::class.java).returnValue @@ -195,12 +186,12 @@ class ContractUpgradeFlowTest { mockNet.runNetwork() val result = resultFuture.getOrThrow() // Check results. - listOf(a, b).forEach { - val signedTX = a.database.transaction { a.services.validatedTransactions.getTransaction(result.ref.txhash) } + listOf(aliceNode, bobNode).forEach { + val signedTX = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash) } requireNotNull(signedTX) // Verify inputs. - val input = a.database.transaction { a.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } + val input = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } requireNotNull(input) assertTrue(input!!.tx.outputs.single().data is DummyContract.State) @@ -213,20 +204,20 @@ class ContractUpgradeFlowTest { @Test fun `upgrade Cash to v2`() { // Create some cash. - val chosenIdentity = a.info.chooseIdentity() - val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture + val chosenIdentity = aliceNode.info.chooseIdentity() + val result = aliceNode.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture mockNet.runNetwork() val stx = result.getOrThrow().stx val anonymisedRecipient = result.get().recipient!! val stateAndRef = stx.tx.outRef(0) - val baseState = a.database.transaction { a.services.vaultQueryService.queryBy().states.single() } + val baseState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy().states.single() } assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") // Starts contract upgrade flow. - val upgradeResult = a.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java)).resultFuture + val upgradeResult = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java)).resultFuture mockNet.runNetwork() upgradeResult.getOrThrow() // Get contract state from the vault. - val firstState = a.database.transaction { a.services.vaultQueryService.queryBy().states.single() } + val firstState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy().states.single() } assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") assertEquals>(listOf(anonymisedRecipient), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index 865026c958..bdc76e1d83 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -6,14 +6,9 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.POUNDS import net.corda.finance.contracts.asset.Cash import net.corda.finance.issuedBy -import net.corda.node.internal.StartedNode -import net.corda.testing.ALICE -import net.corda.testing.chooseIdentity -import net.corda.testing.getDefaultNotary +import net.corda.node.services.api.ServiceHubInternal +import net.corda.testing.* import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockServices -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -21,54 +16,57 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class FinalityFlowTests { - lateinit var mockNet: MockNetwork - lateinit var nodeA: StartedNode - lateinit var nodeB: StartedNode - lateinit var notary: Party - val services = MockServices() + private lateinit var mockNet: MockNetwork + private lateinit var aliceServices: ServiceHubInternal + private lateinit var bobServices: ServiceHubInternal + private lateinit var alice: Party + private lateinit var bob: Party + private lateinit var notary: Party @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes(2) - nodeA = nodes.partyNodes[0] - nodeB = nodes.partyNodes[1] + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) mockNet.runNetwork() - nodeA.internals.ensureRegistered() - notary = nodeA.services.getDefaultNotary() + aliceNode.internals.ensureRegistered() + aliceServices = aliceNode.services + bobServices = bobNode.services + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + notary = notaryNode.services.getDefaultNotary() } @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `finalise a simple transaction`() { - val amount = 1000.POUNDS.issuedBy(nodeA.info.chooseIdentity().ref(0)) + val amount = 1000.POUNDS.issuedBy(alice.ref(0)) val builder = TransactionBuilder(notary) - Cash().generateIssue(builder, amount, nodeB.info.chooseIdentity(), notary) - val stx = nodeA.services.signInitialTransaction(builder) - val flow = nodeA.services.startFlow(FinalityFlow(stx)) + Cash().generateIssue(builder, amount, bob, notary) + val stx = aliceServices.signInitialTransaction(builder) + val flow = aliceServices.startFlow(FinalityFlow(stx)) mockNet.runNetwork() val notarisedTx = flow.resultFuture.getOrThrow() notarisedTx.verifyRequiredSignatures() - val transactionSeenByB = nodeB.services.database.transaction { - nodeB.services.validatedTransactions.getTransaction(notarisedTx.id) + val transactionSeenByB = bobServices.database.transaction { + bobServices.validatedTransactions.getTransaction(notarisedTx.id) } assertEquals(notarisedTx, transactionSeenByB) } @Test fun `reject a transaction with unknown parties`() { - val amount = 1000.POUNDS.issuedBy(nodeA.info.chooseIdentity().ref(0)) - val fakeIdentity = ALICE // Alice isn't part of this network, so node A won't recognise them + val amount = 1000.POUNDS.issuedBy(alice.ref(0)) + val fakeIdentity = CHARLIE // Charlie isn't part of this network, so node A won't recognise them val builder = TransactionBuilder(notary) Cash().generateIssue(builder, amount, fakeIdentity, notary) - val stx = nodeA.services.signInitialTransaction(builder) - val flow = nodeA.services.startFlow(FinalityFlow(stx)) + val stx = aliceServices.signInitialTransaction(builder) + val flow = aliceServices.startFlow(FinalityFlow(stx)) mockNet.runNetwork() assertFailsWith { flow.resultFuture.getOrThrow() diff --git a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt new file mode 100644 index 0000000000..6b6f0492b3 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt @@ -0,0 +1,115 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +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 kotlin.reflect.KClass + +/** + * Allows to simplify writing flows that simply rend a message back to an initiating flow. + */ +class Answer(session: FlowSession, override val answer: R, closure: (result: R) -> Unit = {}) : SimpleAnswer(session, closure) + +/** + * Allows to simplify writing flows that simply rend a message back to an initiating flow. + */ +abstract class SimpleAnswer(private val session: FlowSession, private val closure: (result: R) -> Unit = {}) : FlowLogic() { + @Suspendable + override fun call() { + val tmp = answer + closure(tmp) + session.send(tmp) + } + + protected abstract val answer: R +} + +/** + * A flow that does not do anything when triggered. + */ +class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic() { + @Suspendable + override fun call() = closure() +} + +/** + * Allows to register a flow of type [R] against an initiating flow of type [I]. + */ +inline fun , reified R : FlowLogic<*>> StartedNode<*>.registerInitiatedFlow(initiatingFlowType: KClass, crossinline construct: (session: FlowSession) -> R) { + internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true) +} + +/** + * Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R]. + */ +inline fun , reified R : Any> StartedNode<*>.registerAnswer(initiatingFlowType: KClass, value: R) { + internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true) +} + +/** + * Extracts data from a [Map[FlowSession, UntrustworthyData]] without performing checks and casting to [R]. + */ +@Suppress("UNCHECKED_CAST") +infix fun Map>.from(session: FlowSession): R = this[session]!!.unwrap { it as R } + +/** + * Creates a [Pair([session], [Class])] from this [Class]. + */ +infix fun > T.from(session: FlowSession): Pair = session to this + +/** + * Creates a [Pair([session], [Class])] from this [KClass]. + */ +infix fun KClass.from(session: FlowSession): Pair> = session to this.javaObjectType + +/** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [receiveAll(receiveType: Class, sessions: List): List>] when the same type is expected from all sessions. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them. + */ +@Suspendable +fun FlowLogic<*>.receiveAll(session: Pair>, vararg sessions: Pair>): Map> { + val allSessions = arrayOf(session, *sessions) + allSessions.enforceNoDuplicates() + return receiveAll(mapOf(*allSessions)) +} + +/** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [sessions: Map>): Map>] when sessions are expected to receive different types. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions]. + */ +@Suspendable +fun FlowLogic<*>.receiveAll(receiveType: Class, session: FlowSession, vararg sessions: FlowSession): List> = receiveAll(receiveType, listOf(session, *sessions)) + +/** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [sessions: Map>): Map>] when sessions are expected to receive different types. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions]. + */ +@Suspendable +inline fun FlowLogic<*>.receiveAll(session: FlowSession, vararg sessions: FlowSession): List> = receiveAll(R::class.javaObjectType, listOf(session, *sessions)) + +private fun Array>>.enforceNoDuplicates() { + require(this.size == this.toSet().size) { "A flow session can only appear once as argument." } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt new file mode 100644 index 0000000000..cfadcf4bf5 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -0,0 +1,87 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.identity.Party +import net.corda.core.utilities.UntrustworthyData +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.testing.chooseIdentity +import net.corda.testing.node.network +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ReceiveMultipleFlowTests { + @Test + fun `receive all messages in parallel using map style`() { + network(3) { nodes, _ -> + val doubleValue = 5.0 + nodes[1].registerAnswer(AlgorithmDefinition::class, doubleValue) + val stringValue = "Thriller" + nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue) + + val flow = nodes[0].services.startFlow(ParallelAlgorithmMap(nodes[1].info.chooseIdentity(), nodes[2].info.chooseIdentity())) + runNetwork() + + val result = flow.resultFuture.getOrThrow() + + assertThat(result).isEqualTo(doubleValue * stringValue.length) + } + } + + @Test + fun `receive all messages in parallel using list style`() { + network(3) { nodes, _ -> + val value1 = 5.0 + nodes[1].registerAnswer(ParallelAlgorithmList::class, value1) + val value2 = 6.0 + nodes[2].registerAnswer(ParallelAlgorithmList::class, value2) + + val flow = nodes[0].services.startFlow(ParallelAlgorithmList(nodes[1].info.chooseIdentity(), nodes[2].info.chooseIdentity())) + runNetwork() + val data = flow.resultFuture.getOrThrow() + + assertThat(data[0]).isEqualTo(value1) + assertThat(data[1]).isEqualTo(value2) + assertThat(data.fold(1.0) { a, b -> a * b }).isEqualTo(value1 * value2) + } + } + + class ParallelAlgorithmMap(doubleMember: Party, stringMember: Party) : AlgorithmDefinition(doubleMember, stringMember) { + @Suspendable + override fun askMembersForData(doubleMember: Party, stringMember: Party): Data { + val doubleSession = initiateFlow(doubleMember) + val stringSession = initiateFlow(stringMember) + val rawData = receiveAll(Double::class from doubleSession, String::class from stringSession) + return Data(rawData from doubleSession, rawData from stringSession) + } + } + + @InitiatingFlow + class ParallelAlgorithmList(private val member1: Party, private val member2: Party) : FlowLogic>() { + @Suspendable + override fun call(): List { + val session1 = initiateFlow(member1) + val session2 = initiateFlow(member2) + val data = receiveAll(session1, session2) + return computeAnswer(data) + } + + private fun computeAnswer(data: List>): List { + return data.map { element -> element.unwrap { it } } + } + } + + @InitiatingFlow + abstract class AlgorithmDefinition(private val doubleMember: Party, private val stringMember: Party) : FlowLogic() { + protected data class Data(val double: Double, val string: String) + + @Suspendable + protected abstract fun askMembersForData(doubleMember: Party, stringMember: Party): Data + + @Suspendable + override fun call(): Double { + val (double, string) = askMembersForData(doubleMember, stringMember) + return double * string.length + } + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt index b2f31384db..0a2fb69f26 100644 --- a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt @@ -61,6 +61,7 @@ class InternalUtilsTest { assertArrayEquals(intArrayOf(1, 2, 3, 4), (1 until 5).stream().toArray()) assertArrayEquals(intArrayOf(1, 3), (1..4 step 2).stream().toArray()) assertArrayEquals(intArrayOf(1, 3), (1..3 step 2).stream().toArray()) + @Suppress("EmptyRange") // It's supposed to be empty. assertArrayEquals(intArrayOf(), (1..0).stream().toArray()) assertArrayEquals(intArrayOf(1, 0), (1 downTo 0).stream().toArray()) assertArrayEquals(intArrayOf(3, 1), (3 downTo 0 step 2).stream().toArray()) diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index c5762a136e..e7f43c3096 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -12,7 +12,6 @@ import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockServices import org.junit.After import org.junit.Before import org.junit.Test @@ -25,47 +24,49 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertNull +// DOCSTART 3 class ResolveTransactionsFlowTest { lateinit var mockNet: MockNetwork - lateinit var a: StartedNode - lateinit var b: StartedNode + lateinit var notaryNode: StartedNode + lateinit var megaCorpNode: StartedNode + lateinit var miniCorpNode: StartedNode + lateinit var megaCorp: Party + lateinit var miniCorp: Party lateinit var notary: Party - lateinit var megaCorpServices: MockServices - lateinit var notaryServices: MockServices @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - megaCorpServices = MockServices(MEGA_CORP_KEY) - notaryServices = MockServices(DUMMY_NOTARY_KEY) - mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes() - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - a.internals.registerInitiatedFlow(TestResponseFlow::class.java) - b.internals.registerInitiatedFlow(TestResponseFlow::class.java) + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + notaryNode = mockNet.createNotaryNode() + megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name) + miniCorpNode = mockNet.createPartyNode(MINI_CORP.name) + megaCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java) + miniCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java) mockNet.runNetwork() - notary = a.services.getDefaultNotary() + notary = notaryNode.services.getDefaultNotary() + megaCorp = megaCorpNode.info.chooseIdentity() + miniCorp = miniCorpNode.info.chooseIdentity() } @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } +// DOCEND 3 + // DOCSTART 1 @Test fun `resolve from two hashes`() { val (stx1, stx2) = makeTransactions() - val p = TestFlow(setOf(stx2.id), a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(stx2.id), megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() val results = future.getOrThrow() assertEquals(listOf(stx1.id, stx2.id), results.map { it.id }) - b.database.transaction { - assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) - assertEquals(stx2, b.services.validatedTransactions.getTransaction(stx2.id)) + miniCorpNode.database.transaction { + assertEquals(stx1, miniCorpNode.services.validatedTransactions.getTransaction(stx1.id)) + assertEquals(stx2, miniCorpNode.services.validatedTransactions.getTransaction(stx2.id)) } } // DOCEND 1 @@ -73,8 +74,8 @@ class ResolveTransactionsFlowTest { @Test fun `dependency with an error`() { val stx = makeTransactions(signFirstTX = false).second - val p = TestFlow(setOf(stx.id), a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(stx.id), megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() assertFailsWith(SignedTransaction.SignaturesMissingException::class) { future.getOrThrow() } } @@ -82,14 +83,14 @@ class ResolveTransactionsFlowTest { @Test fun `resolve from a signed transaction`() { val (stx1, stx2) = makeTransactions() - val p = TestFlow(stx2, a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(stx2, megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() future.getOrThrow() - b.database.transaction { - assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) + miniCorpNode.database.transaction { + assertEquals(stx1, miniCorpNode.services.validatedTransactions.getTransaction(stx1.id)) // But stx2 wasn't inserted, just stx1. - assertNull(b.services.validatedTransactions.getTransaction(stx2.id)) + assertNull(miniCorpNode.services.validatedTransactions.getTransaction(stx2.id)) } } @@ -100,15 +101,15 @@ class ResolveTransactionsFlowTest { val count = 50 var cursor = stx2 repeat(count) { - val builder = DummyContract.move(cursor.tx.outRef(0), MINI_CORP) - val stx = megaCorpServices.signInitialTransaction(builder) - a.database.transaction { - a.services.recordTransactions(stx) + val builder = DummyContract.move(cursor.tx.outRef(0), miniCorp) + val stx = megaCorpNode.services.signInitialTransaction(builder) + megaCorpNode.database.transaction { + megaCorpNode.services.recordTransactions(stx) } cursor = stx } - val p = TestFlow(setOf(cursor.id), a.info.chooseIdentity(), 40) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(cursor.id), megaCorp, 40) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() assertFailsWith { future.getOrThrow() } } @@ -117,22 +118,22 @@ class ResolveTransactionsFlowTest { fun `triangle of transactions resolves fine`() { val stx1 = makeTransactions().first - val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP).run { - val ptx = megaCorpServices.signInitialTransaction(this) - notaryServices.addSignature(ptx) + val stx2 = DummyContract.move(stx1.tx.outRef(0), miniCorp).let { builder -> + val ptx = megaCorpNode.services.signInitialTransaction(builder) + notaryNode.services.addSignature(ptx, notary.owningKey) } - val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP).run { - val ptx = megaCorpServices.signInitialTransaction(this) - notaryServices.addSignature(ptx) + val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), miniCorp).let { builder -> + val ptx = megaCorpNode.services.signInitialTransaction(builder) + notaryNode.services.addSignature(ptx, notary.owningKey) } - a.database.transaction { - a.services.recordTransactions(stx2, stx3) + megaCorpNode.database.transaction { + megaCorpNode.services.recordTransactions(stx2, stx3) } - val p = TestFlow(setOf(stx3.id), a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(stx3.id), megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() future.getOrThrow() } @@ -149,43 +150,43 @@ class ResolveTransactionsFlowTest { return bs.toByteArray().sequence().open() } // TODO: this operation should not require an explicit transaction - val id = a.database.transaction { - a.services.attachments.importAttachment(makeJar()) + val id = megaCorpNode.database.transaction { + megaCorpNode.services.attachments.importAttachment(makeJar()) } val stx2 = makeTransactions(withAttachment = id).second - val p = TestFlow(stx2, a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(stx2, megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() future.getOrThrow() // TODO: this operation should not require an explicit transaction - b.database.transaction { - assertNotNull(b.services.attachments.openAttachment(id)) + miniCorpNode.database.transaction { + assertNotNull(miniCorpNode.services.attachments.openAttachment(id)) } } // DOCSTART 2 private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair { // Make a chain of custody of dummy states and insert into node A. - val dummy1: SignedTransaction = DummyContract.generateInitial(0, notary, MEGA_CORP.ref(1)).let { + val dummy1: SignedTransaction = DummyContract.generateInitial(0, notary, megaCorp.ref(1)).let { if (withAttachment != null) it.addAttachment(withAttachment) when (signFirstTX) { true -> { - val ptx = megaCorpServices.signInitialTransaction(it) - notaryServices.addSignature(ptx) + val ptx = megaCorpNode.services.signInitialTransaction(it) + notaryNode.services.addSignature(ptx, notary.owningKey) } false -> { - notaryServices.signInitialTransaction(it) + notaryNode.services.signInitialTransaction(it, notary.owningKey) } } } - val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP).let { - val ptx = megaCorpServices.signInitialTransaction(it) - notaryServices.addSignature(ptx) + val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), miniCorp).let { + val ptx = megaCorpNode.services.signInitialTransaction(it) + notaryNode.services.addSignature(ptx, notary.owningKey) } - a.database.transaction { - a.services.recordTransactions(dummy1, dummy2) + megaCorpNode.database.transaction { + megaCorpNode.services.recordTransactions(dummy1, dummy2) } return Pair(dummy1, dummy2) } diff --git a/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt b/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt index 3f79a6ce36..c4cca75b83 100644 --- a/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt @@ -15,11 +15,11 @@ import kotlin.test.assertFailsWith import kotlin.test.assertTrue class TestX509Key(algorithmId: AlgorithmId, key: BitArray) : X509Key() { - init { - this.algid = algorithmId - this.setKey(key) - this.encode() - } + init { + this.algid = algorithmId + this.setKey(key) + this.encode() + } } class X509EdDSAEngineTest { diff --git a/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt b/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 5692f69722..f6d1eec6b2 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -14,11 +14,10 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.currentDBSession +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork import org.junit.After @@ -46,6 +45,7 @@ private fun Attachment.extractContent() = ByteArrayOutputStream().apply { extrac private fun StartedNode<*>.saveAttachment(content: String) = database.transaction { attachments.importAttachment(createAttachmentData(content).inputStream()) } + private fun StartedNode<*>.hackAttachment(attachmentId: SecureHash, content: String) = database.transaction { updateAttachment(attachmentId, createAttachmentData(content)) } @@ -54,7 +54,7 @@ private fun StartedNode<*>.hackAttachment(attachmentId: SecureHash, content: Str * @see NodeAttachmentService.importAttachment */ private fun updateAttachment(attachmentId: SecureHash, data: ByteArray) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) attachment?.let { attachment.content = data @@ -70,8 +70,8 @@ class AttachmentSerializationTest { @Before fun setUp() { mockNet = MockNetwork() - server = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - client = mockNet.createNode(server.network.myAddress) + server = mockNet.createNode() + client = mockNet.createNode() client.internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. mockNet.runNetwork() server.internals.ensureRegistered() @@ -160,11 +160,10 @@ class AttachmentSerializationTest { private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { client.dispose() - client = mockNet.createNode(server.network.myAddress, client.internals.id, object : MockNetwork.Factory { + client = mockNet.createNode(client.internals.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { + return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad } } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 88ef89b662..6a3324e6dd 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -12,11 +12,12 @@ import org.junit.Before import org.junit.Test import java.security.SignatureException import java.util.* +import kotlin.reflect.jvm.javaField import kotlin.test.assertEquals import kotlin.test.assertFailsWith class TransactionSerializationTests : TestDependencyInjectionBase() { - val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash" + private val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash" class TestCash : Contract { override fun verify(tx: LedgerTransaction) { @@ -65,7 +66,10 @@ class TransactionSerializationTests : TestDependencyInjectionBase() { stx.verifyRequiredSignatures() // Corrupt the data and ensure the signature catches the problem. - stx.id.bytes[5] = stx.id.bytes[5].inc() + val bytesField = stx.id::bytes.javaField?.apply { setAccessible(true) } + val bytes = bytesField?.get(stx.id) as ByteArray + bytes[5] = bytes[5].inc() + assertFailsWith(SignatureException::class) { stx.verifyRequiredSignatures() } diff --git a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt index 60bfdd8369..22ef438a6b 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt @@ -1,14 +1,22 @@ package net.corda.core.utilities +import com.esotericsoftware.kryo.KryoException import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.nodeapi.internal.serialization.KRYO_CHECKPOINT_CONTEXT import net.corda.testing.TestDependencyInjectionBase import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException class KotlinUtilsTest : TestDependencyInjectionBase() { + @JvmField + @Rule + val expectedEx: ExpectedException = ExpectedException.none() + @Test fun `transient property which is null`() { val test = NullTransientProperty() @@ -18,26 +26,58 @@ class KotlinUtilsTest : TestDependencyInjectionBase() { } @Test - fun `transient property with non-capturing lamba`() { + fun `checkpointing a transient property with non-capturing lamba`() { val original = NonCapturingTransientProperty() val originalVal = original.transientVal - val copy = original.serialize().deserialize() + val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) } @Test - fun `transient property with capturing lamba`() { + fun `serialise transient property with non-capturing lamba`() { + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + val original = NonCapturingTransientProperty() + original.serialize() + } + + @Test + fun `deserialise transient property with non-capturing lamba`() { + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + val original = NonCapturingTransientProperty() + original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize() + } + + @Test + fun `checkpointing a transient property with capturing lamba`() { val original = CapturingTransientProperty("Hello") val originalVal = original.transientVal - val copy = original.serialize().deserialize() + val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) assertThat(copy.transientVal).startsWith("Hello") } + @Test + fun `serialise transient property with capturing lamba`() { + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + val original = CapturingTransientProperty("Hello") + original.serialize() + } + + @Test + fun `deserialise transient property with capturing lamba`() { + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + val original = CapturingTransientProperty("Hello") + original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize() + } + private class NullTransientProperty { var evalCount = 0 val transientValue by transient { diff --git a/docs/build.gradle b/docs/build.gradle index daefe91e23..542cc7380f 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -9,7 +9,10 @@ dokka { moduleName = 'corda' outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/kotlin") processConfigurations = ['compile'] - sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node-api/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin', '../testing/node-driver/src/main/kotlin', '../testing/test-utils/src/main/kotlin') + // TODO: Re-add '../testing/node-driver/src/main/kotlin', '../testing/test-utils/src/main/kotlin' when they're API stable + // TODO: Add '../client/jfx/src/main/kotlin' and '../client/mock/src/main/kotlin' if we decide to make them into public API + sourceDirs = files('../core/src/main/kotlin', '../client/rpc/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') + includes = ['packages.md'] jdkVersion = 8 externalDocumentationLink { @@ -28,7 +31,8 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { outputFormat = "javadoc" outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc") processConfigurations = ['compile'] - sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node-api/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin', '../testing/node-driver/src/main/kotlin', '../testing/test-utils/src/main/kotlin') + // TODO: Make this a copy of the list above programmatically. + sourceDirs = files('../core/src/main/kotlin', '../client/rpc/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') includes = ['packages.md'] jdkVersion = 8 diff --git a/docs/packages.md b/docs/packages.md index 61a4b2cb93..7331383ccc 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -3,19 +3,6 @@ Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for the java.time API, some core types, and Kotlin data classes. -# Package net.corda.client.jfx.model - -Data models for binding data feeds from Corda nodes into a JavaFX user interface, by presenting the data as [javafx.beans.Observable] -types. - -# Package net.corda.client.jfx.utils - -Utility classes (i.e. data classes) used by the Corda JavaFX client. - -# Package net.corda.client.mock - -Tools used by the client to produce mock data for testing purposes. - # Package net.corda.client.rpc RPC client interface to Corda, for use both by user-facing client and integration with external systems. @@ -24,6 +11,10 @@ RPC client interface to Corda, for use both by user-facing client and integratio Internal, do not use. These APIs and implementations which are currently being revised and are subject to future change. +# Package net.corda.core + +Exception types and some utilities for working with observables and futures. + # Package net.corda.core.concurrent Provides a simplified [java.util.concurrent.Future] class that allows registration of a callback to execute when the future @@ -37,6 +28,12 @@ with [Contract], or see the examples in [net.corda.finance.contracts]. Corda smart contracts are a combination of state held on the distributed ledger, and verification logic which defines which transformations of state are valid. +# Package net.corda.core.cordapp + +This package contains the interface to CorDapps from within a node. A CorDapp can access it's own context by using +the CordappProvider.getAppContext() class. These classes are not intended to be constructed manually and no interface +to do this will be provided. + # Package net.corda.core.crypto Cryptography data and utility classes used for signing, verifying, key management and data integrity checks. @@ -54,10 +51,6 @@ processes such as handling fixing of interest rate swaps. Data classes which model different forms of identity (potentially with supporting evidence) for legal entities and services. -# Package net.corda.core.internal - -Internal, do not use. These APIs and implementations which are currently being revised and are subject to future change. - # Package net.corda.core.messaging Data types used by the Corda messaging layer to manage state of messaging and sessions between nodes. @@ -93,12 +86,58 @@ actual states rather than state references). Corda utility classes, providing a broad range of functionality to help implement both Corda nodes and CorDapps. + # Package net.corda.finance -The finance module is a CorDapp containing sample cash and obligation contracts, as well as providing several -useful data types such as [Amount]. +Some simple testing utilities like pre-defined top-level values for common currencies. Mostly useful for +writing unit tests in Kotlin. + +WARNING: NOT API STABLE. # Package net.corda.finance.utils A collection of utilities for summing financial states, for example, summing obligations to get total debts. +WARNING: NOT API STABLE. + +# Package net.corda.finance.contracts + +Various types for common financial concepts like day roll conventions, fixes, etc. + +WARNING: NOT API STABLE. + +# net.corda.finance.contracts.asset + +Cash states, obligations and commodities. + +WARNING: NOT API STABLE. + +# net.corda.finance.contracts.asset.cash.selection + +Provisional support for pluggable cash selectors, needed for different database backends. + +WARNING: NOT API STABLE. + +# net.corda.finance.contracts.math + +Splines and interpolation. + +WARNING: NOT API STABLE. + +# net.corda.finance.flows + +Cash payments and issuances. Two party "delivery vs payment" atomic asset swaps. + +WARNING: NOT API STABLE. + +# net.corda.finance.plugin + +JSON/Jackson plugin for business calendars. + +WARNING: NOT API STABLE. + +# net.corda.finance.schemas + +JPA (Java Persistence Architecture) schemas for the financial state types. + +WARNING: NOT API STABLE. \ No newline at end of file diff --git a/docs/source/_static/corda-cheat-sheet.pdf b/docs/source/_static/corda-cheat-sheet.pdf index cc00820e04..b7c3b7d1cb 100644 Binary files a/docs/source/_static/corda-cheat-sheet.pdf and b/docs/source/_static/corda-cheat-sheet.pdf differ diff --git a/docs/source/_static/versions b/docs/source/_static/versions index 4110e2dfd6..1152a8e46a 100644 --- a/docs/source/_static/versions +++ b/docs/source/_static/versions @@ -8,5 +8,6 @@ "https://docs.corda.net/releases/release-M12.1": "M12.1", "https://docs.corda.net/releases/release-M13.0": "M13.0", "https://docs.corda.net/releases/release-M14.0": "M14.0", + "https://docs.corda.net/releases/release-V1.0": "V1.0", "https://docs.corda.net/head/": "Master" } diff --git a/docs/source/api-core-types.rst b/docs/source/api-core-types.rst index e69c5d858d..e7f3c898ee 100644 --- a/docs/source/api-core-types.rst +++ b/docs/source/api-core-types.rst @@ -20,20 +20,6 @@ Any object that needs to be identified by its hash should implement the ``NamedB ``SecureHash`` is a sealed class that only defines a single subclass, ``SecureHash.SHA256``. There are utility methods to create and parse ``SecureHash.SHA256`` objects. -Party ------ -Identities on the network are represented by ``AbstractParty``. There are two types of ``AbstractParty``: - -* ``Party``, identified by a ``PublicKey`` and a ``CordaX500Name`` - -* ``AnonymousParty``, identified by a ``PublicKey`` - -For example, in a transaction sent to your node as part of a chain of custody it is important you can convince yourself -of the transaction's validity, but equally important that you don't learn anything about who was involved in that -transaction. In these cases ``AnonymousParty`` should be used. In contrast, for internal processing where extended -details of a party are required, the ``Party`` class should be used. The identity service provides functionality for -resolving anonymous parties to full parties. - CompositeKey ------------ Corda supports scenarios where more than one signature is required to authorise a state object transition. For example: diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 4c1ef7fd2b..4a0e284621 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -113,9 +113,8 @@ subclass's constructor can take any number of arguments of any type. The generic FlowLogic annotations --------------------- -Any flow that you wish to start either directly via RPC or as a subflow must be annotated with the -``@InitiatingFlow`` annotation. Additionally, if you wish to start the flow via RPC, you must annotate it with the -``@StartableByRPC`` annotation: +Any flow from which you want to initiate other flows must be annotated with the ``@InitiatingFlow`` annotation. +Additionally, if you wish to start the flow via RPC, you must annotate it with the ``@StartableByRPC`` annotation: .. container:: codeset @@ -139,7 +138,7 @@ Meanwhile, any flow that responds to a message from another flow must be annotat .. sourcecode:: kotlin @InitiatedBy(Initiator::class) - class Responder(val otherParty: Party) : FlowLogic() { } + class Responder(val otherSideSession: FlowSession) : FlowLogic() { } .. sourcecode:: java @@ -222,14 +221,14 @@ There are several ways to retrieve a notary from the network map: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 12 + :start-after: DOCSTART 01 + :end-before: DOCEND 01 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 1 - :end-before: DOCEND 1 + :start-after: DOCSTART 01 + :end-before: DOCEND 01 :dedent: 12 Specific counterparties @@ -240,69 +239,78 @@ We can also use the network map to retrieve a specific counterparty: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 12 + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 12 - -Specific services -~~~~~~~~~~~~~~~~~ -Finally, we can use the map to identify nodes providing a specific service (e.g. a regulator or an oracle): - -.. container:: codeset - - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt - :language: kotlin - :start-after: DOCSTART 3 - :end-before: DOCEND 3 - :dedent: 12 - - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java - :language: java - :start-after: DOCSTART 3 - :end-before: DOCEND 3 + :start-after: DOCSTART 02 + :end-before: DOCEND 02 :dedent: 12 Communication between parties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -``FlowLogic`` instances communicate using three functions: -* ``send(otherParty: Party, payload: Any)`` - * Sends the ``payload`` object to the ``otherParty`` -* ``receive(receiveType: Class, otherParty: Party)`` - * Receives an object of type ``receiveType`` from the ``otherParty`` -* ``sendAndReceive(receiveType: Class, otherParty: Party, payload: Any)`` - * Sends the ``payload`` object to the ``otherParty``, and receives an object of type ``receiveType`` back +In order to create a communication session between your initiator flow and the receiver flow you must call +``initiateFlow(party: Party): FlowSession`` + +``FlowSession`` instances in turn provide three functions: + +* ``send(payload: Any)`` + * Sends the ``payload`` object +* ``receive(receiveType: Class): R`` + * Receives an object of type ``receiveType`` +* ``sendAndReceive(receiveType: Class, payload: Any): R`` + * Sends the ``payload`` object and receives an object of type ``receiveType`` back + + +InitiateFlow +~~~~~~~~~~~~ + +``initiateFlow`` creates a communication session with the passed in ``Party``. -Send -~~~~ -We can send arbitrary data to a counterparty: .. container:: codeset .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 4 - :end-before: DOCEND 4 - :dedent: 12 + :start-after: DOCSTART initiateFlow + :end-before: DOCEND initiateFlow + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 4 - :end-before: DOCEND 4 + :start-after: DOCSTART initiateFlow + :end-before: DOCEND initiateFlow :dedent: 12 -If this is the first ``send``, the counterparty will either: +Note that at the time of call to this function no actual communication is done, this is deferred to the first +send/receive, at which point the counterparty will either: 1. Ignore the message if they are not registered to respond to messages from this flow. -2. Start the flow they have registered to respond to this flow, and run the flow until the first call to ``receive``, - at which point they process the message. In other words, we are assuming that the counterparty is registered to - respond to this flow, and has a corresponding ``receive`` call. +2. Start the flow they have registered to respond to this flow. + +Send +~~~~ + +Once we have a ``FlowSession`` object we can send arbitrary data to a counterparty: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 04 + :end-before: DOCEND 04 + :dedent: 8 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 04 + :end-before: DOCEND 04 + :dedent: 12 + +The flow on the other side must eventually reach a corresponding ``receive`` call to get this message. Receive ~~~~~~~ @@ -324,14 +332,14 @@ be what it appears to be! We must unwrap the ``UntrustworthyData`` using a lambd .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 5 - :end-before: DOCEND 5 - :dedent: 12 + :start-after: DOCSTART 05 + :end-before: DOCEND 05 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 5 - :end-before: DOCEND 5 + :start-after: DOCSTART 05 + :end-before: DOCEND 05 :dedent: 12 We're not limited to sending to and receiving from a single counterparty. A flow can send messages to as many parties @@ -341,16 +349,22 @@ as it likes, and each party can invoke a different response flow: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 6 - :end-before: DOCEND 6 - :dedent: 12 + :start-after: DOCSTART 06 + :end-before: DOCEND 06 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 6 - :end-before: DOCEND 6 + :start-after: DOCSTART 06 + :end-before: DOCEND 06 :dedent: 12 +.. warning:: If you initiate several flows from the same ``@InitiatingFlow`` flow then on the receiving side you must be + prepared to be initiated by any of the corresponding ``initiateFlow()`` calls! A good way of handling this ambiguity + is to send as a first message a "role" message to the initiated flow, indicating which part of the initiating flow + the rest of the counter-flow should conform to. For example send an enum, and on the other side start with a switch + statement. + SendAndReceive ~~~~~~~~~~~~~~ We can also use a single call to send data to a counterparty and wait to receive data of a specific type back. The @@ -360,14 +374,14 @@ type of data sent doesn't need to match the type of the data received back: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 7 - :end-before: DOCEND 7 - :dedent: 12 + :start-after: DOCSTART 07 + :end-before: DOCEND 07 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 7 - :end-before: DOCEND 7 + :start-after: DOCSTART 07 + :end-before: DOCEND 07 :dedent: 12 Counterparty response @@ -385,29 +399,138 @@ Our side of the flow must mirror these calls. We could do this as follows: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 8 - :end-before: DOCEND 8 - :dedent: 12 + :start-after: DOCSTART 08 + :end-before: DOCEND 08 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 8 - :end-before: DOCEND 8 + :start-after: DOCSTART 08 + :end-before: DOCEND 08 + :dedent: 12 + +Why sessions? +^^^^^^^^^^^^^ + +Before ``FlowSession`` s were introduced the send/receive API looked a bit different. They were functions on +``FlowLogic`` and took the address ``Party`` as argument. The platform internally maintained a mapping from ``Party`` to +session, hiding sessions from the user completely. + +Although this is a convenient API it introduces subtle issues where a message that was originally meant for a specific +session may end up in another. + +Consider the following contrived example using the old ``Party`` based API: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + :language: kotlin + :start-after: DOCSTART LaunchSpaceshipFlow + :end-before: DOCEND LaunchSpaceshipFlow + +The intention of the flows is very clear: LaunchSpaceshipFlow asks the president whether a spaceship should be launched. +It is expecting a boolean reply. The president in return first tells the secretary that they need coffee, which is also +communicated with a boolean. Afterwards the president replies to the launcher that they don't want to launch. + +However the above can go horribly wrong when the ``launcher`` happens to be the same party ``getSecretary`` returns. In +this case the boolean meant for the secretary will be received by the launcher! + +This indicates that ``Party`` is not a good identifier for the communication sequence, and indeed the ``Party`` based +API may introduce ways for an attacker to fish for information and even trigger unintended control flow like in the +above case. + +Hence we introduced ``FlowSession``, which identifies the communication sequence. With ``FlowSession`` s the above set +of flows would look like this: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + :language: kotlin + :start-after: DOCSTART LaunchSpaceshipFlowCorrect + :end-before: DOCEND LaunchSpaceshipFlowCorrect + +Note how the president is now explicit about which session it wants to send to. + +Porting from the old Party-based API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In the old API the first ``send`` or ``receive`` to a ``Party`` was the one kicking off the counter-flow. This is now +explicit in the ``initiateFlow`` function call. To port existing code: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART FlowSession porting + :end-before: DOCEND FlowSession porting + :dedent: 8 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART FlowSession porting + :end-before: DOCEND FlowSession porting :dedent: 12 Subflows -------- + +Subflows are pieces of reusable flows that may be run by calling ``FlowLogic.subFlow``. There are two broad categories +of subflows, inlined and initiating ones. The main difference lies in the counter-flow's starting method, initiating +ones initiate counter-flows automatically, while inlined ones expect some parent counter-flow to run the inlined +counter-part. + +Inlined subflows +^^^^^^^^^^^^^^^^ + +Inlined subflows inherit their calling flow's type when initiating a new session with a counterparty. For example, say +we have flow A calling an inlined subflow B, which in turn initiates a session with a party. The FlowLogic type used to +determine which counter-flow should be kicked off will be A, not B. Note that this means that the other side of this +inlined flow must therefore be implemented explicitly in the kicked off flow as well. This may be done by calling a +matching inlined counter-flow, or by implementing the other side explicitly in the kicked off parent flow. + +An example of such a flow is ``CollectSignaturesFlow``. It has a counter-flow ``SignTransactionFlow`` that isn't +annotated with ``InitiatedBy``. This is because both of these flows are inlined; the kick-off relationship will be +defined by the parent flows calling ``CollectSignaturesFlow`` and ``SignTransactionFlow``. + +In the code inlined subflows appear as regular ``FlowLogic`` instances, `without` either of the ``@InitiatingFlow`` or +``@InitiatedBy`` annotation. + +.. note:: Inlined flows aren't versioned; they inherit their parent flow's version. + +Initiating subflows +^^^^^^^^^^^^^^^^^^^ + +Initiating subflows are ones annotated with the ``@InitiatingFlow`` annotation. When such a flow initiates a session its +type will be used to determine which ``@InitiatedBy`` flow to kick off on the counterparty. + +An example is the ``@InitiatingFlow InitiatorFlow``/``@InitiatedBy ResponderFlow`` flow pair in the ``FlowCookbook``. + +.. note:: Initiating flows are versioned separately from their parents. + +Core initiating subflows +^^^^^^^^^^^^^^^^^^^^^^^^ + +Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the +platform, and their initiated counter-flows are registered explicitly, so there is no need for the ``InitiatedBy`` +annotation. + +An example is the ``FinalityFlow``/``FinalityHandler`` flow pair. + +Built-in subflows +^^^^^^^^^^^^^^^^^ + Corda provides a number of built-in flows that should be used for handling common tasks. The most important are: -* ``CollectSignaturesFlow``, which should be used to collect a transaction's required signatures -* ``FinalityFlow``, which should be used to notarise and record a transaction -* ``SendTransactionFlow``, which should be used to send a signed transaction if it needed to be resolved on the other side. -* ``ReceiveTransactionFlow``, which should be used receive a signed transaction -* ``ContractUpgradeFlow``, which should be used to change a state's contract -* ``NotaryChangeFlow``, which should be used to change a state's notary +* ``CollectSignaturesFlow`` (inlined), which should be used to collect a transaction's required signatures +* ``FinalityFlow`` (initiating), which should be used to notarise and record a transaction as well as to broadcast it to + all relevant parties +* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on + the other side. +* ``ReceiveTransactionFlow`` (inlined), which should be used receive a signed transaction +* ``ContractUpgradeFlow`` (initiating), which should be used to change a state's contract +* ``NotaryChangeFlow`` (initiating), which should be used to change a state's notary -These flows are designed to be used as building blocks in your own flows. You invoke them by calling -``FlowLogic.subFlow`` from within your flow's ``call`` method. Let's look at three very common examples. +Let's look at three very common examples. FinalityFlow ^^^^^^^^^^^^ @@ -418,14 +541,14 @@ the transaction's states: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 9 - :end-before: DOCEND 9 - :dedent: 12 + :start-after: DOCSTART 09 + :end-before: DOCEND 09 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 9 - :end-before: DOCEND 9 + :start-after: DOCSTART 09 + :end-before: DOCEND 09 :dedent: 12 We can also choose to send the transaction to additional parties who aren't one of the state's participants: @@ -436,7 +559,7 @@ We can also choose to send the transaction to additional parties who aren't one :language: kotlin :start-after: DOCSTART 10 :end-before: DOCEND 10 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -459,7 +582,7 @@ transaction ourselves, we can automatically gather the signatures of the other r :language: kotlin :start-after: DOCSTART 15 :end-before: DOCEND 15 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -476,7 +599,7 @@ transaction and provide their signature if they are satisfied: :language: kotlin :start-after: DOCSTART 16 :end-before: DOCEND 16 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -497,7 +620,7 @@ transaction data vending requests as the receiver walks the dependency chain usi :language: kotlin :start-after: DOCSTART 12 :end-before: DOCEND 12 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -514,7 +637,7 @@ dependencies and verify the transaction: :language: kotlin :start-after: DOCSTART 13 :end-before: DOCEND 13 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -530,7 +653,7 @@ We can also send and receive a ``StateAndRef`` dependency chain and automaticall :language: kotlin :start-after: DOCSTART 14 :end-before: DOCEND 14 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -538,6 +661,20 @@ We can also send and receive a ``StateAndRef`` dependency chain and automaticall :end-before: DOCEND 14 :dedent: 12 +Why inlined subflows? +^^^^^^^^^^^^^^^^^^^^^ + +Inlined subflows provide a way to share commonly used flow code `while forcing users to create a parent flow`. Take for +example ``CollectSignaturesFlow``. Say we made it an initiating flow that automatically kicks off +``SignTransactionFlow`` that signs the transaction. This would mean malicious nodes can just send any old transaction to +us using ``CollectSignaturesFlow`` and we would automatically sign it! + +By making this pair of flows inlined we provide control to the user over whether to sign the transaction or not by +forcing them to nest it in their own parent flows. + +In general if you're writing a subflow the decision of whether you should make it initiating should depend on whether +the counter-flow needs broader context to achieve its goal. + FlowException ------------- Suppose a node throws an exception while running a flow. Any counterparty flows waiting for a message from the node @@ -575,7 +712,7 @@ To provide a progress tracker, we have to override ``FlowLogic.progressTracker`` :language: kotlin :start-after: DOCSTART 17 :end-before: DOCEND 17 - :dedent: 8 + :dedent: 4 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -591,7 +728,7 @@ We then update the progress tracker's current step as we progress through the fl :language: kotlin :start-after: DOCSTART 18 :end-before: DOCEND 18 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java diff --git a/docs/source/api-identity.rst b/docs/source/api-identity.rst new file mode 100644 index 0000000000..fb3a18342e --- /dev/null +++ b/docs/source/api-identity.rst @@ -0,0 +1,138 @@ +API: Identity +============= + +.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-identity`. + +.. contents:: + +Party +----- +Identities on the network are represented by ``AbstractParty``. There are two types of ``AbstractParty``: + +* ``Party``, identified by a ``PublicKey`` and a ``CordaX500Name`` + +* ``AnonymousParty``, identified by a ``PublicKey`` + +For example, in a transaction sent to your node as part of a chain of custody it is important you can convince yourself +of the transaction's validity, but equally important that you don't learn anything about who was involved in that +transaction. In these cases ``AnonymousParty`` should be used by flows constructing when transaction states and commands. +In contrast, for internal processing where extended details of a party are required, the ``Party`` class should be used +instead. The identity service provides functionality for flows to resolve anonymous parties to full parties, dependent +on the anonymous party's identity having been registered with the node earlier (typically this is handled by +``SwapIdentitiesFlow`` or ``IdentitySyncFlow``, discussed below). + +Party names are held within the ``CordaX500Name`` data class, which enforces the structure of names within Corda, as +well as ensuring a consistent rendering of the names in plain text. + +The support for both Party and AnonymousParty classes in Corda enables sophisticated selective disclosure of identity +information. For example, it is possible to construct a Transaction using an AnonymousParty, so nobody can learn of your +involvement by inspection of the transaction, yet prove to specific counterparts that this AnonymousParty actually is +owned by your well known identity. This disclosure is achieved through the use of the PartyAndCertificate data class +which can be propagated to those who need to know, and contains the Party's X.509 certificate path to provide proof of +ownership by a well known identity. + +The PartyAndCertificate class is also used in the network map service to represent well known identities, in which +scenario the certificate path proves its issuance by the Doorman service. + + +Confidential Identities +----------------------- + +Confidential identities are key pairs where the corresponding X.509 certificate (and path) are not made public, so that parties who +are not involved in the transaction cannot identify its participants. They are owned by a well known identity, which +must sign the X.509 certificate. Before constructing a new transaction the involved parties must generate and send new +confidential identities to each other, a process which is managed using ``SwapIdentitiesFlow`` (discussed below). The +public keys of these confidential identities are then used when generating output states and commands for the transaction. + +Where using outputs from a previous transaction in a new transaction, counterparties may need to know who the involved +parties are. One example is in ``TwoPartyTradeFlow`` which delegates to ``CollectSignaturesFlow`` to gather certificates +from both parties. ``CollectSignaturesFlow`` requires that a confidential identity of the initiating node has signed +the transaction, and verifying this requires the receiving node has a copy of the confidential identity for the input +state. ``IdentitySyncFlow`` can be used to synchronize the confidential identities we have the certificate paths for, in +a single transaction, to another node. + +.. note:: ``CollectSignaturesFlow`` requires that the initiating node has signed the transaction, and as such all nodes + providing signatures must recognise the signing key used by the initiating node as being either its well known identity + or a confidential identity they have the certificate for. + +Swap identities flow +~~~~~~~~~~~~~~~~~~~~ + +``SwapIdentitiesFlow`` takes the party to swap identities with in its constructor (the counterparty), and is typically run as a subflow of +another flow. It returns a mapping from well known identities of the calling flow and our counterparty to the new +confidential identities; in future this will be extended to handle swapping identities with multiple parties. +You can see an example of it being used in ``TwoPartyDealFlow.kt``: + +.. container:: codeset + + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 8 + +The swap identities flow goes through the following key steps: + +1. Generate a nonce value to form a challenge to the other nodes. +2. Send nonce value to all counterparties, and receive their nonce values. +3. Generate a new confidential identity from our well known identity. +4. Create a data blob containing the new confidential identity (public key, name and X.509 certificate path), + and the hash of the nonce values. +5. Sign the resulting data blob with the confidential identity's private key. +6. Send the confidential identity and data blob signature to all counterparties, while receiving theirs. +7. Verify the signatures to ensure that identities were generated by the involved set of parties. +8. Verify the confidential identities are owned by the expected well known identities. +9. Store the confidential identities and return them to the calling flow. + +This ensures not only that the confidential identity X.509 certificates are signed by the correct well known identities, +but also that the confidential identity private key is held by the counterparty, and that a party cannot claim ownership +another party's confidential identities belong to its well known identity. + +Identity synchronization flow +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When constructing a transaction whose input states reference confidential identities, it is common for other signing +entities (counterparties) to require to know which well known identities those confidential identities map to. The +``IdentitySyncFlow`` handles distribution of a node's confidential identities, and you can see an example of its +use in ``TwoPartyTradeFlow.kt``: + +.. container:: codeset + + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 12 + +The identity synchronization flow goes through the following key steps: + +1. Extract participant identities from all input and output states. Filter this set down to confidential identities + of the flow's well known identity. Required signers on commands are currently ignored as they are presumed to be + included in the participants on states, or to be well known identities of services (such as an oracle service). +2. For each counterparty node, send a list of the public keys of the confidential identities, and receive back a list + of those the counterparty needs the certificate path for. +3. Verify the requested list of identities contains only confidential identities in the offered list, and abort otherwise. +4. Send the requested confidential identities as ``PartyAndCertificate`` instances to the counterparty. + +.. note:: ``IdentitySyncFlow`` works on a push basis. Receiving nodes can only request confidential identities being + offered by the initiating node. There is no standard flow for nodes to collect + confidential identities before assembling a transaction, and this is left for individual flows to manage if required. + +``IdentitySyncFlow`` will serve only confidential identities in the provided transaction, limited to those that are +signed by the well known identity the flow is initiated by. This is done to avoid a risk of a node including +states it doesn't have the well known identity of participants in, to try convincing one of its counterparties to +reveal the identity. In case of a more complex transaction where multiple well known identities need confidential +identities distributed this flow should be run by each node in turn. For example: + +* Alice is building the transaction, and provides some input state *x* owned by a confidential identity of Alice +* Bob provides some input state *y* owned by a confidential identity of Bob +* Charlie provides some input state *z* owned by a confidential identity of Charlie + +Alice, Bob and Charlie must all run ``IdentitySyncFlow`` to send their involved confidential identities to the other +parties. For an illustration of the security implications of not requiring this, consider: + +1. Alice is building the transaction, and provides some input state *x* owned by a confidential identity of Alice +2. Bob provides some input state *y* owned by a confidential identity it doesn't know the well known identity of, but + Alice does. +3. Alice runs ``IdentitySyncFlow`` and sends not just their confidential identity, but also the confidential identity + in state *y*, violating the privacy model. \ No newline at end of file diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst deleted file mode 100644 index 04e64c7c50..0000000000 --- a/docs/source/api-index.rst +++ /dev/null @@ -1,60 +0,0 @@ -API -=== - -This section describes the APIs that are available for the development of CorDapps: - -.. toctree:: - :maxdepth: 1 - - api-states - api-persistence - api-contracts - api-vault-query - api-transactions - api-flows - api-core-types - -Before reading this page, you should be familiar with the :doc:`key concepts of Corda `. - -Internal APIs and stability guarantees --------------------------------------- - -.. warning:: For Corda 1.0 we do not currently provide a stable wire protocol or support for database upgrades. - Additionally, the JSON format produced by the client-jackson module may change in future. - Therefore, you should not expect to be able to migrate persisted data from 1.0 to future versions. - - Additionally, it may be necessary to recompile applications against future versions of the API until we begin offering - ABI stability as well. We plan to do this soon after the release of Corda 1.0. - - Finally, please note that the 1.0 release has not yet been security audited. You should not run it in situations - where security is required. - -As of Corda 1.0, the following modules export public API that we promise to maintain backwards compatibility for, -unless an incompatible change is required for security reasons: - -* core -* client-rpc -* client-jackson - -The following modules don't yet have a completely stable API, but we will do our best to minimise disruption to -developers using them until we are able to graduate them into the public API: - -* the Gradle plugins (cordformation) -* node-driver -* confidential-identities -* test-utils -* client-jfx, client-mock -* finance -* anything under the experimental directory (sub-components here may never graduate) - -We hope to graduate the node-driver, test-utils and confidential-identities modules in the next feature release -after 1.0. The bulk of the Corda API is found in the core module. Other modules should be assumed to be fully internal. - -The web server module will be removed in future: you should build web frontends for CorDapps using standard frameworks -like Spring Boot, J2EE, Play, etc. - -Code that falls into the following package namespaces are for internal use only and not public. In a future release the -node will not load any CorDapp which uses them. - -* Any package in the ``net.corda`` namespace which contains ``.internal`` -* ``net.corda.node`` \ No newline at end of file diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 84afad26a5..add8f1b785 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -7,6 +7,8 @@ API: Persistence ================ +.. contents:: + Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping* (ORM) tool to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing persisted contract states held in the vault for the purpose of running queries over them and to allow relational joins diff --git a/docs/source/api-rpc.rst b/docs/source/api-rpc.rst index 8c7d79d9a5..442c675a36 100644 --- a/docs/source/api-rpc.rst +++ b/docs/source/api-rpc.rst @@ -9,9 +9,7 @@ The key RPC operations exposed by the node are: * Extract states from the node's vault based on a query criteria * ``CordaRPCOps.vaultTrackBy`` * As above, but also returns an observable of future states matching the query -* ``CordaRPCOps.verifiedTransactions`` - * Extract all transactions from the node's local storage, as well as an observable of all future transactions -* ``CordaRPCOps.networkMapUpdates`` +* ``CordaRPCOps.networkMapFeed`` * A list of network nodes, and an observable of changes to the network map * ``CordaRPCOps.registeredFlows`` * See a list of registered flows on the node @@ -19,10 +17,10 @@ The key RPC operations exposed by the node are: * Start one of the node's registered flows * ``CordaRPCOps.startTrackedFlowDynamic`` * As above, but also returns a progress handle for the flow -* ``CordaRPCOps.nodeIdentity`` - * Returns the node's identity +* ``CordaRPCOps.nodeInfo`` + * Returns information about the node * ``CordaRPCOps.currentNodeTime`` - * Returns the node's current time + * Returns the current time according to the node's clock * ``CordaRPCOps.partyFromKey/CordaRPCOps.wellKnownPartyFromX500Name`` * Retrieves a party on the network based on a public key or X500 name * ``CordaRPCOps.uploadAttachment``/``CordaRPCOps.openAttachment``/``CordaRPCOps.attachmentExists`` diff --git a/docs/source/api-service-hub.rst b/docs/source/api-service-hub.rst index e6804f8cca..edd4874c40 100644 --- a/docs/source/api-service-hub.rst +++ b/docs/source/api-service-hub.rst @@ -24,7 +24,5 @@ Additional, ``ServiceHub`` exposes the following properties: * ``ServiceHub.loadState`` and ``ServiceHub.toStateAndRef`` to resolve a ``StateRef`` into a ``TransactionState`` or a ``StateAndRef`` -* ``ServiceHub.toSignedTransaction`` to sign a ``TransactionBuilder`` and convert it into a ``SignedTransaction`` -* ``ServiceHub.createSignature`` and ``ServiceHub.addSignature`` to create and add signatures to a ``SignedTransaction`` - -Finally, ``ServiceHub`` exposes notary identity key via ``ServiceHub.notaryIdentityKey``. \ No newline at end of file +* ``ServiceHub.signInitialTransaction`` to sign a ``TransactionBuilder`` and convert it into a ``SignedTransaction`` +* ``ServiceHub.createSignature`` and ``ServiceHub.addSignature`` to create and add signatures to a ``SignedTransaction`` \ No newline at end of file diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 7d76a0624b..0c02f4b565 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -59,7 +59,7 @@ An input state is added to a transaction as a ``StateAndRef``, which combines: :language: kotlin :start-after: DOCSTART 21 :end-before: DOCEND 21 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -78,7 +78,7 @@ A ``StateRef`` uniquely identifies an input state, allowing the notary to mark i :language: kotlin :start-after: DOCSTART 20 :end-before: DOCEND 20 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -102,7 +102,7 @@ add them to the transaction directly: :language: kotlin :start-after: DOCSTART 22 :end-before: DOCEND 22 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -119,7 +119,7 @@ it on the input state: :language: kotlin :start-after: DOCSTART 23 :end-before: DOCEND 23 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -139,7 +139,7 @@ wrapping the output state in a ``StateAndContract``, which combines: :language: kotlin :start-after: DOCSTART 47 :end-before: DOCEND 47 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -160,7 +160,7 @@ A command is added to the transaction as a ``Command``, which combines: :language: kotlin :start-after: DOCSTART 24 :end-before: DOCEND 24 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -178,7 +178,7 @@ Attachments are identified by their hash: :language: kotlin :start-after: DOCSTART 25 :end-before: DOCEND 25 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -199,7 +199,7 @@ time, or be open at either end: :language: kotlin :start-after: DOCSTART 26 :end-before: DOCEND 26 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -215,7 +215,7 @@ We can also define a time window as an ``Instant`` plus/minus a time tolerance ( :language: kotlin :start-after: DOCSTART 42 :end-before: DOCEND 42 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -231,7 +231,7 @@ Or as a start-time plus a duration: :language: kotlin :start-after: DOCSTART 43 :end-before: DOCEND 43 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -255,7 +255,7 @@ that will notarise the inputs and verify the time-window: :language: kotlin :start-after: DOCSTART 19 :end-before: DOCEND 19 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -274,7 +274,7 @@ instantiated without one: :language: kotlin :start-after: DOCSTART 46 :end-before: DOCEND 46 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -317,7 +317,7 @@ Here's an example usage of ``TransactionBuilder.withItems``: :language: kotlin :start-after: DOCSTART 27 :end-before: DOCEND 27 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -335,7 +335,7 @@ Here are the methods for adding inputs and attachments: :language: kotlin :start-after: DOCSTART 28 :end-before: DOCEND 28 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -351,7 +351,7 @@ An output state can be added as a ``ContractState``, contract class name and not :language: kotlin :start-after: DOCSTART 49 :end-before: DOCEND 49 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -367,7 +367,7 @@ We can also leave the notary field blank, in which case the transaction's defaul :language: kotlin :start-after: DOCSTART 50 :end-before: DOCEND 50 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -383,7 +383,7 @@ Or we can add the output state as a ``TransactionState``, which already specifie :language: kotlin :start-after: DOCSTART 51 :end-before: DOCEND 51 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -399,7 +399,7 @@ Commands can be added as a ``Command``: :language: kotlin :start-after: DOCSTART 52 :end-before: DOCEND 52 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -415,7 +415,7 @@ Or as ``CommandData`` and a ``vararg PublicKey``: :language: kotlin :start-after: DOCSTART 53 :end-before: DOCEND 53 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -431,7 +431,7 @@ For the time-window, we can set a time-window directly: :language: kotlin :start-after: DOCSTART 44 :end-before: DOCEND 44 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -447,7 +447,7 @@ Or define the time-window as a time plus a duration (e.g. 45 seconds): :language: kotlin :start-after: DOCSTART 45 :end-before: DOCEND 45 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -467,7 +467,7 @@ We can either sign with our legal identity key: :language: kotlin :start-after: DOCSTART 29 :end-before: DOCEND 29 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -483,7 +483,7 @@ Or we can also choose to use another one of our public keys: :language: kotlin :start-after: DOCSTART 30 :end-before: DOCEND 30 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -527,13 +527,13 @@ and output states: :language: kotlin :start-after: DOCSTART 33 :end-before: DOCEND 33 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 33 :end-before: DOCEND 33 - :dedent: 12 + :dedent: 16 Checking that the transaction meets the contract constraints is only part of verifying the transaction's contents. We will usually also want to perform our own additional validation of the transaction contents before signing, to ensure @@ -552,13 +552,13 @@ We achieve this by using the ``ServiceHub`` to convert the ``SignedTransaction`` :language: kotlin :start-after: DOCSTART 32 :end-before: DOCEND 32 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 32 :end-before: DOCEND 32 - :dedent: 12 + :dedent: 16 We can now perform our additional verification. Here's a simple example: @@ -568,13 +568,13 @@ We can now perform our additional verification. Here's a simple example: :language: kotlin :start-after: DOCSTART 34 :end-before: DOCEND 34 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 34 :end-before: DOCEND 34 - :dedent: 12 + :dedent: 16 Verifying the transaction's signatures ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -589,13 +589,13 @@ We can verify that all the transaction's required signatures are present and val :language: kotlin :start-after: DOCSTART 35 :end-before: DOCEND 35 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 35 :end-before: DOCEND 35 - :dedent: 12 + :dedent: 16 However, we'll often want to verify the transaction's existing signatures before all of them have been collected. For this we can use ``SignedTransaction.verifySignaturesExcept``, which takes a ``vararg`` of the public keys for @@ -607,13 +607,13 @@ which the signatures are allowed to be missing: :language: kotlin :start-after: DOCSTART 36 :end-before: DOCEND 36 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 36 :end-before: DOCEND 36 - :dedent: 12 + :dedent: 16 If the transaction is missing any signatures without the corresponding public keys being passed in, a ``SignaturesMissingException`` is thrown. @@ -626,13 +626,13 @@ We can also choose to simply verify the signatures that are present: :language: kotlin :start-after: DOCSTART 37 :end-before: DOCEND 37 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 37 :end-before: DOCEND 37 - :dedent: 12 + :dedent: 16 Be very careful, however - this function neither guarantees that the signatures that are present are required, nor checks whether any signatures are missing. @@ -650,7 +650,7 @@ We can sign using our legal identity key, as follows: :language: kotlin :start-after: DOCSTART 38 :end-before: DOCEND 38 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -666,7 +666,7 @@ Or we can choose to sign using another one of our public keys: :language: kotlin :start-after: DOCSTART 39 :end-before: DOCEND 39 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -684,7 +684,7 @@ We can do this with our legal identity key: :language: kotlin :start-after: DOCSTART 40 :end-before: DOCEND 40 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -700,7 +700,7 @@ Or using another one of our public keys: :language: kotlin :start-after: DOCSTART 41 :end-before: DOCEND 41 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 9f19f1087a..a525a8b413 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -1,33 +1,42 @@ API: Vault Query ================ -Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault). +.. contents:: + +Overview +-------- +Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and +libraries for accessing RDBMS backed transactional stores (including the Vault). Corda provides a number of flexible query mechanisms for accessing the Vault: - Vault Query API -- using a JDBC session (as described in :ref:`Persistence `) -- custom JPA_/JPQL_ queries -- custom 3rd party Data Access frameworks such as `Spring Data `_ +- Using a JDBC session (as described in :ref:`Persistence `) +- Custom JPA_/JPQL_ queries +- Custom 3rd party Data Access frameworks such as `Spring Data `_ -The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultQueryService`` for use directly by flows: +The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the +``VaultService`` for use directly by flows: -.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/VaultService.kt :language: kotlin :start-after: DOCSTART VaultQueryAPI :end-before: DOCEND VaultQueryAPI + :dedent: 4 -and via ``CordaRPCOps`` for use by RPC client applications: +And via ``CordaRPCOps`` for use by RPC client applications: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultQueryByAPI :end-before: DOCEND VaultQueryByAPI + :dedent: 4 .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultTrackByAPI :end-before: DOCEND VaultTrackByAPI + :dedent: 4 Helper methods are also provided with default values for arguments: @@ -35,51 +44,84 @@ Helper methods are also provided with default values for arguments: :language: kotlin :start-after: DOCSTART VaultQueryAPIHelpers :end-before: DOCEND VaultQueryAPIHelpers + :dedent: 4 .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultTrackAPIHelpers :end-before: DOCEND VaultTrackAPIHelpers + :dedent: 4 -The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of filter criteria. +The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of +filter criteria: - Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``) - Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``) .. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL) -Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable. -Defaults are defined for Paging (pageNumber = 1, pageSize = 200) and Sorting (direction = ASC). +Simple pagination (page number and size) and sorting (directional ordering using standard or custom property +attributes) is also specifiable. Defaults are defined for paging (pageNumber = 1, pageSize = 200) and sorting +(direction = ASC). -The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported. +The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including +and/or composition and a rich set of operators to include: + +* Binary logical (AND, OR) +* Comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL) +* Equality (EQUAL, NOT_EQUAL) +* Likeness (LIKE, NOT_LIKE) +* Nullability (IS_NULL, NOT_NULL) +* Collection based (IN, NOT_IN) +* Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) There are four implementations of this interface which can be chained together to define advanced filters. -1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). +1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, + CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). - .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft locked states). + .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft + locked states). -2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). +2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core + ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a + specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable + attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). - .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. + .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common + state attributes to the **vault_fungible_states** table. -3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s). +3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` + and ``DealState`` contract state interfaces, used to represent entities that continuously supersede themselves, all + of which share the same ``linearId`` (e.g. trade entity states such as the ``IRSState`` defined in the SIMM + valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s). - .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. + .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those + interfaces common state attributes to the **vault_linear_states** table. -4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence ` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in ``QueryCriteriaUtils`` for a complete specification of the DSL. +4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined + by a custom contract state that implements its own schema as described in the :doc:`Persistence ` + documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe + ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The + ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator + types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, + max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple + construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in + ``QueryCriteriaUtils`` for a complete specification of the DSL. - .. note:: custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to - :doc:`Persistence ` for mechanisms of registering custom schemas for different testing purposes. + .. note:: Custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to + :doc:`Persistence ` for mechanisms of registering custom schemas for different testing + purposes. All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators. All ``QueryCriteria`` implementations provide an explicitly specifiable set of common attributes: 1. State status attribute (``Vault.StateStatus``), which defaults to filtering on UNCONSUMED states. - When chaining several criterias using AND / OR, the last value of this attribute will override any previous. -2. Contract state types (``>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all state types). - When chaining several criteria using ``and`` and ``or`` operators, all specified contract state types are combined into a single set. + When chaining several criterias using AND / OR, the last value of this attribute will override any previous +2. Contract state types (``>``), which will contain at minimum one type (by default this + will be ``ContractState`` which resolves to all state types). When chaining several criteria using ``and`` and + ``or`` operators, all specified contract state types are combined into a single set An example of a custom query is illustrated here: @@ -87,20 +129,30 @@ An example of a custom query is illustrated here: :language: kotlin :start-after: DOCSTART VaultQueryExample20 :end-before: DOCEND VaultQueryExample20 + :dedent: 12 -.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. +.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types + ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root + ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See + ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java. -.. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States). +.. note:: When specifying the ``ContractType`` as a parameterised type to the ``QueryCriteria`` in Kotlin, queries now + include all concrete implementations of that type if this is an interface. Previously, it was only possible to query + on concrete types (or the universe of all ``ContractState``). -The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based :doc:`Persistence ` framework adopted by Corda. +The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based +:doc:`Persistence ` framework adopted by Corda. .. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/ -.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables. +.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, + read-only access to underlying Corda tables. -.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute types. +.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that + Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute + types. An example of a custom query in Java is illustrated here: @@ -108,17 +160,24 @@ An example of a custom query in Java is illustrated here: :language: java :start-after: DOCSTART VaultJavaQueryExample3 :end-before: DOCEND VaultJavaQueryExample3 + :dedent: 16 -.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not resolve to an X500Name via the IdentityService, no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. +.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, + where an anonymous party does not resolve to an X500 name via the ``IdentityService``, no query results will ever be + produced. For performance reasons, queries do not use ``PublicKey`` as search criteria. Pagination ---------- -The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 results). -Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer from worrying about pagination where -result sets are expected to be constrained to 200 or fewer entries. Where large result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to define a ``PageSpecification`` to correctly process results with efficient memory utilistion. A fail-fast mode is in place to alert API users to the need for pagination where a single query returns more than 200 results and no ``PageSpecification`` -has been supplied. +The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 +results). Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer +from worrying about pagination where result sets are expected to be constrained to 200 or fewer entries. Where large +result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to +define a ``PageSpecification`` to correctly process results with efficient memory utilisation. A fail-fast mode is in +place to alert API users to the need for pagination where a single query returns more than 200 results and no +``PageSpecification`` has been supplied. -.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme caution as results returned may exceed your JVM's memory footprint. +.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme + caution as results returned may exceed your JVM's memory footprint. Example usage ------------- @@ -126,7 +185,7 @@ Example usage Kotlin ^^^^^^ -**General snapshot queries using** ``VaultQueryCriteria`` +**General snapshot queries using** ``VaultQueryCriteria``: Query for all unconsumed states (simplest query possible): @@ -134,6 +193,7 @@ Query for all unconsumed states (simplest query possible): :language: kotlin :start-after: DOCSTART VaultQueryExample1 :end-before: DOCEND VaultQueryExample1 + :dedent: 12 Query for unconsumed states for some state references: @@ -141,6 +201,7 @@ Query for unconsumed states for some state references: :language: kotlin :start-after: DOCSTART VaultQueryExample2 :end-before: DOCEND VaultQueryExample2 + :dedent: 12 Query for unconsumed states for several contract state types: @@ -148,6 +209,7 @@ Query for unconsumed states for several contract state types: :language: kotlin :start-after: DOCSTART VaultQueryExample3 :end-before: DOCEND VaultQueryExample3 + :dedent: 12 Query for unconsumed states for a given notary: @@ -155,6 +217,7 @@ Query for unconsumed states for a given notary: :language: kotlin :start-after: DOCSTART VaultQueryExample4 :end-before: DOCEND VaultQueryExample4 + :dedent: 12 Query for unconsumed states for a given set of participants: @@ -162,6 +225,7 @@ Query for unconsumed states for a given set of participants: :language: kotlin :start-after: DOCSTART VaultQueryExample5 :end-before: DOCEND VaultQueryExample5 + :dedent: 12 Query for unconsumed states recorded between two time intervals: @@ -169,6 +233,7 @@ Query for unconsumed states recorded between two time intervals: :language: kotlin :start-after: DOCSTART VaultQueryExample6 :end-before: DOCEND VaultQueryExample6 + :dedent: 12 .. note:: This example illustrates usage of a ``Between`` ``ColumnPredicate``. @@ -178,17 +243,21 @@ Query for all states with pagination specification (10 results per page): :language: kotlin :start-after: DOCSTART VaultQueryExample7 :end-before: DOCEND VaultQueryExample7 + :dedent: 12 -.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as demonstrated in the following example. +.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as + demonstrated in the following example. -Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further pages available: +Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further +pages available: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExamplePaging :end-before: DOCEND VaultQueryExamplePaging + :dedent: 8 -**LinearState and DealState queries using** ``LinearStateQueryCriteria`` +**LinearState and DealState queries using** ``LinearStateQueryCriteria``: Query for unconsumed linear states for given linear ids: @@ -196,6 +265,7 @@ Query for unconsumed linear states for given linear ids: :language: kotlin :start-after: DOCSTART VaultQueryExample8 :end-before: DOCEND VaultQueryExample8 + :dedent: 12 Query for all linear states associated with a linear id: @@ -203,6 +273,7 @@ Query for all linear states associated with a linear id: :language: kotlin :start-after: DOCSTART VaultQueryExample9 :end-before: DOCEND VaultQueryExample9 + :dedent: 12 Query for unconsumed deal states with deals references: @@ -210,6 +281,7 @@ Query for unconsumed deal states with deals references: :language: kotlin :start-after: DOCSTART VaultQueryExample10 :end-before: DOCEND VaultQueryExample10 + :dedent: 12 Query for unconsumed deal states with deals parties: @@ -217,8 +289,9 @@ Query for unconsumed deal states with deals parties: :language: kotlin :start-after: DOCSTART VaultQueryExample11 :end-before: DOCEND VaultQueryExample11 + :dedent: 12 -**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria`` +**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria``: Query for fungible assets for a given currency: @@ -226,6 +299,7 @@ Query for fungible assets for a given currency: :language: kotlin :start-after: DOCSTART VaultQueryExample12 :end-before: DOCEND VaultQueryExample12 + :dedent: 12 Query for fungible assets for a minimum quantity: @@ -233,19 +307,21 @@ Query for fungible assets for a minimum quantity: :language: kotlin :start-after: DOCSTART VaultQueryExample13 :end-before: DOCEND VaultQueryExample13 + :dedent: 12 .. note:: This example uses the builder DSL. -Query for fungible assets for a specifc issuer party: +Query for fungible assets for a specific issuer party: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExample14 :end-before: DOCEND VaultQueryExample14 + :dedent: 12 -**Aggregate Function queries using** ``VaultCustomQueryCriteria`` +**Aggregate Function queries using** ``VaultCustomQueryCriteria``: -.. note:: Query results for aggregate functions are contained in the `otherResults` attribute of a results Page. +.. note:: Query results for aggregate functions are contained in the ``otherResults`` attribute of a results Page. Aggregations on cash using various functions: @@ -253,8 +329,9 @@ Aggregations on cash using various functions: :language: kotlin :start-after: DOCSTART VaultQueryExample21 :end-before: DOCEND VaultQueryExample21 + :dedent: 12 -.. note:: `otherResults` will contain 5 items, one per calculated aggregate function. +.. note:: ``otherResults`` will contain 5 items, one per calculated aggregate function. Aggregations on cash grouped by currency for various functions: @@ -262,8 +339,10 @@ Aggregations on cash grouped by currency for various functions: :language: kotlin :start-after: DOCSTART VaultQueryExample22 :end-before: DOCEND VaultQueryExample22 + :dedent: 12 -.. note:: `otherResults` will contain 24 items, one result per calculated aggregate function per currency (the grouping attribute - currency in this case - is returned per aggregate result). +.. note:: ``otherResults`` will contain 24 items, one result per calculated aggregate function per currency (the + grouping attribute - currency in this case - is returned per aggregate result). Sum aggregation on cash grouped by issuer party and currency and sorted by sum: @@ -271,10 +350,15 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum: :language: kotlin :start-after: DOCSTART VaultQueryExample23 :end-before: DOCEND VaultQueryExample23 + :dedent: 12 -.. note:: `otherResults` will contain 12 items sorted from largest summed cash amount to smallest, one result per calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). +.. note:: ``otherResults`` will contain 12 items sorted from largest summed cash amount to smallest, one result per + calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). -**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. +Dynamic queries (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an +additional ``QueryResults`` return type in the form of an ``Observable``. Refer to +`ReactiveX Observable `_ for a detailed understanding and usage of +this type. Track unconsumed cash states: @@ -282,6 +366,7 @@ Track unconsumed cash states: :language: kotlin :start-after: DOCSTART VaultQueryExample15 :end-before: DOCEND VaultQueryExample15 + :dedent: 20 Track unconsumed linear states: @@ -289,8 +374,9 @@ Track unconsumed linear states: :language: kotlin :start-after: DOCSTART VaultQueryExample16 :end-before: DOCEND VaultQueryExample16 + :dedent: 20 -.. note:: This will return both Deal and Linear states. +.. note:: This will return both ``DealState`` and ``LinearState`` states. Track unconsumed deal states: @@ -298,8 +384,9 @@ Track unconsumed deal states: :language: kotlin :start-after: DOCSTART VaultQueryExample17 :end-before: DOCEND VaultQueryExample17 + :dedent: 20 -.. note:: This will return only Deal states. +.. note:: This will return only ``DealState`` states. Java examples ^^^^^^^^^^^^^ @@ -310,6 +397,7 @@ Query for all unconsumed linear states: :language: java :start-after: DOCSTART VaultJavaQueryExample0 :end-before: DOCEND VaultJavaQueryExample0 + :dedent: 12 Query for all consumed cash states: @@ -317,6 +405,7 @@ Query for all consumed cash states: :language: java :start-after: DOCSTART VaultJavaQueryExample1 :end-before: DOCEND VaultJavaQueryExample1 + :dedent: 12 Query for consumed deal states or linear ids, specify a paging specification and sort by unique identifier: @@ -324,8 +413,9 @@ Query for consumed deal states or linear ids, specify a paging specification and :language: java :start-after: DOCSTART VaultJavaQueryExample2 :end-before: DOCEND VaultJavaQueryExample2 + :dedent: 12 -**Aggregate Function queries using** ``VaultCustomQueryCriteria`` +**Aggregate Function queries using** ``VaultCustomQueryCriteria``: Aggregations on cash using various functions: @@ -333,6 +423,7 @@ Aggregations on cash using various functions: :language: java :start-after: DOCSTART VaultJavaQueryExample21 :end-before: DOCEND VaultJavaQueryExample21 + :dedent: 16 Aggregations on cash grouped by currency for various functions: @@ -340,6 +431,7 @@ Aggregations on cash grouped by currency for various functions: :language: java :start-after: DOCSTART VaultJavaQueryExample22 :end-before: DOCEND VaultJavaQueryExample22 + :dedent: 16 Sum aggregation on cash grouped by issuer party and currency and sorted by sum: @@ -347,6 +439,7 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum: :language: java :start-after: DOCSTART VaultJavaQueryExample23 :end-before: DOCEND VaultJavaQueryExample23 + :dedent: 16 Track unconsumed cash states: @@ -354,13 +447,16 @@ Track unconsumed cash states: :language: java :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 + :dedent: 12 -Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique identifier): +Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique +identifier): .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java :language: java :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 + :dedent: 12 Troubleshooting --------------- @@ -373,24 +469,27 @@ If the results your were expecting do not match actual returned query results we Behavioural notes ----------------- -1. **TrackBy** updates do not take into account the full criteria specification due to different and more restrictive syntax - in `observables `_ filtering (vs full SQL-92 JDBC filtering as used in snapshot views). - Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType`` (UNCONSUMED, CONSUMED, ALL) only. -2. **QueryBy** and **TrackBy snapshot views** using pagination may return different result sets as each paging request is a - separate SQL query on the underlying database, and it is entirely conceivable that state modifications are taking - place in between and/or in parallel to paging requests. - When using pagination, always check the value of the ``totalStatesAvailable`` (from the ``Vault.Page`` result) and - adjust further paging requests appropriately. +1. ``TrackBy`` updates do not take into account the full criteria specification due to different and more restrictive + syntax in `observables `_ filtering (vs full SQL-92 JDBC filtering as used + in snapshot views). Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType`` + (UNCONSUMED, CONSUMED, ALL) only +2. ``QueryBy`` and ``TrackBy`` snapshot views using pagination may return different result sets as each paging request + is a separate SQL query on the underlying database, and it is entirely conceivable that state modifications are + taking place in between and/or in parallel to paging requests. When using pagination, always check the value of the + ``totalStatesAvailable`` (from the ``Vault.Page`` result) and adjust further paging requests appropriately. Other use case scenarios ------------------------ -For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of the box. Namely, implementations of JPQL (JPA Query Language) such as **Hibernate** for advanced SQL access, and **Spring Data** for advanced pagination and ordering constructs. +For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is +recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of +the box. Namely, implementations of JPQL (JPA Query Language) such as Hibernate for advanced SQL access, and +Spring Data for advanced pagination and ordering constructs. The Corda Tutorials provide examples satisfying these additional Use Cases: - 1. Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State - 2. Template / Tutorial CorDapp service query extension executing Named Queries via JPQL_ + 1. Example CorDapp service using Vault API Custom Query to access attributes of IOU State + 2. Example CorDapp service query extension executing Named Queries via JPQL_ 3. `Advanced pagination `_ queries using Spring Data JPA_ .. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql diff --git a/docs/source/azure-vm.rst b/docs/source/azure-vm.rst index bc5ae24fa4..7f94f0fdcb 100644 --- a/docs/source/azure-vm.rst +++ b/docs/source/azure-vm.rst @@ -97,7 +97,7 @@ Loading the Yo! CordDapp on your Corda nodes lets you send simple Yo! messages t * **Loading the Yo! CorDapp onto your nodes** -The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their plugins directory. +The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their cordapps directory. Connect to one of your Corda nodes (make sure this is not the Notary node) using an SSH client of your choice (e.g. Putty) and log into the virtual machine using the public IP address and your SSH key or username / password combination you defined in Step 1 of the Azure build process. Type the following command: @@ -105,14 +105,14 @@ For Corda nodes running release M10 .. sourcecode:: shell - cd /opt/corda/plugins + cd /opt/corda/cordapps wget http://downloads.corda.net/cordapps/net/corda/yo/0.10.1/yo-0.10.1.jar For Corda nodes running release M11 .. sourcecode:: shell - cd /opt/corda/plugins + cd /opt/corda/cordapps wget http://downloads.corda.net/cordapps/net/corda/yo/0.11.0/yo-0.11.0.jar Now restart Corda and the Corda webserver using the following commands or restart your Corda VM from the Azure portal: diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index e073aef623..a5ea4e9bfe 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -7,6 +7,6 @@ Building a CorDapp cordapp-overview writing-cordapps cordapp-build-systems - api-index + corda-api flow-cookbook cheat-sheet \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 194a00deee..e3be980710 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,42 +6,103 @@ from the previous milestone release. UNRELEASED ---------- +* ``OpaqueBytes.bytes`` now returns a clone of its underlying ``ByteArray``, and has been redeclared as ``final``. + This is a minor change to the public API, but is required to ensure that classes like ``SecureHash`` are immutable. + +* ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions. + +* Renamed "plugins" directory on nodes to "cordapps" + +* The ``Cordformation`` gradle plugin has been split into ``cordformation`` and ``cordapp``. The former builds and + deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in + the standard CorDapp format. + +* ``Cordform`` and node identity generation: + * Removed the parameter ``NetworkMap`` from Cordform. Now at the end of the deployment the following happens: + 1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory. + 2. Every serialized ``NodeInfo`` above is copied in every other node "additional-node-info" folder under the NodeInfo folder. + +* Nodes read and poll the filesystem for serialized ``NodeInfo`` in the ``additional-node-info`` directory. + +* ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup. + +* Enums now respsect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't + either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is + thrown. + +* ``extraAdvertisedServiceIds`` config has been removed as part of the previous work to retire the concept of advertised + services. The remaining use of this config was for notaries, the configuring of which has been cleaned up and simplified. + ``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` have also been removed and replaced by a single + ``notary`` config object. See :doc:`corda-configuration-file` for more details. + +* Gradle task ``deployNodes`` can have an additional parameter `configFile` with the path to a properties file + to be appended to node.conf. + +* Cordformation node building DSL can have an additional parameter `configFile` with the path to a properties file + to be appended to node.conf. + +* ``FlowLogic`` now has a static method called ``sleep`` which can be used in certain circumstances to help with resolving + contention over states in flows. This should be used in place of any other sleep primitive since these are not compatible + with flows and their use will be prevented at some point in the future. Pay attention to the warnings and limitations + described in the documentation for this method. This helps resolve a bug in ``Cash`` coin selection. + A new static property `currentTopLevel` returns the top most `FlowLogic` instance, or null if not in a flow. + +* ``CordaService`` annotated classes should be upgraded to take a constructor parameter of type ``AppServiceHub`` which + allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability + service classes with only ``ServiceHub`` constructors will still work. + +* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window, or ``null`` if the + time-window is open-ended. + +.. _changelog_v1: Release 1.0 ----------- +* Unification of VaultQuery And VaultService APIs + Developers now only need to work with a single Vault Service API for all needs. + * Java 8 lambdas now work property with Kryo during check-pointing. +* Java 8 serializable lambdas now work property with Kryo during check-pointing. + * String constants have been marked as ``const`` type in Kotlin, eliminating cases where functions of the form ``get()`` were created for the Java API. These can now be referenced by their name directly. * ``FlowLogic`` communication has been extensively rewritten to use functions on ``FlowSession`` as the base for communication between nodes. + * Calls to ``send()``, ``receive()`` and ``sendAndReceive()`` on FlowLogic should be replaced with calls to the function of the same name on ``FlowSession``. Note that the replacement functions do not take in a destination parameter, as this is defined in the session. * Initiated flows now take in a ``FlowSession`` instead of ``Party`` in their constructor. If you need to access the counterparty identity, it is in the ``counterparty`` property of the flow session. + * Added X509EdDSAEngine to intercept and rewrite EdDSA public keys wrapped in X509Key instances. This corrects an issue with verifying certificate paths loaded from a Java Keystore where they contain EdDSA keys. -* generateSpend() now creates a new confidential identity for the change address rather than using the identity of the - input state owner. +* Confidential identities are now complete: + + * The identity negotiation flow is now called ``SwapIdentitiesFlow``, renamed from ``TransactionKeyFlow``. + * generateSpend() now creates a new confidential identity for the change address rather than using the identity of the + input state owner. + * Please see the documentation :doc:`key-concepts-identity` and :doc:`api-identity` for more details. * Remove the legacy web front end from the SIMM demo. * ``NodeInfo`` and ``NetworkMapCache`` changes: + * Removed ``NodeInfo::legalIdentity`` in preparation for handling of multiple identities. We left list of ``NodeInfo::legalIdentitiesAndCerts``, - the first identity still plays a special role of main node identity. + the first identity still plays a special role of main node identity. * We no longer support advertising services in network map. Removed ``NodeInfo::advertisedServices``, ``serviceIdentities`` - and ``notaryIdentity``. + and ``notaryIdentity``. * Removed service methods from ``NetworkMapCache``: ``partyNodes``, ``networkMapNodes``, ``notaryNodes``, ``regulatorNodes``, - ``getNodesWithService``, ``getPeersWithService``, ``getRecommended``, ``getNodesByAdvertisedServiceIdentityKey``, ``getAnyNotary``, - ``notaryNode``, ``getAnyServiceOfType``. To get all known ``NodeInfo``s call ``allNodes``. + ``getNodesWithService``, ``getPeersWithService``, ``getRecommended``, ``getNodesByAdvertisedServiceIdentityKey``, ``getAnyNotary``, + ``notaryNode``, ``getAnyServiceOfType``. To get all known ``NodeInfo``'s call ``allNodes``. * In preparation for ``NetworkMapService`` redesign and distributing notaries through ``NetworkParameters`` we added - ``NetworkMapCache::notaryIdentities`` list to enable to lookup for notary parties known to the network. Related ``CordaRPCOps::notaryIdentities`` - was introduced. Other special nodes parties like Oracles or Regulators need to be specified directly in CorDapp or flow. + ``NetworkMapCache::notaryIdentities`` list to enable to lookup for notary parties known to the network. Related ``CordaRPCOps::notaryIdentities`` + was introduced. Other special nodes parties like Oracles or Regulators need to be specified directly in CorDapp or flow. * Moved ``ServiceType`` and ``ServiceInfo`` to ``net.corda.nodeapi`` package as services are only required on node startup. * Adding enum support to the class carpenter @@ -56,9 +117,11 @@ Release 1.0 * About half of the code in test-utils has been moved to a new module ``node-driver``, and the test scope modules are now located in a ``testing`` directory. -* Removed `requireSchemas` CordaPluginRegistry configuration item. - Custom schemas are now automatically located using classpath scanning for deployed CorDapps. - Improved support for testing custom schemas in MockNode and MockServices using explicit registration. +* ``CordaPluginRegistry`` has been renamed to ``SerializationWhitelist`` and moved to the ``net.corda.core.serialization`` + package. The API for whitelisting types that can't be annotated was slightly simplified. This class used to contain + many things, but as we switched to annotations and classpath scanning over time it hollowed out until this was + the only functionality left. You also need to rename your services resource file to the new class name. + An associated property on ``MockNode`` was renamed from ``testPluginRegistries`` to ``testSerializationWhitelists``. * Contract Upgrades: deprecated RPC authorisation / deauthorisation API calls in favour of equivalent flows in ContractUpgradeFlow. Implemented contract upgrade persistence using JDBC backed persistent map. @@ -76,10 +139,14 @@ Release 1.0 * Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria`` * Following deprecated methods have been removed: + * In ``DataFeed`` + * ``first`` and ``current``, replaced by ``snapshot`` * ``second`` and ``future``, replaced by ``updates`` + * In ``CordaRPCOps`` + * ``stateMachinesAndUpdates``, replaced by ``stateMachinesFeed`` * ``verifiedTransactions``, replaced by ``verifiedTransactionsFeed`` * ``stateMachineRecordedTransactionMapping``, replaced by ``stateMachineRecordedTransactionMappingFeed`` @@ -89,13 +156,12 @@ Release 1.0 ``ResolveTransactionsFlow`` has been made internal. Instead merge the receipt of the ``SignedTransaction`` and the subsequent sub-flow call to ``ResolveTransactionsFlow`` with a single call to ``ReceiveTransactionFlow``. The flow running on the counterparty must use ``SendTransactionFlow`` at the correct place. There is also ``ReceiveStateAndRefFlow`` and ``SendStateAndRefFlow`` for - dealing with ``StateAndRef``s. + dealing with ``StateAndRef``'s. * Vault query soft locking enhancements and deprecations + * removed original ``VaultService`` ``softLockedStates` query mechanism. - * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification - of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified - by set of lock ids) + * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified by set of lock ids) * Trader demo now issues cash and commercial paper directly from the bank node, rather than the seller node self-issuing commercial paper but labelling it as if issued by the bank. @@ -105,7 +171,7 @@ Release 1.0 If you specifically need well known identities, use the network map, which is the authoritative source of current well known identities. -* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`. +* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils``. * Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use `CashIssueFlow` instead. @@ -157,15 +223,16 @@ Release 1.0 * The unused ``MetaData`` and ``SignatureType`` in ``crypto`` package have been removed. -* The ``class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata): DigitalSignature(bytes)`` - class is now utilised Vs the old ``DigitalSignature.WithKey`` for Corda transaction signatures. Practically, it takes - the ``signatureMetadata`` as an extra input, in order to support signing both the transaction and the extra metadata. +* The ``class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata:`` + ``SignatureMetadata): DigitalSignature(bytes)`` class is now utilised Vs the old ``DigitalSignature.WithKey`` for + Corda transaction signatures. Practically, it takes the ``signatureMetadata`` as an extra input, in order to support + signing both the transaction and the extra metadata. * To reflect changes in the signing process, the ``Crypto`` object is now equipped with the: ``fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature`` and ``fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean`` functions. -* ``SerializationCustomization.addToWhitelist()` now accepts multiple classes via varargs. +* ``SerializationCustomization.addToWhitelist()`` now accepts multiple classes via varargs. * Two functions to easily sign a ``FilteredTransaction`` have been added to ``ServiceHub``: ``createSignature(filteredTransaction: FilteredTransaction, publicKey: PublicKey)`` and @@ -195,8 +262,8 @@ Release 1.0 * All of the ``serializedHash`` and ``computeNonce`` functions have been removed from ``MerkleTransaction``. The ``serializedHash(x: T)`` and ``computeNonce`` were moved to ``CryptoUtils``. -* Two overloaded methods ``componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int, - internalIndex: Int): SecureHash`` and ``componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash`` have +* Two overloaded methods ``componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt,`` + ``componentGroupIndex: Int, internalIndex: Int): SecureHash`` and ``componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash`` have been added to ``CryptoUtils``. Similarly to ``computeNonce``, they internally use SHA256d for nonce and leaf hash computations. @@ -207,24 +274,25 @@ Release 1.0 ``FilteredTransaction`` now extend ``TraversableTransaction``. * Two classes, ``ComponentGroup(open val groupIndex: Int, open val components: List)`` and - ``FilteredComponentGroup(override val groupIndex: Int, override val components: List, - val nonces: List, val partialMerkleTree: PartialMerkleTree): ComponentGroup(groupIndex, components)`` - have been added, which are properties of the ``WireTransaction`` and ``FilteredTransaction``, respectively. + ``FilteredComponentGroup(override val groupIndex: Int, override val components:`` + ``List, val nonces: List, val partialMerkleTree:`` + ``PartialMerkleTree): ComponentGroup(groupIndex, components)`` have been added, which are properties + of the ``WireTransaction`` and ``FilteredTransaction``, respectively. * ``checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum)`` is added to ``FilteredTransaction``, a new function to check if all components are visible in a specific component-group. * To allow for backwards compatibility, ``WireTransaction`` and ``FilteredTransaction`` have new fields and constructors: ``WireTransaction(componentGroups: List, privacySalt: PrivacySalt = PrivacySalt())``, - ``FilteredTransaction private constructor(id: SecureHash,filteredComponentGroups: List, - groupHashes: List``. ``FilteredTransaction`` is still built via - ``buildFilteredTransaction(wtx: WireTransaction, filtering: Predicate). + ``FilteredTransaction private constructor(id: SecureHash,filteredComponentGroups:`` + ``List, groupHashes: List``. ``FilteredTransaction`` is still built via + ``buildFilteredTransaction(wtx: WireTransaction, filtering: Predicate)``. * ``FilteredLeaves`` class have been removed and as a result we can directly call the components from ``FilteredTransaction``, such as ``ftx.inputs`` Vs the old ``ftx.filteredLeaves.inputs``. * A new ``ComponentGroupEnum`` is added with the following enum items: ``INPUTS_GROUP``, ``OUTPUTS_GROUP``, - ``COMMANDS_GROUP``, ``ATTACHMENTS_GROUP``, ``NOTARY_GROUP``, ``TIMEWINDOW_GROUP``. + ``COMMANDS_GROUP``, ``ATTACHMENTS_GROUP``, ``NOTARY_GROUP``, ``TIMEWINDOW_GROUP``. * ``ContractUpgradeFlow.Initiator`` has been renamed to ``ContractUpgradeFlow.Initiate`` @@ -233,6 +301,8 @@ Release 1.0 * Current implementation of SSL in ``CordaRPCClient`` has been removed until we have a better solution which doesn't rely on the node's keystore. +.. _changelog_m14: + Milestone 14 ------------ @@ -326,6 +396,8 @@ Milestone 14 * Added JPA ``AbstractPartyConverter`` to ensure identity schema attributes are persisted securely according to type (well known party, resolvable anonymous party, completely anonymous party). +.. _changelog_m13: + Milestone 13 ------------ @@ -405,8 +477,10 @@ support for more currencies to the DemoBench and Explorer tools. * Upgraded BouncyCastle to v1.57. * Upgraded Requery to v1.3.1. -Milestone 12 ------------- +.. _changelog_m12: + +Milestone 12 (First Public Beta) +-------------------------------- * Quite a few changes have been made to the flow API which should make things simpler when writing CorDapps: diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index a00b6b0870..13322bdc4e 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -7,95 +7,93 @@ Upgrading contracts =================== -While every care is taken in development of contract code, -inevitably upgrades will be required to fix bugs (in either design or implementation). -Upgrades can involve a substitution of one version of the contract code for another or changing -to a different contract that understands how to migrate the existing state objects. State objects -refer to the contract code (by hash) they are intended for, and even where state objects can be used -with different contract versions, changing this value requires issuing a new state object. +While every care is taken in development of contract code, inevitably upgrades will be required to fix bugs (in either +design or implementation). Upgrades can involve a substitution of one version of the contract code for another or +changing to a different contract that understands how to migrate the existing state objects. When state objects are +added as outputs to transactions, they are linked to the contract code they are intended for via the +``StateAndContract`` type. Changing a state's contract only requires substituting one ``ContractClassName`` for another. Workflow -------- - Here's the workflow for contract upgrades: -1. Two banks, A and B negotiate a trade, off-platform +1. Banks A and B negotiate a trade, off-platform -2. Banks A and B execute a protocol to construct a state object representing the trade, using contract X, and include it in a transaction (which is then signed and sent to the consensus service). +2. Banks A and B execute a flow to construct a state object representing the trade, using contract X, and include it in + a transaction (which is then signed and sent to the consensus service) -3. Time passes. +3. Time passes -4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. The developer will then notify all existing users (e.g. via a mailing list or CorDapp store) to stop their nodes from issuing further states with contract X. +4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. The + developer will then notify all existing users (e.g. via a mailing list or CorDapp store) to stop their nodes from + issuing further states with contract X -5. Banks A and B review the new contract via standard change control processes and identify the contract states they agree to upgrade (they may decide not to upgrade some contract states as these might be needed for some other obligation contract). +5. Banks A and B review the new contract via standard change control processes and identify the contract states they + agree to upgrade (they may decide not to upgrade some contract states as these might be needed for some other + obligation contract) -6. Banks A and B instruct their Corda nodes (via RPC) to be willing to upgrade state objects of contract X, to state objects for contract Y using agreed upgrade path. +6. Banks A and B instruct their Corda nodes (via RPC) to be willing to upgrade state objects with contract X to state + objects with contract Y using the agreed upgrade path -7. One of the parties initiates (``Initiator``) an upgrade of state objects referring to contract X, to a new state object referring to contract Y. +7. One of the parties (the ``Initiator``) initiates a flow to replace state objects referring to contract X with new + state objects referring to contract Y -8. A proposed transaction ``Proposal``, taking in the old state and outputting the reissued version, is created and signed with the node's private key. +8. A proposed transaction (the ``Proposal``), with the old states as input and the reissued states as outputs, is + created and signed with the node's private key -9. The ``Initiator`` node sends the proposed transaction, along with details of the new contract upgrade path its proposing, to all participants of the state object. +9. The ``Initiator`` node sends the proposed transaction, along with details of the new contract upgrade path that it + is proposing, to all participants of the state object -10. Each counterparty ``Acceptor`` verifies the proposal, signs or rejects the state reissuance accordingly, and sends a signature or rejection notification back to the initiating node. +10. Each counterparty (the ``Acceptor``s) verifies the proposal, signs or rejects the state reissuance accordingly, and + sends a signature or rejection notification back to the initiating node -11. If signatures are received from all parties, the initiating node assembles the complete signed transaction and sends it to the consensus service. +11. If signatures are received from all parties, the ``Initiator`` assembles the complete signed transaction and sends + it to the notary +Authorising an upgrade +---------------------- +Each of the participants in the state for which the contract is being upgraded will have to instruct their node that +they agree to the upgrade before the upgrade can take place. The ``ContractUpgradeFlow`` is used to manage the +authorisation process. Each node administrator can use RPC to trigger either an ``Authorise`` or a ``Deauthorise`` flow +for the state in question. -Authorising upgrade -------------------- +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 -Each of the participants in the upgrading contract will have to instruct their node that they are willing to upgrade the state object before the upgrade. -The ``ContractUpgradeFlow`` is used to manage the authorisation records. The administrator can use RPC to trigger either an ``Authorise`` or ``Deauthorise`` flow. - -.. container:: codeset - - .. sourcecode:: kotlin - - /** - * Authorise a contract state upgrade. - * This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. - * Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class. - * This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator]. - */ - @StartableByRPC - class Authorise( - val stateAndRef: StateAndRef<*>, - private val upgradedContractClass: Class> - ) : FlowLogic() - - /** - * Deauthorise a contract state upgrade. - * This will remove the upgrade authorisation from persistent store (and prevent any further upgrade) - */ - @StartableByRPC - class Deauthorise( - val stateRef: StateRef - ) : FlowLogic< Void?>() +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 Proposing an upgrade -------------------- +After all parties have authorised the contract upgrade for the state, one of the contract participants can initiate the +upgrade process by triggering the ``ContractUpgradeFlow.Initiate`` flow. ``Initiate`` creates a transaction including +the old state and the updated state, and sends it to each of the participants. Each participant will verify the +transaction, create a signature over it, and send the signature back to the initiator. Once all the signatures are +collected, the transaction will be notarised and persisted to every participant's vault. -After all parties have registered the intention of upgrading the contract state, one of the contract participants can initiate the upgrade process by triggering the ``Initiator`` contract upgrade flow. -The ``Initiator`` will create a new state and sent to each participant for signatures, each of the participants (Acceptor) will verify, sign the proposal and return to the initiator. -The transaction will be notarised and persisted once every participant verified and signed the upgrade proposal. +Example +------- +Suppose Bank A has entered into an agreement with Bank B which is represented by the state object +``DummyContractState`` and governed by the contract code ``DummyContract``. A few days after the exchange of contracts, +the developer of the contract code discovers a bug in the contract code. -Examples --------- +Bank A and Bank B decide to upgrade the contract to ``DummyContractV2``: -Lets assume Bank A has entered into an agreement with Bank B, and the contract is translated into contract code ``DummyContract`` with state object ``DummyContractState``. +1. The developer creates a new contract ``DummyContractV2`` extending the ``UpgradedContract`` class, and a new state + object ``DummyContractV2.State`` referencing the new contract. -A few days after the exchange of contracts, the developer of the contract code discovered a bug/misrepresentation in the contract code. -Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` - -1. Developer will create a new contract extending the ``UpgradedContract`` class, and a new state object ``DummyContractV2.State`` referencing the new contract. - -.. literalinclude:: /../../test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt +.. literalinclude:: /../../testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 -2. Bank A will instruct its node to accept the contract upgrade to ``DummyContractV2`` for the contract state. +2. Bank A instructs its node to accept the contract upgrade to ``DummyContractV2`` for the contract state. .. container:: codeset @@ -105,9 +103,9 @@ Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` val rpcA = rpcClient.proxy() rpcA.startFlow(ContractUpgradeFlow.Authorise(<>, DummyContractV2::class.java)) -3. Bank B now initiate the upgrade Flow, this will send a upgrade proposal to all contract participants. -Each of the participants of the contract state will sign and return the contract state upgrade proposal once they have validated and agreed with the upgrade. -The upgraded transaction state will be recorded in every participant's node at the end of the flow. +3. Bank B initiates the upgrade flow, which will send an upgrade proposal to all contract participants. Each of the + participants of the contract state will sign and return the contract state upgrade proposal once they have validated + and agreed with the upgrade. The upgraded transaction will be recorded in every participant's node by the flow. .. container:: codeset diff --git a/docs/source/corda-api.rst b/docs/source/corda-api.rst new file mode 100644 index 0000000000..8b5dd94053 --- /dev/null +++ b/docs/source/corda-api.rst @@ -0,0 +1,93 @@ +Corda API +========= + +The following are the core APIs that are used in the development of CorDapps: + +.. toctree:: + :maxdepth: 1 + + api-states + api-persistence + api-contracts + api-vault-query + api-transactions + api-flows + api-identity + api-service-hub + api-rpc + api-core-types + +Before reading this page, you should be familiar with the :doc:`key concepts of Corda `. + +Internal APIs and stability guarantees +-------------------------------------- + +.. warning:: For Corda 1.0 we do not currently provide a stable wire protocol or support for database upgrades. + Additionally, the JSON format produced by the client-jackson module may change in future. + Therefore, you should not expect to be able to migrate persisted data from 1.0 to future versions. + + Additionally, it may be necessary to recompile applications against future versions of the API until we begin offering + ABI stability as well. We plan to do this soon after the release of Corda 1.0. + + Finally, please note that the 1.0 release has not yet been security audited. You should not run it in situations + where security is required. + +Corda artifacts can be required from Java 9 Jigsaw modules. +From within a ``module-info.java``, you can reference one of the modules e.g., ``requires net.corda.core;``. + +.. warning:: while Corda artifacts can be required from ``module-info.java`` files, they are still not proper Jigsaw modules, + because they rely on the automatic module mechanism and declare no module descriptors themselves. We plan to integrate Jigsaw more thoroughly in the future. + +Corda stable modules +-------------------- + +The following modules have a stable API we commit not to break in following releases, unless an incompatible change is required for security reasons: + +* **Core (net.corda.core)**: core Corda libraries such as crypto functions, types for Corda's building blocks: states, contracts, transactions, attachments, etc. and some interfaces for nodes and protocols +* **Client RPC (net.corda.client.rpc)**: client RPC +* **Client Jackson (net.corda.client.jackson)**: JSON support for client applications + +Corda incubating modules +------------------------ + +The following modules don't yet have a completely stable API, but we will do our best to minimise disruption to +developers using them until we are able to graduate them into the public API: + +* **net.corda.node.driver**: test utilities to run nodes programmatically +* **net.corda.confidential.identities**: experimental support for confidential identities on the ledger +* **net.corda.node.test.utils**: generic test utilities +* **net.corda.finance**: a range of elementary contracts (and associated schemas) and protocols, such as abstract fungible assets, cash, obligation and commercial paper +* **net.corda.client.jfx**: support for Java FX UI +* **net.corda.client.mock**: client mock utilities +* **Cordformation**: Gradle integration plugins + +Corda unstable modules +---------------------- + +The following modules are available but we do not commit to their stability or continuation in any sense: + +* **net.corda.buildSrc**: necessary gradle plugins to build Corda +* **net.corda.node**: core code of the Corda node (eg: node driver, node services, messaging, persistence) +* **net.corda.node.api**: data structures shared between the node and the client module, e.g. types sent via RPC +* **net.corda.samples.network.visualiser**: a network visualiser that uses a simulation to visualise the interaction and messages between nodes on the Corda network +* **net.corda.samples.demos.attachment**: demonstrates sending a transaction with an attachment from one to node to another, and the receiving node accessing the attachment +* **net.corda.samples.demos.bankofcorda**: simulates the role of an asset issuing authority (eg. central bank for cash) +* **net.corda.samples.demos.irs**: demonstrates an Interest Rate Swap agreement between two banks +* **net.corda.samples.demos.notary**: a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft or BFT SMaRt) notary +* **net.corda.samples.demos.simmvaluation**: A demo of SIMM valuation and agreement on a distributed ledger +* **net.corda.samples.demos.trader**: demonstrates four nodes, a notary, an issuer of cash (Bank of Corda), and two parties trading with each other, exchanging cash for a commercial paper +* **net.corda.node.smoke.test.utils**: test utilities for smoke testing +* **net.corda.node.test.common**: common test functionality +* **net.corda.tools.demobench**: a GUI tool that allows to run Corda nodes locally for demonstrations +* **net.corda.tools.explorer**: a GUI front-end for Corda +* **net.corda.tools.graphs**: utilities to infer project dependencies +* **net.corda.tools.loadtest**: Corda load tests +* **net.corda.verifier**: allows out-of-node transaction verification, allowing verification to scale horizontally +* **net.corda.webserver**: is a servlet container for CorDapps that export HTTP endpoints. This server is an RPC client of the node +* **net.corda.sandbox-creator**: sandbox utilities +* **net.corda.quasar.hook**: agent to hook into Quasar and provide types exclusion lists + +.. warning:: Code inside any package in the ``net.corda`` namespace which contains ``.internal`` or in ``net.corda.node`` for internal use only. + Future releases will reject any CorDapps that use types from these packages. + +.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow. diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 7af765b7b3..721e6ff74b 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -30,7 +30,7 @@ General node configuration file for hosting the IRSDemo services. .. literalinclude:: example-code/src/main/resources/example-node.conf :language: javascript -NetworkMapService plus Simple Notary configuration file. +Simple Notary configuration file. .. parsed-literal:: @@ -40,7 +40,9 @@ NetworkMapService plus Simple Notary configuration file. p2pAddress : "localhost:12345" rpcAddress : "localhost:12346" webAddress : "localhost:12347" - extraAdvertisedServiceIds : ["corda.notary.simple"] + notary : { + validating : false + } useHTTPS : false devMode : true // Certificate signing service will be hosted by R3 in the near future. @@ -92,19 +94,29 @@ path to the node's base directory. .. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field is present the web server will start. -:extraAdvertisedServiceIds: A list of ServiceType id strings to be advertised to the NetworkMapService and thus be available - when other nodes query the NetworkMapCache for supporting nodes. This can also include plugin services loaded from .jar - files in the plugins folder. Optionally, a custom advertised service name can be provided by appending it to the service - type id: ``"corda.notary.validating|Notary A"`` +:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt + cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. -:notaryNodeAddress: The host and port to which to bind the embedded Raft server. Required only when running a distributed - notary service. A group of Corda nodes can run a distributed notary service by each running an embedded Raft server and - joining them to the same cluster to replicate the committed state log. Note that the Raft cluster uses a separate transport - layer for communication that does not integrate with ArtemisMQ messaging services. + :validating: Boolean to determine whether the notary is a validating or non-validating one. -:notaryClusterAddresses: List of Raft cluster member addresses used to join the cluster. At least one of the specified - members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be - bootstrapped. Required only when running a distributed notary service. + :raft: If part of a distributed Raft cluster specify this config object, with the following settings: + + :nodeAddress: The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a + separate transport layer for communication that does not integrate with ArtemisMQ messaging services. + + :clusterAddresses: List of Raft cluster member addresses used to join the cluster. At least one of the specified + members must be active and be able to communicate with the cluster leader for joining. If empty, a new + cluster will be bootstrapped. + + :bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings: + + :replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id. + + :clusterAddresses: List of all BFT-SMaRt cluster member addresses. + + :custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`. + + Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified. :networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is a config object with the details of the network map service: diff --git a/docs/source/corda-repo-layout.rst b/docs/source/corda-repo-layout.rst index 9c8fc9cb1f..bbec123ab4 100644 --- a/docs/source/corda-repo-layout.rst +++ b/docs/source/corda-repo-layout.rst @@ -6,11 +6,11 @@ The Corda repository comprises the following folders: * **buildSrc** contains necessary gradle plugins to build Corda * **client** contains libraries for connecting to a node, working with it remotely and binding server-side data to JavaFX UI +* **confidential-identities** contains experimental support for confidential identities on the ledger * **config** contains logging configurations and the default node configuration file * **core** containing the core Corda libraries such as crypto functions, types for Corda's building blocks: states, contracts, transactions, attachments, etc. and some interfaces for nodes and protocols -* **docs** contains the Corda docsite in restructured text format as well as the built docs in html. The docs can be - accessed via ``/docs/index.html`` from the root of the repo +* **docs** contains the Corda docsite in restructured text format * **experimental** contains platform improvements that are still in the experimental stage * **finance** defines a range of elementary contracts (and associated schemas) and protocols, such as abstract fungible assets, cash, obligation and commercial paper @@ -20,7 +20,7 @@ The Corda repository comprises the following folders: * **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence) * **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC * **samples** contains all our Corda demos and code samples -* **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the +* **testing** contains some utilities for unit testing contracts (the contracts testing DSL) and flows (the mock network) implementation * **tools** contains the explorer which is a GUI front-end for Corda, and also the DemoBench which is a GUI tool that allows you to run Corda nodes locally for demonstrations diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 398bfa0dc1..e20065838f 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -16,7 +16,7 @@ For example if your Cordapp depends on ``corda-core``, ``your-other-cordapp`` an JAR will contain all classes and resources from the ``apache-commons`` JAR and its dependencies and *nothing* from the other two JARs. -.. note:: The rest of this tutorial assumes you are using ``gradle``, the ``cordformation`` plugin and have forked from +.. note:: The rest of this tutorial assumes you are using ``gradle``, the ``cordapp`` plugin and have forked from one of our cordapp templates. The ``jar`` task included by default in the cordapp templates will automatically build your JAR in this format as long @@ -40,7 +40,7 @@ To make use of the Corda test facilities you must; .. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency. -These configurations work by the ``cordformation`` plugin adding ``cordaCompile`` as a new configuration that ``compile`` +These configurations work by the ``cordapp`` plugin adding ``cordaCompile`` as a new configuration that ``compile`` extends from, and ``cordaRuntime`` which ``runtime`` extends from. Choosing your Corda version @@ -57,7 +57,7 @@ can find the latest published version of both here: https://bintray.com/r3/corda ``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and ``minor`` versions as the Corda version you are using, and the latest ``patch`` version. A list of all the available -versions can be found here: https://bintray.com/r3/corda/cordformation. +versions can be found here: https://bintray.com/r3/corda/cordapp. In certain cases, you may also wish to build against the unstable Master branch. See :doc:`building-against-master`. @@ -78,11 +78,11 @@ For further information about managing dependencies, see Installing CorDapps ------------------- -At runtime, nodes will load any plugins present in their ``plugins`` folder. Therefore in order to install a cordapp to -a node the cordapp JAR must be added to the ``/plugins/`` folder, where ``node_dir`` is the folder in which the +At runtime, nodes will load any CorDapp JARs present in their ``cordapps`` folder. Therefore in order to install a CorDapp to +a node the CorDapp JAR must be added to the ``/cordapps/`` folder, where ``node_dir`` is the folder in which the node's JAR and configuration files are stored). -The ``deployNodes`` gradle task, if correctly configured, will automatically place your cordapp JAR as well as any +The ``deployNodes`` gradle task, if correctly configured, will automatically place your CorDapp JAR as well as any dependent cordapp JARs specified into the directory automatically. Example diff --git a/docs/source/cordapp-overview.rst b/docs/source/cordapp-overview.rst index 2999489ef6..6ac7bbedd3 100644 --- a/docs/source/cordapp-overview.rst +++ b/docs/source/cordapp-overview.rst @@ -22,4 +22,4 @@ CorDapps are made up of definitions for the following components: * Contracts * Flows * Web APIs and static web content -* Services +* Services \ No newline at end of file diff --git a/docs/source/demobench.rst b/docs/source/demobench.rst index 5382547301..374f12234a 100644 --- a/docs/source/demobench.rst +++ b/docs/source/demobench.rst @@ -37,13 +37,13 @@ Profiles notary/ node.conf - plugins/ + cordapps/ banka/ node.conf - plugins/ + cordapps/ bankb/ node.conf - plugins/ + cordapps/ example-cordapp.jar ... @@ -133,7 +133,7 @@ current working directory of the JVM): corda-webserver.jar explorer/ node-explorer.jar - plugins/ + cordapps/ bank-of-corda.jar .. diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 0517d61c4d..74f8bdac65 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -11,16 +11,15 @@ Cordform is the local node deployment system for CorDapps. The nodes generated a debugging, and testing node configurations, but not for production or testnet deployment. Here is an example Gradle task called ``deployNodes`` that uses the Cordform plugin to deploy three nodes, plus a -notary/network map node: +notary node: .. sourcecode:: groovy task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=UK" node { name "O=Controller,OU=corda,L=London,C=UK" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] p2pPort 10002 rpcPort 10003 webPort 10004 @@ -28,7 +27,6 @@ notary/network map node: } node { name "CN=NodeA,O=NodeA,L=London,C=UK" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -37,7 +35,6 @@ notary/network map node: } node { name "CN=NodeB,O=NodeB,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 @@ -46,7 +43,6 @@ notary/network map node: } node { name "CN=NodeC,O=NodeC,L=Paris,C=FR" - advertisedServices = [] p2pPort 10011 rpcPort 10012 webPort 10013 @@ -55,12 +51,18 @@ notary/network map node: } } -You can extend ``deployNodes`` to generate any number of nodes you like. The only requirement is that you must specify -one node as running the network map service, by putting their name in the ``networkMap`` field. In our example, the -``Controller`` is set as the network map service. +You can extend ``deployNodes`` to generate any number of nodes you like. .. warning:: When adding nodes, make sure that there are no port clashes! +If your CorDapp is written in Java, you should also add the following Gradle snippet so that you can pass named arguments to your flows via the Corda shell: + +.. sourcecode:: groovy + + tasks.withType(JavaCompile) { + options.compilerArgs << "-parameters" + } + Any CorDapps defined in the project's source folders are also automatically registered with all the nodes defined in ``deployNodes``, even if the CorDapps are not listed in each node's ``cordapps`` entry. @@ -80,10 +82,15 @@ run all the nodes at once. Each node in the ``nodes`` folder has the following s .. sourcecode:: none . nodeName - ├── corda.jar // The Corda runtime - ├── node.conf // The node's configuration - └── plugins // Any installed CorDapps + ├── corda.jar // The Corda runtime + ├── node.conf // The node's configuration + ├── cordapps // Any installed CorDapps + └── additional-node-infos // Directory containing all the addresses and certificates of the other nodes. + +.. note:: During the build process each node generates a NodeInfo file which is written in its own root directory, +the plug-in proceeds and copies each node NodeInfo to every other node ``additional-node-infos`` directory. +The NodeInfo file contains a node hostname and port, legal name and security certificate. .. note:: Outside of development environments, do not store your node directories in the build folder. -If you make any changes to your ``deployNodes`` task, you will need to re-run the task to see the changes take effect. \ No newline at end of file +If you make any changes to your ``deployNodes`` task, you will need to re-run the task to see the changes take effect. diff --git a/docs/source/event-scheduling.rst b/docs/source/event-scheduling.rst index b5df1bcf1c..3ba100a3ef 100644 --- a/docs/source/event-scheduling.rst +++ b/docs/source/event-scheduling.rst @@ -42,7 +42,7 @@ There are two main steps to implementing scheduled events: ``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance. ``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the - wallet, it can expect to be queried for the next activity when committed to the wallet. The ``FlowLogic`` must be + vault, it can expect to be queried for the next activity when committed to the vault. The ``FlowLogic`` must be annotated with ``@SchedulableFlow``. * If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself. The important thing to remember is that in the current implementation, each node that is party to the transaction @@ -58,7 +58,7 @@ handler to help with obtaining a unique and secure random session. An example i The production and consumption of ``ContractStates`` is observed by the scheduler and the activities associated with any consumed states are unscheduled. Any newly produced states are then queried via the ``nextScheduledActivity`` method and if they do not return ``null`` then that activity is scheduled based on the content of the -``ScheduledActivity`` object returned. Be aware that this *only* happens if the wallet considers the state +``ScheduledActivity`` object returned. Be aware that this *only* happens if the vault considers the state "relevant", for instance, because the owner of the node also owns that state. States that your node happens to encounter but which aren't related to yourself will not have any activities scheduled. @@ -68,21 +68,13 @@ An example Let's take an example of the interest rate swap fixings for our scheduled events. The first task is to implement the ``nextScheduledActivity`` method on the ``State``. - .. container:: codeset - .. sourcecode:: kotlin - - override fun nextScheduledActivity(thisStateRef: StateRef, - flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { - val nextFixingOf = nextFixingOf() ?: return null - - val (instant, duration) = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, - source = floatingLeg.indexSource, - date = nextFixingOf.forDay) - return ScheduledActivity(flowLogicRefFactory.create(TwoPartyDealFlow.FixingRoleDecider::class.java, - thisStateRef, duration), instant) - } + .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 8 The first thing this does is establish if there are any remaining fixings. If there are none, then it returns ``null`` to indicate that there is no activity to schedule. Otherwise it calculates the ``Instant`` at which the interest rate diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 46336ef9f0..0568552add 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'application' apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.quasar-utils' repositories { @@ -30,22 +31,23 @@ sourceSets { compileTestJava.dependsOn tasks.getByPath(':node:capsule:buildCordaJAR') dependencies { - cordaCompile project(':core') - cordaCompile project(':client:jfx') - cordaCompile project(':node-driver') + compile project(':core') + compile project(':client:jfx') + compile project(':node-driver') testCompile project(':verifier') testCompile project(':test-utils') compile "org.graphstream:gs-core:1.3" compile("org.graphstream:gs-ui:1.3") { exclude group: "bouncycastle" + exclude group: "junit" } - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') - cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') // Corda Plugins: dependent flows and services - cordapp project(':finance') + compile project(':finance') } mainClassName = "net.corda.docs.ClientRpcTutorialKt" @@ -71,10 +73,9 @@ task integrationTest(type: Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Notary Service,OU=corda,L=London,C=GB" node { name "O=Notary Service,OU=corda,L=London,C=GB" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] p2pPort 10002 rpcPort 10003 webPort 10004 @@ -82,7 +83,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Alice Corp,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index e4de7b4864..c665a8cb67 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -11,8 +11,6 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.driver @@ -24,7 +22,7 @@ class IntegrationTestingTutorial { fun `alice bob cash exchange example`() { // START 1 driver(startNodesInProcess = true, - extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset")) { + extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset")) { val aliceUser = User("aliceUser", "testPassword1", permissions = setOf( startFlowPermission(), startFlowPermission() @@ -32,10 +30,10 @@ class IntegrationTestingTutorial { val bobUser = User("bobUser", "testPassword2", permissions = setOf( startFlowPermission() )) - val (alice, bob, notary) = listOf( + val (alice, bob) = listOf( startNode(providedName = ALICE.name, rpcUsers = listOf(aliceUser)), startNode(providedName = BOB.name, rpcUsers = listOf(bobUser)), - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name) ).transpose().getOrThrow() // END 1 @@ -104,7 +102,7 @@ class IntegrationTestingTutorial { } ) } + // END 5 } } -} -// END 5 +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index bfa1b329d4..e007e3af6f 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -34,8 +34,6 @@ import java.util.Set; import static net.corda.core.contracts.ContractsDSL.requireThat; import static net.corda.testing.TestConstants.getALICE_KEY; -// We group our two flows inside a singleton object to indicate that they work -// together. @SuppressWarnings("unused") public class FlowCookbookJava { // ``InitiatorFlow`` is our first flow, and will communicate with @@ -121,28 +119,39 @@ public class FlowCookbookJava { // A transaction generally needs a notary: // - To prevent double-spends if the transaction has inputs - // - To serve as a timestamping authority if the transaction has a time-window + // - To serve as a timestamping authority if the transaction has a + // time-window // We retrieve a notary from the network map. - // DOCSTART 1 - Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(new CordaX500Name("Notary Service", "London", "UK")); - // Alternatively, we can pick an arbitrary notary from the notary list. However, it is always preferable to - // specify which notary to use explicitly, as the notary list might change when new notaries are introduced, - // or old ones decommissioned. + // DOCSTART 01 + CordaX500Name notaryName = new CordaX500Name("Notary Service", "London", "GB"); + Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(notaryName); + // Alternatively, we can pick an arbitrary notary from the notary + // list. However, it is always preferable to specify the notary + // explicitly, as the notary list might change when new notaries are + // introduced, or old ones decommissioned. Party firstNotary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); - // DOCEND 1 + // DOCEND 01 - // We may also need to identify a specific counterparty. - // Again, we do so using the network map. - // DOCSTART 2 - Party namedCounterparty = getServiceHub().getIdentityService().wellKnownPartyFromX500Name(new CordaX500Name("NodeA", "London", "UK")); + // We may also need to identify a specific counterparty. We do so + // using the identity service. + // DOCSTART 02 + CordaX500Name counterPartyName = new CordaX500Name("NodeA", "London", "GB"); + Party namedCounterparty = getServiceHub().getIdentityService().wellKnownPartyFromX500Name(counterPartyName); Party keyedCounterparty = getServiceHub().getIdentityService().partyFromKey(dummyPubKey); - // DOCEND 2 + // DOCEND 02 /*------------------------------ * SENDING AND RECEIVING DATA * ------------------------------*/ progressTracker.setCurrentStep(SENDING_AND_RECEIVING_DATA); + // We start by initiating a flow session with the counterparty. We + // will use this session to send and receive messages from the + // counterparty. + // DOCSTART initiateFlow + FlowSession counterpartySession = initiateFlow(counterparty); + // DOCEND initiateFlow + // We can send arbitrary data to a counterparty. // If this is the first ``send``, the counterparty will either: // 1. Ignore the message if they are not registered to respond @@ -153,10 +162,9 @@ public class FlowCookbookJava { // In other words, we are assuming that the counterparty is // registered to respond to this flow, and has a corresponding // ``receive`` call. - // DOCSTART 4 - FlowSession counterpartySession = initiateFlow(counterparty); + // DOCSTART 04 counterpartySession.send(new Object()); - // DOCEND 4 + // DOCEND 04 // We can wait to receive arbitrary data of a specific type from a // counterparty. Again, this implies a corresponding ``send`` call @@ -177,7 +185,7 @@ public class FlowCookbookJava { // instance. This is a reminder that the data we receive may not // be what it appears to be! We must unwrap the // ``UntrustworthyData`` using a lambda. - // DOCSTART 5 + // DOCSTART 05 UntrustworthyData packet1 = counterpartySession.receive(Integer.class); Integer integer = packet1.unwrap(data -> { // Perform checking on the object received. @@ -185,13 +193,13 @@ public class FlowCookbookJava { // Return the object. return data; }); - // DOCEND 5 + // DOCEND 05 // We can also use a single call to send data to a counterparty // and wait to receive data of a specific type back. The type of // data sent doesn't need to match the type of the data received // back. - // DOCSTART 7 + // DOCSTART 07 UntrustworthyData packet2 = counterpartySession.sendAndReceive(Boolean.class, "You can send and receive any class!"); Boolean bool = packet2.unwrap(data -> { // Perform checking on the object received. @@ -199,16 +207,16 @@ public class FlowCookbookJava { // Return the object. return data; }); - // DOCEND 7 + // DOCEND 07 // We're not limited to sending to and receiving from a single // counterparty. A flow can send messages to as many parties as it // likes, and each party can invoke a different response flow. - // DOCSTART 6 + // DOCSTART 06 FlowSession regulatorSession = initiateFlow(regulator); regulatorSession.send(new Object()); UntrustworthyData packet3 = regulatorSession.receive(Object.class); - // DOCEND 6 + // DOCEND 06 /*------------------------------------ * EXTRACTING STATES FROM THE VAULT * @@ -223,7 +231,7 @@ public class FlowCookbookJava { // For example, we would extract any unconsumed ``DummyState``s // from our vault as follows: VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED); - Page results = getServiceHub().getVaultQueryService().queryBy(DummyState.class, criteria); + Page results = getServiceHub().getVaultService().queryBy(DummyState.class, criteria); List> dummyStates = results.getStates(); // For a full list of the available ways of extracting states from @@ -259,8 +267,7 @@ public class FlowCookbookJava { // We then need to pair our output state with a contract. // DOCSTART 47 - String contractName = "net.corda.testing.contracts.DummyContract"; - StateAndContract ourOutput = new StateAndContract(ourOutputState, contractName); + StateAndContract ourOutput = new StateAndContract(ourOutputState, DummyContract.PROGRAM_ID); // DOCEND 47 // Commands pair a ``CommandData`` instance with a list of @@ -546,7 +553,6 @@ public class FlowCookbookJava { // DOCSTART 37 twiceSignedTx.checkSignaturesAreValid(); // DOCEND 37 - } catch (GeneralSecurityException e) { // Handle this as required. } @@ -558,9 +564,9 @@ public class FlowCookbookJava { // We notarise the transaction and get it recorded in the vault of // the participants of all the transaction's states. - // DOCSTART 9 + // DOCSTART 09 SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())); - // DOCEND 9 + // DOCEND 09 // We can also choose to send it to additional parties who aren't one // of the state's participants. // DOCSTART 10 @@ -568,6 +574,13 @@ public class FlowCookbookJava { SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())); // DOCEND 10 + // DOCSTART FlowSession porting + send(regulator, new Object()); // Old API + // becomes + FlowSession session = initiateFlow(regulator); + session.send(new Object()); + // DOCEND FlowSession porting + return null; } } @@ -611,16 +624,16 @@ public class FlowCookbookJava { progressTracker.setCurrentStep(RECEIVING_AND_SENDING_DATA); // We need to respond to the messages sent by the initiator: - // 1. They sent us an ``Any`` instance + // 1. They sent us an ``Object`` instance // 2. They waited to receive an ``Integer`` instance back // 3. They sent a ``String`` instance and waited to receive a // ``Boolean`` instance back // Our side of the flow must mirror these calls. - // DOCSTART 8 + // DOCSTART 08 Object obj = counterpartySession.receive(Object.class).unwrap(data -> data); String string = counterpartySession.sendAndReceive(String.class, 99).unwrap(data -> data); counterpartySession.send(true); - // DOCEND 8 + // DOCEND 08 /*----------------------------------------- * RESPONDING TO COLLECT_SIGNATURES_FLOW * diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java new file mode 100644 index 0000000000..8159838082 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java @@ -0,0 +1,101 @@ +package net.corda.docs.java.tutorial.contract; + +import net.corda.core.contracts.*; +import net.corda.core.transactions.LedgerTransaction; +import net.corda.core.transactions.LedgerTransaction.InOutGroup; + +import java.time.Instant; +import java.util.Currency; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; +import static net.corda.finance.utils.StateSumming.sumCashBy; + +public class CommercialPaper implements Contract { + // DOCSTART 1 + public static final String IOU_CONTRACT_ID = "com.example.contract.IOUContract"; + // DOCEND 1 + + // DOCSTART 3 + @Override + public void verify(LedgerTransaction tx) { + List> groups = tx.groupStates(State.class, State::withoutOwner); + CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.class); + // DOCEND 3 + + // DOCSTART 4 + TimeWindow timeWindow = tx.getTimeWindow(); + + for (InOutGroup group : groups) { + List inputs = group.getInputs(); + List outputs = group.getOutputs(); + + if (cmd.getValue() instanceof Commands.Move) { + State input = inputs.get(0); + requireThat(require -> { + require.using("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner().getOwningKey())); + require.using("the state is propagated", outputs.size() == 1); + // Don't need to check anything else, as if outputs.size == 1 then the output is equal to + // the input ignoring the owner field due to the grouping. + return null; + }); + + } else if (cmd.getValue() instanceof Commands.Redeem) { + // Redemption of the paper requires movement of on-ledger cash. + State input = inputs.get(0); + Amount> received = sumCashBy(tx.getOutputStates(), input.getOwner()); + if (timeWindow == null) throw new IllegalArgumentException("Redemptions must be timestamped"); + Instant time = timeWindow.getFromTime(); + requireThat(require -> { + require.using("the paper must have matured", time.isAfter(input.getMaturityDate())); + require.using("the received amount equals the face value", received == input.getFaceValue()); + require.using("the paper must be destroyed", outputs.size() == 0); + require.using("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner().getOwningKey())); + return null; + }); + } else if (cmd.getValue() instanceof Commands.Issue) { + State output = outputs.get(0); + if (timeWindow == null) throw new IllegalArgumentException("Issuances must be timestamped"); + Instant time = timeWindow.getUntilTime(); + requireThat(require -> { + // Don't allow people to issue commercial paper under other entities identities. + require.using("output states are issued by a command signer", cmd.getSigners().contains(output.getIssuance().getParty().getOwningKey())); + require.using("output values sum to more than the inputs", output.getFaceValue().getQuantity() > 0); + require.using("the maturity date is not in the past", time.isBefore(output.getMaturityDate())); + // Don't allow an existing CP state to be replaced by this issuance. + require.using("can't reissue an existing state", inputs.isEmpty()); + return null; + }); + } else { + throw new IllegalArgumentException("Unrecognised command"); + } + } + // DOCEND 4 + } + + // DOCSTART 2 + public static class Commands implements CommandData { + public static class Move extends Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Move; + } + } + + public static class Redeem extends Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Redeem; + } + } + + public static class Issue extends Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Issue; + } + } + } + // DOCEND 2 +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java new file mode 100644 index 0000000000..295fba8700 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java @@ -0,0 +1,90 @@ +package net.corda.docs.java.tutorial.contract; + +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.*; +import net.corda.core.crypto.NullKeys; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.AnonymousParty; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.util.Currency; +import java.util.List; + +// DOCSTART 1 +public class State implements OwnableState { + private PartyAndReference issuance; + private AbstractParty owner; + private Amount> faceValue; + private Instant maturityDate; + + public State() { + } // For serialization + + public State(PartyAndReference issuance, AbstractParty owner, Amount> faceValue, + Instant maturityDate) { + this.issuance = issuance; + this.owner = owner; + this.faceValue = faceValue; + this.maturityDate = maturityDate; + } + + public State copy() { + return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); + } + + public State withoutOwner() { + return new State(this.issuance, new AnonymousParty(NullKeys.NullPublicKey.INSTANCE), this.faceValue, this.maturityDate); + } + + @NotNull + @Override + public CommandAndState withNewOwner(@NotNull AbstractParty newOwner) { + return new CommandAndState(new CommercialPaper.Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate)); + } + + public PartyAndReference getIssuance() { + return issuance; + } + + public AbstractParty getOwner() { + return owner; + } + + public Amount> getFaceValue() { + return faceValue; + } + + public Instant getMaturityDate() { + return maturityDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + State state = (State) o; + + if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; + if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; + if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; + return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null); + } + + @Override + public int hashCode() { + int result = issuance != null ? issuance.hashCode() : 0; + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); + result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); + return result; + } + + @NotNull + @Override + public List getParticipants() { + return ImmutableList.of(this.owner); + } +} +// DOCEND 1 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java new file mode 100644 index 0000000000..ac7f387ee7 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java @@ -0,0 +1,38 @@ +package net.corda.docs.java.tutorial.flowstatemachines; + +import net.corda.core.flows.SignTransactionFlow; +import net.corda.core.utilities.ProgressTracker; +import org.jetbrains.annotations.Nullable; + +public class TutorialFlowStateMachines { + // DOCSTART 1 + private final ProgressTracker progressTracker = new ProgressTracker( + RECEIVING, + VERIFYING, + SIGNING, + COLLECTING_SIGNATURES, + RECORDING + ); + + private static final ProgressTracker.Step RECEIVING = new ProgressTracker.Step( + "Waiting for seller trading info"); + private static final ProgressTracker.Step VERIFYING = new ProgressTracker.Step( + "Verifying seller assets"); + private static final ProgressTracker.Step SIGNING = new ProgressTracker.Step( + "Generating and signing transaction proposal"); + private static final ProgressTracker.Step COLLECTING_SIGNATURES = new ProgressTracker.Step( + "Collecting signatures from other parties"); + private static final ProgressTracker.Step RECORDING = new ProgressTracker.Step( + "Recording completed transaction"); + // DOCEND 1 + + // DOCSTART 2 + private static final ProgressTracker.Step VERIFYING_AND_SIGNING = new ProgressTracker.Step("Verifying and signing transaction proposal") { + @Nullable + @Override + public ProgressTracker childProgressTracker() { + return SignTransactionFlow.Companion.tracker(); + } + }; + // DOCEND 2 +} diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java new file mode 100644 index 0000000000..3023396e81 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java @@ -0,0 +1,46 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import net.corda.core.contracts.CommandData; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.contracts.Contract; +import net.corda.core.identity.Party; +import net.corda.core.transactions.LedgerTransaction; + +import java.security.PublicKey; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; + +public class IOUContract implements Contract { + // Our Create command. + public static class Create implements CommandData { + } + + @Override + public void verify(LedgerTransaction tx) { + final CommandWithParties command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = tx.outputsOfType(IOUState.class).get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.", out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // Constraints on the signers. + final List signers = command.getSigners(); + check.using("There must only be one signer.", signers.size() == 1); + check.using("The signer must be the lender.", signers.contains(lender.getOwningKey())); + + return null; + }); + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java new file mode 100644 index 0000000000..5b2d5d47f4 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java @@ -0,0 +1,68 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndContract; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; + +@InitiatingFlow +@StartableByRPC +public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; + + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the notary identity from the network map. + final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + + // We create a transaction builder. + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // We create the transaction components. + IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); + String outputContract = IOUContract.class.getName(); + StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); + Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey()); + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Finalising the transaction. + subFlow(new FinalityFlow(signedTx)); + + return null; + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java new file mode 100644 index 0000000000..977457fd29 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java @@ -0,0 +1,39 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; + +import java.util.List; + +public class IOUState implements ContractState { + private final int value; + private final Party lender; + private final Party borrower; + + public IOUState(int value, Party lender, Party borrower) { + this.value = value; + this.lender = lender; + this.borrower = borrower; + } + + public int getValue() { + return value; + } + + public Party getLender() { + return lender; + } + + public Party getBorrower() { + return borrower; + } + + @Override + public List getParticipants() { + return ImmutableList.of(lender, borrower); + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java new file mode 100644 index 0000000000..ee91484dbf --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -0,0 +1,270 @@ +package net.corda.docs.java.tutorial.testdsl; + +import kotlin.Unit; +import net.corda.core.contracts.PartyAndReference; +import net.corda.core.utilities.OpaqueBytes; +import net.corda.finance.contracts.ICommercialPaperState; +import net.corda.finance.contracts.JavaCommercialPaper; +import net.corda.finance.contracts.asset.Cash; +import org.junit.Test; + +import java.time.temporal.ChronoUnit; + +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.CoreTestUtils.*; +import static net.corda.testing.NodeTestUtils.ledger; +import static net.corda.testing.NodeTestUtils.transaction; +import static net.corda.testing.TestConstants.*; + +public class CommercialPaperTest { + private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{123}); + + // DOCSTART 1 + private ICommercialPaperState getPaper() { + return new JavaCommercialPaper.State( + getMEGA_CORP().ref(defaultRef), + getMEGA_CORP(), + issuedBy(DOLLARS(1000), getMEGA_CORP().ref(defaultRef)), + getTEST_TX_TIME().plus(7, ChronoUnit.DAYS) + ); + } + // DOCEND 1 + + // DOCSTART 2 + @Test + public void simpleCP() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.attachments(JCP_PROGRAM_ID); + tx.input(JCP_PROGRAM_ID, inState); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 2 + + // DOCSTART 3 + @Test + public void simpleCPMove() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.input(JCP_PROGRAM_ID, inState); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + tx.attachments(JCP_PROGRAM_ID); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 3 + + // DOCSTART 4 + @Test + public void simpleCPMoveFails() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.input(JCP_PROGRAM_ID, inState); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + tx.attachments(JCP_PROGRAM_ID); + return tx.failsWith("the state is propagated"); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 4 + + // DOCSTART 5 + @Test + public void simpleCPMoveSuccess() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.input(JCP_PROGRAM_ID, inState); + tx.command(getMEGA_CORP_PUBKEY(), 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(getALICE())); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 5 + + // DOCSTART 6 + @Test + public void simpleIssuanceWithTweak() { + ledger(l -> { + l.transaction(tx -> { + 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(getBIG_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tw.timeWindow(getTEST_TX_TIME()); + return tw.failsWith("output states are issued by a command signer"); + }); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 6 + + // DOCSTART 7 + @Test + public void simpleIssuanceWithTweakTopLevelTx() { + transaction(tx -> { + 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(getBIG_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tw.timeWindow(getTEST_TX_TIME()); + return tw.failsWith("output states are issued by a command signer"); + }); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + } + // DOCEND 7 + + // DOCSTART 8 + @Test + public void chainCommercialPaper() { + PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output(Cash.PROGRAM_ID, "alice's $900", + new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); + tx.attachments(Cash.PROGRAM_ID); + return Unit.INSTANCE; + }); + + // Some CP is issued onto the ledger by MegaCorp. + l.transaction("Issuance", tx -> { + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.attachments(JCP_PROGRAM_ID); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + + 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), getMEGA_CORP())); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE())); + tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 8 + + // DOCSTART 9 + @Test + public void chainCommercialPaperDoubleSpend() { + PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output(Cash.PROGRAM_ID, "alice's $900", + new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); + tx.attachments(Cash.PROGRAM_ID); + return Unit.INSTANCE; + }); + + // Some CP is issued onto the ledger by MegaCorp. + l.transaction("Issuance", tx -> { + tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.attachments(JCP_PROGRAM_ID); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + + 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), getMEGA_CORP())); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE())); + tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + + l.transaction(tx -> { + 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(getBOB())); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + l.fails(); + return Unit.INSTANCE; + }); + } + // DOCEND 9 + + // DOCSTART 10 + @Test + public void chainCommercialPaperTweak() { + PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output(Cash.PROGRAM_ID, "alice's $900", + new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); + tx.attachments(Cash.PROGRAM_ID); + return Unit.INSTANCE; + }); + + // Some CP is issued onto the ledger by MegaCorp. + l.transaction("Issuance", tx -> { + tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.attachments(JCP_PROGRAM_ID); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + + 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), getMEGA_CORP())); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE())); + tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + + l.tweak(lw -> { + lw.transaction(tx -> { + 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(getBOB())); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + lw.fails(); + return Unit.INSTANCE; + }); + l.verifies(); + return Unit.INSTANCE; + }); + } + // DOCEND 10 +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java new file mode 100644 index 0000000000..bcf8dded07 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java @@ -0,0 +1,50 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.CommandData; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.contracts.Contract; +import net.corda.core.identity.Party; +import net.corda.core.transactions.LedgerTransaction; + +import java.security.PublicKey; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; +// DOCEND 01 + +public class IOUContract implements Contract { + // Our Create command. + public static class Create implements CommandData { + } + + @Override + public void verify(LedgerTransaction tx) { + final CommandWithParties command = requireSingleCommand(tx.getCommands(), net.corda.docs.java.tutorial.helloworld.IOUContract.Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = tx.outputsOfType(IOUState.class).get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.", out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // DOCSTART 02 + // Constraints on the signers. + final List signers = command.getSigners(); + check.using("There must be two signers.", signers.size() == 2); + check.using("The borrower and lender must be signers.", signers.containsAll( + ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); + // DOCEND 02 + + return null; + }); + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java new file mode 100644 index 0000000000..4e0533d323 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java @@ -0,0 +1,82 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndContract; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; + +import java.security.PublicKey; +import java.util.List; +// DOCEND 01 + +@InitiatingFlow +@StartableByRPC +public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; + + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the notary identity from the network map. + final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + + // We create a transaction builder. + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // DOCSTART 02 + // We create the transaction components. + IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); + String outputContract = IOUContract.class.getName(); + StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); + List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); + Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Creating a session with the other party. + FlowSession otherpartySession = initiateFlow(otherParty); + + // Obtaining the counterparty's signature. + SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( + signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker())); + + // Finalising the transaction. + subFlow(new FinalityFlow(fullySignedTx)); + + return null; + // DOCEND 02 + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java new file mode 100644 index 0000000000..ac1f312ab6 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java @@ -0,0 +1,47 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.contracts.ContractState; +import net.corda.core.flows.*; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.utilities.ProgressTracker; +import net.corda.docs.java.tutorial.helloworld.IOUFlow; +import net.corda.docs.java.tutorial.helloworld.IOUState; + +import static net.corda.core.contracts.ContractsDSL.requireThat; + +@InitiatedBy(IOUFlow.class) +public class IOUFlowResponder extends FlowLogic { + private final FlowSession otherPartySession; + + public IOUFlowResponder(FlowSession otherPartySession) { + this.otherPartySession = otherPartySession; + } + + @Suspendable + @Override + public Void call() throws FlowException { + class SignTxFlow extends SignTransactionFlow { + private SignTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { + super(otherPartySession, progressTracker); + } + + @Override + protected void checkTransaction(SignedTransaction stx) { + requireThat(require -> { + ContractState output = stx.getTx().getOutputs().get(0).getData(); + require.using("This must be an IOU transaction.", output instanceof IOUState); + IOUState iou = (IOUState) output; + require.using("The IOU's value can't be too high.", iou.getValue() < 100); + return null; + }); + } + } + + subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); + + return null; + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java new file mode 100644 index 0000000000..4cc6f9f76c --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java @@ -0,0 +1,37 @@ +package net.corda.docs.java.tutorial.twoparty; + +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; + +import java.util.List; + +public class IOUState implements ContractState { + private final int value; + private final Party lender; + private final Party borrower; + + public IOUState(int value, Party lender, Party borrower) { + this.value = value; + this.lender = lender; + this.borrower = borrower; + } + + public int getValue() { + return value; + } + + public Party getLender() { + return lender; + } + + public Party getBorrower() { + return borrower; + } + + @Override + public List getParticipants() { + return ImmutableList.of(lender, borrower); + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index b2ffa4b3f2..5b11d79549 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -4,9 +4,8 @@ import net.corda.core.contracts.Amount import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultQueryBy -import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationCustomization +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow @@ -16,8 +15,6 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY @@ -49,8 +46,8 @@ fun main(args: Array) { startFlowPermission(), startFlowPermission())) - driver(driverDirectory = baseDirectory) { - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) + driver(driverDirectory = baseDirectory, extraCordappPackagesToScan = listOf("net.corda.finance")) { + startNotaryNode(DUMMY_NOTARY.name) val node = startNode(providedName = ALICE.name, rpcUsers = listOf(user)).get() // END 1 @@ -100,10 +97,9 @@ fun main(args: Array) { } } waitForAllNodesToFinish() + // END 5 } - } -// END 5 // START 6 fun generateTransactions(proxy: CordaRPCOps) { @@ -143,12 +139,8 @@ data class ExampleRPCValue(val foo: String) @CordaSerializable data class ExampleRPCValue2(val bar: Int) -class ExampleRPCCordaPluginRegistry : CordaPluginRegistry() { - override fun customizeSerialization(custom: SerializationCustomization): Boolean { - // Add classes like this. - custom.addToWhitelist(ExampleRPCValue::class.java) - // You should return true, otherwise your plugin will be ignored for registering classes with Kryo. - return true - } +class ExampleRPCSerializationWhitelist : SerializationWhitelist { + // Add classes like this. + override val whitelist = listOf(ExampleRPCValue::class.java) } // END 7 diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt index 47a5ca00a0..1ae88a6015 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -23,7 +23,7 @@ import java.util.* object CustomVaultQuery { @CordaService - class Service(val services: ServiceHub) : SingletonSerializeAsToken() { + class Service(val services: AppServiceHub) : SingletonSerializeAsToken() { private companion object { val log = loggerFor() } @@ -49,7 +49,7 @@ object CustomVaultQuery { val session = services.jdbcSession() val prepStatement = session.prepareStatement(nativeQuery) val rs = prepStatement.executeQuery() - var topUpLimits: MutableList> = mutableListOf() + val topUpLimits: MutableList> = mutableListOf() while (rs.next()) { val currencyStr = rs.getString(1) val amount = rs.getLong(2) 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 3e720145bf..1ef82f8a1b 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 @@ -28,596 +28,614 @@ import net.corda.testing.contracts.DummyState import java.security.PublicKey import java.time.Instant -// We group our two flows inside a singleton object to indicate that they work -// together. -object FlowCookbook { - // ``InitiatorFlow`` is our first flow, and will communicate with - // ``ResponderFlow``, below. - // We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be - // started directly by the node. - @InitiatingFlow - // We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the - // node's owner to start the flow via RPC. - @StartableByRPC - // Every flow must subclass ``FlowLogic``. The generic indicates the - // flow's return type. - class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: Party, val regulator: Party) : FlowLogic() { +// ``InitiatorFlow`` is our first flow, and will communicate with +// ``ResponderFlow``, below. +// We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be +// started directly by the node. +@InitiatingFlow +// We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the +// node's owner to start the flow via RPC. +@StartableByRPC +// Every flow must subclass ``FlowLogic``. The generic indicates the +// flow's return type. +class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: Party, val regulator: Party) : FlowLogic() { - /**--------------------------------- - * WIRING UP THE PROGRESS TRACKER * - ---------------------------------**/ - // Giving our flow a progress tracker allows us to see the flow's - // progress visually in our node's CRaSH shell. - // DOCSTART 17 - companion object { - object ID_OTHER_NODES : Step("Identifying other nodes on the network.") - object SENDING_AND_RECEIVING_DATA : Step("Sending data between parties.") - object EXTRACTING_VAULT_STATES : Step("Extracting states from the vault.") - object OTHER_TX_COMPONENTS : Step("Gathering a transaction's other components.") - object TX_BUILDING : Step("Building a transaction.") - object TX_SIGNING : Step("Signing a transaction.") - object TX_VERIFICATION : Step("Verifying a transaction.") - object SIGS_GATHERING : Step("Gathering a transaction's signatures.") { - // Wiring up a child progress tracker allows us to see the - // subflow's progress steps in our flow's progress tracker. - override fun childProgressTracker() = CollectSignaturesFlow.tracker() - } - - object VERIFYING_SIGS : Step("Verifying a transaction's signatures.") - object FINALISATION : Step("Finalising a transaction.") { - override fun childProgressTracker() = FinalityFlow.tracker() - } - - fun tracker() = ProgressTracker( - ID_OTHER_NODES, - SENDING_AND_RECEIVING_DATA, - EXTRACTING_VAULT_STATES, - OTHER_TX_COMPONENTS, - TX_BUILDING, - TX_SIGNING, - TX_VERIFICATION, - SIGS_GATHERING, - VERIFYING_SIGS, - FINALISATION - ) + /**--------------------------------- + * WIRING UP THE PROGRESS TRACKER * + ---------------------------------**/ + // Giving our flow a progress tracker allows us to see the flow's + // progress visually in our node's CRaSH shell. + // DOCSTART 17 + companion object { + object ID_OTHER_NODES : Step("Identifying other nodes on the network.") + object SENDING_AND_RECEIVING_DATA : Step("Sending data between parties.") + object EXTRACTING_VAULT_STATES : Step("Extracting states from the vault.") + object OTHER_TX_COMPONENTS : Step("Gathering a transaction's other components.") + object TX_BUILDING : Step("Building a transaction.") + object TX_SIGNING : Step("Signing a transaction.") + object TX_VERIFICATION : Step("Verifying a transaction.") + object SIGS_GATHERING : Step("Gathering a transaction's signatures.") { + // Wiring up a child progress tracker allows us to see the + // subflow's progress steps in our flow's progress tracker. + override fun childProgressTracker() = CollectSignaturesFlow.tracker() } - // DOCEND 17 - override val progressTracker: ProgressTracker = tracker() - - @Suppress("RemoveExplicitTypeArguments") - @Suspendable - override fun call() { - // We'll be using a dummy public key for demonstration purposes. - // These are built in to Corda, and are generally used for writing - // tests. - val dummyPubKey: PublicKey = ALICE_PUBKEY - - /**-------------------------- - * IDENTIFYING OTHER NODES * - --------------------------**/ - // DOCSTART 18 - progressTracker.currentStep = ID_OTHER_NODES - // DOCEND 18 - - // A transaction generally needs a notary: - // - To prevent double-spends if the transaction has inputs - // - To serve as a timestamping authority if the transaction has a time-window - // We retrieve the notary from the network map. - // DOCSTART 1 - val specificNotary: Party = serviceHub.networkMapCache.getNotary(CordaX500Name(organisation = "Notary Service", locality = "London", country = "UK"))!! - // Alternatively, we can pick an arbitrary notary from the notary list. However, it is always preferable to - // specify which notary to use explicitly, as the notary list might change when new notaries are introduced, - // or old ones decommissioned. - val firstNotary: Party = serviceHub.networkMapCache.notaryIdentities.first() - // DOCEND 1 - - // We may also need to identify a specific counterparty. We - // do so using identity service. - // DOCSTART 2 - val namedCounterparty: Party = serviceHub.identityService.wellKnownPartyFromX500Name(CordaX500Name(organisation = "NodeA", locality = "London", country = "UK")) ?: - throw IllegalArgumentException("Couldn't find counterparty for NodeA in identity service") - val keyedCounterparty: Party = serviceHub.identityService.partyFromKey(dummyPubKey) ?: - throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service") - // DOCEND 2 - - /**----------------------------- - * SENDING AND RECEIVING DATA * - -----------------------------**/ - progressTracker.currentStep = SENDING_AND_RECEIVING_DATA - - // We can send arbitrary data to a counterparty. - // If this is the first ``send``, the counterparty will either: - // 1. Ignore the message if they are not registered to respond - // to messages from this flow. - // 2. Start the flow they have registered to respond to this flow, - // and run the flow until the first call to ``receive``, at - // which point they process the message. - // In other words, we are assuming that the counterparty is - // registered to respond to this flow, and has a corresponding - // ``receive`` call. - // DOCSTART 4 - val counterpartySession = initiateFlow(counterparty) - counterpartySession.send(Any()) - // DOCEND 4 - - // We can wait to receive arbitrary data of a specific type from a - // counterparty. Again, this implies a corresponding ``send`` call - // in the counterparty's flow. A few scenarios: - // - We never receive a message back. In the current design, the - // flow is paused until the node's owner kills the flow. - // - Instead of sending a message back, the counterparty throws a - // ``FlowException``. This exception is propagated back to us, - // and we can use the error message to establish what happened. - // - We receive a message back, but it's of the wrong type. In - // this case, a ``FlowException`` is thrown. - // - We receive back a message of the correct type. All is good. - // - // Upon calling ``receive()`` (or ``sendAndReceive()``), the - // ``FlowLogic`` is suspended until it receives a response. - // - // We receive the data wrapped in an ``UntrustworthyData`` - // instance. This is a reminder that the data we receive may not - // be what it appears to be! We must unwrap the - // ``UntrustworthyData`` using a lambda. - // DOCSTART 5 - val packet1: UntrustworthyData = counterpartySession.receive() - val int: Int = packet1.unwrap { data -> - // Perform checking on the object received. - // T O D O: Check the received object. - // Return the object. - data - } - // DOCEND 5 - - // We can also use a single call to send data to a counterparty - // and wait to receive data of a specific type back. The type of - // data sent doesn't need to match the type of the data received - // back. - // DOCSTART 7 - val packet2: UntrustworthyData = counterpartySession.sendAndReceive("You can send and receive any class!") - val boolean: Boolean = packet2.unwrap { data -> - // Perform checking on the object received. - // T O D O: Check the received object. - // Return the object. - data - } - // DOCEND 7 - - // We're not limited to sending to and receiving from a single - // counterparty. A flow can send messages to as many parties as it - // likes, and each party can invoke a different response flow. - // DOCSTART 6 - val regulatorSession = initiateFlow(regulator) - regulatorSession.send(Any()) - val packet3: UntrustworthyData = regulatorSession.receive() - // DOCEND 6 - - /**----------------------------------- - * EXTRACTING STATES FROM THE VAULT * - -----------------------------------**/ - progressTracker.currentStep = EXTRACTING_VAULT_STATES - - // Let's assume there are already some ``DummyState``s in our - // node's vault, stored there as a result of running past flows, - // and we want to consume them in a transaction. There are many - // ways to extract these states from our vault. - - // For example, we would extract any unconsumed ``DummyState``s - // from our vault as follows: - val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED - val results: Page = serviceHub.vaultQueryService.queryBy(criteria) - val dummyStates: List> = results.states - - // For a full list of the available ways of extracting states from - // the vault, see the Vault Query docs page. - - // When building a transaction, input states are passed in as - // ``StateRef`` instances, which pair the hash of the transaction - // that generated the state with the state's index in the outputs - // of that transaction. In practice, we'd pass the transaction hash - // or the ``StateRef`` as a parameter to the flow, or extract the - // ``StateRef`` from our vault. - // DOCSTART 20 - val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) - // DOCEND 20 - // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. - // DOCSTART 21 - val ourStateAndRef: StateAndRef = serviceHub.toStateAndRef(ourStateRef) - // DOCEND 21 - - /**----------------------------------------- - * GATHERING OTHER TRANSACTION COMPONENTS * - -----------------------------------------**/ - progressTracker.currentStep = OTHER_TX_COMPONENTS - - // Output states are constructed from scratch. - // DOCSTART 22 - val ourOutputState: DummyState = DummyState() - // DOCEND 22 - // Or as copies of other states with some properties changed. - // DOCSTART 23 - val ourOtherOutputState: DummyState = ourOutputState.copy(magicNumber = 77) - // DOCEND 23 - - // We then need to pair our output state with a contract. - // DOCSTART 47 - val contractName: String = "net.corda.testing.contracts.DummyContract" - val ourOutput: StateAndContract = StateAndContract(ourOutputState, contractName) - // DOCEND 47 - - // Commands pair a ``CommandData`` instance with a list of - // public keys. To be valid, the transaction requires a signature - // matching every public key in all of the transaction's commands. - // DOCSTART 24 - val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create() - val ourPubKey: PublicKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey - val counterpartyPubKey: PublicKey = counterparty.owningKey - val requiredSigners: List = listOf(ourPubKey, counterpartyPubKey) - val ourCommand: Command = Command(commandData, requiredSigners) - // DOCEND 24 - - // ``CommandData`` can either be: - // 1. Of type ``TypeOnlyCommandData``, in which case it only - // serves to attach signers to the transaction and possibly - // fork the contract's verification logic. - val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create() - // 2. Include additional data which can be used by the contract - // during verification, alongside fulfilling the roles above. - val commandDataWithData: CommandData = Cash.Commands.Issue() - - // Attachments are identified by their hash. - // The attachment with the corresponding hash must have been - // uploaded ahead of time via the node's RPC interface. - // DOCSTART 25 - val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment") - // DOCEND 25 - - // Time windows can have a start and end time, or be open at either end. - // DOCSTART 26 - val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX) - val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN) - val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX) - // DOCEND 26 - - // We can also define a time window as an ``Instant`` +/- a time - // tolerance (e.g. 30 seconds): - // DOCSTART 42 - val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(serviceHub.clock.instant(), 30.seconds) - // DOCEND 42 - // Or as a start-time plus a duration: - // DOCSTART 43 - val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(serviceHub.clock.instant(), 30.seconds) - // DOCEND 43 - - /**----------------------- - * TRANSACTION BUILDING * - -----------------------**/ - progressTracker.currentStep = TX_BUILDING - - // If our transaction has input states or a time-window, we must instantiate it with a - // notary. - // DOCSTART 19 - val txBuilder: TransactionBuilder = TransactionBuilder(specificNotary) - // DOCEND 19 - - // Otherwise, we can choose to instantiate it without one: - // DOCSTART 46 - val txBuilderNoNotary: TransactionBuilder = TransactionBuilder() - // DOCEND 46 - - // We add items to the transaction builder using ``TransactionBuilder.withItems``: - // DOCSTART 27 - txBuilder.withItems( - // Inputs, as ``StateAndRef``s that reference the outputs of previous transactions - ourStateAndRef, - // Outputs, as ``StateAndContract``s - ourOutput, - // Commands, as ``Command``s - ourCommand, - // Attachments, as ``SecureHash``es - ourAttachment, - // A time-window, as ``TimeWindow`` - ourTimeWindow - ) - // DOCEND 27 - - // We can also add items using methods for the individual components. - - // The individual methods for adding input states and attachments: - // DOCSTART 28 - txBuilder.addInputState(ourStateAndRef) - txBuilder.addAttachment(ourAttachment) - // DOCEND 28 - - // An output state can be added as a ``ContractState``, contract class name and notary. - // DOCSTART 49 - txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) - // DOCEND 49 - // We can also leave the notary field blank, in which case the transaction's default - // notary is used. - // DOCSTART 50 - txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID) - // DOCEND 50 - // Or we can add the output state as a ``TransactionState``, which already specifies - // the output's contract and notary. - // DOCSTART 51 - val txState: TransactionState = TransactionState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) - // DOCEND 51 - - // Commands can be added as ``Command``s. - // DOCSTART 52 - txBuilder.addCommand(ourCommand) - // DOCEND 52 - // Or as ``CommandData`` and a ``vararg PublicKey``. - // DOCSTART 53 - txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey) - // DOCEND 53 - - // We can set a time-window directly. - // DOCSTART 44 - txBuilder.setTimeWindow(ourTimeWindow) - // DOCEND 44 - // Or as a start time plus a duration (e.g. 45 seconds). - // DOCSTART 45 - txBuilder.setTimeWindow(serviceHub.clock.instant(), 45.seconds) - // DOCEND 45 - - /**---------------------- - * TRANSACTION SIGNING * - ----------------------**/ - progressTracker.currentStep = TX_SIGNING - - // We finalise the transaction by signing it, converting it into a - // ``SignedTransaction``. - // DOCSTART 29 - val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder) - // DOCEND 29 - // We can also sign the transaction using a different public key: - // DOCSTART 30 - val otherKey: PublicKey = serviceHub.keyManagementService.freshKey() - val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherKey) - // DOCEND 30 - - // If instead this was a ``SignedTransaction`` that we'd received - // from a counterparty and we needed to sign it, we would add our - // signature using: - // DOCSTART 38 - val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx) - // DOCEND 38 - // Or, if we wanted to use a different public key: - val otherKey2: PublicKey = serviceHub.keyManagementService.freshKey() - // DOCSTART 39 - val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherKey2) - // DOCEND 39 - - // We can also generate a signature over the transaction without - // adding it to the transaction itself. We may do this when - // sending just the signature in a flow instead of returning the - // entire transaction with our signature. This way, the receiving - // node does not need to check we haven't changed anything in the - // transaction. - // DOCSTART 40 - val sig: TransactionSignature = serviceHub.createSignature(onceSignedTx) - // DOCEND 40 - // And again, if we wanted to use a different public key: - // DOCSTART 41 - val sig2: TransactionSignature = serviceHub.createSignature(onceSignedTx, otherKey2) - // DOCEND 41 - - // In practice, however, the process of gathering every signature - // but the first can be automated using ``CollectSignaturesFlow``. - // See the "Gathering Signatures" section below. - - /**--------------------------- - * TRANSACTION VERIFICATION * - ---------------------------**/ - progressTracker.currentStep = TX_VERIFICATION - - // Verifying a transaction will also verify every transaction in - // the transaction's dependency chain, which will require - // transaction data access on counterparty's node. The - // ``SendTransactionFlow`` can be used to automate the sending and - // data vending process. The ``SendTransactionFlow`` will listen - // for data request until the transaction is resolved and verified - // on the other side: - // DOCSTART 12 - subFlow(SendTransactionFlow(counterpartySession, twiceSignedTx)) - - // Optional request verification to further restrict data access. - subFlow(object : SendTransactionFlow(counterpartySession, twiceSignedTx) { - override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) { - // Extra request verification. - } - }) - // DOCEND 12 - - // We can receive the transaction using ``ReceiveTransactionFlow``, - // which will automatically download all the dependencies and verify - // the transaction - // DOCSTART 13 - val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterpartySession)) - // DOCEND 13 - - // We can also send and receive a `StateAndRef` dependency chain - // and automatically resolve its dependencies. - // DOCSTART 14 - subFlow(SendStateAndRefFlow(counterpartySession, dummyStates)) - - // On the receive side ... - val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow(counterpartySession)) - // DOCEND 14 - - // We can now verify the transaction to ensure that it satisfies - // the contracts of all the transaction's input and output states. - // DOCSTART 33 - twiceSignedTx.verify(serviceHub) - // DOCEND 33 - - // We'll often want to perform our own additional verification - // too. Just because a transaction is valid based on the contract - // rules and requires our signature doesn't mean we have to - // sign it! We need to make sure the transaction represents an - // agreement we actually want to enter into. - - // To do this, we need to convert our ``SignedTransaction`` - // into a ``LedgerTransaction``. This will use our ServiceHub - // to resolve the transaction's inputs and attachments into - // actual objects, rather than just references. - // DOCSTART 32 - val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub) - // DOCEND 32 - - // We can now perform our additional verification. - // DOCSTART 34 - val outputState: DummyState = ledgerTx.outputsOfType().single() - if (outputState.magicNumber == 777) { - // ``FlowException`` is a special exception type. It will be - // propagated back to any counterparty flows waiting for a - // message from this flow, notifying them that the flow has - // failed. - throw FlowException("We expected a magic number of 777.") - } - // DOCEND 34 - - // Of course, if you are not a required signer on the transaction, - // you have no power to decide whether it is valid or not. If it - // requires signatures from all the required signers and is - // contractually valid, it's a valid ledger update. - - /**----------------------- - * GATHERING SIGNATURES * - -----------------------**/ - progressTracker.currentStep = SIGS_GATHERING - - // The list of parties who need to sign a transaction is dictated - // by the transaction's commands. Once we've signed a transaction - // ourselves, we can automatically gather the signatures of the - // other required signers using ``CollectSignaturesFlow``. - // The responder flow will need to call ``SignTransactionFlow``. - // DOCSTART 15 - val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, emptySet(), SIGS_GATHERING.childProgressTracker())) - // DOCEND 15 - - /**----------------------- - * VERIFYING SIGNATURES * - -----------------------**/ - progressTracker.currentStep = VERIFYING_SIGS - - // We can verify that a transaction has all the required - // signatures, and that they're all valid, by running: - // DOCSTART 35 - fullySignedTx.verifyRequiredSignatures() - // DOCEND 35 - - // If the transaction is only partially signed, we have to pass in - // a list of the public keys corresponding to the missing - // signatures, explicitly telling the system not to check them. - // DOCSTART 36 - onceSignedTx.verifySignaturesExcept(counterpartyPubKey) - // DOCEND 36 - - // We can also choose to only check the signatures that are - // present. BE VERY CAREFUL - this function provides no guarantees - // that the signatures are correct, or that none are missing. - // DOCSTART 37 - twiceSignedTx.checkSignaturesAreValid() - // DOCEND 37 - - /**----------------------------- - * FINALISING THE TRANSACTION * - -----------------------------**/ - progressTracker.currentStep = FINALISATION - - // We notarise the transaction and get it recorded in the vault of - // the participants of all the transaction's states. - // DOCSTART 9 - val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())) - // DOCEND 9 - // We can also choose to send it to additional parties who aren't one - // of the state's participants. - // DOCSTART 10 - val additionalParties: Set = setOf(regulator) - val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())) - // DOCEND 10 + object VERIFYING_SIGS : Step("Verifying a transaction's signatures.") + object FINALISATION : Step("Finalising a transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() } + + fun tracker() = ProgressTracker( + ID_OTHER_NODES, + SENDING_AND_RECEIVING_DATA, + EXTRACTING_VAULT_STATES, + OTHER_TX_COMPONENTS, + TX_BUILDING, + TX_SIGNING, + TX_VERIFICATION, + SIGS_GATHERING, + VERIFYING_SIGS, + FINALISATION + ) } + // DOCEND 17 - // ``ResponderFlow`` is our second flow, and will communicate with - // ``InitiatorFlow``. - // We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it - // can only be started in response to a message from its initiating flow. - // That's ``InitiatorFlow`` in this case. - // Each node also has several flow pairs registered by default - see - // ``AbstractNode.installCoreFlows``. - @InitiatedBy(InitiatorFlow::class) - class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic() { + override val progressTracker: ProgressTracker = tracker() - companion object { - object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.") - object SIGNING : Step("Responding to CollectSignaturesFlow.") - object FINALISATION : Step("Finalising a transaction.") + @Suppress("RemoveExplicitTypeArguments") + @Suspendable + override fun call() { + // We'll be using a dummy public key for demonstration purposes. + // These are built in to Corda, and are generally used for writing + // tests. + val dummyPubKey: PublicKey = ALICE_PUBKEY - fun tracker() = ProgressTracker( - RECEIVING_AND_SENDING_DATA, - SIGNING, - FINALISATION - ) + /**-------------------------- + * IDENTIFYING OTHER NODES * + --------------------------**/ + // DOCSTART 18 + progressTracker.currentStep = ID_OTHER_NODES + // DOCEND 18 + + // A transaction generally needs a notary: + // - To prevent double-spends if the transaction has inputs + // - To serve as a timestamping authority if the transaction has a + // time-window + // We retrieve the notary from the network map. + // DOCSTART 01 + val notaryName: CordaX500Name = CordaX500Name( + organisation = "Notary Service", + locality = "London", + country = "GB") + val specificNotary: Party = serviceHub.networkMapCache.getNotary(notaryName)!! + // Alternatively, we can pick an arbitrary notary from the notary + // list. However, it is always preferable to specify the notary + // explicitly, as the notary list might change when new notaries are + // introduced, or old ones decommissioned. + val firstNotary: Party = serviceHub.networkMapCache.notaryIdentities.first() + // DOCEND 01 + + // We may also need to identify a specific counterparty. We do so + // using the identity service. + // DOCSTART 02 + val counterpartyName: CordaX500Name = CordaX500Name( + organisation = "NodeA", + locality = "London", + country = "GB") + val namedCounterparty: Party = serviceHub.identityService.wellKnownPartyFromX500Name(counterpartyName) ?: + throw IllegalArgumentException("Couldn't find counterparty for NodeA in identity service") + val keyedCounterparty: Party = serviceHub.identityService.partyFromKey(dummyPubKey) ?: + throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service") + // DOCEND 02 + + /**----------------------------- + * SENDING AND RECEIVING DATA * + -----------------------------**/ + progressTracker.currentStep = SENDING_AND_RECEIVING_DATA + + // We start by initiating a flow session with the counterparty. We + // will use this session to send and receive messages from the + // counterparty. + // DOCSTART initiateFlow + val counterpartySession: FlowSession = initiateFlow(counterparty) + // DOCEND initiateFlow + + // We can send arbitrary data to a counterparty. + // If this is the first ``send``, the counterparty will either: + // 1. Ignore the message if they are not registered to respond + // to messages from this flow. + // 2. Start the flow they have registered to respond to this flow, + // and run the flow until the first call to ``receive``, at + // which point they process the message. + // In other words, we are assuming that the counterparty is + // registered to respond to this flow, and has a corresponding + // ``receive`` call. + // DOCSTART 04 + counterpartySession.send(Any()) + // DOCEND 04 + + // We can wait to receive arbitrary data of a specific type from a + // counterparty. Again, this implies a corresponding ``send`` call + // in the counterparty's flow. A few scenarios: + // - We never receive a message back. In the current design, the + // flow is paused until the node's owner kills the flow. + // - Instead of sending a message back, the counterparty throws a + // ``FlowException``. This exception is propagated back to us, + // and we can use the error message to establish what happened. + // - We receive a message back, but it's of the wrong type. In + // this case, a ``FlowException`` is thrown. + // - We receive back a message of the correct type. All is good. + // + // Upon calling ``receive()`` (or ``sendAndReceive()``), the + // ``FlowLogic`` is suspended until it receives a response. + // + // We receive the data wrapped in an ``UntrustworthyData`` + // instance. This is a reminder that the data we receive may not + // be what it appears to be! We must unwrap the + // ``UntrustworthyData`` using a lambda. + // DOCSTART 05 + val packet1: UntrustworthyData = counterpartySession.receive() + val int: Int = packet1.unwrap { data -> + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + data } + // DOCEND 05 - override val progressTracker: ProgressTracker = tracker() + // We can also use a single call to send data to a counterparty + // and wait to receive data of a specific type back. The type of + // data sent doesn't need to match the type of the data received + // back. + // DOCSTART 07 + val packet2: UntrustworthyData = counterpartySession.sendAndReceive("You can send and receive any class!") + val boolean: Boolean = packet2.unwrap { data -> + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + data + } + // DOCEND 07 - @Suspendable - override fun call() { - // The ``ResponderFlow` has all the same APIs available. It looks - // up network information, sends and receives data, and constructs - // transactions in exactly the same way. + // We're not limited to sending to and receiving from a single + // counterparty. A flow can send messages to as many parties as it + // likes, and each party can invoke a different response flow. + // DOCSTART 06 + val regulatorSession: FlowSession = initiateFlow(regulator) + regulatorSession.send(Any()) + val packet3: UntrustworthyData = regulatorSession.receive() + // DOCEND 06 - /**----------------------------- - * SENDING AND RECEIVING DATA * - -----------------------------**/ - progressTracker.currentStep = RECEIVING_AND_SENDING_DATA + /**----------------------------------- + * EXTRACTING STATES FROM THE VAULT * + -----------------------------------**/ + progressTracker.currentStep = EXTRACTING_VAULT_STATES - // We need to respond to the messages sent by the initiator: - // 1. They sent us an ``Any`` instance - // 2. They waited to receive an ``Integer`` instance back - // 3. They sent a ``String`` instance and waited to receive a - // ``Boolean`` instance back - // Our side of the flow must mirror these calls. - // DOCSTART 8 - val any: Any = counterpartySession.receive().unwrap { data -> data } - val string: String = counterpartySession.sendAndReceive(99).unwrap { data -> data } - counterpartySession.send(true) - // DOCEND 8 + // Let's assume there are already some ``DummyState``s in our + // node's vault, stored there as a result of running past flows, + // and we want to consume them in a transaction. There are many + // ways to extract these states from our vault. - /**---------------------------------------- - * RESPONDING TO COLLECT_SIGNATURES_FLOW * - ----------------------------------------**/ - progressTracker.currentStep = SIGNING + // For example, we would extract any unconsumed ``DummyState``s + // from our vault as follows: + val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED + val results: Page = serviceHub.vaultService.queryBy(criteria) + val dummyStates: List> = results.states - // The responder will often need to respond to a call to - // ``CollectSignaturesFlow``. It does so my invoking its own - // ``SignTransactionFlow`` subclass. - // DOCSTART 16 - val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterpartySession) { - override fun checkTransaction(stx: SignedTransaction) = requireThat { - // Any additional checking we see fit... - val outputState = stx.tx.outputsOfType().single() - assert(outputState.magicNumber == 777) - } + // For a full list of the available ways of extracting states from + // the vault, see the Vault Query docs page. + + // When building a transaction, input states are passed in as + // ``StateRef`` instances, which pair the hash of the transaction + // that generated the state with the state's index in the outputs + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the + // ``StateRef`` from our vault. + // DOCSTART 20 + val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) + // DOCEND 20 + // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + // DOCSTART 21 + val ourStateAndRef: StateAndRef = serviceHub.toStateAndRef(ourStateRef) + // DOCEND 21 + + /**----------------------------------------- + * GATHERING OTHER TRANSACTION COMPONENTS * + -----------------------------------------**/ + progressTracker.currentStep = OTHER_TX_COMPONENTS + + // Output states are constructed from scratch. + // DOCSTART 22 + val ourOutputState: DummyState = DummyState() + // DOCEND 22 + // Or as copies of other states with some properties changed. + // DOCSTART 23 + val ourOtherOutputState: DummyState = ourOutputState.copy(magicNumber = 77) + // DOCEND 23 + + // We then need to pair our output state with a contract. + // DOCSTART 47 + val ourOutput: StateAndContract = StateAndContract(ourOutputState, DummyContract.PROGRAM_ID) + // DOCEND 47 + + // Commands pair a ``CommandData`` instance with a list of + // public keys. To be valid, the transaction requires a signature + // matching every public key in all of the transaction's commands. + // DOCSTART 24 + val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create() + val ourPubKey: PublicKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey + val counterpartyPubKey: PublicKey = counterparty.owningKey + val requiredSigners: List = listOf(ourPubKey, counterpartyPubKey) + val ourCommand: Command = Command(commandData, requiredSigners) + // DOCEND 24 + + // ``CommandData`` can either be: + // 1. Of type ``TypeOnlyCommandData``, in which case it only + // serves to attach signers to the transaction and possibly + // fork the contract's verification logic. + val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create() + // 2. Include additional data which can be used by the contract + // during verification, alongside fulfilling the roles above. + val commandDataWithData: CommandData = Cash.Commands.Issue() + + // Attachments are identified by their hash. + // The attachment with the corresponding hash must have been + // uploaded ahead of time via the node's RPC interface. + // DOCSTART 25 + val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment") + // DOCEND 25 + + // Time windows can have a start and end time, or be open at either end. + // DOCSTART 26 + val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX) + val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN) + val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX) + // DOCEND 26 + + // We can also define a time window as an ``Instant`` +/- a time + // tolerance (e.g. 30 seconds): + // DOCSTART 42 + val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(serviceHub.clock.instant(), 30.seconds) + // DOCEND 42 + // Or as a start-time plus a duration: + // DOCSTART 43 + val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(serviceHub.clock.instant(), 30.seconds) + // DOCEND 43 + + /**----------------------- + * TRANSACTION BUILDING * + -----------------------**/ + progressTracker.currentStep = TX_BUILDING + + // If our transaction has input states or a time-window, we must instantiate it with a + // notary. + // DOCSTART 19 + val txBuilder: TransactionBuilder = TransactionBuilder(specificNotary) + // DOCEND 19 + + // Otherwise, we can choose to instantiate it without one: + // DOCSTART 46 + val txBuilderNoNotary: TransactionBuilder = TransactionBuilder() + // DOCEND 46 + + // We add items to the transaction builder using ``TransactionBuilder.withItems``: + // DOCSTART 27 + txBuilder.withItems( + // Inputs, as ``StateAndRef``s that reference the outputs of previous transactions + ourStateAndRef, + // Outputs, as ``StateAndContract``s + ourOutput, + // Commands, as ``Command``s + ourCommand, + // Attachments, as ``SecureHash``es + ourAttachment, + // A time-window, as ``TimeWindow`` + ourTimeWindow + ) + // DOCEND 27 + + // We can also add items using methods for the individual components. + + // The individual methods for adding input states and attachments: + // DOCSTART 28 + txBuilder.addInputState(ourStateAndRef) + txBuilder.addAttachment(ourAttachment) + // DOCEND 28 + + // An output state can be added as a ``ContractState``, contract class name and notary. + // DOCSTART 49 + txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) + // DOCEND 49 + // We can also leave the notary field blank, in which case the transaction's default + // notary is used. + // DOCSTART 50 + txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID) + // DOCEND 50 + // Or we can add the output state as a ``TransactionState``, which already specifies + // the output's contract and notary. + // DOCSTART 51 + val txState: TransactionState = TransactionState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) + // DOCEND 51 + + // Commands can be added as ``Command``s. + // DOCSTART 52 + txBuilder.addCommand(ourCommand) + // DOCEND 52 + // Or as ``CommandData`` and a ``vararg PublicKey``. + // DOCSTART 53 + txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey) + // DOCEND 53 + + // We can set a time-window directly. + // DOCSTART 44 + txBuilder.setTimeWindow(ourTimeWindow) + // DOCEND 44 + // Or as a start time plus a duration (e.g. 45 seconds). + // DOCSTART 45 + txBuilder.setTimeWindow(serviceHub.clock.instant(), 45.seconds) + // DOCEND 45 + + /**---------------------- + * TRANSACTION SIGNING * + ----------------------**/ + progressTracker.currentStep = TX_SIGNING + + // We finalise the transaction by signing it, converting it into a + // ``SignedTransaction``. + // DOCSTART 29 + val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder) + // DOCEND 29 + // We can also sign the transaction using a different public key: + // DOCSTART 30 + val otherKey: PublicKey = serviceHub.keyManagementService.freshKey() + val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherKey) + // DOCEND 30 + + // If instead this was a ``SignedTransaction`` that we'd received + // from a counterparty and we needed to sign it, we would add our + // signature using: + // DOCSTART 38 + val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx) + // DOCEND 38 + // Or, if we wanted to use a different public key: + val otherKey2: PublicKey = serviceHub.keyManagementService.freshKey() + // DOCSTART 39 + val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherKey2) + // DOCEND 39 + + // We can also generate a signature over the transaction without + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the + // transaction. + // DOCSTART 40 + val sig: TransactionSignature = serviceHub.createSignature(onceSignedTx) + // DOCEND 40 + // And again, if we wanted to use a different public key: + // DOCSTART 41 + val sig2: TransactionSignature = serviceHub.createSignature(onceSignedTx, otherKey2) + // DOCEND 41 + + // In practice, however, the process of gathering every signature + // but the first can be automated using ``CollectSignaturesFlow``. + // See the "Gathering Signatures" section below. + + /**--------------------------- + * TRANSACTION VERIFICATION * + ---------------------------**/ + progressTracker.currentStep = TX_VERIFICATION + + // Verifying a transaction will also verify every transaction in + // the transaction's dependency chain, which will require + // transaction data access on counterparty's node. The + // ``SendTransactionFlow`` can be used to automate the sending and + // data vending process. The ``SendTransactionFlow`` will listen + // for data request until the transaction is resolved and verified + // on the other side: + // DOCSTART 12 + subFlow(SendTransactionFlow(counterpartySession, twiceSignedTx)) + + // Optional request verification to further restrict data access. + subFlow(object : SendTransactionFlow(counterpartySession, twiceSignedTx) { + override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) { + // Extra request verification. } + }) + // DOCEND 12 - subFlow(signTransactionFlow) - // DOCEND 16 + // We can receive the transaction using ``ReceiveTransactionFlow``, + // which will automatically download all the dependencies and verify + // the transaction + // DOCSTART 13 + val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterpartySession)) + // DOCEND 13 - /**----------------------------- - * FINALISING THE TRANSACTION * - -----------------------------**/ - progressTracker.currentStep = FINALISATION + // We can also send and receive a `StateAndRef` dependency chain + // and automatically resolve its dependencies. + // DOCSTART 14 + subFlow(SendStateAndRefFlow(counterpartySession, dummyStates)) - // Nothing to do here! As long as some other party calls - // ``FinalityFlow``, the recording of the transaction on our node - // we be handled automatically. + // On the receive side ... + val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow(counterpartySession)) + // DOCEND 14 + + // We can now verify the transaction to ensure that it satisfies + // the contracts of all the transaction's input and output states. + // DOCSTART 33 + twiceSignedTx.verify(serviceHub) + // DOCEND 33 + + // We'll often want to perform our own additional verification + // too. Just because a transaction is valid based on the contract + // rules and requires our signature doesn't mean we have to + // sign it! We need to make sure the transaction represents an + // agreement we actually want to enter into. + + // To do this, we need to convert our ``SignedTransaction`` + // into a ``LedgerTransaction``. This will use our ServiceHub + // to resolve the transaction's inputs and attachments into + // actual objects, rather than just references. + // DOCSTART 32 + val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub) + // DOCEND 32 + + // We can now perform our additional verification. + // DOCSTART 34 + val outputState: DummyState = ledgerTx.outputsOfType().single() + if (outputState.magicNumber == 777) { + // ``FlowException`` is a special exception type. It will be + // propagated back to any counterparty flows waiting for a + // message from this flow, notifying them that the flow has + // failed. + throw FlowException("We expected a magic number of 777.") } + // DOCEND 34 + + // Of course, if you are not a required signer on the transaction, + // you have no power to decide whether it is valid or not. If it + // requires signatures from all the required signers and is + // contractually valid, it's a valid ledger update. + + /**----------------------- + * GATHERING SIGNATURES * + -----------------------**/ + progressTracker.currentStep = SIGS_GATHERING + + // The list of parties who need to sign a transaction is dictated + // by the transaction's commands. Once we've signed a transaction + // ourselves, we can automatically gather the signatures of the + // other required signers using ``CollectSignaturesFlow``. + // The responder flow will need to call ``SignTransactionFlow``. + // DOCSTART 15 + val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, setOf(counterpartySession, regulatorSession), SIGS_GATHERING.childProgressTracker())) + // DOCEND 15 + + /**----------------------- + * VERIFYING SIGNATURES * + -----------------------**/ + progressTracker.currentStep = VERIFYING_SIGS + + // We can verify that a transaction has all the required + // signatures, and that they're all valid, by running: + // DOCSTART 35 + fullySignedTx.verifyRequiredSignatures() + // DOCEND 35 + + // If the transaction is only partially signed, we have to pass in + // a list of the public keys corresponding to the missing + // signatures, explicitly telling the system not to check them. + // DOCSTART 36 + onceSignedTx.verifySignaturesExcept(counterpartyPubKey) + // DOCEND 36 + + // We can also choose to only check the signatures that are + // present. BE VERY CAREFUL - this function provides no guarantees + // that the signatures are correct, or that none are missing. + // DOCSTART 37 + twiceSignedTx.checkSignaturesAreValid() + // DOCEND 37 + + /**----------------------------- + * FINALISING THE TRANSACTION * + -----------------------------**/ + progressTracker.currentStep = FINALISATION + + // We notarise the transaction and get it recorded in the vault of + // the participants of all the transaction's states. + // DOCSTART 09 + val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())) + // DOCEND 09 + // We can also choose to send it to additional parties who aren't one + // of the state's participants. + // DOCSTART 10 + val additionalParties: Set = setOf(regulator) + val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())) + // DOCEND 10 + + // DOCSTART FlowSession porting + send(regulator, Any()) // Old API + // becomes + val session = initiateFlow(regulator) + session.send(Any()) + // DOCEND FlowSession porting + } +} + +// ``ResponderFlow`` is our second flow, and will communicate with +// ``InitiatorFlow``. +// We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it +// can only be started in response to a message from its initiating flow. +// That's ``InitiatorFlow`` in this case. +// Each node also has several flow pairs registered by default - see +// ``AbstractNode.installCoreFlows``. +@InitiatedBy(InitiatorFlow::class) +class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic() { + + companion object { + object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.") + object SIGNING : Step("Responding to CollectSignaturesFlow.") + object FINALISATION : Step("Finalising a transaction.") + + fun tracker() = ProgressTracker( + RECEIVING_AND_SENDING_DATA, + SIGNING, + FINALISATION + ) + } + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call() { + // The ``ResponderFlow` has all the same APIs available. It looks + // up network information, sends and receives data, and constructs + // transactions in exactly the same way. + + /**----------------------------- + * SENDING AND RECEIVING DATA * + -----------------------------**/ + progressTracker.currentStep = RECEIVING_AND_SENDING_DATA + + // We need to respond to the messages sent by the initiator: + // 1. They sent us an ``Any`` instance + // 2. They waited to receive an ``Integer`` instance back + // 3. They sent a ``String`` instance and waited to receive a + // ``Boolean`` instance back + // Our side of the flow must mirror these calls. + // DOCSTART 08 + val any: Any = counterpartySession.receive().unwrap { data -> data } + val string: String = counterpartySession.sendAndReceive(99).unwrap { data -> data } + counterpartySession.send(true) + // DOCEND 08 + + /**---------------------------------------- + * RESPONDING TO COLLECT_SIGNATURES_FLOW * + ----------------------------------------**/ + progressTracker.currentStep = SIGNING + + // The responder will often need to respond to a call to + // ``CollectSignaturesFlow``. It does so my invoking its own + // ``SignTransactionFlow`` subclass. + // DOCSTART 16 + val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterpartySession) { + override fun checkTransaction(stx: SignedTransaction) = requireThat { + // Any additional checking we see fit... + val outputState = stx.tx.outputsOfType().single() + assert(outputState.magicNumber == 777) + } + } + + subFlow(signTransactionFlow) + // DOCEND 16 + + /**----------------------------- + * FINALISING THE TRANSACTION * + -----------------------------**/ + progressTracker.currentStep = FINALISATION + + // Nothing to do here! As long as some other party calls + // ``FinalityFlow``, the recording of the transaction on our node + // we be handled automatically. } } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt new file mode 100644 index 0000000000..e6826fa213 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt @@ -0,0 +1,99 @@ +package net.corda.docs + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.Party +import net.corda.core.utilities.unwrap + +// DOCSTART LaunchSpaceshipFlow +@InitiatingFlow +class LaunchSpaceshipFlow : FlowLogic() { + @Suspendable + override fun call() { + val shouldLaunchSpaceship = receive(getPresident()).unwrap { it } + if (shouldLaunchSpaceship) { + launchSpaceship() + } + } + + fun launchSpaceship() { + } + + fun getPresident(): Party { + TODO() + } +} + +@InitiatedBy(LaunchSpaceshipFlow::class) +@InitiatingFlow +class PresidentSpaceshipFlow(val launcher: Party) : FlowLogic() { + @Suspendable + override fun call() { + val needCoffee = true + send(getSecretary(), needCoffee) + val shouldLaunchSpaceship = false + send(launcher, shouldLaunchSpaceship) + } + + fun getSecretary(): Party { + TODO() + } +} + +@InitiatedBy(PresidentSpaceshipFlow::class) +class SecretaryFlow(val president: Party) : FlowLogic() { + @Suspendable + override fun call() { + // ignore + } +} +// DOCEND LaunchSpaceshipFlow + +// DOCSTART LaunchSpaceshipFlowCorrect +@InitiatingFlow +class LaunchSpaceshipFlowCorrect : FlowLogic() { + @Suspendable + override fun call() { + val presidentSession = initiateFlow(getPresident()) + val shouldLaunchSpaceship = presidentSession.receive().unwrap { it } + if (shouldLaunchSpaceship) { + launchSpaceship() + } + } + + fun launchSpaceship() { + } + + fun getPresident(): Party { + TODO() + } +} + +@InitiatedBy(LaunchSpaceshipFlowCorrect::class) +@InitiatingFlow +class PresidentSpaceshipFlowCorrect(val launcherSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val needCoffee = true + val secretarySession = initiateFlow(getSecretary()) + secretarySession.send(needCoffee) + val shouldLaunchSpaceship = false + launcherSession.send(shouldLaunchSpaceship) + } + + fun getSecretary(): Party { + TODO() + } +} + +@InitiatedBy(PresidentSpaceshipFlowCorrect::class) +class SecretaryFlowCorrect(val presidentSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + // ignore + } +} +// DOCEND LaunchSpaceshipFlowCorrect diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt index 2532577f9a..9b31523586 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt @@ -129,7 +129,7 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf override fun call(): StateAndRef { // DOCSTART 1 val criteria = VaultQueryCriteria(stateRefs = listOf(ref)) - val latestRecord = serviceHub.vaultQueryService.queryBy(criteria).states.single() + val latestRecord = serviceHub.vaultService.queryBy(criteria).states.single() // DOCEND 1 // Check the protocol hasn't already been run diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt new file mode 100644 index 0000000000..8389e73a99 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt @@ -0,0 +1,136 @@ +package net.corda.docs.tutorial.contract + +import net.corda.core.contracts.* +import net.corda.core.crypto.NullKeys +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.utils.sumCashBy +import net.corda.testing.chooseIdentityAndCert +import java.time.Instant +import java.util.* + +class CommercialPaper : Contract { + // DOCSTART 8 + companion object { + const val CP_PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.CommercialPaper" + } + // DOCEND 8 + + // DOCSTART 3 + override fun verify(tx: LedgerTransaction) { + // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. + val groups = tx.groupStates(State::withoutOwner) + + // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming + // it for cash on or after the maturity date. + val command = tx.commands.requireSingleCommand() + // DOCEND 3 + + // DOCSTART 4 + val timeWindow: TimeWindow? = tx.timeWindow + + for ((inputs, outputs, _) in groups) { + when (command.value) { + is Commands.Move -> { + val input = inputs.single() + requireThat { + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) + "the state is propagated" using (outputs.size == 1) + // Don't need to check anything else, as if outputs.size == 1 then the output is equal to + // the input ignoring the owner field due to the grouping. + } + } + + is Commands.Redeem -> { + // Redemption of the paper requires movement of on-ledger cash. + val input = inputs.single() + val received = tx.outputs.map { it.data }.sumCashBy(input.owner) + val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must be timestamped") + requireThat { + "the paper must have matured" using (time >= input.maturityDate) + "the received amount equals the face value" using (received == input.faceValue) + "the paper must be destroyed" using outputs.isEmpty() + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) + } + } + + is Commands.Issue -> { + val output = outputs.single() + val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must be timestamped") + requireThat { + // Don't allow people to issue commercial paper under other entities identities. + "output states are issued by a command signer" using (output.issuance.party.owningKey in command.signers) + "output values sum to more than the inputs" using (output.faceValue.quantity > 0) + "the maturity date is not in the past" using (time < output.maturityDate) + // Don't allow an existing CP state to be replaced by this issuance. + "can't reissue an existing state" using inputs.isEmpty() + } + } + + else -> throw IllegalArgumentException("Unrecognised command") + } + } + // DOCEND 4 + } + + // DOCSTART 2 + interface Commands : CommandData { + class Move : TypeOnlyCommandData(), Commands + class Redeem : TypeOnlyCommandData(), Commands + class Issue : TypeOnlyCommandData(), Commands + } + // DOCEND 2 + + // DOCSTART 5 + fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, + notary: Party): TransactionBuilder { + val state = State(issuance, issuance.party, faceValue, maturityDate) + val stateAndContract = StateAndContract(state, CP_PROGRAM_ID) + return TransactionBuilder(notary = notary).withItems(stateAndContract, Command(Commands.Issue(), issuance.party.owningKey)) + } + // DOCEND 5 + + // DOCSTART 6 + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { + tx.addInputState(paper) + val outputState = paper.state.data.withNewOwner(newOwner).ownableState + tx.addOutputState(outputState, CP_PROGRAM_ID) + tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey)) + } + // DOCEND 6 + + // DOCSTART 7 + @Throws(InsufficientBalanceException::class) + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub) { + // Add the cash movement using the states in our vault. + Cash.generateSpend( + services = services, + tx = tx, + amount = paper.state.data.faceValue.withoutIssuer(), + ourIdentity = services.myInfo.chooseIdentityAndCert(), + to = paper.state.data.owner + ) + tx.addInputState(paper) + tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) + } + // DOCEND 7 +} + +// DOCSTART 1 +data class State( + val issuance: PartyAndReference, + override val owner: AbstractParty, + val faceValue: Amount>, + val maturityDate: Instant +) : OwnableState { + override val participants = listOf(owner) + + fun withoutOwner() = copy(owner = AnonymousParty(NullKeys.NullPublicKey)) + override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(CommercialPaper.Commands.Move(), copy(owner = newOwner)) +} +// DOCEND 1 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt new file mode 100644 index 0000000000..0131077a8f --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt @@ -0,0 +1,64 @@ +package net.corda.docs.tutorial.flowstatemachines + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.contracts.OwnableState +import net.corda.core.contracts.StateAndRef +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.finance.flows.TwoPartyTradeFlow +import java.util.* + +// DOCSTART 1 +object TwoPartyTradeFlow { + class UnacceptablePriceException(givenPrice: Amount) : FlowException("Unacceptable price: $givenPrice") + class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() { + override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" + } + + /** + * This object is serialised to the network and is the first flow message the seller sends to the buyer. + * + * @param payToIdentity anonymous identity of the seller, for payment to be sent to. + */ + @CordaSerializable + data class SellerTradeInfo( + val price: Amount, + val payToIdentity: PartyAndCertificate + ) + + open class Seller(private val otherSideSession: FlowSession, + private val assetToSell: StateAndRef, + private val price: Amount, + private val myParty: PartyAndCertificate, + override val progressTracker: ProgressTracker = TwoPartyTradeFlow.Seller.tracker()) : FlowLogic() { + + companion object { + fun tracker() = ProgressTracker() + } + + @Suspendable + override fun call(): SignedTransaction { + TODO() + } + } + + open class Buyer(private val sellerSession: FlowSession, + private val notary: Party, + private val acceptablePrice: Amount, + private val typeToBuy: Class, + private val anonymous: Boolean) : FlowLogic() { + + @Suspendable + override fun call(): SignedTransaction { + TODO() + } + } +} +// DOCEND 1 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt new file mode 100644 index 0000000000..39f00d60ea --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt @@ -0,0 +1,33 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.requireSingleCommand +import net.corda.core.contracts.requireThat +import net.corda.core.transactions.LedgerTransaction + +class IOUContract : Contract { + // Our Create command. + class Create : CommandData + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputsOfType().single() + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // Constraints on the signers. + "There must only be one signer." using (command.signers.toSet().size == 1) + "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) + } + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt new file mode 100644 index 0000000000..7bb068cba5 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt @@ -0,0 +1,52 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndContract +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import kotlin.reflect.jvm.jvmName + +@InitiatingFlow +@StartableByRPC +class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the notary identity from the network map. + val notary = serviceHub.networkMapCache.notaryIdentities[0] + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // We create the transaction components. + val outputState = IOUState(iouValue, ourIdentity, otherParty) + val outputContract = IOUContract::class.jvmName + val outputContractAndState = StateAndContract(outputState, outputContract) + val cmd = Command(IOUContract.Create(), ourIdentity.owningKey) + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Finalising the transaction. + subFlow(FinalityFlow(signedTx)) + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt new file mode 100644 index 0000000000..447265a2ae --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt @@ -0,0 +1,12 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import net.corda.core.contracts.ContractState +import net.corda.core.identity.Party + +class IOUState(val value: Int, + val lender: Party, + val borrower: Party) : ContractState { + override val participants get() = listOf(lender, borrower) +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt new file mode 100644 index 0000000000..c560c4f3f4 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt @@ -0,0 +1,45 @@ +package net.corda.docs.tutorial.tearoffs + +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.MerkleTreeException +import net.corda.core.transactions.FilteredTransaction +import net.corda.core.transactions.FilteredTransactionVerificationException +import net.corda.core.transactions.SignedTransaction +import net.corda.finance.contracts.Fix +import net.corda.testing.ALICE +import java.util.function.Predicate + +fun main(args: Array) { + // Typealias to make the example coherent. + val oracle = ALICE + val stx = Any() as SignedTransaction + + // DOCSTART 1 + val filtering = Predicate { + when (it) { + is Command<*> -> oracle.owningKey in it.signers && it.value is Fix + else -> false + } + } + // DOCEND 1 + + // DOCSTART 2 + val ftx: FilteredTransaction = stx.buildFilteredTransaction(filtering) + // DOCEND 2 + + // DOCSTART 3 + // Direct access to included commands, inputs, outputs, attachments etc. + val cmds: List> = ftx.commands + val ins: List = ftx.inputs + val timeWindow: TimeWindow? = ftx.timeWindow + // ... + // DOCEND 3 + + try { + ftx.verify() + } catch (e: FilteredTransactionVerificationException) { + throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt new file mode 100644 index 0000000000..dd3fd47ff1 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -0,0 +1,246 @@ +package net.corda.docs.tutorial.testdsl + +import net.corda.core.utilities.days +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.CP_PROGRAM_ID +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.contracts.ICommercialPaperState +import net.corda.finance.contracts.asset.CASH +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.ownedBy +import net.corda.testing.* +import org.junit.Test + +class CommercialPaperTest { + // DOCSTART 1 + fun getPaper(): ICommercialPaperState = CommercialPaper.State( + issuance = MEGA_CORP.ref(123), + owner = MEGA_CORP, + faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + maturityDate = TEST_TX_TIME + 7.days + ) + // DOCEND 1 + + // DOCSTART 2 + @Test + fun simpleCP() { + val inState = getPaper() + ledger { + transaction { + attachments(CP_PROGRAM_ID) + input(CP_PROGRAM_ID) { inState } + verifies() + } + } + } + // DOCEND 2 + + // DOCSTART 3 + @Test + fun simpleCPMove() { + val inState = getPaper() + ledger { + transaction { + input(CP_PROGRAM_ID) { inState } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + attachments(CP_PROGRAM_ID) + verifies() + } + } + } + // DOCEND 3 + + // DOCSTART 4 + @Test + fun simpleCPMoveFails() { + val inState = getPaper() + ledger { + transaction { + input(CP_PROGRAM_ID) { inState } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + attachments(CP_PROGRAM_ID) + `fails with`("the state is propagated") + } + } + } + // DOCEND 4 + + // DOCSTART 5 + @Test + fun simpleCPMoveSuccess() { + val inState = getPaper() + ledger { + transaction { + input(CP_PROGRAM_ID) { inState } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + attachments(CP_PROGRAM_ID) + `fails with`("the state is propagated") + output(CP_PROGRAM_ID, "alice's paper") { inState.withOwner(ALICE) } + verifies() + } + } + } + // DOCEND 5 + + // DOCSTART 6 + @Test + fun `simple issuance with tweak`() { + ledger { + 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() } + timeWindow(TEST_TX_TIME) + `fails with`("output states are issued by a command signer") + } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timeWindow(TEST_TX_TIME) + verifies() + } + } + } + // DOCEND 6 + + // DOCSTART 7 + @Test + fun `simple issuance with tweak and top level transaction`() { + 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() } + timeWindow(TEST_TX_TIME) + `fails with`("output states are issued by a command signer") + } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timeWindow(TEST_TX_TIME) + verifies() + } + } + // DOCEND 7 + + // DOCSTART 8 + @Test + fun `chain commercial paper`() { + val issuer = MEGA_CORP.ref(123) + + ledger { + unverifiedTransaction { + attachments(Cash.PROGRAM_ID) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + } + + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + output(CP_PROGRAM_ID, "paper") { getPaper() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + attachments(CP_PROGRAM_ID) + timeWindow(TEST_TX_TIME) + verifies() + } + + + 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() } + verifies() + } + } + } + // DOCEND 8 + + // DOCSTART 9 + @Test + fun `chain commercial paper double spend`() { + val issuer = MEGA_CORP.ref(123) + ledger { + unverifiedTransaction { + attachments(Cash.PROGRAM_ID) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + } + + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + output(CP_PROGRAM_ID, "paper") { getPaper() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + attachments(CP_PROGRAM_ID) + timeWindow(TEST_TX_TIME) + verifies() + } + + 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() } + 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() } + verifies() + } + + fails() + } + } + // DOCEND 9 + + // DOCSTART 10 + @Test + fun `chain commercial tweak`() { + val issuer = MEGA_CORP.ref(123) + ledger { + unverifiedTransaction { + attachments(Cash.PROGRAM_ID) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + } + + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + output(CP_PROGRAM_ID, "paper") { getPaper() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + attachments(CP_PROGRAM_ID) + timeWindow(TEST_TX_TIME) + verifies() + } + + 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() } + verifies() + } + + tweak { + 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() } + verifies() + } + fails() + } + + verifies() + } + } + // DOCEND 10 +} diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt new file mode 100644 index 0000000000..25b5b3f6a1 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt @@ -0,0 +1,36 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.requireSingleCommand +import net.corda.core.contracts.requireThat +import net.corda.core.transactions.LedgerTransaction +// DOCEND 01 + +class IOUContract : Contract { + // Our Create command. + class Create : CommandData + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputsOfType().single() + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // DOCSTART 02 + // Constraints on the signers. + "There must be two signers." using (command.signers.toSet().size == 2) + "The borrower and lender must be signers." using (command.signers.containsAll(listOf( + out.borrower.owningKey, out.lender.owningKey))) + // DOCEND 02 + } + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt new file mode 100644 index 0000000000..0d8ac221ad --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt @@ -0,0 +1,57 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndContract +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import kotlin.reflect.jvm.jvmName +// DOCEND 01 + +@InitiatingFlow +@StartableByRPC +class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the notary identity from the network map. + val notary = serviceHub.networkMapCache.notaryIdentities[0] + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // DOCSTART 02 + // We create the transaction components. + val outputState = IOUState(iouValue, ourIdentity, otherParty) + val outputContract = IOUContract::class.jvmName + val outputContractAndState = StateAndContract(outputState, outputContract) + val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Creating a session with the other party. + val otherpartySession = initiateFlow(otherParty) + + // Obtaining the counterparty's signature. + val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker())) + + // Finalising the transaction. + subFlow(FinalityFlow(fullySignedTx)) + // DOCEND 02 + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt new file mode 100644 index 0000000000..b8007cc2ec --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt @@ -0,0 +1,30 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.requireThat +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.SignTransactionFlow +import net.corda.core.transactions.SignedTransaction +import net.corda.docs.tutorial.helloworld.IOUFlow +import net.corda.docs.tutorial.helloworld.IOUState + +@InitiatedBy(IOUFlow::class) +class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { + override fun checkTransaction(stx: SignedTransaction) = requireThat { + val output = stx.tx.outputs.single().data + "This must be an IOU transaction." using (output is IOUState) + val iou = output as IOUState + "The IOU's value can't be too high." using (iou.value < 100) + } + } + + subFlow(signTransactionFlow) + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt new file mode 100644 index 0000000000..a690625d65 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt @@ -0,0 +1,10 @@ +package net.corda.docs.tutorial.twoparty + +import net.corda.core.contracts.ContractState +import net.corda.core.identity.Party + +class IOUState(val value: Int, + val lender: Party, + val borrower: Party) : ContractState { + override val participants get() = listOf(lender, borrower) +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf index fe00d9aa38..e75807bbc7 100644 --- a/docs/source/example-code/src/main/resources/example-network-map-node.conf +++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf @@ -4,5 +4,4 @@ trustStorePassword : "trustpass" p2pAddress : "my-network-map:10000" webAddress : "localhost:10001" sshdAddress : "localhost:10002" -extraAdvertisedServiceIds : [] useHTTPS : false diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index d12273c18a..c9bad1eb5b 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -10,7 +10,6 @@ dataSourceProperties : { p2pAddress : "my-corda-node:10002" rpcAddress : "my-corda-node:10003" webAddress : "localhost:10004" -extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "my-network-map:10000" legalName : "O=Network Map Service,OU=corda,L=London,C=GB" diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index c8a8c89fc6..1ec43e7271 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -2,16 +2,14 @@ package net.corda.docs import net.corda.core.contracts.Amount import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.* import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow -import net.corda.node.internal.StartedNode import net.corda.finance.schemas.CashSchemaV1 -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.node.internal.StartedNode import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -23,34 +21,24 @@ import java.util.* class CustomVaultQueryTest { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode lateinit var nodeA: StartedNode lateinit var nodeB: StartedNode lateinit var notary: Party @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(threadPerNode = true) - val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) - + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)) + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + nodeA = mockNet.createPartyNode() + nodeB = mockNet.createPartyNode() nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java) nodeA.internals.installCordaService(CustomVaultQuery.Service::class.java) - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1)) notary = nodeA.services.getDefaultNotary() } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test 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 15c16c32b1..7a5193c0b0 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 @@ -1,6 +1,7 @@ package net.corda.docs import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow @@ -9,9 +10,6 @@ import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -21,24 +19,16 @@ import kotlin.test.assertEquals class FxTransactionBuildTutorialTest { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode lateinit var nodeA: StartedNode lateinit var nodeB: StartedNode lateinit var notary: Party @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(threadPerNode = true) - val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1)) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)) + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + nodeA = mockNet.createPartyNode() + nodeB = mockNet.createPartyNode() nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java) notary = nodeA.services.getDefaultNotary() } @@ -46,7 +36,6 @@ class FxTransactionBuildTutorialTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index d625c1483e..f7b99ead79 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -3,15 +3,13 @@ package net.corda.docs import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.toFuture import net.corda.core.utilities.getOrThrow -import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.api.ServiceHubInternal import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -21,83 +19,83 @@ import kotlin.test.assertEquals class WorkflowTransactionBuildTutorialTest { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode - lateinit var nodeA: StartedNode - lateinit var nodeB: StartedNode + lateinit var aliceServices: ServiceHubInternal + lateinit var bobServices: ServiceHubInternal + lateinit var alice: Party + lateinit var bob: Party // Helper method to locate the latest Vault version of a LinearState private inline fun ServiceHub.latest(ref: UniqueIdentifier): StateAndRef { - val linearHeads = vaultQueryService.queryBy(QueryCriteria.LinearStateQueryCriteria(uuid = listOf(ref.id))) + val linearHeads = vaultService.queryBy(QueryCriteria.LinearStateQueryCriteria(uuid = listOf(ref.id))) return linearHeads.states.single() } @Before fun setup() { - setCordappPackages("net.corda.docs") - mockNet = MockNetwork(threadPerNode = true) - val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeA.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs")) + // While we don't use the notary, we need there to be one on the network + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + aliceNode.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) + aliceServices = aliceNode.services + bobServices = bobNode.services + alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) + bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `Run workflow to completion`() { // Setup a vault subscriber to wait for successful upload of the proposal to NodeB - val nodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture() + val nodeBVaultUpdate = bobServices.vaultService.updates.toFuture() // Kick of the proposal flow - val flow1 = nodeA.services.startFlow(SubmitTradeApprovalFlow("1234", nodeB.info.chooseIdentity())) + val flow1 = aliceServices.startFlow(SubmitTradeApprovalFlow("1234", bob)) // Wait for the flow to finish val proposalRef = flow1.resultFuture.getOrThrow() val proposalLinearId = proposalRef.state.data.linearId // Wait for NodeB to include it's copy in the vault nodeBVaultUpdate.get() // Fetch the latest copy of the state from both nodes - val latestFromA = nodeA.database.transaction { - nodeA.services.latest(proposalLinearId) + val latestFromA = aliceServices.database.transaction { + aliceServices.latest(proposalLinearId) } - val latestFromB = nodeB.database.transaction { - nodeB.services.latest(proposalLinearId) + val latestFromB = bobServices.database.transaction { + bobServices.latest(proposalLinearId) } // Confirm the state as as expected assertEquals(WorkflowState.NEW, proposalRef.state.data.state) assertEquals("1234", proposalRef.state.data.tradeId) - assertEquals(nodeA.info.chooseIdentity(), proposalRef.state.data.source) - assertEquals(nodeB.info.chooseIdentity(), proposalRef.state.data.counterparty) + assertEquals(alice, proposalRef.state.data.source) + assertEquals(bob, proposalRef.state.data.counterparty) assertEquals(proposalRef, latestFromA) assertEquals(proposalRef, latestFromB) // Setup a vault subscriber to pause until the final update is in NodeA and NodeB - val nodeAVaultUpdate = nodeA.services.vaultService.updates.toFuture() - val secondNodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture() + val nodeAVaultUpdate = aliceServices.vaultService.updates.toFuture() + val secondNodeBVaultUpdate = bobServices.vaultService.updates.toFuture() // Run the manual completion flow from NodeB - val flow2 = nodeB.services.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED)) + val flow2 = bobServices.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED)) // wait for the flow to end val completedRef = flow2.resultFuture.getOrThrow() // wait for the vault updates to stabilise nodeAVaultUpdate.get() secondNodeBVaultUpdate.get() // Fetch the latest copies from the vault - val finalFromA = nodeA.database.transaction { - nodeA.services.latest(proposalLinearId) + val finalFromA = aliceServices.database.transaction { + aliceServices.latest(proposalLinearId) } - val finalFromB = nodeB.database.transaction { - nodeB.services.latest(proposalLinearId) + val finalFromB = bobServices.database.transaction { + bobServices.latest(proposalLinearId) } // Confirm the state is as expected assertEquals(WorkflowState.APPROVED, completedRef.state.data.state) assertEquals("1234", completedRef.state.data.tradeId) - assertEquals(nodeA.info.chooseIdentity(), completedRef.state.data.source) - assertEquals(nodeB.info.chooseIdentity(), completedRef.state.data.counterparty) + assertEquals(alice, completedRef.state.data.source) + assertEquals(bob, completedRef.state.data.counterparty) assertEquals(completedRef, finalFromA) assertEquals(completedRef, finalFromB) } diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index 2ca3b56759..c0946f07a6 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -86,13 +86,14 @@ Our flow has two parties (B and S for buyer and seller) and will proceed as foll 1. S sends a ``StateAndRef`` pointing to the state they want to sell to B, along with info about the price they require B to pay. -2. B sends to S a ``SignedTransaction`` that includes the state as input, B's cash as input, the state with the new - owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because - it lacks a signature from S authorising movement of the asset. +2. B sends to S a ``SignedTransaction`` that includes two inputs (the state owned by S, and cash owned by B) and three + outputs (the state now owned by B, the cash now owned by S, and any change cash still owned by B). The + ``SignedTransaction`` has a single signature from B but isn't valid because it lacks a signature from S authorising + movement of the asset. 3. S signs the transaction and sends it back to B. -4. B *finalises* the transaction by sending it to the notary who checks the transaction for validity, - recording the transaction in B's local vault, and then sending it on to S who also checks it and commits - the transaction to S's local vault. +4. B *finalises* the transaction by sending it to the notary who checks the transaction for validity, recording the + transaction in B's local vault, and then sending it on to S who also checks it and commits the transaction to S's + local vault. You can find the implementation of this flow in the file ``finance/src/main/kotlin/net/corda/finance/TwoPartyTradeFlow.kt``. @@ -109,64 +110,31 @@ each side. .. container:: codeset - .. sourcecode:: kotlin - - object TwoPartyTradeFlow { - class UnacceptablePriceException(val givenPrice: Amount) : FlowException("Unacceptable price: $givenPrice") - class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() { - override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" - } - - // This object is serialised to the network and is the first flow message the seller sends to the buyer. - @CordaSerializable - data class SellerTradeInfo( - val assetForSale: StateAndRef, - val price: Amount, - val sellerOwnerKey: PublicKey - ) - - open class Seller(val otherParty: Party, - val notaryNode: NodeInfo, - val assetToSell: StateAndRef, - val price: Amount, - val myKey: PublicKey, - override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - TODO() - } - } - - open class Buyer(val otherParty: Party, - val notary: Party, - val acceptablePrice: Amount, - val typeToBuy: Class) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - TODO() - } - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 This code defines several classes nested inside the main ``TwoPartyTradeFlow`` singleton. Some of the classes are simply flow messages or exceptions. The other two represent the buyer and seller side of the flow. Going through the data needed to become a seller, we have: -- ``otherParty: Party`` - the party with which you are trading. -- ``notaryNode: NodeInfo`` - the entry in the network map for the chosen notary. See ":doc:`key-concepts-notaries`" for more - information on notaries. -- ``assetToSell: StateAndRef`` - a pointer to the ledger entry that represents the thing being sold. -- ``price: Amount`` - the agreed on price that the asset is being sold for (without an issuer constraint). -- ``myKey: PublicKey`` - the PublicKey part of the node's internal KeyPair that controls the asset being sold. -The matching PrivateKey stored in the KeyManagementService will be used to sign the transaction. +- ``otherSideSession: FlowSession`` - a flow session for communication with the buyer +- ``assetToSell: StateAndRef`` - a pointer to the ledger entry that represents the thing being sold +- ``price: Amount`` - the agreed on price that the asset is being sold for (without an issuer constraint) +- ``myParty: PartyAndCertificate`` - the certificate representing the party that controls the asset being sold And for the buyer: +- ``sellerSession: FlowSession`` - a flow session for communication with the seller +- ``notary: Party`` - the entry in the network map for the chosen notary. See “Notaries” for more information on + notaries - ``acceptablePrice: Amount`` - the price that was agreed upon out of band. If the seller specifies - a price less than or equal to this, then the trade will go ahead. + a price less than or equal to this, then the trade will go ahead - ``typeToBuy: Class`` - the type of state that is being purchased. This is used to check that the - sell side of the flow isn't trying to sell us the wrong thing, whether by accident or on purpose. + sell side of the flow isn't trying to sell us the wrong thing, whether by accident or on purpose +- ``anonymous: Boolean`` - whether to generate a fresh, anonymous public key for the transaction Alright, so using this flow shouldn't be too hard: in the simplest case we can just create a Buyer or Seller with the details of the trade, depending on who we are. We then have to start the flow in some way. Just @@ -191,9 +159,9 @@ and try again. Whitelisted classes with the Corda node --------------------------------------- -For security reasons, we do not want Corda nodes to be able to receive instances of any class on the classpath +For security reasons, we do not want Corda nodes to be able to just receive instances of any class on the classpath via messaging, since this has been exploited in other Java application containers in the past. Instead, we require -that every class contained in messages is whitelisted. Some classes are whitelisted by default (see ``DefaultWhitelist``), +every class contained in messages to be whitelisted. Some classes are whitelisted by default (see ``DefaultWhitelist``), but others outside of that set need to be whitelisted either by using the annotation ``@CordaSerializable`` or via the plugin framework. See :doc:`serialization`. You can see above that the ``SellerTradeInfo`` has been annotated. @@ -231,26 +199,28 @@ These will return a ``FlowProgressHandle``, which is just like a ``FlowHandle`` Implementing the seller ----------------------- -Let's implement the ``Seller.call`` method. This will be run when the flow is invoked. +Let's implement the ``Seller.call`` method that will be run when the flow is invoked. .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 4 - :end-before: DOCEND 4 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 8 We start by sending information about the asset we wish to sell to the buyer. We fill out the initial flow message with -the trade info, and then call ``send``. which takes two arguments: +the trade info, and then call ``otherSideSession.send``. which takes two arguments: -- The party we wish to send the message to. -- The payload being sent. +- The party we wish to send the message to +- The payload being sent -``send`` will serialise the payload and send it to the other party automatically. +``otherSideSession.send`` will serialise the payload and send it to the other party automatically. -Next, we call a *subflow* called ``SignTransactionFlow`` (see :ref:`subflows`). ``SignTransactionFlow`` automates the -process of: +Next, we call a *subflow* called ``IdentitySyncFlow.Receive`` (see :ref:`subflows`). ``IdentitySyncFlow.Receive`` +ensures that our node can de-anonymise any confidential identities in the transaction it's about to be asked to sign. + +Next, we call another subflow called ``SignTransactionFlow``. ``SignTransactionFlow`` automates the process of: * Receiving a proposed trade transaction from the buyer, with the buyer's signature attached. * Checking that the proposed transaction is valid. @@ -273,7 +243,7 @@ OK, let's do the same for the buyer side: :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 - :dedent: 4 + :dedent: 8 This code is longer but no more complicated. Here are some things to pay attention to: @@ -297,50 +267,19 @@ time, and perhaps communicating with the same counterparty node but for differen way to segregate communication channels so that concurrent conversations between flows on the same set of nodes do not interfere with each other. -To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party`` -for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to -the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party`` -will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends -pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known -sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is -``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate. +To achieve this in order to communicate with a counterparty one needs to first initiate such a session with a ``Party`` +using ``initiateFlow``, which returns a ``FlowSession`` object, identifying this communication. Subsequently the first +actual communication will kick off a counter-flow on the other side, receiving a "reply" session object. A session ends +when either flow ends, whether as expected or pre-maturely. If a flow ends pre-maturely then the other side will be +notified of that and they will also end, as the whole point of flows is a known sequence of message transfers. Flows end +pre-maturely due to exceptions, and as described above, if that exception is ``FlowException`` or a sub-type then it +will propagate to the other side. Any other exception will not propagate. Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is -making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp -which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method -specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist -then no further communication takes place and the initiating flow ends with an exception. - -Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done -with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side. -In this case it doesn't matter which flow is the initiator and which is the initiated. If we choose the seller side as -the initiator then the buyer side would need to register their flow, perhaps with something like: - -.. container:: codeset - - .. sourcecode:: kotlin - - class TwoPartyTradeFlowPlugin : CordaPluginRegistry() { - override val servicePlugins = listOf(Function(TwoPartyTradeFlowService::Service)) - } - - object TwoPartyTradeFlowService { - class Service(services: PluginServiceHub) { - init { - services.registerServiceFlow(TwoPartyTradeFlow.Seller::class.java) { - TwoPartyTradeFlow.Buyer( - it, - notary = services.networkMapCache.notaryIdentities[0].party, - acceptablePrice = TODO(), - typeToBuy = TODO()) - } - } - } - } - -This is telling the buyer node to fire up an instance of ``TwoPartyTradeFlow.Buyer`` (the code in the lambda) when -they receive a message from the initiating seller side of the flow (``TwoPartyTradeFlow.Seller::class.java``). +making the request - each session initiation includes the initiating flow type. The *initiated* (server) flow must name the +*initiating* (client) flow using the ``@InitiatedBy`` annotation and passing the class name that will be starting the +flow session as the annotation parameter. .. _subflows: @@ -472,7 +411,7 @@ Exception handling Flows can throw exceptions to prematurely terminate their execution. The flow framework gives special treatment to ``FlowException`` and its subtypes. These exceptions are treated as error responses of the flow and are propagated to all counterparties it is communicating with. The receiving flows will throw the same exception the next time they do -a ``receive`` or ``sendAndReceive`` and thus end the flow session. If the receiver was invoked via ``subFlow`` (details below) +a ``receive`` or ``sendAndReceive`` and thus end the flow session. If the receiver was invoked via ``subFlow`` then the exception can be caught there enabling re-invocation of the sub-flow. If the exception thrown by the erroring flow is not a ``FlowException`` it will still terminate but will not propagate to @@ -505,59 +444,40 @@ A flow might declare some steps with code inside the flow class like this: .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 8 - .. sourcecode:: java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 - private final ProgressTracker progressTracker = new ProgressTracker( - RECEIVING, - VERIFYING, - SIGNING, - COLLECTING_SIGNATURES, - RECORDING - ); +Each step exposes a label. By defining your own step types, you can export progress in a way that's both human readable +and machine readable. - private static final ProgressTracker.Step RECEIVING = new ProgressTracker.Step( - "Waiting for seller trading info"); - private static final ProgressTracker.Step VERIFYING = new ProgressTracker.Step( - "Verifying seller assets"); - private static final ProgressTracker.Step SIGNING = new ProgressTracker.Step( - "Generating and signing transaction proposal"); - private static final ProgressTracker.Step COLLECTING_SIGNATURES = new ProgressTracker.Step( - "Collecting signatures from other parties"); - private static final ProgressTracker.Step RECORDING = new ProgressTracker.Step( - "Recording completed transaction"); - -Each step exposes a label. By default labels are fixed, but by subclassing ``RelabelableStep`` you can make a step -that can update its label on the fly. That's useful for steps that want to expose non-structured progress information -like the current file being downloaded. By defining your own step types, you can export progress in a way that's both -human readable and machine readable. - -Progress trackers are hierarchical. Each step can be the parent for another tracker. By altering the -``ProgressTracker.childrenFor`` map, a tree of steps can be created. It's allowed to alter the hierarchy -at runtime, on the fly, and the progress renderers will adapt to that properly. This can be helpful when you don't -fully know ahead of time what steps will be required. If you *do* know what is required, configuring as much of the -hierarchy ahead of time is a good idea, as that will help the users see what is coming up. You can pre-configure -steps by overriding the ``Step`` class like this: +Progress trackers are hierarchical. Each step can be the parent for another tracker. By setting +``Step.childProgressTracker``, a tree of steps can be created. It's allowed to alter the hierarchy at runtime, on the +fly, and the progress renderers will adapt to that properly. This can be helpful when you don't fully know ahead of +time what steps will be required. If you *do* know what is required, configuring as much of the hierarchy ahead of time +is a good idea, as that will help the users see what is coming up. You can pre-configure steps by overriding the +``Step`` class like this: .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 3 - :end-before: DOCEND 3 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 12 - .. sourcecode:: java - - private static final ProgressTracker.Step VERIFYING_AND_SIGNING = new ProgressTracker.Step("Verifying and signing transaction proposal") { - @Nullable @Override public ProgressTracker childProgressTracker() { - return SignTransactionFlow.Companion.tracker(); - } - }; + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java + :language: java + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 Every tracker has not only the steps given to it at construction time, but also the singleton ``ProgressTracker.UNSTARTED`` step and the ``ProgressTracker.DONE`` step. Once a tracker has become ``DONE`` its @@ -586,22 +506,6 @@ and linked ahead of time. In future, the progress tracking framework will become a vital part of how exceptions, errors, and other faults are surfaced to human operators for investigation and resolution. -Versioning ----------- - -Fibers involve persisting object-serialised stack frames to disk. Although we may do some R&D into in-place upgrades -in future, for now the upgrade process for flows is simple: you duplicate the code and rename it so it has a -new set of class names. Old versions of the flow can then drain out of the system whilst new versions are -initiated. When enough time has passed that no old versions are still waiting for anything to happen, the previous -copy of the code can be deleted. - -Whilst kind of ugly, this is a very simple approach that should suffice for now. - -.. warning:: Flows are not meant to live for months or years, and by implication they are not meant to implement entire deal - lifecycles. For instance, implementing the entire life cycle of an interest rate swap as a single flow - whilst - technically possible - would not be a good idea. The platform provides a job scheduler tool that can invoke - flows for this reason (see ":doc:`event-scheduling`") - Future features --------------- @@ -611,6 +515,6 @@ the features we have planned: * Exception management, with a "flow hospital" tool to manually provide solutions to unavoidable problems (e.g. the other side doesn't know the trade) * Being able to interact with people, either via some sort of external ticketing system, or email, or a custom UI. - For example to implement human transaction authorisations. + For example to implement human transaction authorisations * A standard library of flows that can be easily sub-classed by local developers in order to integrate internal - reporting logic, or anything else that might be required as part of a communications lifecycle. \ No newline at end of file + reporting logic, or anything else that might be required as part of a communications lifecycle \ No newline at end of file diff --git a/docs/source/flow-testing.rst b/docs/source/flow-testing.rst index 1d53696b61..967dbe9877 100644 --- a/docs/source/flow-testing.rst +++ b/docs/source/flow-testing.rst @@ -15,74 +15,52 @@ A good example to examine for learning how to unit test flows is the ``ResolveTr flow takes care of downloading and verifying transaction graphs, with all the needed dependencies. We start with this basic skeleton: -.. container:: codeset - - .. sourcecode:: kotlin - - class ResolveTransactionsFlowTest { - lateinit var mockNet: MockNetwork - lateinit var a: MockNetwork.MockNode - lateinit var b: MockNetwork.MockNode - lateinit var notary: Party - - @Before - fun setup() { - mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes() - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - mockNet.runNetwork() - notary = a.services.getDefaultNotary() - } - - @After - fun tearDown() { - mockNet.stopNodes() - } - } +.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 We create a mock network in our ``@Before`` setup method and create a couple of nodes. We also record the identity of the notary in our test network, which will come in handy later. We also tidy up when we're done. Next, we write a test case: -.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 4 We'll take a look at the ``makeTransactions`` function in a moment. For now, it's enough to know that it returns two -``SignedTransaction`` objects, the second of which spends the first. Both transactions are known by node A -but not node B. +``SignedTransaction`` objects, the second of which spends the first. Both transactions are known by MegaCorpNode but +not MiniCorpNode. -The test logic is simple enough: we create the flow, giving it node A's identity as the target to talk to. -Then we start it on node B and use the ``mockNet.runNetwork()`` method to bounce messages around until things have +The test logic is simple enough: we create the flow, giving it MegaCorpNode's identity as the target to talk to. +Then we start it on MiniCorpNode and use the ``mockNet.runNetwork()`` method to bounce messages around until things have settled (i.e. there are no more messages waiting to be delivered). All this is done using an in memory message routing implementation that is fast to initialise and use. Finally, we obtain the result of the flow and do -some tests on it. We also check the contents of node B's database to see that the flow had the intended effect +some tests on it. We also check the contents of MiniCorpNode's database to see that the flow had the intended effect on the node's persistent state. Here's what ``makeTransactions`` looks like: -.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 We're using the ``DummyContract``, a simple test smart contract which stores a single number in its states, along with ownership and issuer information. You can issue such states, exit them and re-assign ownership (move them). It doesn't do anything else. This code simply creates a transaction that issues a dummy state (the issuer is ``MEGA_CORP``, a pre-defined unit test identity), signs it with the test notary and MegaCorp keys and then converts the builder to the final ``SignedTransaction``. It then does so again, but this time instead of issuing -it re-assigns ownership instead. The chain of two transactions is finally committed to node A by sending them -directly to the ``a.services.recordTransaction`` method (note that this method doesn't check the transactions are -valid) inside a ``database.transaction``. All node flows run within a database transaction in the nodes themselves, -but any time we need to use the database directly from a unit test, you need to provide a database transaction as shown -here. +it re-assigns ownership instead. The chain of two transactions is finally committed to MegaCorpNode by sending them +directly to the ``megaCorpNode.services.recordTransaction`` method (note that this method doesn't check the +transactions are valid) inside a ``database.transaction``. All node flows run within a database transaction in the +nodes themselves, but any time we need to use the database directly from a unit test, you need to provide a database +transaction as shown here. With regards to initiated flows (see :doc:`flow-state-machines` for information on initiated and initiating flows), the full node automatically registers them by scanning the CorDapp jars. In a unit test environment this is not possible so -``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. - -And that's it: you can explore the documentation for the `MockNetwork API `_ -here. +``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. \ No newline at end of file diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 04901d29fa..f6ce762df1 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -74,14 +74,12 @@ IntelliJ Download a sample project ^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a command prompt -2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` -3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` -4. Retrieve a list of all the releases by running ``git branch -a --list`` -5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) +2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` +3. Move into the cordapp-example folder by running ``cd cordapp-example`` Run from the command prompt ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -1. From the cordapp-tutorial folder, deploy the nodes by running ``gradlew deployNodes`` +1. From the cordapp-example folder, deploy the nodes by running ``gradlew deployNodes`` 2. Start the nodes by running ``call kotlin-source/build/nodes/runnodes.bat`` 3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" 4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ @@ -89,7 +87,7 @@ Run from the command prompt Run from IntelliJ ^^^^^^^^^^^^^^^^^ 1. Open IntelliJ Community Edition -2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-tutorial folder +2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-example folder .. warning:: If you click "Import Project" instead of "Open", the project's run configurations will be erased! @@ -123,14 +121,12 @@ IntelliJ Download a sample project ^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a terminal -2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` -3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` -4. Retrieve a list of all the releases by running ``git branch -a --list`` -5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) +2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` +3. Move into the cordapp-example folder by running ``cd cordapp-example`` Run from the terminal ^^^^^^^^^^^^^^^^^^^^^ -1. From the cordapp-tutorial folder, deploy the nodes by running ``./gradlew deployNodes`` +1. From the cordapp-example folder, deploy the nodes by running ``./gradlew deployNodes`` 2. Start the nodes by running ``kotlin-source/build/nodes/runnodes``. Do not click while 8 additional terminal windows start up. 3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" 4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ @@ -138,7 +134,7 @@ Run from the terminal Run from IntelliJ ^^^^^^^^^^^^^^^^^ 1. Open IntelliJ Community Edition -2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-tutorial folder +2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-example folder 3. Once the project is open, click "File > Project Structure". Under "Project SDK:", set the project SDK by clicking "New...", clicking "JDK", and navigating to /Library/Java/JavaVirtualMachines/jdk1.8.0_XXX (where "XXX" is the latest minor version number). Click "OK". 4. Click "View > Tool Windows > Event Log", and click "Import Gradle project", then "OK". Wait, and click "OK" again when the "Gradle Project Data To Import" window appears 5. Wait for indexing to finish (a progress bar will display at the bottom-right of the IntelliJ window until indexing is complete) @@ -161,22 +157,19 @@ A CorDapp template that you can use as the basis for your own CorDapps is availa And a simple example CorDapp for you to explore basic concepts is available here: - https://github.com/corda/cordapp-tutorial.git + https://github.com/corda/cordapp-example.git You can clone these repos to your local machine by running the command ``git clone [repo URL]``. -By default, these repos will be on the unstable ``master`` branch. You should check out the latest milestone release -instead by running ``git checkout release-MX`` (where “X” is the latest milestone). - Next steps ---------- -The best way to check that everything is working fine is by running the :doc:`tutorial CorDapp ` and -the :doc:`samples `. +The best way to check that everything is working fine is by taking a deeper look at the +:doc:`example CorDapp `. Next, you should read through :doc:`Corda Key Concepts ` to understand how Corda works. By then, you'll be ready to start writing your own CorDapps. Learn how to do this in the -:doc:`Hello, World tutorial `. You may want to refer to the :doc:`API docs ` along the +:doc:`Hello, World tutorial `. You may want to refer to the :doc:`API documentation ` along the way. If you encounter any issues, please see the :doc:`troubleshooting` page, or get in touch with us on the diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index d5e4de152f..05ceaace01 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -41,7 +41,8 @@ Just as every Corda state must implement the ``ContractState`` interface, every You can read about function declarations in Kotlin `here `_. -We can see that ``Contract`` expresses its constraints through a ``verify`` function that takes a transaction as input, and: +We can see that ``Contract`` expresses its constraints through a ``verify`` function that takes a transaction as input, +and: * Throws an ``IllegalArgumentException`` if it rejects the transaction proposal * Returns silently if it accepts the transaction proposal @@ -77,81 +78,15 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.contracts.* - - ... - - class IOUContract : Contract { - // Our Create command. - class Create : CommandData - - override fun verify(tx: LedgerTransaction) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "There should be one output state of type IOUState." using (tx.outputs.size == 1) - - // IOU-specific constraints. - val out = tx.outputs.single().data as IOUState - "The IOU's value must be non-negative." using (out.value > 0) - "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) - - // Constraints on the signers. - "There must only be one signer." using (command.signers.toSet().size == 1) - "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) - } - } - } - - .. code-block:: java - - package com.template.contract; - - import com.template.state.IOUState; - import net.corda.core.contracts.CommandWithParties; - import net.corda.core.contracts.CommandData; - import net.corda.core.contracts.Contract; - import net.corda.core.transactions.LedgerTransaction; - import net.corda.core.crypto.SecureHash; - import net.corda.core.identity.Party; - - import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; - import static net.corda.core.contracts.ContractsDSL.requireThat; - - public class IOUContract implements Contract { - // Our Create command. - public static class Create implements CommandData {} - - @Override - public void verify(LedgerTransaction tx) { - final CommandWithParties command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); - - // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().get(0).getData(); - final Party lender = out.getLender(); - final Party borrower = out.getBorrower(); - check.using("The IOU's value must be non-negative.",out.getValue() > 0); - check.using("The lender and the borrower cannot be the same entity.", lender != borrower); - - // Constraints on the signers. - check.using("There must only be one signer.", command.getSigners().size() == 1); - check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey())); - - return null; - }); - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateContract.java`` to ``IOUContract.java``. @@ -258,7 +193,5 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta * The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower must be different entities -Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class. - The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow a node to orchestrate the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index e3bd8db52b..bedcf6842a 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -33,118 +33,20 @@ FlowLogic Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding ``FlowLogic.call``. -We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Overwrite both the existing flows in the template -with the following: +We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Delete both the existing flows in the template, and +replace them with the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.utilities.ProgressTracker - import net.corda.core.transactions.TransactionBuilder - import net.corda.core.flows.* - - ... - - @InitiatingFlow - @StartableByRPC - class IOUFlow(val iouValue: Int, - val otherParty: Party) : FlowLogic() { - - /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ - override val progressTracker = ProgressTracker() - - /** The flow logic is encapsulated within the call() method. */ - @Suspendable - override fun call() { - val notary = serviceHub.networkMapCache.getAnyNotary() - - // We create a transaction builder - val txBuilder = TransactionBuilder(notary = notary) - - // We add the items to the builder. - val state = IOUState(iouValue, me, otherParty) - val cmd = Command(IOUContract.Create(), me.owningKey) - txBuilder.withItems(state, cmd) - - // Verifying the transaction. - txBuilder.verify(serviceHub) - - // Signing the transaction. - val signedTx = serviceHub.signInitialTransaction(txBuilder) - - // Finalising the transaction. - subFlow(FinalityFlow(signedTx)) - } - } - - .. code-block:: java - - package com.template.flow; - - import co.paralleluniverse.fibers.Suspendable; - import com.template.contract.IOUContract; - import com.template.state.IOUState; - import net.corda.core.contracts.Command; - import net.corda.core.flows.*; - import net.corda.core.identity.Party; - import net.corda.core.transactions.SignedTransaction; - import net.corda.core.transactions.TransactionBuilder; - import net.corda.core.utilities.ProgressTracker; - - @InitiatingFlow - @StartableByRPC - public class IOUFlow extends FlowLogic { - private final Integer iouValue; - private final Party otherParty; - - /** - * The progress tracker provides checkpoints indicating the progress of the flow to observers. - */ - private final ProgressTracker progressTracker = new ProgressTracker(); - - public IOUFlow(Integer iouValue, Party otherParty) { - this.iouValue = iouValue; - this.otherParty = otherParty; - } - - @Override - public ProgressTracker getProgressTracker() { - return progressTracker; - } - - /** - * The flow logic is encapsulated within the call() method. - */ - @Suspendable - @Override - public Void call() throws FlowException { - // We retrieve the required identities from the network map. - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - - // We create a transaction builder. - final TransactionBuilder txBuilder = new TransactionBuilder(); - txBuilder.setNotary(notary); - - // We add the items to the builder. - IOUState state = new IOUState(iouValue, me, otherParty); - Command cmd = new Command(new IOUContract.Create(), me.getOwningKey()); - txBuilder.withItems(state, cmd); - - // Verifying the transaction. - txBuilder.verify(getServiceHub()); - - // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Finalising the transaction. - subFlow(new FinalityFlow(signedTx)); - - return null; - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateFlow.java`` to ``IOUFlow.java``. @@ -200,7 +102,7 @@ the following transaction: So we'll need the following: -* The output ``IOUState`` +* The output ``IOUState`` and its associated contract * A ``Create`` command listing the IOU's lender as a signer The command we use pairs the ``IOUContract.Create`` command defined earlier with our public key. Including this command @@ -208,8 +110,8 @@ in the transaction makes us one of the transaction's required signers. We add these items to the transaction using the ``TransactionBuilder.withItems`` method, which takes a ``vararg`` of: -* ``ContractState`` or ``TransactionState`` objects, which are added to the builder as output states -* ``StateRef`` objects (references to the outputs of previous transactions), which are added to the builder as input +* ``StateAndContract`` or ``TransactionState`` objects, which are added to the builder as output states +* ``StateAndRef`` objects (references to the outputs of previous transactions), which are added to the builder as input state references * ``Command`` objects, which are added to the builder as commands * ``SecureHash`` objects, which are added to the builder as attachments diff --git a/docs/source/hello-world-introduction.rst b/docs/source/hello-world-introduction.rst index 9760d91a6d..25bb4e189d 100644 --- a/docs/source/hello-world-introduction.rst +++ b/docs/source/hello-world-introduction.rst @@ -5,7 +5,7 @@ By this point, :doc:`your dev environment should be set up `, yo :doc:`your first CorDapp `, and you're familiar with Corda's :doc:`key concepts `. What comes next? -If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a plugin that is +If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a JAR that is installed on one or more Corda nodes, and gives them the ability to conduct some new process - anything from issuing a debt instrument to making a restaurant booking. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 2e15aaf74e..377fd90e05 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -17,41 +17,36 @@ Kotlin) file. We won't be using it, and it will cause build errors unless we rem Deploying our CorDapp --------------------- Let's take a look at the nodes we're going to deploy. Open the project's ``build.gradle`` file and scroll down to the -``task deployNodes`` section. This section defines three nodes - the Controller, NodeA, and NodeB: +``task deployNodes`` section. This section defines three nodes - the Controller, PartyA, and PartyB: -.. container:: codeset +.. code:: bash - .. code-block:: kotlin - - task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=UK" - node { - name "O=Controller,OU=corda,L=London,C=UK" - advertisedServices = ["corda.notary.validating"] - p2pPort 10002 - rpcPort 10003 - cordapps = [] - } - node { - name "CN=NodeA,O=NodeA,L=London,C=UK" - advertisedServices = [] - p2pPort 10005 - rpcPort 10006 - webPort 10007 - cordapps = [] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } - node { - name "CN=NodeB,O=NodeB,L=New York,C=US" - advertisedServices = [] - p2pPort 10008 - rpcPort 10009 - webPort 10010 - cordapps = [] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + directory "./build/nodes" + node { + name "O=Controller,L=London,C=GB" + advertisedServices = ["corda.notary.validating"] + p2pPort 10002 + rpcPort 10003 + cordapps = ["net.corda:corda-finance:$corda_release_version"] } + node { + name "O=PartyA,L=London,C=GB" + p2pPort 10005 + rpcPort 10006 + webPort 10007 + cordapps = ["net.corda:corda-finance:$corda_release_version"] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + node { + name "O=PartyB,L=New York,C=US" + p2pPort 10008 + rpcPort 10009 + webPort 10010 + cordapps = ["net.corda:corda-finance:$corda_release_version"] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + } We have three standard nodes, plus a special Controller node that is running the network map service, and is also advertising a validating notary service. Feel free to add additional node definitions here to expand the size of the @@ -64,7 +59,7 @@ We can run this ``deployNodes`` task using Gradle. For each node definition, Gra We can do that now by running the following commands from the root of the project: -.. code:: python +.. code:: bash // On Windows gradlew clean deployNodes @@ -77,19 +72,18 @@ Running the nodes Running ``deployNodes`` will build the nodes under ``build/nodes``. If we navigate to one of these folders, we'll see the three node folders. Each node folder has the following structure: - .. code:: python + .. code:: bash . |____corda.jar // The runnable node |____corda-webserver.jar // The node's webserver - |____dependencies |____node.conf // The node's configuration file - |____plugins - |____java/kotlin-source-0.1.jar // Our IOU CorDapp + |____cordapps + |____java/kotlin-source-0.1.jar // Our IOU CorDapp Let's start the nodes by running the following commands from the root of the project: -.. code:: python +.. code:: bash // On Windows build/nodes/runnodes.bat @@ -111,57 +105,66 @@ Now that our nodes are running, let's order one of them to create an IOU by kick app, we'd generally provide a web API sitting on top of our node. Here, for simplicity, we'll be interacting with the node via its built-in CRaSH shell. -Go to the terminal window displaying the CRaSH shell of Node A. Typing ``help`` will display a list of the available +Go to the terminal window displaying the CRaSH shell of PartyA. Typing ``help`` will display a list of the available commands. -We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing: +We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing: + +.. code:: bash + + start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" + +PartyA and PartyB will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU +in the vaults of both PartyA and PartyB. + +We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run`` +will display a list of the available commands. We can examine the contents of a node's vault by running: .. container:: codeset .. code-block:: java - start IOUFlow arg0: 99, arg1: "NodeB" + run vaultQuery contractStateType: com.template.state.IOUState .. code-block:: kotlin - start IOUFlow iouValue: 99, otherParty: "NodeB" + run vaultQuery contractStateType: com.template.IOUState -Node A and Node B will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU -in the vaults of both Node A and Node B. +The vaults of PartyA and PartyB should both display the following output: -We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run`` -will display a list of the available commands. We can examine the contents of a node's vault by running: +.. code:: bash -.. code:: python - - run vaultAndUpdates - -And we can also examine a node's transaction storage, by running: - -.. code:: python - - run verifiedTransactions - -The vaults of Node A and Node B should both display the following output: - -.. code:: python - - first: + states: - state: data: value: 99 - lender: "CN=NodeA,O=NodeA,L=London,C=GB" - borrower: "CN=NodeB,O=NodeB,L=New York,C=US" - contract: {} + lender: "C=GB,L=London,O=PartyA" + borrower: "C=US,L=New York,O=PartyB" participants: - - "CN=NodeA,O=NodeA,L=London,C=GB" - - "CN=NodeB,O=NodeB,L=New York,C=US" - notary: "O=Controller,OU=corda,L=London,C=GB,OU=corda.notary.validating" + - "C=GB,L=London,O=PartyA" + - "C=US,L=New York,O=PartyB" + contract: "com.template.contract.IOUContract" + notary: "C=GB,L=London,O=Controller,CN=corda.notary.validating" encumbrance: null + constraint: + attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552" ref: - txhash: "656A1BF64D5AEEC6F6C944E287F34EF133336F5FC2C5BFB9A0BFAE25E826125F" + txhash: "5CED068E790A347B0DD1C6BB5B2B463406807F95E080037208627565E6A2103B" index: 0 - second: "(observable)" + statesMetadata: + - ref: + txhash: "5CED068E790A347B0DD1C6BB5B2B463406807F95E080037208627565E6A2103B" + index: 0 + contractStateClassName: "com.template.state.IOUState" + recordedTime: 1506415268.875000000 + consumedTime: null + status: "UNCONSUMED" + notary: "C=GB,L=London,O=Controller,CN=corda.notary.validating" + lockId: null + lockUpdateTime: 1506415269.548000000 + totalStatesAvailable: -1 + stateTypes: "UNCONSUMED" + otherResults: [] Conclusion ---------- @@ -185,11 +188,9 @@ There are a number of improvements we could make to this CorDapp: * We could add an API, to make it easier to interact with the CorDapp We will explore some of these improvements in future tutorials. But you should now be ready to develop your own -CorDapps. There's `a more fleshed-out version of the IOU CorDapp `_ with an -API and web front-end, and a set of example CorDapps in `the main Corda repo `_, under -``samples``. An explanation of how to run these samples :doc:`here `. +CorDapps. You can find a list of sample CorDapps `here `_. -As you write CorDapps, you can learn more about the API available :doc:`here `. +As you write CorDapps, you can learn more about the Corda API :doc:`here `. If you get stuck at any point, please reach out on `Slack `_, `Discourse `_, or `Stack Overflow `_. diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 1677e3276a..a933cc920b 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -20,9 +20,6 @@ defined as follows: .. code-block:: kotlin interface ContractState { - // The contract that imposes constraints on how this state can evolve over time. - val contract: Contract - // The list of entities considered to have a stake in this state. val participants: List } @@ -38,13 +35,10 @@ If you do want to dive into Kotlin, there's an official `getting started guide `_, and a series of `Kotlin Koans `_. -We can see that the ``ContractState`` interface declares two properties: +We can see that the ``ContractState`` interface has a single field, ``participants``. ``participants`` is a list of +the entities for which this state is relevant. -* ``contract``: the contract controlling transactions involving this state -* ``participants``: the list of entities that have to approve state changes such as changing the state's notary or - upgrading the state's contract - -Beyond this, our state is free to define any properties, methods, helpers or inner classes it requires to accurately +Beyond this, our state is free to define any fields, methods, helpers or inner classes it requires to accurately represent a given class of shared facts on the ledger. ``ContractState`` also has several child interfaces that you may wish to implement depending on your state, such as @@ -69,62 +63,15 @@ define an ``IOUState``: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - class IOUState(val value: Int, - val lender: Party, - val borrower: Party) : ContractState { - override val contract = "net.corda.contract.TemplateContract" - override val participants get() = listOf(lender, borrower) - } - - .. code-block:: java - - package com.template.state; - - import com.google.common.collect.ImmutableList; - import com.template.contract.TemplateContract; - import net.corda.core.contracts.ContractState; - import net.corda.core.identity.AbstractParty; - import net.corda.core.identity.Party; - - import java.util.List; - - public class IOUState implements ContractState { - private final int value; - private final Party lender; - private final Party borrower; - private final TemplateContract contract = new TemplateContract(); - - public IOUState(int value, Party lender, Party borrower) { - this.value = value; - this.lender = lender; - this.borrower = borrower; - } - - public int getValue() { - return value; - } - - public Party getLender() { - return lender; - } - - public Party getBorrower() { - return borrower; - } - - @Override - // TODO: Once we've defined IOUContract, come back and update this. - public TemplateContract getContract() { - return contract; - } - - @Override - public List getParticipants() { - return ImmutableList.of(lender, borrower); - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateState.java`` to ``IOUState.java``. @@ -141,9 +88,6 @@ We've made the following changes: * Actions such as changing a state's contract or notary will require approval from all the ``participants`` -We've left ``IOUState``'s contract as ``TemplateContract`` for now. We'll update this once we've defined the -``IOUContract``. - Progress so far --------------- We've defined an ``IOUState`` that can be used to represent IOUs as shared facts on the ledger. As we've seen, states in diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index acb131dc6f..d9358d60eb 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -22,7 +22,7 @@ Downloading the template ------------------------ Open a terminal window in the directory where you want to download the CorDapp template, and run the following commands: -.. code-block:: text +.. code-block:: bash # Clone the template from GitHub: git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java @@ -31,12 +31,6 @@ Open a terminal window in the directory where you want to download the CorDapp t git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin - # Retrieve a list of the stable Milestone branches using: - git branch -a --list *release-M* - - # Check out the Milestone branch with the latest version number: - git checkout release-M[*version number*] ; git pull - Template structure ------------------ We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. To @@ -48,18 +42,23 @@ implement our IOU CorDapp in Java, we'll need to modify three files. For Kotlin, .. code-block:: java // 1. The state - src/main/java/com/template/state/TemplateState.java + src/main/java/com/template/TemplateState.java // 2. The contract - src/main/java/com/template/contract/TemplateContract.java + src/main/java/com/template/TemplateContract.java // 3. The flow - src/main/java/com/template/flow/TemplateFlow.java + src/main/java/com/template/TemplateFlow.java .. code-block:: kotlin src/main/kotlin/com/template/App.kt +To prevent build errors later on, you should delete the following file: + +* Java: ``src/test/java/com/template/FlowTests.java`` +* Kotlin: ``src/test/kotlin/com/template/FlowTests.kt`` + Progress so far --------------- We now have a template that we can build upon to define our IOU CorDapp. diff --git a/docs/source/index.rst b/docs/source/index.rst index fab9e1ec77..75f9733f4e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,6 @@ Welcome to Corda ! ================== -.. warningX:: This build of the docs is from the "|version|" branch, not a milestone release. It may not reflect the - current state of the code. `Read the docs for milestone release M12.1 `_. - `Corda `_ is a blockchain-inspired open source distributed ledger platform. If you’d like a quick introduction to distributed ledgers and how Corda is different, then watch this short video: diff --git a/docs/source/json.rst b/docs/source/json.rst index 126c9f5791..773ad9bccc 100644 --- a/docs/source/json.rst +++ b/docs/source/json.rst @@ -19,8 +19,8 @@ connection to the node (see ":doc:`clientrpc`") then your JSON mapper can resolv The API is described in detail here: -* `Kotlin API docs `_ -* `JavaDoc `_ +* `Kotlin API docs `_ +* `JavaDoc `_ .. container:: codeset diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/key-concepts-contract-constraints.rst new file mode 100644 index 0000000000..edd4c6694d --- /dev/null +++ b/docs/source/key-concepts-contract-constraints.rst @@ -0,0 +1,149 @@ +Contract Constraints +==================== + +A basic understanding of contract key concepts, which can be found :doc:`here `, +is required reading for this page. + +Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be +valid, the verify() function associated with each state must run successfully. However, for this to be secure, it is +not sufficient to specify the verify() function by name as there may exist multiple different implementations with the +same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer to +constrain which verify() functions out of the universe of implementations can be used. +(ie the universe is everything that matches the signature and contract constraints restricts this universe to a subset.) + +A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases +include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be +specified when constructing a transaction; if unspecified, an automatic constraint is used. + +A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party +constructs a ``TransactionState`` without specifying the constraint parameter a default value +(``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific +``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that +``TransactionState``. This automatic resolution occurs when a ``TransactionBuilder`` is converted to a +``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a transaction. + +It is possible to specify the constraint explicitly with any other class that implements the ``AttachmentConstraint`` +interface. To specify a hash manually the ``HashAttachmentConstraint`` can be used and to not provide any constraint +the ``AlwaysAcceptAttachmentConstraint`` can be used - though this is intended for testing only. An example below +shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow; + +.. sourcecode:: java + + // Constructing a transaction with a custom hash constraint on a state + TransactionBuilder tx = new TransactionBuilder() + + Party notaryParty = ... // a notary party + DummyState contractState = new DummyState() + SecureHash myAttachmentsHash = serviceHub.cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID) + TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentsHash)) + + tx.addOutputState(transactionState) + WireTransaction wtx = tx.toWireTransaction(serviceHub) // This is where an automatic constraint would be resolved + LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub) + ltx.verify() // Verifies both the attachment constraints and contracts + + +This mechanism exists both for integrity and security reasons. It is important not to verify against the wrong contract, +which could happen if the wrong version of the contract is attached. More importantly when resolving transaction chains +there will, in a future release, be attachments loaded from the network into the attachment sandbox that are used +to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification will +not be tamperable by providing a fake contract. + +CorDapps as attachments +----------------------- + +CorDapp JARs (:doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract`` +interface are automatically loaded into the ``AttachmentStorage`` of a node at startup. + +After CorDapps are loaded into the attachment store the node creates a link between contract classes and the +attachment that they were loaded from. This makes it possible to find the attachment for any given contract. +This is how the automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying +the constraints and contracts, attachments are associated with their respective contracts. + +Implementations +--------------- + +There are three implementations of ``AttachmentConstraints`` with more planned in the future. + +``AlwaysAcceptAttachmentConstraint``: Any attachment (except a missing one) will satisfy this constraint. + +``AutomaticHashConstraint``: This will be resolved to a ``HashAttachmentConstraint`` when a ``TransactionBuilder`` is +converted to a ``WireTransaction``. The ``HashAttachmentConstraint`` will include the attachment hash of the CorDapp +that contains the ``ContractState`` on the ``TransactionState.contract`` field. + +``HashAttachmentConstraint``: Will require that the hash of the attachment containing the contract matches the hash +stored in the constraint. + +We plan to add a future ``AttachmentConstraint`` that will only be satisfied by the presence of signatures on the +attachment JAR. This allows for trusting of attachments from trusted entities. + +Limitations +----------- + +An ``AttachmentConstraint`` is verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called +it is provided only the relevant attachment by the transaction that is verifying it. + +Testing +------- + +Since all tests involving transactions now require attachments it is also required to load the correct attachments +for tests. Unit test environments in JVM ecosystems tend to use class directories rather than JARs, and so CorDapp JARs +typically aren't built for testing. Requiring this would add significant complexity to the build systems of Corda +and CorDapps, so the test suite has a set of convenient functions to generate CorDapps from package names or +to specify JAR URLs in the case that the CorDapp(s) involved in testing already exist. + +MockNetwork/MockNode +******************** + +The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to make a call +to ``setCordappPackages`` before the MockNetwork/Node are created and then ``unsetCordappPackages`` after the test +has finished. These calls will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files +within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the +``CordappLoader``. An example of this usage would be: + +.. sourcecode:: java + + class SomeTestClass { + MockNetwork network = null + + @Before + void setup() { + // The ordering of the two below lines is important - if the MockNetwork is created before the nodes and network + // are created the CorDapps will not be loaded into the MockNodes correctly. + setCordappPackages(Arrays.asList("com.domain.cordapp")) + network = new MockNetwork() + } + + @After + void teardown() { + // This must be called at the end otherwise the global state set by setCordappPackages may leak into future + // tests in the same test runner environment. + unsetCordappPackages() + } + + ... // Your tests go here + } + +MockServices +************ + +If your test uses a ``MockServices`` directly you can instantiate it using a constructor that takes a list of packages +to use as CorDapps using the ``cordappPackages`` parameter. + +.. sourcecode:: java + + MockServices mockServices = new MockServices(Arrays.asList("com.domain.cordapp")) + +Driver +****** + +The driver takes a parameter called ``extraCordappPackagesToScan`` which is a list of packages to use as CorDapps. + +.. sourcecode:: java + + driver(new DriverParameters().setExtraCordappPackagesToScan(Arrays.asList("com.domain.cordapp"))) ... + +Full Nodes +********** + +When testing against full nodes simply place your CorDapp into the cordapps directory of the node. diff --git a/docs/source/key-concepts-identity.rst b/docs/source/key-concepts-identity.rst new file mode 100644 index 0000000000..7f1f9bbcc2 --- /dev/null +++ b/docs/source/key-concepts-identity.rst @@ -0,0 +1,78 @@ +Identity +======== + +.. topic:: Summary + + * *Identities in Corda can represent legal identities or service identities* + * *Identities are attested to by X.509 certificate signed by the Doorman or a well known identity* + * *Well known identities are published in the network map* + * *Confidential identities are only shared on a need to know basis* + +Identities in Corda can represent: + +* Legal identity of an organisation +* Service identity of a network service + +Legal identities are used for parties in a transaction, such as the owner of a cash state. Service identities are used +for those providing transaction-related services, such as notary, or oracle. Service identities are distinct to legal +identities so that distributed services can exist on nodes owned by different organisations. Such distributed service +identities are based on ``CompositeKeys``, which describe the valid sets of signers for a signature from the service. +See :doc:`api-core-types` for more technical detail on composite keys. + +Identities are either well known or confidential, depending on whether their X.509 certificate (and corresponding +certificate path to a trusted root certificate) is published: + +* Well known identities are the generally identifiable public key of a legal entity or service, which makes them + ill-suited to transactions where confidentiality of participants is required. This certificate is published in the + network map service for anyone to access. +* Confidential identities are only published to those who are involved in transactions with the identity. The public + key may be exposed to third parties (for example to the notary service), but distribution of the name and X.509 + certificate is limited. + +Although there are several elements to the Corda transaction privacy model, including ensuring that transactions are +only shared with those who need to see them, and planned use of Intel SGX, it is important to provide defense in depth against +privacy breaches. Confidential identities are used to ensure that even if a third party gets access to an unencrypted +transaction, they cannot identify the participants without additional information. + +Name +---- + +Identity names are X.500 distinguished names with Corda-specific constraints applied. In order to be compatible with +other implementations (particularly TLS implementations), we constrain the allowed X.500 attribute types to a subset of +the minimum supported set for X.509 certificates (specified in RFC 3280), plus the locality attribute: + +* organization (O) +* state (ST) +* locality (L) +* country (C) +* organizational-unit (OU) +* common name (CN) - used only for service identities + +The organisation, locality and country attributes are required, while state, organisational-unit and common name are +optional. Attributes cannot be be present more than once in the name. The "country" code is strictly restricted to valid +ISO 3166-1 two letter codes. + +Certificates +------------ + +Nodes must be able to verify the identity of the owner of a public key, which is achieved using X.509 certificates. +When first run a node generates a key pair and submits a certificate signing request to the network Doorman service +(see :doc:`permissioning`). +The Doorman service applies appropriate identity checks then issues a certificate to the node, which is used as the +node certificate authority (CA). From this initial CA certificate the node automatically creates and signs two further +certificates, a TLS certificate and a signing certificate for the node's well known identity. Finally the node +builds a node info record containing its address and well known identity, and registers it with the network map service. + +From the signing certificate the organisation can create both well known and confidential identities. Use-cases for +well known identities include clusters of nodes representing a single identity for redundancy purposes, or creating +identities for organisational units. + +It is up to organisations to decide which identities they wish to publish in the network map service, making them +well known, and which they wish to keep as confidential identities for privacy reasons (typically to avoid exposing +business sensitive details of transactions). In some cases nodes may also use private network map services in addition +to the main network map service, for operational reasons. Identities registered with such network maps must be +considered well known, and it is never appropriate to store confidential identities in a central directory without +controls applied at the record level to ensure only those who require access to an identity can retrieve its +certificate. + +.. TODO: Revisit once design & use cases of private maps is further fleshed out \ No newline at end of file diff --git a/docs/source/key-concepts-node.rst b/docs/source/key-concepts-node.rst index aa6517249a..624f5308af 100644 --- a/docs/source/key-concepts-node.rst +++ b/docs/source/key-concepts-node.rst @@ -36,7 +36,7 @@ The core elements of the architecture are: * A network interface for interacting with other nodes * An RPC interface for interacting with the node's owner * A service hub for allowing the node's flows to call upon the node's other services -* A plugin registry for extending the node by installing CorDapps +* A cordapp interface and provider for extending the node by installing CorDapps Persistence layer ----------------- @@ -55,7 +55,7 @@ node's owner does not interact with other network nodes directly. RPC interface ------------- The node's owner interacts with the node via remote procedure calls (RPC). The key RPC operations the node exposes -are documented in :doc:``api-rpc``. +are documented in :doc:`api-rpc`. The service hub --------------- @@ -68,11 +68,11 @@ updates. The key services provided are: * Information about the node itself * The current time, as tracked by the node -The plugin registry -------------------- -The plugin registry is where new CorDapps are installed to extend the behavior of the node. +The CorDapp provider +-------------------- +The CorDapp provider is where new CorDapps are installed to extend the behavior of the node. -The node also has several plugins installed by default to handle common tasks such as: +The node also has several CorDapps installed by default to handle common tasks such as: * Retrieving transactions and attachments from counterparties * Upgrading contracts diff --git a/docs/source/key-concepts.rst b/docs/source/key-concepts.rst index fdf2b0231c..a252e1a853 100644 --- a/docs/source/key-concepts.rst +++ b/docs/source/key-concepts.rst @@ -13,8 +13,10 @@ This section should be read in order: key-concepts-ecosystem key-concepts-ledger + key-concepts-identity key-concepts-states key-concepts-contracts + key-concepts-contract-constraints key-concepts-transactions key-concepts-flows key-concepts-consensus diff --git a/docs/source/node-explorer.rst b/docs/source/node-explorer.rst index 4fc1a53068..48c0a96088 100644 --- a/docs/source/node-explorer.rst +++ b/docs/source/node-explorer.rst @@ -73,6 +73,9 @@ Explorer login credentials to the Issuer nodes are defaulted to ``manager`` and Explorer login credentials to the Participants nodes are defaulted to ``user1`` and ``test``. Please note you are not allowed to login to the notary. +.. note:: When you start the nodes in Windows using the command prompt, they might not be killed when you close the + window or terminate the task. If that happens you need to manually terminate the Java processes running the nodes. + .. note:: Alternatively, you may start the demo nodes from within IntelliJ using either of the run configurations ``Explorer - demo nodes`` or ``Explorer - demo nodes (simulation)`` diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index 8e25eb6115..b63dbfc942 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -209,10 +209,8 @@ NodeAttachmentService The ``NodeAttachmentService`` provides an implementation of the ``AttachmentStorage`` interface exposed on the ``ServiceHub`` allowing transactions to add documents, copies of the contract code and binary -data to transactions. The data is persisted to the local file system -inside the attachments subfolder of the node workspace. The service is -also interfaced to by the web server, which allows files to be uploaded -via an HTTP post request. +data to transactions. The service is also interfaced to by the web server, +which allows files to be uploaded via an HTTP post request. Flow framework and event scheduling services -------------------------------------------- @@ -295,12 +293,8 @@ NotaryService (SimpleNotaryService, ValidatingNotaryService, RaftValidatingNotar The ``NotaryService`` is an abstract base class for the various concrete implementations of the Notary server flow. By default, a node does -not run any ``NotaryService`` server component. However, the appropriate -implementation service is automatically started if the relevant -``ServiceType`` id is included in the node's -``extraAdvertisedServiceIds`` configuration property. The node will then -advertise itself as a Notary via the ``NetworkMapService`` and may then -participate in controlling state uniqueness when contacted by nodes +not run any ``NotaryService`` server component. For that you need to specify the ``notary`` config. +The node may then participate in controlling state uniqueness when contacted by nodes using the ``NotaryFlow.Client`` ``subFlow``. The ``SimpleNotaryService`` only offers protection against double spend, but does no further verification. The ``ValidatingNotaryService`` checks @@ -324,7 +318,7 @@ does this by tracking update notifications from the ``TransactionStorage`` service and processing relevant updates to delete consumed states and insert new states. The resulting update is then persisted to the database. The ``VaultService`` then exposes query and -event notification APIs to flows and CorDapp plugins to allow them +event notification APIs to flows and CorDapp services to allow them to respond to updates, or query for states meeting various conditions to begin the formation of new transactions consuming them. The equivalent services are also forwarded to RPC clients, so that they may show diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index c8c6de060a..802e9f33c0 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -17,25 +17,25 @@ Introduction to oracles ----------------------- Oracles are a key concept in the block chain/decentralised ledger space. They can be essential for many kinds of -application, because we often wish to condition a transaction on some fact being true or false, but the ledger itself +application, because we often wish to condition the validity of a transaction on some fact being true or false, but the ledger itself has a design that is essentially functional: all transactions are *pure* and *immutable*. Phrased another way, a -smart contract cannot perform any input/output or depend on any state outside of the transaction itself. There is no -way to download a web page or interact with the user, in a smart contract. It must be this way because everyone must -be able to independently check a transaction and arrive at an identical conclusion for the ledger to maintain its +contract cannot perform any input/output or depend on any state outside of the transaction itself. For example, there is no +way to download a web page or interact with the user from within a contract. It must be this way because everyone must +be able to independently check a transaction and arrive at an identical conclusion regarding its validity for the ledger to maintain its integrity: if a transaction could evaluate to "valid" on one computer and then "invalid" a few minutes later on a different computer, the entire shared ledger concept wouldn't work. -But it is often essential that transactions do depend on data from the outside world, for example, verifying that an +But transaction validity does often depend on data from the outside world - verifying that an interest rate swap is paying out correctly may require data on interest rates, verifying that a loan has reached maturity requires knowledge about the current time, knowing which side of a bet receives the payment may require -arbitrary facts about the real world (e.g. the bankruptcy or solvency of a company or country) ... and so on. +arbitrary facts about the real world (e.g. the bankruptcy or solvency of a company or country), and so on. We can solve this problem by introducing services that create digitally signed data structures which assert facts. These structures can then be used as an input to a transaction and distributed with the transaction data itself. Because the statements are themselves immutable and signed, it is impossible for an oracle to change its mind later and invalidate transactions that were previously found to be valid. In contrast, consider what would happen if a contract could do an HTTP request: it's possible that an answer would change after being downloaded, resulting in loss of -consensus (breaks). +consensus. The two basic approaches ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -78,7 +78,7 @@ Asserting continuously varying data Let's look at the interest rates oracle that can be found in the ``NodeInterestRates`` file. This is an example of an oracle that uses a command because the current interest rate fix is a constantly changing fact. -The obvious way to implement such a service is like this: +The obvious way to implement such a service is this: 1. The creator of the transaction that depends on the interest rate sends it to the oracle. 2. The oracle inserts a command with the rate and signs the transaction. @@ -101,40 +101,38 @@ class that binds it to the network layer. Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting types: +.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + .. sourcecode:: kotlin - /** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */ - data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) - - /** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ - data class Fix(val of: FixOf, val value: BigDecimal) : CommandData - class Oracle { - fun query(queries: List, deadline: Instant): List + fun query(queries: List): List - fun sign(ftx: FilteredTransaction, txId: SecureHash): DigitalSignature.WithKey + fun sign(ftx: FilteredTransaction): TransactionSignature } -Because the fix contains a timestamp (the ``forDay`` field), that identifies the version of the data being requested, -there can be an arbitrary delay between a fix being requested via ``query`` and the signature being requested via ``sign`` -as the Oracle can know which, potentially historical, value it is being asked to sign for. This is an important -technique for continously varying data. - -The ``query`` method takes a deadline, which is a point in time the requester is willing to wait until for the necessary -data to be available. Not every oracle will need this. This can be useful where data is expected to be available on a -particular schedule and we use scheduling functionality to automatically launch the processing associated with it. -We can schedule for the expected announcement (or publish) time and give a suitable deadline at which the lack of the -information being available and the delay to processing becomes significant and may need to be escalated. +The fix contains a timestamp (the ``forDay`` field) that identifies the version of the data being requested. Since +there can be an arbitrary delay between a fix being requested via ``query`` and the signature being requested via +``sign``, this timestamp allows the Oracle to know which, potentially historical, value it is being asked to sign for. This is an +important technique for continuously varying data. Hiding transaction data from the oracle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Because the transaction is sent to the oracle for signing, ordinarily the oracle would be able to see the entire contents of that transaction including the inputs, output contract states and all the commands, not just the one (in this case) -relevant command. This is an obvious privacy leak for the other participants. We currently solve this with -``FilteredTransaction``-s and the use of Merkle Trees. These reveal only the necessary parts of the transaction to the -oracle but still allow it to sign it by providing the Merkle hashes for the remaining parts. See :doc:`merkle-trees` for -more details. +relevant command. This is an obvious privacy leak for the other participants. We currently solve this using a +``FilteredTransaction``, which implements a Merkle Tree. These reveal only the necessary parts of the transaction to the +oracle but still allow it to sign it by providing the Merkle hashes for the remaining parts. See :doc:`key-concepts-oracles` +for more details. Pay-per-play oracles ~~~~~~~~~~~~~~~~~~~~ @@ -143,7 +141,7 @@ Because the signature covers the transaction, and transactions may end up being is independently checkable. However, this approach can still be useful when the data itself costs money, because the act of issuing the signature in the first place can be charged for (e.g. by requiring the submission of a fresh ``Cash.State`` that has been re-assigned to a key owned by the oracle service). Because the signature covers the -*transaction* and not only the *fact*, this allows for a kind of weak pseudo-DRM over data feeds. Whilst a smart +*transaction* and not only the *fact*, this allows for a kind of weak pseudo-DRM over data feeds. Whilst a contract could in theory include a transaction parsing and signature checking library, writing a contract in this way would be conclusive evidence of intent to disobey the rules of the service (*res ipsa loquitur*). In an environment where parties are legally identifiable, usage of such a contract would by itself be sufficient to trigger some sort of @@ -156,24 +154,12 @@ Implement the core classes ~~~~~~~~~~~~~~~~~~~~~~~~~~ The key is to implement your oracle in a similar way to the ``NodeInterestRates.Oracle`` outline we gave above with -both ``query`` and ``sign`` methods. Typically you would want one class that encapsulates the parameters to the ``query`` -method (``FixOf`` above), and a ``CommandData`` implementation (``Fix`` above) that encapsulates both an instance of +both a ``query`` and a ``sign`` method. Typically you would want one class that encapsulates the parameters to the ``query`` +method (``FixOf``, above), and a ``CommandData`` implementation (``Fix``, above) that encapsulates both an instance of that parameter class and an instance of whatever the result of the ``query`` is (``BigDecimal`` above). -The ``NodeInterestRates.Oracle`` allows querying for multiple ``Fix``-es but that is not necessary and is -provided for the convenience of callers who might need multiple and can do it all in one query request. Likewise -the *deadline* functionality is optional and can be avoided initially. - -Let's see what parameters we pass to the constructor of this oracle. - -.. sourcecode:: kotlin - - class Oracle(val identity: Party, private val signingKey: PublicKey, val clock: Clock) = TODO() - -Here we see the oracle needs to have its own identity, so it can check which transaction commands it is expected to -sign for, and also needs the PublicKey portion of its signing key. Later this PublicKey will be passed to the KeyManagementService -to identify the internal PrivateKey used for transaction signing. -The clock is used for the deadline functionality which we will not discuss further here. +The ``NodeInterestRates.Oracle`` allows querying for multiple ``Fix`` objects but that is not necessary and is +provided for the convenience of callers who need multiple fixes and want to be able to do it all in one query request. Assuming you have a data source and can query it, it should be very easy to implement your ``query`` method and the parameter and ``CommandData`` classes. @@ -184,16 +170,17 @@ Let's see how the ``sign`` method for ``NodeInterestRates.Oracle`` is written: :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 8 Here we can see that there are several steps: 1. Ensure that the transaction we have been sent is indeed valid and passes verification, even though we cannot see all - of it. + of it 2. Check that we only received commands as expected, and each of those commands expects us to sign for them and is of - the expected type (``Fix`` here). + the expected type (``Fix`` here) 3. Iterate over each of the commands we identified in the last step and check that the data they represent matches exactly our data source. The final step, assuming we have got this far, is to generate a signature for the - transaction and return it. + transaction and return it Binding to the network ~~~~~~~~~~~~~~~~~~~~~~ @@ -209,6 +196,7 @@ done: :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 + :dedent: 4 The Corda node scans for any class with this annotation and initialises them. The only requirement is that the class provide a constructor with a single parameter of type ``ServiceHub``. @@ -217,9 +205,10 @@ a constructor with a single parameter of type ``ServiceHub``. :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 These two flows leverage the oracle to provide the querying and signing operations. They get reference to the oracle, -which will have already been initialised by the node, using ``ServiceHub.cordappProvider``. Both flows are annotated with +which will have already been initialised by the node, using ``ServiceHub.cordaService``. Both flows are annotated with ``@InitiatedBy``. This tells the node which initiating flow (which are discussed in the next section) they are meant to be executed with. @@ -227,17 +216,18 @@ Providing sub-flows for querying and signing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We mentioned the client sub-flow briefly above. They are the mechanism that clients, in the form of other flows, will -interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at +use to interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at those for ``NodeInterestRates.Oracle``. .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 4 You'll note that the ``FixSignFlow`` requires a ``FilterTransaction`` instance which includes only ``Fix`` commands. -You can find a further explanation of this in :doc:`merkle-trees`. Below you will see how to build such transaction with -hidden fields. +You can find a further explanation of this in :doc:`key-concepts-oracles`. Below you will see how to build such a +transaction with hidden fields. .. _filtering_ref: @@ -252,16 +242,16 @@ called ``RatesFixFlow``. Here's the ``call`` method of that flow. :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 As you can see, this: -1. Queries the oracle for the fact using the client sub-flow for querying from above. -2. Does some quick validation. -3. Adds the command to the transaction containing the fact to be signed for by the oracle. -4. Calls an extension point that allows clients to generate output states based on the fact from the oracle. -5. Builds filtered transaction based on filtering function extended from ``RatesFixFlow``. -6. Requests the signature from the oracle using the client sub-flow for signing from above. -7. Adds the signature returned from the oracle. +1. Queries the oracle for the fact using the client sub-flow for querying defined above +2. Does some quick validation +3. Adds the command to the transaction containing the fact to be signed for by the oracle +4. Calls an extension point that allows clients to generate output states based on the fact from the oracle +5. Builds filtered transaction based on filtering function extended from ``RatesFixFlow`` +6. Requests the signature from the oracle using the client sub-flow for signing from above Here's an example of it in action from ``FixingFlow.Fixer``. @@ -269,6 +259,7 @@ Here's an example of it in action from ``FixingFlow.Fixer``. :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 4 .. note:: When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin), @@ -279,6 +270,6 @@ Testing ------- When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode`` -is a simplified node suitable for tests. One feature that isn't available (and which is not suitable in unit testing +is a simplified node suitable for tests. One feature that isn't available (and which is not suitable for unit testing anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node. \ No newline at end of file diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 4da3939152..72baf8cd6f 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -5,17 +5,112 @@ Here are release notes for each snapshot release from M9 onwards. Unreleased ---------- + Release 1.0 ----------- +Corda 1.0 is finally here! -* Flow communications API has been redesigned around session based communication. +This critical step in the Corda journey enables the developer community, clients, and partners to build on Corda with confidence. +Corda 1.0 is the first released version to provide API stability for Corda application (CorDapp) developers. +Corda applications will continue to work against this API with each subsequent release of Corda. The public API for Corda +will only evolve to include new features. -* Merged handling of well known and confidential identities in the identity service. +As of Corda 1.0, the following modules export public APIs for which we guarantee to maintain backwards compatibility, +unless an incompatible change is required for security reasons: -* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. + * **core**: + Contains the bulk of the APIs to be used for building CorDapps: contracts, transactions, flows, identity, node services, + cryptographic libraries, and general utility functions. -* Remove the legacy web front end from the SIMM demo. This was a very early sample, and does not reflect the quality of - current Corda code. It may be replaced with a new front end based on a more recent version of AngularJS in a later release. + * **client-rpc**: + An RPC client interface to Corda, for use by both UI facing clients and integration with external systems. + + * **client-jackson**: + Utilities and serialisers for working with JSON representations of basic types. + +Our extensive testing frameworks will continue to evolve alongside future Corda APIs. As part of our commitment to ease of use and modularity +we have introduced a new test node driver module to encapsulate all test functionality in support of building standalone node integration +tests using our DSL driver. + +Please read :doc:`corda-api` for complete details. + +.. note:: it may be necessary to recompile applications against future versions of the API until we begin offering + `ABI (Application Binary Interface) `_ stability as well. + We plan to do this soon after this release of Corda. + +Significant changes implemented in reaching Corda API stability include: + +* **Flow framework**: + The Flow framework communications API has been redesigned around session based communication with the introduction of a new + ``FlowSession`` to encapsulate the counterparty information associated with a flow. + All shipped Corda flows have been upgraded to use the new `FlowSession`. Please read :doc:`api-flows` for complete details. + +* **Complete API cleanup**: + Across the board, all our public interfaces have been thoroughly revised and updated to ensure a productive and intuitive developer experience. + Methods and flow naming conventions have been aligned with their semantic use to ease the understanding of CorDapps. + In addition, we provide ever more powerful re-usable flows (such as `CollectSignaturesFlow`) to minimize the boiler-plate code developers need to write. + +* **Simplified annotation driven scanning**: + CorDapp configuration has been made simpler through the removal of explicit configuration items in favour of annotations + and classpath scanning. As an example, we have now completely removed the `CordaPluginRegistry` configuration. + Contract definitions are no longer required to explicitly define a legal contract reference hash. In their place an + optional `LegalProseReference` annotation to specify a URI is used. + +* **Java usability**: + All code has been updated to enable simple access to static API parameters. Developers no longer need to + call getter methods, and can reference static API variables directly. + +In addition to API stability this release encompasses a number of major functional improvements, including: + +* **Contract constraints**: + Provides a means with which to enforce a specific implementation of a State's verify method during transaction verification. + When loading an attachment via the attachment classloader, constraints of a transaction state are checked against the + list of attachment hashes provided, and the attachment is rejected if the constraints are not matched. + +* **Signature Metadata support**: + Signers now have the ability to add metadata to their digital signatures. Whereas previously a user could only sign the Merkle root of a + transaction, it is now possible for extra information to be attached to a signature, such as a platform version + and the signature-scheme used. + + .. image:: resources/signatureMetadata.png + +* **Backwards compatibility and improvements to core transaction data structures**: + A new Merkle tree model has been introduced that utilises sub-Merkle trees per component type. Components of the + same type, such as inputs or commands, are grouped together and form their own Merkle tree. Then, the roots of + each group are used as leaves in the top-level Merkle tree. This model enables backwards compatibility, in the + sense that if new component types are added in the future, old clients will still be able to compute the Merkle root + and relay transactions even if they cannot read (deserialise) the new component types. Due to the above, + `FilterTransaction` has been made simpler with a structure closer to `WireTransaction`. This has the effect of making the API + more user friendly and intuitive for both filtered and unfiltered transactions. + +* **Enhanced component privacy**: + Corda 1.0 is equipped with a scalable component visibility design based on the above sophisticated + sub-tree model and the introduction of nonces per component. Roughly, an initial base-nonce, the "privacy-salt", + is used to deterministically generate nonces based on the path of each component in the tree. Because each component + is accompanied by a nonce, we protect against brute force attacks, even against low-entropy components. In addition, + a new privacy feature is provided that allows non-validating notaries to ensure they see all inputs and if there was a + `TimeWindow` in the original transaction. Due to the above, a malicious user cannot selectively hide one or more + input states from the notary that would enable her to bypass the double-spending check. The aforementioned + functionality could also be applied to Oracles so as to ensure all of the commands are visible to them. + + .. image:: resources/subTreesPrivacy.png + +* **Full support for confidential identities**: + This includes rework and improvements to the identity service to handle both `well known` and `confidential` identities. + This work ships in an experimental module in Corda 1.0, called `confidential-identities`. API stabilisation of confidential + identities will occur as we make the integration of this privacy feature into applications even easier for developers. + +* **Re-designed network map service**: + The foundations for a completely redesigned network map service have been implemented to enable future increased network + scalability and redundancy, support for multiple notaries, and administration of network compatibility zones and business networks. + +Finally, please note that the 1.0 release has not yet been security audited. + +We have provided a comprehensive :doc:`upgrade-notes` to ease the transition of migrating CorDapps to Corda 1.0 + +Upgrading to this release is strongly recommended, and you will be safe in the knowledge that core APIs will no longer break. + +Thank you to all contributors for this release! Milestone 14 ------------ diff --git a/docs/source/release-process-index.rst b/docs/source/release-process-index.rst index f1c9ea2037..eb81e833f0 100644 --- a/docs/source/release-process-index.rst +++ b/docs/source/release-process-index.rst @@ -6,4 +6,6 @@ Release process release-notes changelog + upgrade-notes + codestyle testing \ No newline at end of file diff --git a/docs/source/resources/node-architecture.png b/docs/source/resources/node-architecture.png index cfc8b22cf7..7004cda149 100644 Binary files a/docs/source/resources/node-architecture.png and b/docs/source/resources/node-architecture.png differ diff --git a/docs/source/resources/signatureMetadata.png b/docs/source/resources/signatureMetadata.png new file mode 100644 index 0000000000..61f0d11e17 Binary files /dev/null and b/docs/source/resources/signatureMetadata.png differ diff --git a/docs/source/resources/subTreesPrivacy.png b/docs/source/resources/subTreesPrivacy.png new file mode 100644 index 0000000000..3a7dae6110 Binary files /dev/null and b/docs/source/resources/subTreesPrivacy.png differ diff --git a/docs/source/resources/tutorial-state.png b/docs/source/resources/tutorial-state.png index 11b0a0d385..d621277ba9 100644 Binary files a/docs/source/resources/tutorial-state.png and b/docs/source/resources/tutorial-state.png differ diff --git a/docs/source/running-a-node.rst b/docs/source/running-a-node.rst index b87378bd29..4373e93a3e 100644 --- a/docs/source/running-a-node.rst +++ b/docs/source/running-a-node.rst @@ -10,7 +10,7 @@ already installed. You run each node by navigating to ```` in a termin java -jar corda.jar -.. warning:: If your working directory is not ```` your plugins and configuration will not be used. +.. warning:: If your working directory is not ```` your cordapps and configuration will not be used. The configuration file and workspace paths can be overridden on the command line. For example: diff --git a/docs/source/running-a-notary.rst b/docs/source/running-a-notary.rst index 81ade65043..57ce2d0964 100644 --- a/docs/source/running-a-notary.rst +++ b/docs/source/running-a-notary.rst @@ -6,24 +6,25 @@ At present we have several prototype notary implementations: 1. ``SimpleNotaryService`` (single node) -- commits the provided transaction input states without any validation. 2. ``ValidatingNotaryService`` (single node) -- retrieves and validates the whole transaction history (including the given transaction) before committing. -3. ``RaftValidatingNotaryService`` (distributed) -- functionally equivalent to ``ValidatingNotaryService``, but stores +3. ``RaftNonValidatingNotaryService`` (distributed) -- functionally equivalent to ``SimpleNotaryService``, but stores the committed states in a distributed collection replicated and persisted in a Raft cluster. For the consensus layer - we are using the `Copycat `_ framework. + we are using the `Copycat `_ framework +4. ``RaftValidatingNotaryService`` (distributed) -- as above, but performs validation on the transactions received -To have a node run a notary service, you need to set appropriate configuration values before starting it +To have a node run a notary service, you need to set appropriate ``notary`` configuration before starting it (see :doc:`corda-configuration-file` for reference). -For ``SimpleNotaryService``, simply add the following service id to the list of advertised services: +For ``SimpleNotaryService`` the config is simply: .. parsed-literal:: - extraAdvertisedServiceIds : [ "net.corda.notary.simple" ] + notary : { validating : false } For ``ValidatingNotaryService``, it is: .. parsed-literal:: - extraAdvertisedServiceIds : [ "net.corda.notary.validating" ] + notary : { validating : true } -Setting up a ``RaftValidatingNotaryService`` is currently slightly more involved and is not recommended for prototyping -purposes. There is work in progress to simplify it. To see it in action, however, you can try out the :ref:`notary-demo`. +Setting up a Raft notary is currently slightly more involved and is not recommended for prototyping purposes. There is +work in progress to simplify it. To see it in action, however, you can try out the :ref:`notary-demo`. diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index cd19580688..11ebb794c2 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -35,7 +35,7 @@ To run from the command line in Unix: 2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes 3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively. 4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -45,7 +45,7 @@ To run from the command line in Windows: 2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes 3. Run ``gradlew samples:trader-demo:runBank`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node 4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -112,8 +112,11 @@ Notary demo This demo shows a party getting transactions notarised by either a single-node or a distributed notary service. All versions of the demo start two counterparty nodes. One of the counterparties will generate transactions that transfer a self-issued asset to the other party and submit them for notarisation. -The `Raft `_ version of the demo will start three distributed notary nodes. -The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. + +* The `Raft `_ version of the demo will start three distributed notary nodes. +* The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. +* The Single version of the demo will start a single-node validating notary service. +* The Custom version of the demo will load and start a custom single-node notary service that is defined the demo CorDapp. The output will display a list of notarised transaction IDs and corresponding signer public keys. In the Raft distributed notary, every node in the cluster can service client requests, and one signature is sufficient to satisfy the notary composite key requirement. @@ -122,9 +125,9 @@ You will notice that successive transactions get signed by different members of To run the Raft version of the demo from the command line in Unix: -1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). +1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create node directories for all versions of the demo, + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 2. Run ``./samples/notary-demo/build/nodes/nodesRaft/runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests @@ -133,8 +136,8 @@ To run the Raft version of the demo from the command line in Unix: To run from the command line in Windows: 1. Run ``gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 2. Run ``samples\notary-demo\build\nodes\nodesRaft\runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests @@ -142,20 +145,20 @@ To run from the command line in Windows: To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``. +For the custom notary service use ``nodesCustom`. -Notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. +Distributed notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores by using the H2 web console: - Firstly, download `H2 web console `_ (download the "platform-independent zip"), - and start it using a script in the extracted folder: ``h2/bin/h2.sh`` (or ``h2\bin\h2`` for Windows) + and start it using a script in the extracted folder: ``sh h2/bin/h2.sh`` (or ``h2\bin\h2`` for Windows) - If you are uncertain as to which version of h2 to install or if you have connectivity issues, refer to ``build.gradle`` - located in the ``node`` directory and locate the compile step for ``com.h2database``. Use a client of the same - major version - even if still in beta. + located in the corda directory and locate ``h2_version``. Use a client of the same major version - even if still in beta. - The H2 web console should start up in a web browser tab. To connect we first need to obtain a JDBC connection string. - Each node outputs its connection string in the terminal window as it starts up. In a terminal window where a node is running, + Each node outputs its connection string in the terminal window as it starts up. In a terminal window where a **notary** node is running, look for the following string: ``Database connection url is : jdbc:h2:tcp://10.18.0.150:56736/node`` @@ -163,8 +166,8 @@ by using the H2 web console: You can use the string on the right to connect to the h2 database: just paste it into the `JDBC URL` field and click *Connect*. You will be presented with a web application that enumerates all the available tables and provides an interface for you to query them using SQL -- The committed states are stored in the ``NOTARY_COMMITTED_STATES`` table. Note that the raw data is not human-readable, - but we're only interested in the row count for this demo +- The committed states are stored in the ``NOTARY_COMMITTED_STATES`` table (for Raft) or ``NODE_BFT_SMART_NOTARY_COMMITTED_STATES`` (for BFT). + Note that in the Raft case the raw data is not human-readable, but we're only interested in the row count for this demo .. _bank-of-corda-demo: @@ -218,3 +221,217 @@ Using the following login details: See https://docs.corda.net/node-explorer.html for further details on usage. +.. _simm-demo: + +SIMM and Portfolio Demo - aka the Initial Margin Agreement Demo +--------------------------------------------------------------- + +Background and SIMM Introduction +******************************** + +This app is a demonstration of how Corda can be used for the real world requirement of initial margin calculation and +agreement; featuring the integration of complex and industry proven third party libraries into Corda nodes. + +SIMM is an acronym for "Standard Initial Margin Model". It is effectively the calculation of a "margin" that is paid +by one party to another when they agree a trade on certain types of transaction. + +The SIMM was introduced to standardise the calculation of how much margin counterparties charge each other on their +bilateral transactions. Before SIMM, each counterparty computed margins according to its own model and it was made it very + difficult to agree the exact margin with the counterparty that faces the same trade on the other side. + +To enact this, in September 2016, the ISDA committee - with full backing from various governing bodies - +`issued a ruling on what is known as the ISDA SIMM ™ model `_, +a way of fairly and consistently calculating this margin. Any parties wishing to trade a financial product that is +covered under this ruling would, independently, use this model and calculate their margin payment requirement, +agree it with their trading counterparty and then pay (or receive, depending on the results of this calculation) +this amount. In the case of disagreement that is not resolved in a timely fashion, this payment would increase +and so therefore it is in the parties' interest to reach agreement in as short as time frame as possible. + +To be more accurate, the SIMM calculation is not performed on just one trade - it is calculated on an aggregate of +intermediary values (which in this model are sensitivities to risk factors) from a portfolio of trades; therefore +the input to a SIMM is actually this data, not the individual trades themselves. + +Also note that implementations of the SIMM are actually protected and subject to license restrictions by ISDA +(this is due to the model itself being protected). We were fortunate enough to technically partner with +`OpenGamma `_ who allowed us to demonstrate the SIMM process using their proprietary model. +In the source code released, we have replaced their analytics engine with very simple stub functions that allow +the process to run without actually calculating correct values, and can easily be swapped out in place for their real libraries. + +What happens in the demo (notionally) +************************************* + +Preliminaries + - Ensure that there are a number of live trades with another party based on financial products that are covered under the + ISDA SIMM agreement (if none, then use the demo to enter some simple trades as described below). + +Initial Margin Agreement Process + - Agree that one will be performing the margining calculation against a portfolio of trades with another party, and agree the trades in that portfolio. In practice, one node will start the flow but it does not matter which node does. + - Individually (at the node level), identify the data (static, reference etc) one will need in order to be able to calculate the metrics on those trades + - Confirm with the other counterparty the dataset from the above set + - Calculate any intermediary steps and values needed for the margin calculation (ie sensitivities to risk factors) + - Agree on the results of these steps + - Calculate the initial margin + - Agree on the calculation of the above with the other party + - In practice, pay (or receive) this margin (omitted for the sake of complexity for this example) + +Demo execution (step by step) +***************************** + +**Setting up the Corda infrastructure** + +To run from the command line in Unix: + +1. Deploy the nodes using ``./gradlew samples:simm-valuation-demo:deployNodes`` +2. Run the nodes using ``./samples/simm-valuation-demo/build/nodes/runnodes`` + +To run from the command line in Windows: + +1. Deploy the nodes using ``gradlew samples:simm-valuation-demo:deployNodes`` +2. Run the nodes using ``samples\simm-valuation-demo\build\nodes\runnodes`` + +**Getting Bank A's details** + +From the command line run + +.. sourcecode:: bash + + curl http://localhost:10005/api/simmvaluationdemo/whoami + +The response should be something like + +.. sourcecode:: none + + { + "self" : { + "id" : "8Kqd4oWdx4KQGHGQW3FwXHQpjiv7cHaSsaAWMwRrK25bBJj792Z4rag7EtA", + "text" : "C=GB,L=London,O=Bank A" + }, + "counterparties" : [ + { + "id" : "8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM", + "text" : "C=JP,L=Tokyo,O=Bank C" + }, + { + "id" : "8Kqd4oWdx4KQGHGTBm34eCM2nrpcWKeM1ZG3DUYat3JTFUQTwB3Lv2WbPM8", + "text" : "C=US,L=New York,O=Bank B" + } + ] + } + +Now, if we ask the same question of Bank C we will see that it's id matches the id for Bank C as a counter +party to Bank A and Bank A will appear as a counter party + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" -X GET http://localhost:10011/api/simmvaluationdemo/whoami + +**Creating a trade with Bank C** + +In what follows, we assume we are Bank A (which is listening on port 10005) + +Notice the id field in the output of the ``whoami`` command. We are going to use the id assocatied +with Bank C, one of our counter parties, to create a trade. The general command for this is: + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" -X PUT -d <<>> http://localhost:10005/api/simmvaluationdemo/<<>>/trades + +where the representation of the trade is + +.. sourcecode:: none + + { + "id" : "trade1", + "description" : "desc", + "tradeDate" : [ 2016, 6, 6 ], + "convention" : "EUR_FIXED_1Y_EURIBOR_3M", + "startDate" : [ 2016, 6, 6 ], + "endDate" : [ 2020, 1, 2 ], + "buySell" : "BUY", + "notional" : "1000", + "fixedRate" : "0.1" + } + +Continuing our example, the specific command we would run is + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X PUT \ + -d '{"id":"trade1","description" : "desc","tradeDate" : [ 2016, 6, 6 ], "convention" : "EUR_FIXED_1Y_EURIBOR_3M", "startDate" : [ 2016, 6, 6 ], "endDate" : [ 2020, 1, 2 ], "buySell" : "BUY", "notional" : "1000", "fixedRate" : "0.1"}' \ + http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/trades + +With an expected response of + +.. sourcecode:: none + + HTTP/1.1 202 Accepted + Date: Thu, 28 Sep 2017 17:19:39 GMT + Content-Type: text/plain + Access-Control-Allow-Origin: * + Content-Length: 2 + Server: Jetty(9.3.9.v20160517) + +**Verifying trade completion** + +With the trade completed and stored by both parties, the complete list of trades with our couterparty can be seen with the following command + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/<<>>/trades + +The command for our example, using Bank A, would thus be + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/trades + +whilst a specific trade can be seen with + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/<<>>/trades/<<>> + +If we look at the trade we created above, we assigned it the id "trade1", the complete command in this case would be + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/trades/trade1 + +**Generating a valuation** + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X POST \ + -d <<>> + http://localhost:10005/api/simmvaluationdemo/<<>>/portfolio/valuations/calculate + +Again, the specific command to continue our example would be + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X POST \ + -d '{"valuationDate":[2016,6,6]}' \ + http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzLumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/portfolio/valuations/calculate + +**Viewing a valuation** + +In the same way we can ask for specific instances of trades with a counter party, we can request details of valuations + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" -X GET http://localhost:10005/api/simmvaluationdemo/<<>>/portfolio/valuations + +The specific command for out Bank A example is + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X GET http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65YwQM/portfolio/valuations + + + + + diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index ca86da886c..b9eb1b7e78 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -22,8 +22,8 @@ Classes get onto the whitelist via one of three mechanisms: #. Via the ``@CordaSerializable`` annotation. In order to whitelist a class, this annotation can be present on the class itself, on any of the super classes or on any interface implemented by the class or super classes or any interface extended by an interface implemented by the class or superclasses. -#. By returning the class as part of a plugin via the method ``customizeSerialization``. It's important to return - true from this method if you override it, otherwise the plugin will be excluded. See :doc:`writing-cordapps`. +#. By implementing the ``SerializationWhitelist`` interface and specifying a list of `whitelist` classes. + See :doc:`writing-cordapps`. #. Via the built in Corda whitelist (see the class ``DefaultWhitelist``). Whilst this is not user editable, it does list common JDK classes that have been whitelisted for your convenience. @@ -39,8 +39,8 @@ It's reproduced here as an example of both ways you can do this for a couple of them will automatically be whitelisted. This includes `Contract`, `ContractState` and `CommandData`. .. warning:: Java 8 Lambda expressions are not serializable except in flow checkpoints, and then not by default. The syntax to declare a serializable Lambda -expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or -``Callable c = (Callable & Serializable) () -> "Hello World";``. + expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or + ``Callable c = (Callable & Serializable) () -> "Hello World";``. .. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. @@ -63,7 +63,7 @@ The long term goal is to migrate the current serialization format for everything #. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time and do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states. #. Increased security from deserialized objects being constructed through supported constructors rather than having - data poked directy into their fields without an opportunity to validate consistency or intercept attempts to manipulate + data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate supposed invariants. Documentation on that format, and how JVM classes are translated to AMQP, will be linked here when it is available. @@ -259,9 +259,10 @@ Kotlin Objects `````````````` #. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties - and deserialize back to the singleton instance. - -Currently, the same is not true of Java singletons, and they will deserialize to new instances of the class. + and deserialize back to the singleton instance. Currently, the same is not true of Java singletons, + and they will deserialize to new instances of the class. + #. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like: + ``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration. The Carpenter ````````````` diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 8cc68a4313..cdddc4548e 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -99,6 +99,10 @@ Nested objects can be created using curly braces, as in ``{ a: 1, b: 2}``. This parser is defined for the type you need, for instance, if an API requires a ``Pair`` which could be represented as ``{ first: foo, second: 123 }``. +.. note:: If your CorDapp is written in Java, + named arguments won't work unless you compiled using the ``-parameters`` argument to javac. + See :doc:`deploying-a-node` for how to specify it via Gradle. + The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this: ``run registeredFlows`` @@ -139,6 +143,6 @@ The shell will be enhanced over time. The currently known limitations include: * The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using. .. _Yaml: http://www.yaml.org/spec/1.2/spec.html -.. _the defined parsers: api/kotlin/corda/net.corda.jackson/-jackson-support/index.html +.. _the defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html .. _Groovy: http://groovy-lang.org/ .. _CRaSH: http://www.crashub.org/ diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst index 434cdbe579..51e60133c4 100644 --- a/docs/source/troubleshooting.rst +++ b/docs/source/troubleshooting.rst @@ -1,13 +1,6 @@ Troubleshooting =============== -Milestone releases ------------------- - -When you clone the corda or cordapp-template repos, they will default to the master branch. The master branch is being continuously developed upon, and its features may not align with the state of Corda as described in the docs. Additionally, the master branch of the CorDapp template may break in response to changes in the main corda repo. - -When developing on Corda, you should always check out the latest milestone (i.e. stable) branch instead. For example, to check out milestone 0, you'd run ``git checkout release-M0``. - Java issues ----------- @@ -30,6 +23,11 @@ Some of the unit tests, and our serialization framework in general, rely on the to Java reflection. Make sure you have specified the ``-parameters`` option to the Java compiler. We attempt to set this globally for gradle and IntelliJ, but it's possible this option is not present in your environment for some reason. +"No matching constructor found: - [arg0: int, arg1: Party]: missing parameter arg0" +*********************************************************************************** + +Your CorDapp is written in Java and you haven't specified the ``-parameters`` compiler argument. See :doc:`deploying-a-node` for how it can be done using Gradle. + IDEA issues ----------- diff --git a/docs/source/tut-two-party-contract.rst b/docs/source/tut-two-party-contract.rst index 077133b378..2e59e2f0ef 100644 --- a/docs/source/tut-two-party-contract.rst +++ b/docs/source/tut-two-party-contract.rst @@ -11,31 +11,37 @@ Remember that each state references a contract. The contract imposes constraints If the transaction does not obey the constraints of all the contracts of all its states, it cannot become a valid ledger update. -We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. This will -only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final two lines of -the ``requireThat`` block as follows: +We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. + +In ``IOUContract.java``/``IOUContract.kt``, change the imports block to the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - // Constraints on the signers. - "There must be two signers." using (command.signers.toSet().size == 2) - "The borrower and lender must be signers." using (command.signers.containsAll(listOf( - out.borrower.owningKey, out.lender.owningKey))) + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - .. code-block:: java +And update the final block of constraints in the ``requireThat`` block as follows: - ... +.. container:: codeset - import com.google.common.collect.ImmutableList; + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt + :language: kotlin + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 12 - ... - - // Constraints on the signers. - check.using("There must be two signers.", command.getSigners().size() == 2); - check.using("The borrower and lender must be signers.", command.getSigners().containsAll( - ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java + :language: java + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 12 Progress so far --------------- diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index 441bd43c3b..4d5866b2c8 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -21,66 +21,52 @@ by invoking a built-in flow called ``FinalityFlow`` as a subflow. We're going to We also need to add the borrower's public key to the transaction's command, making the borrower one of the required signers on the transaction. -In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: +In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - // We add the items to the builder. - val state = IOUState(iouValue, me, otherParty) - val cmd = Command(IOUContract.Create(), listOf(me.owningKey, otherParty.owningKey)) - txBuilder.withItems(state, cmd) +And update ``IOUFlow.call`` by changing the code following the creation of the ``TransactionBuilder`` as follows: - // Verifying the transaction. - txBuilder.verify(serviceHub) +.. container:: codeset - // Signing the transaction. - val signedTx = serviceHub.signInitialTransaction(txBuilder) + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + :language: kotlin + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 - // Obtaining the counterparty's signature - val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, CollectSignaturesFlow.tracker())) - - // Finalising the transaction. - subFlow(FinalityFlow(fullySignedTx)) - - .. code-block:: java - - ... - - import com.google.common.collect.ImmutableList; - import java.security.PublicKey; - import java.util.List; - - ... - - // We add the items to the builder. - IOUState state = new IOUState(iouValue, me, otherParty); - List requiredSigners = ImmutableList.of(me.getOwningKey(), otherParty.getOwningKey()); - Command cmd = new Command(new IOUContract.Create(), requiredSigners); - txBuilder.withItems(state, cmd); - - // Verifying the transaction. - txBuilder.verify(getServiceHub()); - - // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Obtaining the counterparty's signature - final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, CollectSignaturesFlow.Companion.tracker())); - - // Finalising the transaction. - subFlow(new FinalityFlow(fullySignedTx)); - - return null; + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java + :language: java + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command. -``CollectSignaturesFlow``, meanwhile, takes a transaction signed by the flow initiator, and returns a transaction -signed by all the transaction's other required signers. We then pass this fully-signed transaction into -``FinalityFlow``. +We now need to communicate with the borrower to request their signature. Whenever you want to communicate with another +party in the context of a flow, you first need to establish a flow session with them. If the counterparty has a +``FlowLogic`` registered to respond to the ``FlowLogic`` initiating the session, a session will be established. All +communication between the two ``FlowLogic`` instances will then place as part of this session. + +Once we have a session with the borrower, we gather the borrower's signature using ``CollectSignaturesFlow``, which +takes: + +* A transaction signed by the flow initiator +* A list of flow-sessions between the flow initiator and the required signers + +And returns a transaction signed by all the required signers. + +We then pass this fully-signed transaction into ``FinalityFlow``. Creating the borrower's flow ---------------------------- @@ -89,81 +75,15 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.transactions.SignedTransaction - - ... - - @InitiatedBy(IOUFlow::class) - class IOUFlowResponder(val otherParty: Party) : FlowLogic() { - @Suspendable - override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherParty, SignTransactionFlow.tracker()) { - override fun checkTransaction(stx: SignedTransaction) = requireThat { - val output = stx.tx.outputs.single().data - "This must be an IOU transaction." using (output is IOUState) - val iou = output as IOUState - "The IOU's value can't be too high." using (iou.value < 100) - } - } - - subFlow(signTransactionFlow) - } - } - - .. code-block:: java - - package com.template.flow; - - import co.paralleluniverse.fibers.Suspendable; - import com.template.state.IOUState; - import net.corda.core.contracts.ContractState; - import net.corda.core.flows.FlowException; - import net.corda.core.flows.FlowLogic; - import net.corda.core.flows.InitiatedBy; - import net.corda.core.flows.SignTransactionFlow; - import net.corda.core.identity.Party; - import net.corda.core.transactions.SignedTransaction; - import net.corda.core.utilities.ProgressTracker; - - import static net.corda.core.contracts.ContractsDSL.requireThat; - - @InitiatedBy(IOUFlow.class) - public class IOUFlowResponder extends FlowLogic { - private final Party otherParty; - - public IOUFlowResponder(Party otherParty) { - this.otherParty = otherParty; - } - - @Suspendable - @Override - public Void call() throws FlowException { - class signTxFlow extends SignTransactionFlow { - private signTxFlow(Party otherParty, ProgressTracker progressTracker) { - super(otherParty, progressTracker); - } - - @Override - protected void checkTransaction(SignedTransaction stx) { - requireThat(require -> { - ContractState output = stx.getTx().getOutputs().get(0).getData(); - require.using("This must be an IOU transaction.", output instanceof IOUState); - IOUState iou = (IOUState) output; - require.using("The IOU's value can't be too high.", iou.getValue() < 100); - return null; - }); - } - } - - subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker())); - - return null; - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``. diff --git a/docs/source/tut-two-party-introduction.rst b/docs/source/tut-two-party-introduction.rst index 95ad302e2c..66f4f14acc 100644 --- a/docs/source/tut-two-party-introduction.rst +++ b/docs/source/tut-two-party-introduction.rst @@ -20,4 +20,4 @@ IOU onto the ledger. We'll need to make two changes: signature (as well as the lender's) to become valid ledger updates * The ``IOUFlow`` will need to be updated to allow for the gathering of the borrower's signature -We'll start by updating the contract. +We'll start by updating the contract. \ No newline at end of file diff --git a/docs/source/tutorial-attachments.rst b/docs/source/tutorial-attachments.rst index 09a2736b38..7047cfb959 100644 --- a/docs/source/tutorial-attachments.rst +++ b/docs/source/tutorial-attachments.rst @@ -68,57 +68,22 @@ RPC, which returns both a snapshot and an observable of changes. The observable transaction the node verifies is retrieved. That transaction is checked to see if it has the expected attachment and if so, printed out. -.. sourcecode:: kotlin +.. container:: codeset - fun recipient(rpc: CordaRPCOps) { - println("Waiting to receive transaction ...") - val stx = rpc.verifiedTransactions().second.toBlocking().first() - val wtx = stx.tx - if (wtx.attachments.isNotEmpty()) { - assertEquals(PROSPECTUS_HASH, wtx.attachments.first()) - require(rpc.attachmentExists(PROSPECTUS_HASH)) - println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}") - } else { - println("Error: no attachments found in ${wtx.id}") - } - } + .. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 The sender correspondingly builds a transaction with the attachment, then calls ``FinalityFlow`` to complete the transaction and send it to the recipient node: -.. sourcecode:: kotlin - - fun sender(rpc: CordaRPCOps) { - // Get the identity key of the other side (the recipient). - val otherSide: Party = rpc.wellKnownPartyFromName("Bank B")!! - - // Make sure we have the file in storage - // TODO: We should have our own demo file, not share the trader demo file - if (!rpc.attachmentExists(PROSPECTUS_HASH)) { - Thread.currentThread().contextClassLoader.getResourceAsStream("bank-of-london-cp.jar").use { - val id = rpc.uploadAttachment(it) - assertEquals(PROSPECTUS_HASH, id) - } - } - - // Create a trivial transaction that just passes across the attachment - in normal cases there would be - // inputs, outputs and commands that refer to this attachment. - val ptx = TransactionBuilder(notary = null) - require(rpc.attachmentExists(PROSPECTUS_HASH)) - ptx.addAttachment(PROSPECTUS_HASH) - // TODO: Add a dummy state and specify a notary, so that the tx hash is randomised each time and the demo can be repeated. - - // Despite not having any states, we have to have at least one signature on the transaction - ptx.signWith(ALICE_KEY) - - // Send the transaction to the other recipient - val stx = ptx.toSignedTransaction() - println("Sending ${stx.id}") - val protocolHandle = rpc.startFlow(::FinalityFlow, stx, setOf(otherSide)) - protocolHandle.progress.subscribe(::println) - protocolHandle.returnValue.toBlocking().first() - } +.. container:: codeset + .. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 This side is a bit more complex. Firstly it looks up its counterparty by name in the network map. Then, if the node doesn't already have the attachment in its storage, we upload it from a JAR resource and check the hash was what diff --git a/docs/source/tutorial-building-transactions.rst b/docs/source/tutorial-building-transactions.rst index 18734ca89f..d7011e57f4 100644 --- a/docs/source/tutorial-building-transactions.rst +++ b/docs/source/tutorial-building-transactions.rst @@ -18,67 +18,54 @@ of a flow. The Basic Lifecycle Of Transactions ----------------------------------- -Transactions in Corda are constructed in stages and contain a number of -elements. In particular a transaction’s core data structure is the -``net.corda.core.transactions.WireTransaction``, which is usually -manipulated via a -``net.corda.core.transactions.TransactionBuilder`` and contains: +Transactions in Corda contain a number of elements: 1. A set of Input state references that will be consumed by the final -accepted transaction. + accepted transaction 2. A set of Output states to create/replace the consumed states and thus -become the new latest versions of data on the ledger. + become the new latest versions of data on the ledger 3. A set of ``Attachment`` items which can contain legal documents, contract -code, or private encrypted sections as an extension beyond the native -contract states. + code, or private encrypted sections as an extension beyond the native + contract states -4. A set of ``Command`` items which give a context to the type of ledger -transition that is encoded in the transaction. Also each command has an -associated set of signer keys, which will be required to sign the -transaction. +4. A set of ``Command`` items which indicate the type of ledger + transition that is encoded in the transaction. Each command also has an + associated set of signer keys, which will be required to sign the + transaction -5. A signers list, which is populated by the ``TransactionBuilder`` to -be the union of the signers on the individual Command objects. +5. A signers list, which is the union of the signers on the individual + Command objects -6. A notary identity to specify the Notary node which is tracking the -state consumption. (If the input states are registered with different -notary nodes the flow will have to insert additional ``NotaryChange`` -transactions to migrate the states across to a consistent notary node, -before being allowed to mutate any states.) +6. A notary identity to specify which notary node is tracking the + state consumption (if the transaction's input states are registered with different + notary nodes the flow will have to insert additional ``NotaryChange`` + transactions to migrate the states across to a consistent notary node + before being allowed to mutate any states) -7. Optionally a timestamp that can used in the Notary to time bound the -period in which the proposed transaction stays valid. +7. Optionally a timestamp that can used by the notary to bound the + period during which the proposed transaction can be committed to the + ledger -Typically, the ``WireTransaction`` should be regarded as a proposal and -may need to be exchanged back and forth between parties before it can be -fully populated. This is an immediate consequence of the Corda privacy -model, which means that the input states are likely to be unknown to the -other node. +A transaction is built by populating a ``TransactionBuilder``. Typically, +the ``TransactionBuilder`` will need to be exchanged back and forth between +parties before it is fully populated. This is an immediate consequence of +the Corda privacy model, in which the input states are likely to be unknown +to the other node. -Once the proposed data is fully populated the flow code should freeze -the ``WireTransaction`` and form a ``SignedTransaction``. This is key to -the ledger agreement process, as once a flow has attached a node’s -signature it has stated that all details of the transaction are -acceptable to it. A flow should take care not to attach signatures to -intermediate data, which might be maliciously used to construct a -different ``SignedTransaction``. For instance in a foreign exchange -scenario we shouldn't send a ``SignedTransaction`` with only our sell -side populated as that could be used to take the money without the -expected return of the other currency. Also, it is best practice for -flows to receive back the ``DigitalSignature.WithKey`` of other parties -rather than a full ``SignedTransaction`` objects, because otherwise we -have to separately check that this is still the same +Once the builder is fully populated, the flow should freeze the ``TransactionBuilder`` by signing it to create a +``SignedTransaction``. This is key to the ledger agreement process - once a flow has attached a node’s signature to a +transaction, it has effectively stated that it accepts all the details of the transaction. + +It is best practice for flows to receive back the ``TransactionSignature`` of other parties rather than a full +``SignedTransaction`` objects, because otherwise we have to separately check that this is still the same ``SignedTransaction`` and not a malicious substitute. -The final stage of committing the transaction to the ledger is to -notarise the ``SignedTransaction``, distribute this to all appropriate -parties and record the data into the ledger. These actions are best -delegated to the ``FinalityFlow``, rather than calling the individual -steps manually. However, do note that the final broadcast to the other -nodes is asynchronous, so care must be used in unit testing to -correctly await the Vault updates. +The final stage of committing the transaction to the ledger is to notarise the ``SignedTransaction``, distribute it to +all appropriate parties and record the data into the ledger. These actions are best delegated to the ``FinalityFlow``, +rather than calling the individual steps manually. However, do note that the final broadcast to the other nodes is +asynchronous, so care must be used in unit testing to correctly await the vault updates. Gathering Inputs ---------------- @@ -87,39 +74,36 @@ One of the first steps to forming a transaction is gathering the set of input references. This process will clearly vary according to the nature of the business process being captured by the smart contract and the parameterised details of the request. However, it will generally involve -searching the Vault via the ``VaultQueryService`` interface on the +searching the vault via the ``VaultService`` interface on the ``ServiceHub`` to locate the input states. To give a few more specific details consider two simplified real world -scenarios. First, a basic foreign exchange Cash transaction. This +scenarios. First, a basic foreign exchange cash transaction. This transaction needs to locate a set of funds to exchange. A flow modelling this is implemented in ``FxTransactionBuildTutorial.kt``. -Second, a simple business model in which parties manually accept, or -reject each other's trade proposals which is implemented in +Second, a simple business model in which parties manually accept or +reject each other's trade proposals, which is implemented in ``WorkflowTransactionBuildTutorial.kt``. To run and explore these -examples using the IntelliJ IDE one can run/step the respective unit +examples using the IntelliJ IDE one can run/step through the respective unit tests in ``FxTransactionBuildTutorialTest.kt`` and ``WorkflowTransactionBuildTutorialTest.kt``, which drive the flows as part of a simulated in-memory network of nodes. -.. |nbsp| unicode:: 0xA0 - :trim: - .. note:: Before creating the IntelliJ run configurations for these unit tests go to Run -> Edit |nbsp| Configurations -> Defaults -> JUnit, add - ``-javaagent:lib/quasar.jar -Dco.paralleluniverse.fibers.verifyInstrumentation`` + ``-javaagent:lib/quasar.jar`` to the VM options, and set Working directory to ``$PROJECT_DIR$`` so that the ``Quasar`` instrumentation is correctly configured. -For the Cash transaction let’s assume the cash resources are using the -standard ``CashState`` in the ``:financial`` Gradle module. The Cash +For the cash transaction, let’s assume we are using the +standard ``CashState`` in the ``:financial`` Gradle module. The ``Cash`` contract uses ``FungibleAsset`` states to model holdings of -interchangeable assets and allow the split/merge and summing of +interchangeable assets and allow the splitting, merging and summing of states to meet a contractual obligation. We would normally use the ``Cash.generateSpend`` method to gather the required -amount of cash into a ``TransactionBuilder``, set the outputs and move -command. However, to elucidate more clearly example flow code is shown -here that will manually carry out the inputs queries by specifying relevant +amount of cash into a ``TransactionBuilder``, set the outputs and generate the ``Move`` +command. However, to make things clearer, the example flow code shown +here will manually carry out the input queries by specifying relevant query criteria filters to the ``tryLockFungibleStatesForSpending`` method of the ``VaultService``. @@ -128,14 +112,11 @@ of the ``VaultService``. :start-after: DOCSTART 1 :end-before: DOCEND 1 -As a foreign exchange transaction we expect an exchange of two -currencies, so we will also require a set of input states from the other -counterparty. However, the Corda privacy model means we do not know the -other node’s states. Our flow must therefore negotiate with the other -node for them to carry out a similar query and populate the inputs (See -the ``ForeignExchangeFlow`` for more details of the exchange). Having -identified a set of Input ``StateRef`` items we can then create the -output as discussed below. +This is a foreign exchange transaction, so we expect another set of input states of another currency from a +counterparty. However, the Corda privacy model means we are not aware of the other node’s states. Our flow must +therefore ask the other node to carry out a similar query and return the additional inputs to the transaction (see the +``ForeignExchangeFlow`` for more details of the exchange). We now have all the required input ``StateRef`` items, and +can turn to gathering the outputs. For the trade approval flow we need to implement a simple workflow pattern. We start by recording the unconfirmed trade details in a state @@ -161,12 +142,13 @@ in the right workflow state over the RPC interface. The RPC will then initiate the relevant flow using ``StateRef``, or ``linearId`` values as parameters to the flow to identify the states being operated upon. Thus code to gather the latest input state for a given ``StateRef`` would use -the ``VaultQueryService`` as follows: +the ``VaultService`` as follows: .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 8 Generating Commands ------------------- @@ -187,7 +169,7 @@ environment the ``Contract.verify``, transaction is the only allowed to use the content of the transaction to decide validity. Another essential requirement for commands is that the correct set of -``CompositeKeys`` are added to the Command on the builder, which will be +``PublicKey`` objects are added to the ``Command`` on the builder, which will be used to form the set of required signers on the final validated transaction. These must correctly align with the expectations of the ``Contract.verify`` method, which should be written to defensively check @@ -200,7 +182,7 @@ exchange of assets. Generating Outputs ------------------ -Having located a set of ``StateAndRefs`` as the transaction inputs, the +Having located a ``StateAndRefs`` set as the transaction inputs, the flow has to generate the output states. Typically, this is a simple call to the Kotlin ``copy`` method to modify the few fields that will transitioned in the transaction. The contract code may provide a @@ -210,7 +192,7 @@ usually sufficient, especially as it is expected that we wish to preserve the ``linearId`` between state revisions, so that Vault queries can find the latest revision. -For fungible contract states such as ``Cash`` it is common to distribute +For fungible contract states such as ``cash`` it is common to distribute and split the total amount e.g. to produce a remaining balance output state for the original owner when breaking up a large amount input state. Remember that the result of a successful transaction is always to @@ -221,36 +203,39 @@ the total cash. For example from the demo code: :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 -Building the WireTransaction ----------------------------- +Building the SignedTransaction +------------------------------ -Having gathered all the ingredients for the transaction we now need to -use a ``TransactionBuilder`` to construct the full ``WireTransaction``. -The initial ``TransactionBuilder`` should be created by calling the -``TransactionBuilder`` method. At this point the -Notary to associate with the states should be recorded. Then we keep -adding inputs, outputs, commands and attachments to fill the -transaction. Examples of this process are: +Having gathered all the components for the transaction we now need to use a ``TransactionBuilder`` to construct the +full ``SignedTransaction``. We instantiate a ``TransactionBuilder`` and provide a notary that will be associated with +the output states. Then we keep adding inputs, outputs, commands and attachments to complete the transaction. + +Once the transaction is fully formed, we call ``ServiceHub.signInitialTransaction`` to sign the ``TransactionBuilder`` +and convert it into a ``SignedTransaction``. + +Examples of this process are: .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 8 .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 + :dedent: 4 Completing the SignedTransaction -------------------------------- -Having created an initial ``WireTransaction`` and converted this to an -initial ``SignedTransaction`` the process of verifying and forming a -full ``SignedTransaction`` begins and then completes with the +Having created an initial ``TransactionBuilder`` and converted this to a ``SignedTransaction``, the process of +verifying and forming a full ``SignedTransaction`` begins and then completes with the notarisation. In practice this is a relatively stereotypical process, -because assuming the ``WireTransaction`` is correctly constructed the +because assuming the ``SignedTransaction`` is correctly constructed the verification should be immediate. However, it is also important to recheck the business details of any data received back from an external node, because a malicious party could always modify the contents before @@ -263,12 +248,11 @@ of the transaction has not been altered by the remote parties. The typical code therefore checks the received ``SignedTransaction`` using the ``verifySignaturesExcept`` method, excluding itself, the -notary and any other parties yet to apply their signature. The contents of the -``WireTransaction`` inside the ``SignedTransaction`` should be fully +notary and any other parties yet to apply their signature. The contents of the ``SignedTransaction`` should be fully verified further by expanding with ``toLedgerTransaction`` and calling ``verify``. Further context specific and business checks should then be made, because the ``Contract.verify`` is not allowed to access external -context. For example the flow may need to check that the parties are the +context. For example, the flow may need to check that the parties are the right ones, or that the ``Command`` present on the transaction is as expected for this specific flow. An example of this from the demo code is: @@ -276,6 +260,7 @@ expected for this specific flow. An example of this from the demo code is: :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 + :dedent: 8 After verification the remote flow will return its signature to the originator. The originator should apply that signature to the starting @@ -284,18 +269,15 @@ originator. The originator should apply that signature to the starting Committing the Transaction -------------------------- -Once all the party signatures are applied to the SignedTransaction the -final step is notarisation. This involves calling ``NotaryFlow.Client`` -to confirm the transaction, consume the inputs and return its confirming -signature. Then the flow should ensure that all nodes end with all -signatures and that they call ``ServiceHub.recordTransactions``. The -code for this is standardised in the ``FinalityFlow``, or more explicitly -an example is: +Once all the signatures are applied to the ``SignedTransaction``, the +final steps are notarisation and ensuring that all nodes record the fully-signed transaction. The +code for this is standardised in the ``FinalityFlow``: .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 + :dedent: 8 Partially Visible Transactions ------------------------------ @@ -307,12 +289,12 @@ a regulator, but does not wish to share that with the other trading partner. The tear-off/Merkle tree support in Corda allows flows to send portions of the full transaction to restrict visibility to remote parties. To do this one can use the -``WireTransaction.buildFilteredTransaction`` extension method to produce +``SignedTransaction.buildFilteredTransaction`` extension method to produce a ``FilteredTransaction``. The elements of the ``SignedTransaction`` which we wish to be hide will be replaced with their secure hash. The -overall transaction txid is still provable from the +overall transaction id is still provable from the ``FilteredTransaction`` preventing change of the private data, but we do not expose that data to the other node directly. A full example of this can be found in the ``NodeInterestRates`` Oracle code from the ``irs-demo`` project which interacts with the ``RatesFixFlow`` flow. -Also, refer to the :doc:`merkle-trees` documentation. +Also, refer to the :doc:`merkle-trees` documentation. \ No newline at end of file diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 0181e0778b..aa49889b4f 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -3,58 +3,58 @@ Using the client RPC API ======================== -In this tutorial we will build a simple command line utility that connects to a node, creates some Cash transactions and -meanwhile dumps the transaction graph to the standard output. We will then put some simple visualisation on top. For an -explanation on how the RPC works see :doc:`clientrpc`. +In this tutorial we will build a simple command line utility that connects to a node, creates some cash transactions +and dumps the transaction graph to the standard output. We will then put some simple visualisation on top. For an +explanation on how RPC works in Corda see :doc:`clientrpc`. We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary -and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes -directly. +and a Alice node that can issue, move and exit cash. -Note how we configure the node to create a user that has permission to start the CashFlow. +Here's how we configure the node to create a user that has the permissions to start the ``CashIssueFlow``, +``CashPaymentFlow``, and ``CashExitFlow``: .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt :language: kotlin :start-after: START 1 :end-before: END 1 -Now we can connect to the node itself using a valid RPC login. We login using the configured user. +Now we can connect to the node itself using a valid RPC user login and start generating transactions in a different +thread using ``generateTransactions`` (to be defined later): .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt :language: kotlin :start-after: START 2 :end-before: END 2 + :dedent: 8 -We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, -which exposes the full RPC interface of the node: +``proxy`` exposes the full RPC interface of the node: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: interface CordaRPCOps :end-before: } -.. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made - available to RPC clients. - -The one we need in order to dump the transaction graph is ``verifiedTransactions``. The type signature tells us that the -RPC will return a list of transactions and an Observable stream. This is a general pattern, we query some data and the -node will return the current snapshot and future updates done to it. Observables are described in further detail in -:doc:`clientrpc` +The RPC operation we need in order to dump the transaction graph is ``internalVerifiedTransactionsFeed``. The type +signature tells us that the RPC operation will return a list of transactions and an ``Observable`` stream. This is a +general pattern, we query some data and the node will return the current snapshot and future updates done to it. +Observables are described in further detail in :doc:`clientrpc` .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt :language: kotlin :start-after: START 3 :end-before: END 3 + :dedent: 8 -The graph will be defined by nodes and edges between them. Each node represents a transaction and edges represent -output-input relations. For now let's just print ``NODE `` for the former and ``EDGE `` for the -latter. +The graph will be defined as follows: + +* Each transaction is a vertex, represented by printing ``NODE `` +* Each input-output relationship is an edge, represented by prining ``EDGE `` .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt :language: kotlin :start-after: START 4 :end-before: END 4 - + :dedent: 8 Now we just need to create the transactions themselves! @@ -65,17 +65,16 @@ Now we just need to create the transactions themselves! We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates -here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable`` -or mark it as not used. Failing to do this will leak resources in the node.) +here and so we mark these observables as ``notUsed`` (as a rule, you should always either subscribe to an ``Observable`` +or mark it as not used. Failing to do so will leak resources in the node). Then in a loop we generate randomly either an Issue, a Pay or an Exit transaction. -The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient -permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that -type-checks the arguments for us. +The RPC we need to initiate a cash transaction is ``startFlow`` which starts an arbitrary flow given sufficient +permissions to do so. Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while -listening on successfully created ones, which are dumped to the console. We just need to run it!: +listening on successfully created ones, which are dumped to the console. We just need to run it! .. code-block:: text @@ -84,12 +83,13 @@ listening on successfully created ones, which are dumped to the console. We just # Start it ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Print -Now let's try to visualise the transaction graph. We will use a graph drawing library called graphstream_ +Now let's try to visualise the transaction graph. We will use a graph drawing library called graphstream_. .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt :language: kotlin :start-after: START 5 :end-before: END 5 + :dedent: 8 If we run the client with ``Visualise`` we should see a simple random graph being drawn as new transactions are being created. @@ -106,11 +106,9 @@ requests or responses with the Corda node. Here's an example of both ways you c See more on plugins in :doc:`running-a-node`. -.. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. - Security -------- -RPC credentials associated with a Client must match the permission set configured on the server Node. +RPC credentials associated with a Client must match the permission set configured on the server node. This refers to both authentication (username and password) and role-based authorisation (a permissioned set of RPC operations an authenticated user is entitled to run). @@ -161,5 +159,5 @@ You can then deploy and launch the nodes (Notary and Alice) as follows: With regards to the start flow RPCs, there is an extra layer of security whereby the flow to be executed has to be annotated with ``@StartableByRPC``. Flows without this annotation cannot execute using RPC. -See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and -Cordformation in :doc:`running-a-node`. +See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and +Cordformation in :doc:`running-a-node`. \ No newline at end of file diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 2aca2177f7..23c1430286 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -32,26 +32,16 @@ This lifecycle for commercial paper is illustrated in the diagram below: .. image:: resources/contract-cp.png -Where to put your code ----------------------- - -A CorDapp is a collection of contracts, state definitions, flows and other ways to extend the Corda platform. -To create one you would typically clone the CorDapp template project ("cordapp-template"), which provides an example -structure for the code. Alternatively you can just create a Java-style project as normal, with your choice of build -system (Maven, Gradle, etc), then add a dependency on ``net.corda.core:0.X`` where X is the milestone number you are -depending on. The core module defines the base classes used in this tutorial. - Starting the commercial paper class ----------------------------------- A smart contract is a class that implements the ``Contract`` interface. This can be either implemented directly, as done here, or by subclassing an abstract contract such as ``OnLedgerAsset``. The heart of any contract in Corda is the -``verify()`` function, which determined whether any given transaction is valid. This example shows how to write a -``verify()`` function from scratch. +``verify`` function, which determines whether a given transaction is valid. This example shows how to write a +``verify`` function from scratch. -You can see the full Kotlin version of this contract in the code as ``CommercialPaperLegacy``. The code in this -tutorial is available in both Kotlin and Java. You can quickly switch between them to get a feeling for how -Kotlin syntax works. +The code in this tutorial is available in both Kotlin and Java. You can quickly switch between them to get a feeling +for Kotlin's syntax. .. container:: codeset @@ -72,13 +62,8 @@ Kotlin syntax works. } } -Every contract must have at least a ``verify()`` method. - -.. note:: In the future there will be a way to bind legal contract prose to a smart contract implementation, - that may take precedence over the code in case of a dispute. - -The verify method returns nothing. This is intentional: the function either completes correctly, or throws an exception, -in which case the transaction is rejected. +Every contract must have at least a ``verify`` method. The verify method returns nothing. This is intentional: the +function either completes correctly, or throws an exception, in which case the transaction is rejected. So far, so simple. Now we need to define the commercial paper *state*, which represents the fact of ownership of a piece of issued paper. @@ -90,111 +75,21 @@ A state is a class that stores data that is checked by the contract. A commercia .. image:: resources/contract-cp-state.png - .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 - data class State( - val issuance: PartyAndReference, - override val owner: AbstractParty, - val faceValue: Amount>, - val maturityDate: Instant - ) : OwnableState { - override val contract = "net.corda.finance.contracts.CommercialPaper" - override val participants = listOf(owner) - - fun withoutOwner() = copy(owner = AnonymousParty(NullPublicKey)) - override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) - } - - .. sourcecode:: java - - public static class State implements OwnableState { - private PartyAndReference issuance; - private AbstractParty owner; - private Amount> faceValue; - private Instant maturityDate; - - public State() { - } // For serialization - - public State(PartyAndReference issuance, PublicKey owner, Amount> faceValue, - Instant maturityDate) { - this.issuance = issuance; - this.owner = owner; - this.faceValue = faceValue; - this.maturityDate = maturityDate; - } - - public State copy() { - return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); - } - - @NotNull - @Override - public Pair withNewOwner(@NotNull AbstractParty newOwner) { - return new Pair<>(new Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate)); - } - - public PartyAndReference getIssuance() { - return issuance; - } - - public AbstractParty getOwner() { - return owner; - } - - public Amount> getFaceValue() { - return faceValue; - } - - public Instant getMaturityDate() { - return maturityDate; - } - - @NotNull - @Override - public Contract getContract() { - return new JavaCommercialPaper(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - State state = (State) o; - - if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; - if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; - if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null); - } - - @Override - public int hashCode() { - int result = issuance != null ? issuance.hashCode() : 0; - result = 31 * result + (owner != null ? owner.hashCode() : 0); - result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); - result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); - return result; - } - - @NotNull - @Override - public List getParticipants() { - return ImmutableList.of(this.owner); - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 We define a class that implements the ``ContractState`` interface. -The ``ContractState`` interface requires us to provide a ``getContract`` method that returns an instance of the -contract class itself. In future, this may change to support dynamic loading of contracts with versioning -and signing constraints, but for now this is how it's written. - We have four fields in our state: * ``issuance``, a reference to a specific piece of commercial paper issued by some party. @@ -234,39 +129,17 @@ Let's define a few commands now: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 - interface Commands : CommandData { - class Move : TypeOnlyCommandData(), Commands - class Redeem : TypeOnlyCommandData(), Commands - class Issue : TypeOnlyCommandData(), Commands - } - - - .. sourcecode:: java - - public static class Commands implements core.contract.Command { - public static class Move extends Commands { - @Override - public boolean equals(Object obj) { - return obj instanceof Move; - } - } - - public static class Redeem extends Commands { - @Override - public boolean equals(Object obj) { - return obj instanceof Redeem; - } - } - - public static class Issue extends Commands { - @Override - public boolean equals(Object obj) { - return obj instanceof Issue; - } - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 We define a simple grouping interface or static class, this gives us a type that all our commands have in common, then we go ahead and create three commands: ``Move``, ``Redeem``, ``Issue``. ``TypeOnlyCommandData`` is a helpful utility @@ -287,22 +160,17 @@ run two contracts one time each: Cash and CommercialPaper. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 - override fun verify(tx: LedgerTransaction) { - // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. - val groups = tx.groupStates(State::withoutOwner) - - // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming - // it for cash on or after the maturity date. - val command = tx.commands.requireSingleCommand() - - .. sourcecode:: java - - @Override - public void verify(LedgerTransaction tx) { - List> groups = tx.groupStates(State.class, State::withoutOwner); - CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.class); + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 We start by using the ``groupStates`` method, which takes a type and a function. State grouping is a way of ensuring your contract can handle multiple unrelated states of the same type in the same transaction, which is needed for @@ -312,8 +180,6 @@ The second line does what the code suggests: it searches for a command object th ``CommercialPaper.Commands`` supertype, and either returns it, or throws an exception if there's zero or more than one such command. -.. _state_ref: - Using state groups ------------------ @@ -362,12 +228,12 @@ Here are some code examples: .. sourcecode:: kotlin // Type of groups is List>> - val groups = tx.groupStates() { it: Cash.State -> Pair(it.deposit, it.amount.currency) } - for ((inputs, outputs, key) in groups) { - // Either inputs or outputs could be empty. - val (deposit, currency) = key + val groups = tx.groupStates { it: Cash.State -> it.amount.token } + for ((inputs, outputs, key) in groups) { + // Either inputs or outputs could be empty. + val (deposit, currency) = key - ... + ... } .. sourcecode:: java @@ -414,101 +280,37 @@ in equals and hashCode. Checking the requirements ------------------------- - After extracting the command and the groups, we then iterate over each group and verify it meets the required business logic. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 8 - val timeWindow: TimeWindow? = tx.timeWindow - - for ((inputs, outputs, key) in groups) { - when (command.value) { - is Commands.Move -> { - val input = inputs.single() - requireThat { - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - "the state is propagated" using (outputs.size == 1) - // Don't need to check anything else, as if outputs.size == 1 then the output is equal to - // the input ignoring the owner field due to the grouping. - } - } - - is Commands.Redeem -> { - // Redemption of the paper requires movement of on-ledger cash. - val input = inputs.single() - val received = tx.outputs.map{ it.data }.sumCashBy(input.owner) - val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must be timestamped") - requireThat { - "the paper must have matured" using (time >= input.maturityDate) - "the received amount equals the face value" using (received == input.faceValue) - "the paper must be destroyed" using outputs.isEmpty() - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - } - } - - is Commands.Issue -> { - val output = outputs.single() - val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must be timestamped") - requireThat { - // Don't allow people to issue commercial paper under other entities identities. - "output states are issued by a command signer" using (output.issuance.party.owningKey in command.signers) - "output values sum to more than the inputs" using (output.faceValue.quantity > 0) - "the maturity date is not in the past" using (time < output.maturityDate) - // Don't allow an existing CP state to be replaced by this issuance. - "can't reissue an existing state" by inputs.isEmpty() - } - } - - else -> throw IllegalArgumentException("Unrecognised command") - } - } - - .. sourcecode:: java - - Timestamp time = tx.getTimestamp(); // Can be null/missing. - for (InOutGroup group : groups) { - List inputs = group.getInputs(); - List outputs = group.getOutputs(); - - // For now do not allow multiple pieces of CP to trade in a single transaction. Study this more! - State input = single(filterIsInstance(inputs, State.class)); - - checkState(cmd.getSigners().contains(input.getOwner()), "the transaction is signed by the owner of the CP"); - - if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) { - checkState(outputs.size() == 1, "the state is propagated"); - // Don't need to check anything else, as if outputs.size == 1 then the output is equal to - // the input ignoring the owner field due to the grouping. - } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { - TimeWindow timeWindow = tx.getTimeWindow(); - Instant time = null == timeWindow - ? null - : timeWindow.getUntilTime(); - Amount> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner()); - - checkState(received.equals(input.getFaceValue()), "received amount equals the face value"); - checkState(time != null && !time.isBefore(input.getMaturityDate(), "the paper must have matured"); - checkState(outputs.isEmpty(), "the paper must be destroyed"); - } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { - // .. etc .. (see Kotlin for full definition) - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 8 This loop is the core logic of the contract. The first line simply gets the timestamp out of the transaction. Timestamping of transactions is optional, so a time may be missing here. We check for it being null later. -.. note:: In future timestamping may be mandatory for all transactions. - .. warning:: In the Kotlin version as long as we write a comparison with the transaction time first the compiler will verify we didn't forget to check if it's missing. Unfortunately due to the need for smooth Java interop, this check won't happen if we write e.g. ``someDate > time``, it has to be ``time < someDate``. So it's good practice to always write the transaction timestamp first. +Next, we take one of three paths, depending on what the type of the command object is. + +**If the command is a ``Move`` command:** + The first line (first three lines in Java) impose a requirement that there be a single piece of commercial paper in this group. We do not allow multiple units of CP to be split or merged even if they are owned by the same owner. The ``single()`` method is a static *extension method* defined by the Kotlin standard library: given a list, it throws an @@ -520,26 +322,26 @@ behind the scenes, the code compiles to the same bytecodes. Next, we check that the transaction was signed by the public key that's marked as the current owner of the commercial paper. Because the platform has already verified all the digital signatures before the contract begins execution, all we have to do is verify that the owner's public key was one of the keys that signed the transaction. The Java code -is straightforward: we are simply using the ``Preconditions.checkState`` method from Guava. The Kotlin version looks a little odd: we have a *requireThat* construct that looks like it's -built into the language. In fact *requireThat* is an ordinary function provided by the platform's contract API. Kotlin -supports the creation of *domain specific languages* through the intersection of several features of the language, and -we use it here to support the natural listing of requirements. To see what it compiles down to, look at the Java version. -Each ``"string" using (expression)`` statement inside a ``requireThat`` turns into an assertion that the given expression is -true, with an ``IllegalStateException`` being thrown that contains the string if not. It's just another way to write out a regular -assertion, but with the English-language requirement being put front and center. +is straightforward: we are simply using the ``Preconditions.checkState`` method from Guava. The Kotlin version looks a +little odd: we have a *requireThat* construct that looks like it's built into the language. In fact *requireThat* is an +ordinary function provided by the platform's contract API. Kotlin supports the creation of *domain specific languages* +through the intersection of several features of the language, and we use it here to support the natural listing of +requirements. To see what it compiles down to, look at the Java version. Each ``"string" using (expression)`` statement +inside a ``requireThat`` turns into an assertion that the given expression is true, with an ``IllegalArgumentException`` +being thrown that contains the string if not. It's just another way to write out a regular assertion, but with the +English-language requirement being put front and center. -Next, we take one of two paths, depending on what the type of the command object is. +Next, we simply verify that the output state is actually present: a move is not allowed to delete the CP from the ledger. +The grouping logic already ensured that the details are identical and haven't been changed, save for the public key of +the owner. -If the command is a ``Move`` command, then we simply verify that the output state is actually present: a move is not -allowed to delete the CP from the ledger. The grouping logic already ensured that the details are identical and haven't -been changed, save for the public key of the owner. +**If the command is a ``Redeem`` command, then the requirements are more complex:** -If the command is a ``Redeem`` command, then the requirements are more complex: - -1. We want to see that the face value of the CP is being moved as a cash claim against some party, that is, the +1. We still check there is a CP input state. +2. We want to see that the face value of the CP is being moved as a cash claim against some party, that is, the issuer of the CP is really paying back the face value. -2. The transaction must be happening after the maturity date. -3. The commercial paper must *not* be propagated by this transaction: it must be deleted, by the group having no +3. The transaction must be happening after the maturity date. +4. The commercial paper must *not* be propagated by this transaction: it must be deleted, by the group having no output state. This prevents the same CP being considered redeemable multiple times. To calculate how much cash is moving, we use the ``sumCashBy`` utility function. Again, this is an extension function, @@ -551,8 +353,9 @@ represented in the outputs! So we can see that this contract imposes a limitatio transaction: you are not allowed to move currencies in the same transaction that the CP does not involve. This limitation could be addressed with better APIs, if it were to be a real limitation. -Finally, we support an ``Issue`` command, to create new instances of commercial paper on the ledger. It likewise -enforces various invariants upon the issuance. +**Finally, we support an ``Issue`` command, to create new instances of commercial paper on the ledger.** + +It likewise enforces various invariants upon the issuance, such as, there must be one output CP state, for instance. This contract is simple and does not implement all the business logic a real commercial paper lifecycle management program would. For instance, there is no logic requiring a signature from the issuer for redemption: @@ -577,7 +380,6 @@ error message. Testing contracts with this domain specific language is covered in the separate tutorial, :doc:`tutorial-test-dsl`. - Adding a generation API to your contract ---------------------------------------- @@ -602,13 +404,11 @@ a method to wrap up the issuance process: .. container:: codeset - .. sourcecode:: kotlin - - fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, - notary: Party): TransactionBuilder { - val state = State(issuance, issuance.party, faceValue, maturityDate) - return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 We take a reference that points to the issuing party (i.e. the caller) and which can contain any internal bookkeeping/reference numbers that we may require. The reference field is an ideal place to put (for example) a @@ -625,12 +425,33 @@ outputs and commands to it and is designed to be passed around, potentially betw The function we define creates a ``CommercialPaper.State`` object that mostly just uses the arguments we were given, but it fills out the owner field of the state to be the same public key as the issuing party. +We then combine the ``CommercialPaper.State`` object with a reference to the ``CommercialPaper`` contract, which is +defined inside the contract itself + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 + +This value, which is the fully qualified class name of the contract, tells the Corda platform where to find the contract +code that should be used to validate a transaction containing an output state of this contract type. Typically the contract +code will be included in the transaction as an attachment (see :doc:`tutorial-attachments`). + The returned partial transaction has a ``Command`` object as a parameter. This is a container for any object that implements the ``CommandData`` interface, along with a list of keys that are expected to sign this transaction. In this case, issuance requires that the issuing party sign, so we put the key of the party there. The ``TransactionBuilder`` has a convenience ``withItems`` method that takes a variable argument list. You can pass in -any ``StateAndRef`` (input), ``ContractState`` (output) or ``Command`` objects and it'll build up the transaction +any ``StateAndRef`` (input), ``StateAndContract`` (output) or ``Command`` objects and it'll build up the transaction for you. There's one final thing to be aware of: we ask the caller to select a *notary* that controls this state and @@ -643,13 +464,11 @@ What about moving the paper, i.e. reassigning ownership to someone else? .. container:: codeset - .. sourcecode:: kotlin - - fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { - tx.addInputState(paper) - tx.addOutputState(paper.state.data.withOwner(newOwner)) - tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey)) - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 4 Here, the method takes a pre-existing ``TransactionBuilder`` and adds to it. This is correct because typically you will want to combine a sale of CP atomically with the movement of some other asset, such as cash. So both @@ -657,7 +476,7 @@ generate methods should operate on the same transaction. You can see an example for the commercial paper contract. The paper is given to us as a ``StateAndRef`` object. This is exactly what it sounds like: -a small object that has a (copy of) a state object, and also the (txhash, index) that indicates the location of this +a small object that has a (copy of a) state object, and also the ``(txhash, index)`` that indicates the location of this state on the ledger. We add the existing paper state as an input, the same paper state with the owner field adjusted as an output, @@ -668,15 +487,11 @@ Finally, we can do redemption. .. container:: codeset - .. sourcecode:: kotlin - - @Throws(InsufficientBalanceException::class) - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub) { - // Add the cash movement using the states in our vault. - Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) - tx.addInputState(paper) - tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 Here we can see an example of composing contracts together. When an owner wishes to redeem the commercial paper, the issuer (i.e. the caller) must gather cash from its vault and send the face value to the owner of the paper. @@ -747,15 +562,14 @@ Encumbrances All contract states may be *encumbered* by up to one other state, which we call an **encumbrance**. -The encumbrance state, if present, forces additional controls over the encumbered state, since the encumbrance state contract -will also be verified during the execution of the transaction. For example, a contract state could be encumbered -with a time-lock contract state; the state is then only processable in a transaction that verifies that the time -specified in the encumbrance time-lock has passed. +The encumbrance state, if present, forces additional controls over the encumbered state, since the encumbrance state +contract will also be verified during the execution of the transaction. For example, a contract state could be +encumbered with a time-lock contract state; the state is then only processable in a transaction that verifies that the +time specified in the encumbrance time-lock has passed. -The encumbered state refers to its encumbrance by index, and the referred encumbrance state -is an output state in a particular position on the same transaction that created the encumbered state. Note that an -encumbered state that is being consumed must have its encumbrance consumed in the same transaction, otherwise the -transaction is not valid. +The encumbered state refers to its encumbrance by index, and the referred encumbrance state is an output state in a +particular position on the same transaction that created the encumbered state. Note that an encumbered state that is +being consumed must have its encumbrance consumed in the same transaction, otherwise the transaction is not valid. The encumbrance reference is optional in the ``ContractState`` interface: diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 3eec271545..7073a67af3 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -17,7 +17,7 @@ if: We will deploy the CorDapp on 4 test nodes: -* **Controller**, which hosts the network map service and a validating notary service +* **Controller**, which hosts a validating notary service * **PartyA** * **PartyB** * **PartyC** @@ -36,16 +36,6 @@ We need to download the example CorDapp from GitHub. * Change directories to the freshly cloned repo: ``cd cordapp-example`` -* We want to work off the latest Milestone release - - * To enumerate all the Milestone releases, run: ``git tag`` - - * Check out the latest (highest-numbered) Milestone release using: ``git checkout [tag_name]`` - - Where ``tag_name`` is the name of the tag you wish to checkout - - * Gradle will grab all the required dependencies for you from `Maven `_ - .. note:: If you wish to build off the latest, unstable version of the codebase, follow the instructions in :doc:`building against Master ` instead. @@ -220,9 +210,9 @@ Building the example CorDapp . nodeName ├── corda.jar ├── node.conf - └── plugins + └── cordapps - ``corda.jar`` is the Corda runtime, ``plugins`` contains our node's CorDapps, and the node's configuration is + ``corda.jar`` is the Corda runtime, ``cordapps`` contains our node's CorDapps, and the node's configuration is given by ``node.conf`` Running the example CorDapp @@ -286,7 +276,7 @@ IntelliJ The node driver defined in ``/src/test/kotlin/com/example/Main.kt`` allows you to specify how many nodes you would like to run and the configuration settings for each node. For the example CorDapp, the driver starts up four nodes - and adds an RPC user for all but the "Controller" node (which serves as the notary and network map service): + and adds an RPC user for all but the "Controller" node (which serves as the notary): .. sourcecode:: kotlin @@ -499,9 +489,6 @@ You must now edit the configuration file for each node, including the controller and make the following changes: * Change the Artemis messaging address to the machine's IP address (e.g. ``p2pAddress="10.18.0.166:10006"``) -* Change the network map service's address to the IP address of the machine where the controller node is running - (e.g. ``networkMapService { address="10.18.0.166:10002" legalName="O=Controller,L=London,C=GB" ``). The controller - will not have the ``networkMapService`` configuration entry After starting each node, the nodes will be able to see one another and agree IOUs among themselves. diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index 190d51533a..28d5dc1158 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -1,17 +1,17 @@ .. highlight:: kotlin -Writing a custom notary service -=============================== +Writing a custom notary service (experimental) +============================================== -.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently, +.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently, customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to implement a custom database connector (or use a separate database for the notary), and use a custom configuration file. Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it -with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The only requirement -is that the class provide a constructor with a single parameter of type ``ServiceHub``. +with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary +service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 1 :end-before: END 1 @@ -20,13 +20,16 @@ The next step is to write a notary service flow. You are free to copy and modify as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the ``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 2 :end-before: END 2 -To ensure the custom notary is installed and advertised by the node, specify it in the configuration file: +To enable the service, add the following to the node configuration: .. parsed-literal:: - extraAdvertisedServiceIds : ["corda.notary.validating.mycustom"] + notary : { + validating : true # Set to false if your service is non-validating + custom : true + } \ No newline at end of file diff --git a/docs/source/tutorial-integration-testing.rst b/docs/source/tutorial-integration-testing.rst index 0cbed18a15..f2e410d347 100644 --- a/docs/source/tutorial-integration-testing.rst +++ b/docs/source/tutorial-integration-testing.rst @@ -4,14 +4,14 @@ Integration testing Integration testing involves bringing up nodes locally and testing invariants about them by starting flows and inspecting their state. -In this tutorial we will bring up three nodes Alice, Bob and a -Notary. Alice will issue Cash to Bob, then Bob will send this Cash +In this tutorial we will bring up three nodes - Alice, Bob and a +notary. Alice will issue cash to Bob, then Bob will send this cash back to Alice. We will see how to test some simple deterministic and nondeterministic invariants in the meantime. -(Note that this example where Alice is self-issuing Cash is purely for -demonstration purposes, in reality Cash would be issued by a bank and -subsequently passed around.) +.. note:: This example where Alice is self-issuing cash is purely for + demonstration purposes, in reality, cash would be issued by a bank + and subsequently passed around. In order to spawn nodes we will use the Driver DSL. This DSL allows one to start up node processes from code. It manages a network map @@ -21,53 +21,55 @@ service and safe shutting down of nodes in the background. :language: kotlin :start-after: START 1 :end-before: END 1 + :dedent: 8 -The above code creates a ``User`` permissioned to start the -``CashFlow`` protocol. It then starts up Alice and Bob with this user, -allowing us to later connect to the nodes. +The above code starts three nodes: -Then the notary is started up. Note that we need to add -``ValidatingNotaryService`` as an advertised service in order for this -node to serve notary functionality. This is also where flows added in -plugins should be specified. Note also that we won't connect to the -notary directly, so there's no need to pass in the test ``User``. +* Alice, who has user permissions to start the ``CashIssueFlow`` and + ``CashPaymentFlow`` flows +* Bob, who only has user permissions to start the ``CashPaymentFlow`` +* A notary that offers a ``ValidatingNotaryService``. We won't connect + to the notary directly, so there's no need to provide a ``User`` The ``startNode`` function returns a future that completes once the node is fully started. This allows starting of the nodes to be parallel. We wait on these futures as we need the information -returned; their respective ``NodeHandles`` s. After getting the handles we -wait for both parties to register with the network map to ensure we don't -have race conditions with network map registration. +returned; their respective ``NodeHandles`` s. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 2 :end-before: END 2 + :dedent: 12 -Next we connect to Alice and Bob respectively from the test process -using the test user we created. Then we establish RPC links that allow -us to start flows and query state. +After getting the handles we wait for both parties to register with +the network map to ensure we don't have race conditions with network +map registration. Next we connect to Alice and Bob respectively from +the test process using the test user we created. Then we establish RPC +links that allow us to start flows and query state. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 3 :end-before: END 3 + :dedent: 12 We will be interested in changes to Alice's and Bob's vault, so we query a stream of vault updates from each. -Now that we're all set up we can finally get some Cash action going! +Now that we're all set up we can finally get some cash action going! .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 4 :end-before: END 4 + :dedent: 12 The first loop creates 10 threads, each starting a ``CashFlow`` flow on the Alice node. We specify that we want to issue ``i`` dollars to -Bob, using the Notary as the notary responsible for notarising the +Bob, setting our notary as the notary responsible for notarising the created states. Note that no notarisation will occur yet as we're not -spending any states, only entering new ones to the ledger. +spending any states, only creating new ones on the ledger. We started the flows from different threads for the sake of the tutorial, to demonstrate how to test non-determinism, which is what @@ -76,20 +78,22 @@ the ``expectEvents`` block does. The Expect DSL allows ordering constraints to be checked on a stream of events. The above code specifies that we are expecting 10 updates to be emitted on the ``bobVaultUpdates`` stream in unspecified order -(this is what the ``parallel`` construct does). We specify a +(this is what the ``parallel`` construct does). We specify an (otherwise optional) ``match`` predicate to identify specific updates we are interested in, which we then print. If we run the code written so far we should see 4 nodes starting up -(Alice,Bob,Notary + implicit Network Map service), then 10 logs of Bob -receiving 1,2,...10 dollars from Alice in some unspecified order. +(Alice, Bob, the notary and an implicit Network Map service), then +10 logs of Bob receiving 1,2,...10 dollars from Alice in some unspecified +order. -Next we want Bob to send this Cash back to Alice. +Next we want Bob to send this cash back to Alice. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 5 :end-before: END 5 + :dedent: 12 This time we'll do it sequentially. We make Bob pay 1,2,..10 dollars to Alice in order. We make sure that a the ``CashFlow`` has finished @@ -109,8 +113,7 @@ To run the complete test you can open ``example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt`` from IntelliJ and run the test, or alternatively use gradle: - .. sourcecode:: bash # Run example-code integration tests - ./gradlew docs/source/example-code:integrationTest -i + ./gradlew docs/source/example-code:integrationTest -i \ No newline at end of file diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst index 5187aaac42..fe665bf3bf 100644 --- a/docs/source/tutorial-tear-offs.rst +++ b/docs/source/tutorial-tear-offs.rst @@ -1,80 +1,58 @@ Transaction tear-offs ===================== -Example of usage ----------------- -Let’s focus on a code example. We want to construct a transaction with commands containing interest rate fix data as in: -:doc:`oracles`. After construction of a partial transaction, with included ``Fix`` commands in it, we want to send it -to the Oracle for checking and signing. To do so we need to specify which parts of the transaction are going to be -revealed. That can be done by constructing filtering function over fields of ``WireTransaction`` of type ``(Any) -> -Boolean``. +Suppose we want to construct a transaction that includes commands containing interest rate fix data as in +:doc:`oracles`. Before sending the transaction to the oracle to obtain its signature, we need to filter out every part +of the transaction except for the ``Fix`` commands. + +To do so, we need to create a filtering function that specifies which fields of the transaction should be included. +Each field will only be included if the filtering function returns `true` when the field is passed in as input. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 - val partialTx = ... - val oracle: Party = ... - fun filtering(elem: Any): Boolean { - return when (elem) { - is Command -> oracleParty.owningKey in elem.signers && elem.value is Fix - else -> false - } - } - -Assuming that we already assembled partialTx with some commands and know the identity of Oracle service, we construct -filtering function over commands - ``filtering``. It performs type checking and filters only ``Fix`` commands as in -IRSDemo example. Then we can construct ``FilteredTransaction``: +We can now use our filtering function to construct a ``FilteredTransaction``: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 - val wtx: WireTransaction = partialTx.toWireTransaction() - val ftx: FilteredTransaction = wtx.buildFilteredTransaction(filtering) - -In the Oracle example this step takes place in ``RatesFixFlow`` by overriding ``filtering`` function, see: +In the Oracle example this step takes place in ``RatesFixFlow`` by overriding the ``filtering`` function. See :ref:`filtering_ref`. -``FilteredTransaction`` holds ``filteredLeaves`` (data that we wanted to reveal) and Merkle branch for them. +Both ``WireTransaction`` and ``FilteredTransaction`` inherit from ``TraversableTransaction``, so access to the +transaction components is exactly the same. Note that unlike ``WireTransaction``, +``FilteredTransaction`` only holds data that we wanted to reveal (after filtering). .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 - // Direct accsess to included commands, inputs, outputs, attachments etc. - val cmds: List = ftx.filteredLeaves.commands - val ins: List = ftx.filteredLeaves.inputs - val timeWindow: TimeWindow? = ftx.filteredLeaves.timeWindow - ... +The following code snippet is taken from ``NodeInterestRates.kt`` and implements a signing part of an Oracle. .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 - -Above code snippet is taken from ``NodeInterestRates.kt`` file and implements a signing part of an Oracle. You can -check only leaves using ``leaves.checkWithFun { check(it) }`` and then verify obtained ``FilteredTransaction`` to see -if data from ``PartialMerkleTree`` belongs to ``WireTransaction`` with provided ``id``. All you need is the root hash -of the full transaction: - -.. container:: codeset - - .. sourcecode:: kotlin - - if (!ftx.verify(merkleRoot)){ - throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") - } - -Or combine the two steps together: - -.. container:: codeset - - .. sourcecode:: kotlin - - ftx.verifyWithFunction(merkleRoot, ::check) + :dedent: 8 .. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove - leaves. However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle. + components (leaves). However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle. As signing is done now over the Merkle root hash, the service signs all commands of given type, even though it didn't see - all of them. This issue will be handled after implementing partial signatures. + all of them. In the case however where all of the commands should be visible to an Oracle, one can type ``ftx.checkAllComponentsVisible(COMMANDS_GROUP)`` before invoking ``ftx.verify``. + ``checkAllComponentsVisible`` is using a sophisticated underlying partial Merkle tree check to guarantee that all of + the components of a particular group that existed in the original ``WireTransaction`` are included in the received + ``FilteredTransaction``. \ No newline at end of file diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index 51aa2789c8..ce66251a5c 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -52,27 +52,17 @@ We will start with defining helper function that returns a ``CommercialPaper`` s .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 - fun getPaper(): ICommercialPaperState = CommercialPaper.State( - issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP, - faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), - maturityDate = TEST_TX_TIME + 7.days - ) - - .. sourcecode:: java - - private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{123}); - - private ICommercialPaperState getPaper() { - return new JavaCommercialPaper.State( - getMEGA_CORP().ref(defaultRef), - getMEGA_CORP(), - issuedBy(DOLLARS(1000), getMEGA_CORP().ref(defaultRef)), - getTEST_TX_TIME().plus(7, ChronoUnit.DAYS) - ); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 It's a ``CommercialPaper`` issued by ``MEGA_CORP`` with face value of $1000 and maturity date in 7 days. @@ -87,7 +77,7 @@ Let's add a ``CommercialPaper`` transaction: val inState = getPaper() ledger { transaction { - input(inState) + input(CommercialPaper.CP_PROGRAM_ID) { inState } } } } @@ -124,70 +114,38 @@ The above code however doesn't compile: Error:(35, 27) java: incompatible types: bad return type in lambda expression missing return value -This is deliberate: The DSL forces us to specify either ``this.verifies()`` or ``this `fails with` "some text"`` on the +This is deliberate: The DSL forces us to specify either ``verifies()`` or ```fails with`("some text")`` on the last line of ``transaction``: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 - @Test - fun simpleCP() { - val inState = getPaper() - ledger { - transaction { - input(inState) - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCP() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 Let's take a look at a transaction that fails. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 - @Test - fun simpleCPMove() { - val inState = getPaper() - ledger { - transaction { - input(inState) - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCPMove() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 When run, that code produces the following error: @@ -202,75 +160,37 @@ When run, that code produces the following error: net.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalStateException: the state is propagated The transaction verification failed, because we wanted to move paper but didn't specify an output - but the state should be propagated. -However we can specify that this is an intended behaviour by changing ``this.verifies()`` to ``this `fails with` "the state is propagated"``: +However we can specify that this is an intended behaviour by changing ``verifies()`` to ```fails with`("the state is propagated")``: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 4 - @Test - fun simpleCPMoveFails() { - val inState = getPaper() - ledger { - transaction { - input(inState) - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this `fails with` "the state is propagated" - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCPMoveFails() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.failsWith("the state is propagated"); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 4 We can continue to build the transaction until it ``verifies``: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 - @Test - fun simpleCPMoveSuccess() { - val inState = getPaper() - ledger { - transaction { - input(inState) - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this `fails with` "the state is propagated" - output("alice's paper") { inState `owned by` ALICE_PUBKEY } - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCPMoveSuccess() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - tx.failsWith("the state is propagated"); - tx.output("alice's paper", inState.withOwner(getALICE_PUBKEY())); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 ``output`` specifies that we want the input state to be transferred to ``ALICE`` and ``command`` adds the ``Move`` command itself, signed by the current owner of the input state, ``MEGA_CORP_PUBKEY``. @@ -283,45 +203,17 @@ What should we do if we wanted to test what happens when the wrong party signs t .. container:: codeset - .. sourcecode:: kotlin - - @Test - fun `simple issuance with tweak`() { - ledger { - transaction { - output("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. - tweak { - command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this `fails with` "output states are issued by a command signer" - } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleIssuanceWithTweak() { - ledger(l -> { - l.transaction(tx -> { - tx.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. - tx.tweak(tw -> { - tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue()); - tw.timestamp(getTEST_TX_TIME()); - return tw.failsWith("output states are issued by a command signer"); - }); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 4 + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 4 ``tweak`` creates a local copy of the transaction. This makes possible to locally "ruin" the transaction while not modifying the original one, allowing testing of different error conditions. @@ -332,39 +224,17 @@ ledger with a single transaction: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 - @Test - fun `simple issuance with tweak and top level transaction`() { - transaction { - output("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. - tweak { - command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this `fails with` "output states are issued by a command signer" - } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - } - - .. sourcecode:: java - - @Test - public void simpleIssuanceWithTweakTopLevelTx() { - transaction(tx -> { - tx.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. - tx.tweak(tw -> { - tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue()); - tw.timestamp(getTEST_TX_TIME()); - return tw.failsWith("output states are issued by a command signer"); - }); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 Chaining transactions --------------------- @@ -373,75 +243,20 @@ Now that we know how to define a single transaction, let's look at how to define .. container:: codeset - .. sourcecode:: kotlin - - @Test - fun `chain commercial paper`() { - val issuer = MEGA_CORP.ref(123) - - ledger { - unverifiedTransaction { - output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) - } - - // Some CP is issued onto the ledger by MegaCorp. - transaction("Issuance") { - output("paper") { getPaper() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - - transaction("Trade") { - input("paper") - input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } - command(ALICE_PUBKEY) { Cash.Commands.Move() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void chainCommercialPaper() { - PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output("alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); - return Unit.INSTANCE; - }); - - // Some CP is issued onto the ledger by MegaCorp. - l.transaction("Issuance", tx -> { - tx.output("paper", getPaper()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - - l.transaction("Trade", tx -> { - tx.input("paper"); - tx.input("alice's $900"); - tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); - tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 4 + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 4 In this example we declare that ``ALICE`` has $900 but we don't care where from. For this we can use -``unverifiedTransaction``. Note how we don't need to specify ``this.verifies()``. +``unverifiedTransaction``. Note how we don't need to specify ``verifies()``. Notice that we labelled output with ``"alice's $900"``, also in transaction named ``"Issuance"`` we labelled a commercial paper with ``"paper"``. Now we can subsequently refer to them in other transactions, e.g. @@ -450,187 +265,36 @@ by ``input("alice's $900")`` or ``"paper".output()``. The last transaction named ``"Trade"`` exemplifies simple fact of selling the ``CommercialPaper`` to Alice for her $900, $100 less than the face value at 10% interest after only 7 days. -We can also test whole ledger calling ``this.verifies()`` and ``this.fails()`` on the ledger level. +We can also test whole ledger calling ``verifies()`` and ``fails()`` on the ledger level. To do so let's create a simple example that uses the same input twice: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + :dedent: 4 - @Test - fun `chain commercial paper double spend`() { - val issuer = MEGA_CORP.ref(123) - ledger { - unverifiedTransaction { - output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) - } - - // Some CP is issued onto the ledger by MegaCorp. - transaction("Issuance") { - output("paper") { getPaper() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - transaction("Trade") { - input("paper") - input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } - command(ALICE_PUBKEY) { Cash.Commands.Move() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - - transaction { - input("paper") - // We moved a paper to another pubkey. - output("bob's paper") { "paper".output() `owned by` BOB_PUBKEY } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - - this.fails() - } - } - - .. sourcecode:: java - - @Test - public void chainCommercialPaperDoubleSpend() { - PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output("alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); - return Unit.INSTANCE; - }); - - // Some CP is issued onto the ledger by MegaCorp. - l.transaction("Issuance", tx -> { - tx.output("paper", getPaper()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - - l.transaction("Trade", tx -> { - tx.input("paper"); - tx.input("alice's $900"); - tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); - tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - - l.transaction(tx -> { - tx.input("paper"); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - // We moved a paper to other pubkey. - tx.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - l.fails(); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + :dedent: 4 The transactions ``verifies()`` individually, however the state was spent twice! That's why we need the global ledger -verification (``this.fails()`` at the end). As in previous examples we can use ``tweak`` to create a local copy of the whole ledger: +verification (``fails()`` at the end). As in previous examples we can use ``tweak`` to create a local copy of the whole ledger: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 10 + :end-before: DOCEND 10 + :dedent: 4 - @Test - fun `chain commercial tweak`() { - val issuer = MEGA_CORP.ref(123) - ledger { - unverifiedTransaction { - output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) - } - - // Some CP is issued onto the ledger by MegaCorp. - transaction("Issuance") { - output("paper") { getPaper() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - transaction("Trade") { - input("paper") - input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } - command(ALICE_PUBKEY) { Cash.Commands.Move() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - - tweak { - transaction { - input("paper") - // We moved a paper to another pubkey. - output("bob's paper") { "paper".output() `owned by` BOB_PUBKEY } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - this.fails() - } - - this.verifies() - } - } - - .. sourcecode:: java - - @Test - public void chainCommercialPaperTweak() { - PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output("alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); - return Unit.INSTANCE; - }); - - // Some CP is issued onto the ledger by MegaCorp. - l.transaction("Issuance", tx -> { - tx.output("paper", getPaper()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - - l.transaction("Trade", tx -> { - tx.input("paper"); - tx.input("alice's $900"); - tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); - tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - - l.tweak(lw -> { - lw.transaction(tx -> { - tx.input("paper"); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - // We moved a paper to another pubkey. - tx.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - lw.fails(); - return Unit.INSTANCE; - }); - l.verifies(); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 10 + :end-before: DOCEND 10 + :dedent: 4 diff --git a/docs/source/tutorials-index.rst b/docs/source/tutorials-index.rst index f9ffad2dd3..f14be4385f 100644 --- a/docs/source/tutorials-index.rst +++ b/docs/source/tutorials-index.rst @@ -15,8 +15,8 @@ Tutorials flow-state-machines flow-testing running-a-notary - using-a-notary oracles + tutorial-custom-notary tutorial-tear-offs tutorial-attachments event-scheduling \ No newline at end of file diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst new file mode 100644 index 0000000000..0040c48e05 --- /dev/null +++ b/docs/source/upgrade-notes.rst @@ -0,0 +1,351 @@ +Upgrade notes +============= + +These notes provide helpful instructions to upgrade your Corda Applications (CorDapps) from previous versions, starting +from our first public Beta (:ref:`Milestone 12 `), to :ref:`V1.0 ` + +General +------- +Always remember to update the version identifiers in your project gradle file: + +.. sourcecode:: shell + + ext.corda_release_version = '1.0.0' + ext.corda_gradle_plugins_version = '1.0.0' + +It may be necessary to update the version of major dependencies: + +.. sourcecode:: shell + + ext.kotlin_version = '1.1.4' + ext.quasar_version = '0.7.9' + +Please consult the relevant release notes of the release in question. If not specified, you may assume the +versions you are currently using are still in force. + +We also strongly recommend cross referencing with the :doc:`changelog` to confirm changes. + +:ref:`Milestone 14 ` +------------ + +Build +^^^^^ + +* MockNetwork has moved. + + A new test driver module dependency needs to be including in your project: `corda-node-driver`. To continue using the + mock network for testing, add the following entry to your gradle build file: + +.. sourcecode:: shell + + testCompile "net.corda:corda-node-driver:$corda_release_version" + +.. note:: you may only need `testCompile "net.corda:corda-test-utils:$corda_release_version"` if not using the Driver DSL. + +Configuration +^^^^^^^^^^^^^ + +* ``CordaPluginRegistry`` has been removed. + The one remaining configuration item ``customizeSerialisation``, which defined a optional whitelist of types for use in + object serialization, has been replaced with the ``SerializationWhitelist`` interface which should be implemented to + define a list of equivalent whitelisted classes. + You will need to rename your services resource file to the new class name: + 'resources/META-INF/services/net.corda.core.node.CordaPluginRegistry' becomes 'resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist' + An associated property on ``MockNode`` was renamed from ``testPluginRegistries`` to ``testSerializationWhitelists``. + In general, the ``@CordaSerializable`` annotation is the preferred method for whitelisting as described in :doc:`serialization` + +Missing imports +^^^^^^^^^^^^^^^ + +Use the automatic imports feature of IntelliJ to intelligently resolve the new imports. + +* Missing imports for contract types. + + CommercialPaper and Cash are now contained within the `finance` module, as are associated helpers functions. + For example: + ``import net.corda.contracts.ICommercialPaperState`` becomes ``import net.corda.finance.contracts.ICommercialPaperState`` + + ``import net.corda.contracts.asset.sumCashBy`` becomes ``import net.corda.finance.utils.sumCashBy`` + + ``import net.corda.core.contracts.DOLLARS`` becomes ``import net.corda.finance.DOLLARS`` + + ``import net.corda.core.contracts.issued by`` becomes ``import net.corda.finance.issued by`` + + ``import net.corda.contracts.asset.Cash`` becomes ``import net.corda.finance.contracts.asset.Cash`` + +* Missing imports for utility functions. + + Many common types and helper methods have been consolidated into `net.corda.core.utilities` package. + For example: + ``import net.corda.core.crypto.commonName`` becomes ``import net.corda.core.utilities.commonName`` + + ``import net.corda.core.crypto.toBase58String`` becomes ``import net.corda.core.utilities.toBase58String`` + + ``import net.corda.core.getOrThrow`` becomes ``import net.corda.core.utilities.getOrThrow`` + +* Missing flow imports. + + In general all reusable library flows are contained within the **core** API `net.corda.core.flows` package. + Financial domain library flows are contained within the **finance** module `net.corda.finance.flows` package. + Other flows that have moved include: + + ``import net.corda.core.flows.ResolveTransactionsFlow`` becomes ``import net.corda.core.internal.ResolveTransactionsFlow`` + +Core data structures +^^^^^^^^^^^^^^^^^^^^ + +* Missing Contract override. + + The contract interace attribute ``legalContractReference`` has been removed, and replaced by + the optional annotation ``@LegalProseReference(uri = "")`` + +* Unresolved reference. + + Calls to ``AuthenticatedObject`` are replaced by ``CommandWithParties`` + +* Overrides nothing: ``isRelevant`` in ``LinearState``. + + Removed the concept of relevancy from ``LinearState``. A ``ContractState``'s relevance to the vault is now resolved + internally; the vault will process any transaction from a flow which is not derived from transaction resolution verification. + The notion of relevancy is subject to further improvements to enable a developer to control what state the vault thinks + are relevant. + +* Calls to ``txBuilder.toLedgerTransaction()`` now requires a serviceHub parameter. + + Used by the new Contract Constraints functionality to validate and resolve attachments. + +Flow framework +^^^^^^^^^^^^^^ + +* Flow session deprecations + + ``FlowLogic`` communication has been upgraded to use functions on ``FlowSession`` as the base for communication + between nodes. + + * Calls to ``send()``, ``receive()`` and ``sendAndReceive()`` on FlowLogic should be replaced with calls + to the function of the same name on ``FlowSession``. Note that the replacement functions do not take in a destination + parameter, as this is defined in the session. + + * Initiated flows now take in a ``FlowSession`` instead of ``Party`` in their constructor. If you need to access the + counterparty identity, it is in the ``counterparty`` property of the flow session. + + See ``FlowSession`` for step by step instructions on porting existing flows to use the new mechanism. + +* ``FinalityFlow`` now returns a single ``SignedTransaction``, instead of a ``List`` + +* ``TransactionKeyFlow`` renamed to ``SwapIdentitiesFlow`` + + Note that ``SwapIdentitiesFlow`` must be imported from the *confidential-identities** package ''net.corda.confidential'' + +Node services (ServiceHub) +^^^^^^^^^^^^^^ + +* VaultQueryService: unresolved reference to `vaultQueryService`. + + Replace all references to ``.vaultQueryService`` with ``.vaultService``. + Previously there were two vault APIs. Now there is a single unified API with the same functions: ``VaultService``. + +* ``serviceHub.myInfo.legalIdentity`` no longer exists; use the ``ourIdentity`` property of the flow instead. + + ``FlowLogic.ourIdentity`` has been introduced as a shortcut for retrieving our identity in a flow + +* ``getAnyNotary`` is gone - use ``serviceHub.networkMapCache.notaryIdentities[0]`` instead + + Note: ongoing work to support multiple notary identities is still in progress. + +* ``ServiceHub.networkMapUpdates`` is replaced by ``ServiceHub.networkMapFeed`` + +* ``ServiceHub.partyFromX500Name`` is replaced by ``ServiceHub.wellKnownPartyFromX500Name`` + Note: A "well known" party is one that isn't anonymous and this change was motivated by the confidential identities work. + +RPC Client +^^^^^^^^^^ + +* Missing API methods on `CordaRPCOps` interface. + + * Calls to ``verifiedTransactionsFeed()`` and ``verifiedTransactions()`` have been replaced with: + ``internalVerifiedTransactionsSnapshot()`` and ``internalVerifiedTransactionsFeed()`` respectively + + This is in preparation for the planned integration of Intel SGX™, which will encrypt the transactions feed. + Apps that use this API will not work on encrypted ledgers: you should probably be using the vault query API instead. + + * Accessing the `networkMapCache` via ``services.nodeInfo().legalIdentities`` returns a list of identities. + The first element in the list is the Party object referring to a node's single identity. + + This is in preparation for allowing a node to host multiple separate identities in future. + +Testing +^^^^^^^ + +Please note that `Clauses` have been removed completely as of V1.0. +We will be revisiting this capability in a future release. + +* CorDapps must be explicitly registered in ``MockNetwork`` unit tests. + + This is done by calling ``setCordappPackages``, an extension helper function in the ``net.corda.testing`` package, + on the first line of your `@Before` method. This takes a variable number of `String` arguments which should be the + package names of the CorDapps containing the contract verification code you wish to load. + You should unset CorDapp packages in your `@After` method by using ``unsetCordappPackages()`` after `stopNodes()`. + +* CorDapps must be explicitly registered in ``DriverDSL`` and ``RPCDriverDSL`` integration tests. + + Similarly, you must also register package names of the CorDapps containing the contract verification code you wish to load + using the ``extraCordappPackagesToScan: List`` constructor parameter of the driver DSL. + +Finance +^^^^^^^ + +* `FungibleAsset` interface simplification. + + The ``FungibleAsset`` interface has been made simpler. The ``Commands`` grouping interface + that included the ``Move``, ``Issue`` and ``Exit`` interfaces have all been removed, while the ``move`` function has + been renamed to ``withNewOwnerAndAmount`` to be consistent with the ``withNewOwner`` function of the ``OwnableState``. + + The following errors may be reported: + + * override nothing (FungibleAsset): `move` + * not a subtype of overridden FungibleAsset: `withNewOwner` + * no longer need to override `override val contractHash: SecureHash? = null` + * need to override `override val contract: Class? = null` + + +Miscellaneous +^^^^^^^^^^^^^ + +* ``args[0].parseNetworkHostAndPort()`` becomes ``NetworkHostAndPort.parse(args[0])`` + +* There is no longer a ``NodeInfo.advertisedServices`` property. + + The concept of advertised services has been removed from Corda. This is because it was vaguely defined and real world + apps would not typically select random, unknown counterparties from the network map based on self-declared capabilities. + We will introduce a replacement for this functionality, business networks, in a future release. + + For now, your should retrieve the service by legal name using ``NetworkMapCache.getNodeByLegalName``. + +Gotchas +^^^^^^^ + +* Beware to use the correct identity when issuing cash: + + The 3rd parameter to ``CashIssueFlow`` should be the ** notary ** (not the ** node identity **) + + +:ref:`Milestone 13 ` +------------ + +Core data structures +^^^^^^^^^^^^^^^^^^^^ + +* `TransactionBuilder` changes. + + Use convenience class ``StateAndContract`` instead of ``TransactionBuilder.withItems()`` for passing + around a state and its contract. + +* Transaction building DSL changes: + + * now need to explicitly pass the ContractClassName into all inputs and outputs. + * `ContractClassName` refers to the class containing the “verifier” method. + +* Contract verify method signature change. + + ``override fun verify(tx: TransactionForContract)`` becomes ``override fun verify(tx: LedgerTransaction)`` + +* No longer need to override Contract ``contract()`` function. + +Node services (ServiceHub) +^^^^^^^^^^^^^ + +* ServiceHub API method changes. + + ``services.networkMapUpdates().justSnapshot`` becomes ``services.networkMapSnapshot()`` + +Configuration +^^^^^^^^^^^^^ + +* No longer need to define ``CordaPluginRegistry`` and configure ``requiredSchemas`` + + Custom contract schemas are automatically detected at startup time by class path scanning. + For testing purposes, use the ``SchemaService`` method to register new custom schemas: + eg. ``services.schemaService.registerCustomSchemas(setOf(YoSchemaV1))`` + +Identity +^^^^^^^^ + +* Party names are now ``CordaX500Name``, not ``X500Name`` + + ``CordaX500Name`` specifies a predefined set of mandatory (organisation, locality, country) + and optional fields (commonName, organisationUnit, state) with validation checking. + Use new builder CordaX500Name.build(X500Name(target)) or, preferably, explicitly define X500Name parameters using + ``CordaX500Name`` constructor. + +Testing +^^^^^^^ + +* MockNetwork Testing. + + Mock nodes in node tests are now of type ``StartedNode``, rather than ``MockNode`` + MockNetwork now returns a BasketOf(>) + Must call internals on StartedNode to get MockNode: + a = nodes.partyNodes[0].internals + b = nodes.partyNodes[1].internals + +* Host and Port change. + + Use string helper function ``parseNetworkHostAndPort()`` to parse a URL on startup. + eg. ``val hostAndPort = args[0].parseNetworkHostAndPort()`` + +* The node driver parameters for starting a node have been reordered, and the node’s name needs to be given as an + ``CordaX500Name``, instead of using ``getX509Name`` + + +:ref:`Milestone 12 ` (First Public Beta) +----------------------------------- + +Core data structures +^^^^^^^^^^^^^^^^^^^^ + +* Transaction building + + You no longer need to specify the type of a ``TransactionBuilder`` as ``TransactionType.General`` + ``TransactionType.General.Builder(notary)`` becomes ``TransactionBuilder(notary)`` + +Build +^^^^^ + +* Gradle dependency reference changes. + + Module name has changed to include `corda` in the artifacts jar name: + +.. sourcecode:: shell + + compile "net.corda:core:$corda_release_version" -> compile "net.corda:corda-core:$corda_release_version" + compile "net.corda:finance:$corda_release_version" -> compile "net.corda:corda-finance:$corda_release_version" + compile "net.corda:jackson:$corda_release_version" -> compile "net.corda:corda-jackson:$corda_release_version" + compile "net.corda:node:$corda_release_version" -> compile "net.corda:corda-node:$corda_release_version" + compile "net.corda:rpc:$corda_release_version" -> compile "net.corda:corda-rpc:$corda_release_version" + +Node services (ServiceHub) +^^^^^^^^^^^^^ + +* ServiceHub API changes. + + ``services.networkMapUpdates()`` becomes ``services.networkMapFeed()`` + ``services.getCashBalances()`` becomes a helper method within the **finance** module contracts package: ``net.corda.finance.contracts.getCashBalances`` + +Finance +^^^^^^^ + +* Financial asset contracts (Cash, CommercialPaper, Obligations) are now a standalone CorDapp within the **finance** module. + + Need to import from respective package within `finance` module: + eg. ``net.corda.finance.contracts.asset.Cash`` + + Likewise, need to import associated asset flows from respective package within `finance` module: + eg. ``net.corda.finance.flows.CashIssueFlow`` + ``net.corda.finance.flows.CashIssueAndPaymentFlow`` + ``net.corda.finance.flows.CashExitFlow`` + +* Moved ``finance`` gradle project files into a ``net.corda.finance`` package namespace. + + This may require adjusting imports of Cash flow references and also of ``StartFlow`` permission in ``gradle.build`` files. + Associated flows (`Cash*Flow`, `TwoPartyTradeFlow`, `TwoPartyDealFlow`) must now be imported from this package. diff --git a/docs/source/using-a-notary.rst b/docs/source/using-a-notary.rst deleted file mode 100644 index d37d05809d..0000000000 --- a/docs/source/using-a-notary.rst +++ /dev/null @@ -1,138 +0,0 @@ -Using a notary service ----------------------- - -This tutorial describes how to assign a notary to a newly issued state, and how to get a transaction notarised by -obtaining a signature of the required notary. It assumes some familiarity with *flows* and how to write them, as described -in :doc:`flow-state-machines`. - -Assigning a notary -================== - -The first step is to choose a notary and obtain its identity. Identities of all notaries on the network are kept by -the :ref:`network-map-service`. The network map cache exposes two methods for obtaining a notary: - -.. sourcecode:: kotlin - - /** - * Gets a notary identity by the given name. - */ - fun getNotary(name: String): Party? - - /** - * Returns a notary identity advertised by any of the nodes on the network (chosen at random) - * - * @param type Limits the result to notaries of the specified type (optional) - */ - fun getAnyNotary(type: ServiceType? = null): Party? - -Currently notaries can only be differentiated by name and type, but in the future the network map service will be -able to provide more metadata, such as location or legal identities of the nodes operating it. - -Now, let's say we want to issue an asset and assign it to a notary named "Notary A". -The first step is to obtain the notary identity -- ``Party``: - -.. sourcecode:: kotlin - - val ourNotary: Party = serviceHub.networkMapCache.getNotary("Notary A") - -Then we initialise the transaction builder: - -.. sourcecode:: kotlin - - val builder: TransactionBuilder = TransactionBuilder(notary = ourNotary) - -For any output state we add to this transaction builder, ``ourNotary`` will be assigned as its notary. -Next we create a state object and assign ourselves as the owner. For this example we'll use a -``DummyContract.State``, which is a simple state that just maintains an integer and can change ownership. - -.. sourcecode:: kotlin - - val myIdentity = serviceHub.chooseIdentity() - val state = DummyContract.SingleOwnerState(magicNumber = 42, owner = myIdentity.owningKey) - -Then we add the state as the transaction output along with the relevant command. The state will automatically be assigned -to our previously specified "Notary A". - -.. sourcecode:: kotlin - - builder.addOutputState(state) - val createCommand = DummyContract.Commands.Create() - builder.addCommand(Command(createCommand, myIdentity)) - -We then sign the transaction, build and record it to our transaction storage: - -.. sourcecode:: kotlin - - val mySigningKey: PublicKey = serviceHub.chooseIdentity().owningKey - val issueTransaction = serviceHub.toSignedTransaction(issueTransaction, mySigningKey) - serviceHub.recordTransactions(issueTransaction) - -The transaction is recorded and we now have a state (asset) in possession that we can transfer to someone else. Note -that the issuing transaction does not need to be notarised, as it doesn't consume any input states. - -Notarising a transaction -======================== - -Following our example for the previous section, let's say we now want to transfer our issued state to Alice. - -First we obtain a reference to the state, which will be the input to our "move" transaction: - -.. sourcecode:: kotlin - - val stateRef = StateRef(txhash = issueTransaction.id, index = 0) - -Then we create a new state -- a copy of our state but with the owner set to Alice. This is a bit more involved so -we just use a helper that handles it for us. We also assume that we already have the ``Party`` for Alice, ``aliceParty``. - -.. sourcecode:: kotlin - - val inputState = StateAndRef(state, stateRef) - val moveTransactionBuilder = DummyContract.withNewOwnerAndAmount(inputState, newOwner = aliceParty.owningKey) - -The ``DummyContract.withNewOwnerAndAmount()`` method will a new transaction builder with our old state as the input, a new state -with Alice as the owner, and a relevant contract command for "move". - -Again we sign the transaction, and build it: - -.. sourcecode:: kotlin - - // We build it and add our default identity signature without checking if all signatures are present, - // Note we know that the notary signature is missing, so thie SignedTransaction is still partial. - val moveTransaction = serviceHub.toSignedTransaction(moveTransactionBuilder) - -Next we need to obtain a signature from the notary for the transaction to be valid. Prior to signing, the notary will -commit our old (input) state so it cannot be used again. - -To manually obtain a signature from a notary we can run the ``NotaryFlow.Client`` flow. The flow will work out -which notary needs to be called based on the input states (and the timestamp command, if it's present). - -.. sourcecode:: kotlin - - // The subFlow() helper is available within the context of a Flow - val notarySignature: DigitalSignature = subFlow(NotaryFlow.Client(moveTransaction)) - -.. note:: If our input state has already been consumed in another transaction, then ``NotaryFlow`` with throw a ``NotaryException`` - containing the conflict details: - - .. sourcecode:: kotlin - - /** Specifies the consuming transaction for the conflicting input state */ - data class Conflict(val stateHistory: Map) - - /** - * Specifies the transaction id, the position of the consumed state in the inputs, and - * the caller identity requesting the commit - */ - data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party) - - Conflict handling and resolution is currently the responsibility of the flow author. - -Note that instead of calling the notary directly, we would normally call ``FinalityFlow`` passing in the ``SignedTransaction`` -(including signatures from the participants) and a list of participants to notify. The flow will request a notary signature -if needed, record the notarised transaction, and then send a copy of the transaction to all participants for them to store. -``FinalityFlow`` delegates to ``NotaryFlow.Client`` followed by ``BroadcastTransactionFlow`` to do the -actual work of notarising and broadcasting the transaction. For example: - -.. sourcecode:: kotlin - - subFlow(FinalityFlow(moveTransaction, setOf(aliceParty)) diff --git a/docs/source/writing-cordapps.rst b/docs/source/writing-cordapps.rst index 13c14f314e..55eda0e091 100644 --- a/docs/source/writing-cordapps.rst +++ b/docs/source/writing-cordapps.rst @@ -44,7 +44,7 @@ The ``src`` directory of the Template CorDapp, where we define our CorDapp's sou │ └── resources │ ├── META-INF │ │ └── services - │ │ ├── net.corda.core.node.CordaPluginRegistry + │ │ ├── net.corda.core.serialization.SerializationWhitelist │ │ └── net.corda.webserver.services.WebServerPluginRegistry │ ├── certificates │ │ ├── sslkeystore.jks @@ -58,40 +58,4 @@ The ``src`` directory of the Template CorDapp, where we define our CorDapp's sou └── com └── template └── contract - └── TemplateTests.java - -Defining plugins ----------------- -Your CorDapp may need to define two types of plugins: - -* ``CordaPluginRegistry`` subclasses, which define additional serializable classes and vault schemas -* ``WebServerPluginRegistry`` subclasses, which define the APIs and static web content served by your CorDapp - -The fully-qualified class path of each ``CordaPluginRegistry`` subclass must then be added to the -``net.corda.core.node.CordaPluginRegistry`` file in the CorDapp's ``resources/META-INF/services`` folder. Meanwhile, -the fully-qualified class path of each ``WebServerPluginRegistry`` subclass must be added to the -``net.corda.webserver.services.WebServerPluginRegistry`` file, again in the CorDapp's ``resources/META-INF/services`` -folder. - -The ``CordaPluginRegistry`` class defines the following: - -* ``customizeSerialization``, which can be overridden to provide a list of the classes to be whitelisted for object - serialisation, over and above those tagged with the ``@CordaSerializable`` annotation. See :doc:`serialization` - -The ``WebServerPluginRegistry`` class defines the following: - -* ``webApis``, which can be overridden to return a list of JAX-RS annotated REST access classes. These classes will be - constructed by the bundled web server and must have a single argument constructor taking a ``CordaRPCOps`` object. - This will allow the API to communicate with the node process via the RPC interface. These web APIs will not be - available if the bundled web server is not started - -* ``staticServeDirs``, which can be overridden to map static web content to virtual paths and allow simple web demos to - be distributed within the CorDapp jars. This static content will not be available if the bundled web server is not - started - -* ``customizeJSONSerialization``, which can be overridden to register custom JSON serializers if required by the REST api. - - * The static web content itself should be placed inside the ``src/main/resources`` directory - -To learn about how to use gradle to build your cordapp against Corda and generate an artifact please read -:doc:`cordapp-build-systems`. \ No newline at end of file + └── TemplateTests.java \ No newline at end of file diff --git a/experimental/kryo-hook/build.gradle b/experimental/kryo-hook/build.gradle new file mode 100644 index 0000000000..cf52f3c9bb --- /dev/null +++ b/experimental/kryo-hook/build.gradle @@ -0,0 +1,53 @@ +buildscript { + // For sharing constants between builds + Properties constants = new Properties() + file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } + + ext.kotlin_version = constants.getProperty("kotlinVersion") + ext.javaassist_version = "3.12.1.GA" + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +repositories { + mavenLocal() + mavenCentral() + jcenter() +} + +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' +apply plugin: 'idea' + +description 'A javaagent to allow hooking into Kryo' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile "javassist:javassist:$javaassist_version" + compile "com.esotericsoftware:kryo:4.0.0" + compile "co.paralleluniverse:quasar-core:$quasar_version:jdk8" +} + +jar { + archiveName = "${project.name}.jar" + manifest { + attributes( + 'Premain-Class': 'net.corda.kryohook.KryoHookAgent', + 'Can-Redefine-Classes': 'true', + 'Can-Retransform-Classes': 'true', + 'Can-Set-Native-Method-Prefix': 'true', + 'Implementation-Title': "KryoHook", + 'Implementation-Version': rootProject.version + ) + } + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt new file mode 100644 index 0000000000..5afbd0e4ff --- /dev/null +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt @@ -0,0 +1,167 @@ +package net.corda.kryohook + +import co.paralleluniverse.strands.Strand +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Output +import javassist.ClassPool +import javassist.CtClass +import java.io.ByteArrayInputStream +import java.lang.StringBuilder +import java.lang.instrument.ClassFileTransformer +import java.lang.instrument.Instrumentation +import java.security.ProtectionDomain +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +class KryoHookAgent { + companion object { + @JvmStatic + fun premain(argumentsString: String?, instrumentation: Instrumentation) { + Runtime.getRuntime().addShutdownHook(Thread { + val statsTrees = KryoHook.events.values.flatMap { + readTrees(it, 0).second + } + val builder = StringBuilder() + statsTrees.forEach { + prettyStatsTree(0, it, builder) + } + print(builder.toString()) + }) + instrumentation.addTransformer(KryoHook) + } + } +} + +fun prettyStatsTree(indent: Int, statsTree: StatsTree, builder: StringBuilder) { + when (statsTree) { + is StatsTree.Object -> { + builder.append(kotlin.CharArray(indent) { ' ' }) + builder.append(statsTree.className) + builder.append(" ") + builder.append(statsTree.size) + builder.append("\n") + for (child in statsTree.children) { + prettyStatsTree(indent + 2, child, builder) + } + } + } +} + +/** + * The hook simply records the write() entries and exits together with the output offset at the time of the call. + * This is recorded in a StrandID -> List map. + * + * Later we "parse" these lists into a tree. + */ +object KryoHook : ClassFileTransformer { + val classPool = ClassPool.getDefault() + + val hookClassName = javaClass.name + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray + ): ByteArray? { + if (className.startsWith("java") || className.startsWith("javassist") || className.startsWith("kotlin")) { + return null + } + return try { + val clazz = classPool.makeClass(ByteArrayInputStream(classfileBuffer)) + instrumentClass(clazz)?.toBytecode() + } catch (throwable: Throwable) { + println("SOMETHING WENT WRONG") + throwable.printStackTrace(System.out) + null + } + } + + private fun instrumentClass(clazz: CtClass): CtClass? { + for (method in clazz.declaredBehaviors) { + if (method.name == "write") { + val parameterTypeNames = method.parameterTypes.map { it.name } + if (parameterTypeNames == listOf("com.esotericsoftware.kryo.Kryo", "com.esotericsoftware.kryo.io.Output", "java.lang.Object")) { + if (method.isEmpty) continue + println("Instrumenting ${clazz.name}") + method.insertBefore("$hookClassName.${this::writeEnter.name}($1, $2, $3);") + method.insertAfter("$hookClassName.${this::writeExit.name}($1, $2, $3);") + return clazz + } + } + } + return null + } + + // StrandID -> StatsEvent map + val events = ConcurrentHashMap>() + + @JvmStatic + fun writeEnter(kryo: Kryo, output: Output, obj: Any) { + events.getOrPut(Strand.currentStrand().id) { ArrayList() }.add( + StatsEvent.Enter(obj.javaClass.name, output.total()) + ) + } + @JvmStatic + fun writeExit(kryo: Kryo, output: Output, obj: Any) { + events.get(Strand.currentStrand().id)!!.add( + StatsEvent.Exit(obj.javaClass.name, output.total()) + ) + } +} + +/** + * TODO we could add events on entries/exits to field serializers to get more info on what's being serialised. + */ +sealed class StatsEvent { + data class Enter(val className: String, val offset: Long) : StatsEvent() + data class Exit(val className: String, val offset: Long) : StatsEvent() +} + +/** + * TODO add Field constructor. + */ +sealed class StatsTree { + data class Object( + val className: String, + val size: Long, + val children: List + ) : StatsTree() +} + +fun readTree(events: List, index: Int): Pair { + val event = events[index] + when (event) { + is StatsEvent.Enter -> { + val (nextIndex, children) = readTrees(events, index + 1) + val exit = events[nextIndex] as StatsEvent.Exit + require(event.className == exit.className) + return Pair(nextIndex + 1, StatsTree.Object(event.className, exit.offset - event.offset, children)) + } + is StatsEvent.Exit -> { + throw IllegalStateException("Wasn't expecting Exit") + } + } +} + +fun readTrees(events: List, index: Int): Pair> { + val trees = ArrayList() + var i = index + while (true) { + val event = events.getOrNull(i) + when (event) { + is StatsEvent.Enter -> { + val (nextIndex, tree) = readTree(events, i) + trees.add(tree) + i = nextIndex + } + is StatsEvent.Exit -> { + return Pair(i, trees) + } + null -> { + return Pair(i, trees) + } + } + } +} diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md new file mode 100644 index 0000000000..ec7899a290 --- /dev/null +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md @@ -0,0 +1,23 @@ +What is this +------------ + +This is a javaagent that hooks into Kryo serializers to record a breakdown of how many bytes objects take in the output. + +The dump is quite ugly now, but the in-memory representation is a simple tree so we could put some nice visualisation on +top if we want. + +How do I run it +--------------- + +Build the agent: +``` +./gradlew experimental:kryo-hook:jar +``` + +Add this JVM flag to what you're running: + +``` +-javaagent:/experimental/kryo-hook/build/libs/kryo-hook.jar +``` + +The agent will dump the output when the JVM shuts down. diff --git a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt index c32328ba4a..acd586fa31 100644 --- a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt +++ b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt @@ -32,7 +32,7 @@ fun recordUsedInstrumentedCallStack() { val throwable = Throwable() var index = 0 while (true) { - require (index < throwable.stackTrace.size) { "Can't find getStack call" } + require(index < throwable.stackTrace.size) { "Can't find getStack call" } val stackElement = throwable.stackTrace[index] if (stackElement.className == "co.paralleluniverse.fibers.Stack" && stackElement.methodName == "getStack") { break @@ -129,7 +129,7 @@ class QuasarInstrumentationHookAgent { // The separator append is a hack, it causes a package with an empty name to be added to the exclude tree, // which practically causes that level of the tree to be always expanded in the output globs. val expand = arguments.expand?.let { PackageTree.fromStrings(it.map { "$it${arguments.separator}" }, arguments.separator) } - val truncatedTree = truncate?.let { scannedTree.truncate(it)} ?: scannedTree + val truncatedTree = truncate?.let { scannedTree.truncate(it) } ?: scannedTree val expandedTree = expand?.let { alwaysExcludedTree.merge(it) } ?: alwaysExcludedTree val globs = truncatedTree.toGlobs(expandedTree) globs.forEach { @@ -152,7 +152,7 @@ object QuasarInstrumentationHook : ClassFileTransformer { val instrumentMap = mapOf Unit>( "co/paralleluniverse/fibers/Stack" to { clazz -> // This is called on each suspend, we hook into it to get the stack trace of actually used Suspendables - val getStackMethod = clazz.methods.single { it.name == "getStack" } + val getStackMethod = clazz.methods.single { it.name == "getStack" } getStackMethod.insertBefore( "$hookClassName.${::recordUsedInstrumentedCallStack.name}();" ) @@ -194,7 +194,7 @@ object QuasarInstrumentationHook : ClassFileTransformer { throwable.printStackTrace(System.out) classfileBuffer } - } + } } data class Glob(val parts: List, val isFull: Boolean) { @@ -271,6 +271,7 @@ data class PackageTree(val branches: Map) { val exclude: PackageTree, val globSoFar: List ) + val toExpandList = LinkedList(listOf(State(this, excludeTree, emptyList()))) val globs = ArrayList() while (true) { diff --git a/experimental/sandbox/build.gradle b/experimental/sandbox/build.gradle index 53e244815a..2e64b70651 100644 --- a/experimental/sandbox/build.gradle +++ b/experimental/sandbox/build.gradle @@ -30,7 +30,10 @@ task standaloneJar(type: Jar) { } with jar manifest { - attributes 'Main-Class': 'net.corda.sandbox.tools.SandboxCreator' + attributes( + 'Main-Class': 'net.corda.sandbox.tools.SandboxCreator', + 'Automatic-Module-Name': 'net.corda.sandbox.creator' + ) } archiveName "corda-sandbox-creator-${version}.jar" } diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java index 7bcaf86684..f0fd4bf668 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java @@ -43,7 +43,6 @@ public class CandidacyStatus { } /** - * * @param signature * @return true if the input was absent from the underlying map */ @@ -52,7 +51,6 @@ public class CandidacyStatus { } /** - * * @param methodSignature * @return true if the input was absent from the underlying map */ @@ -62,7 +60,7 @@ public class CandidacyStatus { /** * Static factory method - * + * * @param startingSet * @return a candidacy status based on the starting set */ @@ -81,7 +79,7 @@ public class CandidacyStatus { /** * Static factory method - * + * * @return a candidacy status based on the starting set */ public static CandidacyStatus of() { @@ -90,8 +88,8 @@ public class CandidacyStatus { /** * Add additional methods that are known to be deterministic - * - * @param methodNames + * + * @param methodNames */ public void addKnownDeterministicMethods(final Set methodNames) { for (String known : methodNames) { @@ -101,7 +99,7 @@ public class CandidacyStatus { /** * Getter method for candidate methods - * + * * @param methodSignature * @return the candidate method corresponding to a method signature */ @@ -149,10 +147,10 @@ public class CandidacyStatus { } /** - * Increases the recursive depth of this classloading process, throwing a + * Increases the recursive depth of this classloading process, throwing a * ClassNotFoundException if it becomes too high - * - * @throws ClassNotFoundException + * + * @throws ClassNotFoundException */ public void incRecursiveCount() throws ClassNotFoundException { if (recursiveDepth >= MAX_CLASSLOADING_RECURSIVE_DEPTH - 1) { @@ -174,7 +172,7 @@ public class CandidacyStatus { out.add(candidateName); } } - + return out; } diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java index 49e5091fbe..b806d58b65 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java @@ -6,15 +6,14 @@ import java.util.Set; /** * A candidate method that is under evaluation. Candidate methods have one of the following states: - * + *

*

    - *
  • {@link CandidateMethod.State#DETERMINISTIC} - It's deterministic and therefore is allowed to be loaded.
  • - *
  • {@link CandidateMethod.State#DISALLOWED} - It's not deterministic and won't be allowed to be loaded.
  • - *
  • {@link CandidateMethod.State#SCANNED} - We're not sure if it's deterministic or not.
  • + *
  • {@link CandidateMethod.State#DETERMINISTIC} - It's deterministic and therefore is allowed to be loaded.
  • + *
  • {@link CandidateMethod.State#DISALLOWED} - It's not deterministic and won't be allowed to be loaded.
  • + *
  • {@link CandidateMethod.State#SCANNED} - We're not sure if it's deterministic or not.
  • *
- * + *

* CandidateMethods themselves reference other CandidateMethods which are be checked for their deterministic state - * */ public final class CandidateMethod { @@ -43,7 +42,7 @@ public final class CandidateMethod { private final Set referencedCandidateMethods = new HashSet<>(); - + public State getCurrentState() { return currentState; } @@ -59,7 +58,7 @@ public final class CandidateMethod { public void deterministic() { if (currentState == State.DISALLOWED) { - throw new IllegalArgumentException("Method "+ internalMethodName +" attempted to transition from DISALLOWED to DETERMINISTIC"); + throw new IllegalArgumentException("Method " + internalMethodName + " attempted to transition from DISALLOWED to DETERMINISTIC"); } currentState = State.DETERMINISTIC; } @@ -79,7 +78,7 @@ public final class CandidateMethod { public String getInternalMethodName() { return internalMethodName; } - + public void addReferencedCandidateMethod(final CandidateMethod referenceCandidateMethod) { referencedCandidateMethods.add(referenceCandidateMethod); } @@ -94,8 +93,9 @@ public final class CandidateMethod { /** * This factory constructor is only called for methods that are known to be deterministic in advance + * * @param methodSignature - * @return + * @return */ public static CandidateMethod proven(String methodSignature) { final CandidateMethod provenCandidateMethod = new CandidateMethod(methodSignature); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java index 03e3cf9d56..f5e742b95c 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java @@ -1,11 +1,11 @@ package net.corda.sandbox; import static net.corda.sandbox.Utils.*; + import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; /** - * * @author ben */ public final class SandboxAwareClassWriter extends ClassWriter { @@ -25,17 +25,15 @@ public final class SandboxAwareClassWriter extends ClassWriter { * without actually loading any class, or to take into account the class * that is currently being generated by this ClassWriter, which can of * course not be loaded since it is under construction. - * - * @param type1 - * the internal name of a class. - * @param type2 - * the internal name of another class. + * + * @param type1 the internal name of a class. + * @param type2 the internal name of another class. * @return the internal name of the common super class of the two given - * classes. + * classes. */ @Override public String getCommonSuperClass(final String type1, final String type2) { - if (OBJECT.equals(type1) || OBJECT.equals(type2) + if (OBJECT.equals(type1) || OBJECT.equals(type2) || OBJECT.equals(unsandboxNameIfNeedBe(type1)) || OBJECT.equals(unsandboxNameIfNeedBe(type2))) { return OBJECT; } @@ -58,7 +56,7 @@ public final class SandboxAwareClassWriter extends ClassWriter { c = Class.forName(type1.replace('/', '.'), false, loader); d = Class.forName(type2.replace('/', '.'), false, loader); } catch (Exception e) { - + c = Class.forName(unsandboxNameIfNeedBe(type1).replace('/', '.'), false, loader); d = Class.forName(unsandboxNameIfNeedBe(type2).replace('/', '.'), false, loader); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java index 2f55c4edb6..32157b6ff5 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java @@ -3,7 +3,6 @@ package net.corda.sandbox; import org.objectweb.asm.commons.Remapper; /** - * * @author ben */ public final class SandboxRemapper extends Remapper { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java b/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java index 85fa4fc078..08cbbd794b 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java @@ -4,7 +4,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * * @author ben */ public final class Utils { @@ -12,7 +11,7 @@ public final class Utils { public final static String SANDBOX_PREFIX_INTERNAL = "sandbox/"; public final static String CLASSFILE_NAME_SUFFIX = "^(.*)\\.class$"; - + public static final Pattern JAVA_LANG_PATTERN_INTERNAL = Pattern.compile("^java/lang/(.*)"); public static final Pattern SANDBOX_PATTERN_INTERNAL = Pattern.compile("^" + SANDBOX_PREFIX_INTERNAL + "(.*)"); @@ -28,13 +27,13 @@ public final class Utils { public static final Pattern CLASSNAME_PATTERN_QUALIFIED = Pattern.compile("([^\\.]+)\\."); public static final String OBJECT = "java/lang/Object"; - + public static final String THROWABLE = "java/lang/Throwable"; - + public static final String ERROR = "java/lang/Error"; - + public static final String THREAD_DEATH = "java/lang/ThreadDeath"; - + // Hide constructor private Utils() { } @@ -43,6 +42,7 @@ public final class Utils { * Helper method that converts from the internal class name format (as used in the * Constant Pool) to a fully-qualified class name. No obvious library method to do this * appears to exist, hence this code. If one exists, rip this out. + * * @param classInternalName * @return */ @@ -52,12 +52,11 @@ public final class Utils { } /** - * This method takes in an internal method name but needs to return a qualified + * This method takes in an internal method name but needs to return a qualified * classname (suitable for loading) - * - * + * * @param internalMethodName - * @return + * @return */ public static String convertInternalMethodNameToQualifiedClassName(final String internalMethodName) { final Matcher classMatch = CLASSNAME_PATTERN_QUALIFIED.matcher(internalMethodName); @@ -72,6 +71,7 @@ public final class Utils { * Helper method that converts from a fully-qualified class name to the internal class * name format (as used in the Constant Pool). No obvious library method to do this * appears to exist, hence this code. If one exists, rip this out. + * * @param qualifiedClassName * @return */ @@ -81,7 +81,7 @@ public final class Utils { } /** - * This method potentially rewrites the classname. + * This method potentially rewrites the classname. * * @param internalClassname - specified in internal form * @return @@ -102,9 +102,8 @@ public final class Utils { } /** - * * @param qualifiedTypeName - * @return + * @return */ public static String sandboxQualifiedTypeName(final String qualifiedTypeName) { final String internal = convertQualifiedClassNameToInternalForm(qualifiedTypeName); @@ -118,7 +117,7 @@ public final class Utils { /** * This method removes the sandboxing prefix from a method or type name, if it has * one, otherwise it returns the input string. - * + * * @param internalClassname * @return the internal classname, unsandboxed if that was required */ @@ -131,7 +130,6 @@ public final class Utils { } /** - * * @param desc - internal * @return the rewritten desc string */ @@ -169,9 +167,9 @@ public final class Utils { * loading. This should not attempt to load a classname that starts with java. as * the only permissable classes have already been transformed into sandboxed * methods - * + * * @param qualifiedClassName - * @return + * @return */ public static boolean shouldAttemptToTransitivelyLoad(final String qualifiedClassName) { return !JAVA_PATTERN_QUALIFIED.asPredicate().test(qualifiedClassName); @@ -179,7 +177,7 @@ public final class Utils { /** * Helper method that determines whether this class requires sandboxing - * + * * @param clazzName - specified in internal form * @return true if the class should be sandboxed */ diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java index 171c2770f9..2a99ffbdca 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java @@ -2,6 +2,7 @@ package net.corda.sandbox; import net.corda.sandbox.visitors.CostInstrumentingMethodVisitor; import net.corda.sandbox.visitors.WhitelistCheckingClassVisitor; + import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -9,13 +10,13 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.*; import java.util.*; + import org.objectweb.asm.*; import org.objectweb.asm.commons.ClassRemapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * * @author ben */ public final class WhitelistClassLoader extends ClassLoader { @@ -61,8 +62,8 @@ public final class WhitelistClassLoader extends ClassLoader { } /** - * Static factory method. Throws URISyntaxException currently, as this method is - * called with user data, so a checked exception is not unreasonable. Could use a + * Static factory method. Throws URISyntaxException currently, as this method is + * called with user data, so a checked exception is not unreasonable. Could use a * runtime exception instead. * * @param auxiliaryClassPath @@ -70,7 +71,7 @@ public final class WhitelistClassLoader extends ClassLoader { * methods to be deterministic, instead the classloader * will remove all non-deterministic methods. * @return a suitably constructed whitelisting classloader - * @throws URISyntaxException + * @throws URISyntaxException */ public static WhitelistClassLoader of(final String auxiliaryClassPath, final boolean stripNonDeterministic) throws URISyntaxException { final WhitelistClassLoader out = new WhitelistClassLoader(stripNonDeterministic); @@ -96,10 +97,10 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Static factory method. Used for recursive classloading - * + * * @param other * @return a suitably constructed whitelisting classloader based on the state - * of the passed classloader + * of the passed classloader */ public static WhitelistClassLoader of(final WhitelistClassLoader other) { final WhitelistClassLoader out = new WhitelistClassLoader(other); @@ -110,7 +111,7 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Helper method that adds a jar to the path to be searched * - * @param knownGoodJar + * @param knownGoodJar */ void addJarToSandbox(final Path knownGoodJar) { fileSystemSearchPath.add(knownGoodJar); @@ -120,9 +121,9 @@ public final class WhitelistClassLoader extends ClassLoader { * Setup the auxiliary classpath so that classes that are not on the original * classpath can be scanned for. * Note that this this method hardcodes Unix conventions, so won't work on e.g. Windows - * + * * @param auxiliaryClassPath - * @throws URISyntaxException + * @throws URISyntaxException */ void setupClasspath(final String auxiliaryClassPath) throws URISyntaxException { for (String entry : auxiliaryClassPath.split(":")) { @@ -136,11 +137,10 @@ public final class WhitelistClassLoader extends ClassLoader { } /** - * * @param qualifiedClassName * @return a class object that has been whitelist checked and is known to be - * deterministic - * @throws ClassNotFoundException + * deterministic + * @throws ClassNotFoundException */ @Override public Class findClass(final String qualifiedClassName) throws ClassNotFoundException { @@ -244,10 +244,10 @@ public final class WhitelistClassLoader extends ClassLoader { * around a limitation of the ASM library that does not integrate cleanly with Java 7 * NIO.2 Path APIs. This method also performs a couple of basic sanity check on the * class file (e.g. that it exists, is a regular file and is readable). - * + * * @param internalClassName * @return a path object that corresponds to a class that has been found - * @throws ClassNotFoundException + * @throws ClassNotFoundException */ Path locateClassfileDir(final String internalClassName) throws ClassNotFoundException { // Check the primaryClasspathSearchPath @@ -300,7 +300,7 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Creates a jar archive of all the transformed classes that this classloader * has loaded. - * + * * @return true on success, false on failure * @throws java.io.IOException * @throws java.net.URISyntaxException @@ -328,7 +328,8 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Getter method for the reason for failure - * @return + * + * @return */ public WhitelistClassloadingException reason() { return candidacyStatus.getReason(); @@ -336,7 +337,8 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Getter method for the method candidacy status - * @return + * + * @return */ public CandidacyStatus getCandidacyStatus() { return candidacyStatus; diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java index 5f6c13ca9f..9705d4bf0e 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java @@ -4,7 +4,7 @@ package net.corda.sandbox; * */ public class WhitelistClassloadingException extends Exception { - + public WhitelistClassloadingException() { super(); } @@ -22,10 +22,10 @@ public class WhitelistClassloadingException extends Exception { } protected WhitelistClassloadingException(String message, Throwable cause, - boolean enableSuppression, - boolean writableStackTrace) { + boolean enableSuppression, + boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } - + } diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java index ec098ecfbd..f20aaa0b79 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java @@ -5,7 +5,7 @@ import org.slf4j.LoggerFactory; /** * This class is the runtime representation of a running contract. - * + * * @author ben */ public class Contract { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java index 243a3ddf11..5b719978a4 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java @@ -3,17 +3,17 @@ package net.corda.sandbox.costing; /** * This interface is to decouple the actual executable code from the entry point and * how vetted deterministic code will be used inside the sandbox - * + * * @author ben */ public interface ContractExecutor { /** * Executes a smart contract - * + * * @param contract the contract to be executed */ void execute(Contract contract); - + /** * Checks to see if the supplied Contract is suitable * diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java index f00c39defa..7274d914c7 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java @@ -4,7 +4,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * * @author ben */ public class RuntimeCostAccounter { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java b/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java index 01b0906dc2..33ab308a2e 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java @@ -2,6 +2,7 @@ package net.corda.sandbox.tools; import net.corda.sandbox.WhitelistClassLoader; import net.corda.sandbox.visitors.SandboxPathVisitor; + import java.io.FileInputStream; import java.io.IOException; import java.net.URISyntaxException; @@ -18,7 +19,6 @@ import joptsimple.OptionSet; * This class takes in an exploded set of JRE classes, and a whitelist, and rewrites all * classes (note: not methods) that have at least one whitelisted method to create a * sandboxed version of the class. - * */ // java8.scan.java.lang_and_util java8.interfaces_for_compat java8 sandbox public final class SandboxCreator { @@ -30,7 +30,7 @@ public final class SandboxCreator { private final String outputJarName; private final WhitelistClassLoader wlcl; private final boolean hasInputJar; - + private final static OptionParser parser = new OptionParser(); private static void usage() { @@ -53,7 +53,7 @@ public final class SandboxCreator { static String unpackJar(final String zipFilePath) throws IOException { final Path tmpDir = Files.createTempDirectory(Paths.get("/tmp"), "wlcl-extract"); - + try (final ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath))) { ZipEntry entry = zipIn.getNextEntry(); @@ -68,13 +68,13 @@ public final class SandboxCreator { entry = zipIn.getNextEntry(); } } - + return tmpDir.toString(); } - + void cleanup() { if (hasInputJar) { - + } } @@ -107,10 +107,9 @@ public final class SandboxCreator { } /** - * * @param basePath * @param packageName - * @throws IOException + * @throws IOException */ void walk() throws IOException { final Path scanDir = Paths.get(basePathName); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java index 5f7dc3fdea..8b108fed89 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java @@ -9,7 +9,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * * @author ben */ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { @@ -34,9 +33,10 @@ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { } /** - * This method replaces MONITORENTER / MONITOREXIT opcodes with POP - basically + * This method replaces MONITORENTER / MONITOREXIT opcodes with POP - basically * stripping the synchronization out of any sandboxed code. - * @param opcode + * + * @param opcode */ @Override public void visitInsn(final int opcode) { @@ -60,7 +60,7 @@ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { * For our purposes this is a NEWARRAY opcode. * * @param opcode - * @param operand + * @param operand */ @Override public void visitIntInsn(final int opcode, final int operand) { @@ -103,11 +103,11 @@ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { /** * This method is called when visiting an opcode with a single operand, that * is a type (represented here as a String). - * + *

* For our purposes this is either a NEW opcode or a ANEWARRAY * - * @param opcode - * @param type + * @param opcode + * @param type */ @Override public void visitTypeInsn(final int opcode, final String type) { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java index 62b882f269..d4ade3415a 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java @@ -11,5 +11,5 @@ class DefinitelyDisallowedMethodVisitor extends MethodVisitor { DefinitelyDisallowedMethodVisitor(MethodVisitor baseMethodVisitor) { super(Opcodes.ASM5, baseMethodVisitor); } - + } diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java index bdc81bf995..e5528acbd9 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java @@ -2,15 +2,17 @@ package net.corda.sandbox.visitors; import net.corda.sandbox.Utils; import net.corda.sandbox.WhitelistClassLoader; + import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This helper class visits each file (represented as a Path) in some directory * tree containing classes to be sandboxed. - * + * * @author ben */ public final class SandboxPathVisitor extends SimpleFileVisitor { @@ -30,10 +32,10 @@ public final class SandboxPathVisitor extends SimpleFileVisitor { public FileVisitResult visitFile(final Path path, final BasicFileAttributes attr) { // Check that this is a class file if (!path.toString().matches(Utils.CLASSFILE_NAME_SUFFIX)) { - System.out.println("Skipping: "+ path); + System.out.println("Skipping: " + path); return FileVisitResult.CONTINUE; } - + // Check to see if this path corresponds to an allowedClass final String classFileName = startFrom.relativize(path).toString().replace(".class", ""); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java index 5994b445a6..534c89bc6b 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java @@ -2,10 +2,12 @@ package net.corda.sandbox.visitors; import net.corda.sandbox.WhitelistClassLoader; import net.corda.sandbox.CandidacyStatus; + import java.util.Arrays; import net.corda.sandbox.CandidateMethod; import net.corda.sandbox.Utils; + import java.util.HashSet; import java.util.Set; @@ -14,6 +16,7 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import static org.objectweb.asm.Opcodes.*; /** @@ -57,7 +60,7 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { /** * We initially take the method passed in and store an internal representation of * the method signature in the our CandidacyStatus working set. - * + *

* We then get an ASM MethodVisitor (which can read the byte code of the method) and pass that to our * custom method visitor which perform additional checks. * @@ -66,7 +69,7 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { * @param desc * @param signature * @param exceptions - * @return + * @return */ @Override public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { @@ -77,7 +80,7 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { // Force new access control flags - for now just strictfp for deterministic // compliance to IEEE 754 final int maskedAccess = access | ACC_STRICT; - + final String internalName = classname + "." + name + ":" + desc; internalMethodNames.add(internalName); candidacyStatus.putIfAbsent(internalName); @@ -151,10 +154,10 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { } /** - * Take the name of a class and attempts to load it using a WLCL. - * + * Take the name of a class and attempts to load it using a WLCL. + * * @param qualifiedClassname - * @return + * @return */ CandidateMethod.State resolveState(final String qualifiedClassname) { Class clz = null; diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java index 28338f5285..0d392f342b 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java @@ -13,7 +13,6 @@ import org.objectweb.asm.Label; /** * A MethodVisitor which checks method instructions in order to determine if this * method is deterministic or not - * */ final class WhitelistCheckingMethodVisitor extends MethodVisitor { @@ -29,7 +28,7 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { } /** - * Visits a method instruction. A method instruction is an instruction that + * Visits a method instruction. A method instruction is an instruction that * invokes a method. *

* Some method instructions are by their nature un-deterministic, so we set those methods to have a @@ -84,15 +83,15 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { } /** - * Currently a no-op. - * + * Currently a no-op. + *

* The JVMspec seems to permit the possibility of using a backwards branch in a - * tableswitch to try to create an infinite loop. However, it seems to be + * tableswitch to try to create an infinite loop. However, it seems to be * impossible in practice - the specification of StackMapFrame seems to prevent * it in modern classfile formats, and even by explicitly generating a version * 49 (Java 5) classfile, the verifier seems to be specifically resistant to a - * backwards branch from a tableswitch. - * + * backwards branch from a tableswitch. + *

* We could still add a belt-and-braces static instrumentation to protect * against this but it currently seems unnecessary - at worse it is a branch that * should count against the branch limit, or an explicit disallow of a backwards @@ -102,7 +101,7 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { * @param min * @param max * @param dflt - * @param labels + * @param labels */ @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { @@ -112,11 +111,11 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { /** * Visits an invokedynamic instruction - which is specifically disallowed for * deterministic apps. - * + * * @param name * @param desc * @param bsm - * @param bsmArgs + * @param bsmArgs */ @Override public void visitInvokeDynamicInsn(final String name, final String desc, final Handle bsm, final Object... bsmArgs) { diff --git a/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java b/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java index 7acc1f445d..22e895c41b 100644 --- a/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java +++ b/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java @@ -4,7 +4,7 @@ package sandbox.net.corda.sandbox.costing; * A helper class that just forwards any static sandboxed calls to the real runtime * cost accounting class. This removes the need to special case the accounting * method calls during rewriting of method names - * + * * @author ben */ public class RuntimeCostAccounter { diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java b/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java index d8e0fd0007..16cbda6e84 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java @@ -22,7 +22,7 @@ public class TestUtils { // Copy resource jar to tmp dir tmpdir = Files.createTempDirectory("wlcl-tmp-test"); Path copiedJar = tmpdir.resolve("tmp-resource.jar"); - try(final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { + try (final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { Files.copy(in, copiedJar, StandardCopyOption.REPLACE_EXISTING); } final FileSystem fs = FileSystems.newFileSystem(copiedJar, null); @@ -33,20 +33,20 @@ public class TestUtils { public static Path copySandboxJarToTmpDir(final String resourcePathToJar) throws IOException { Path sandboxJar = tmpdir.resolve("tmp-sandbox.jar"); - try(final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { + try (final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { Files.copy(in, sandboxJar, StandardCopyOption.REPLACE_EXISTING); } final FileSystem sandboxFs = FileSystems.newFileSystem(sandboxJar, null); tmpFileSystems.add(sandboxFs); return sandboxFs.getRootDirectories().iterator().next(); } - + public static Path getJarFSRoot() { return jarFSDir; } public static void cleanupTmpJar() throws IOException { - for (FileSystem fs: tmpFileSystems) { + for (FileSystem fs : tmpFileSystems) { fs.close(); } tmpFileSystems.clear(); @@ -92,15 +92,15 @@ public class TestUtils { // Helper for finding the correct offsets if they change public static void printBytes(byte[] data) { byte[] datum = new byte[1]; - for (int i=0; i < data.length; i++) { + for (int i = 0; i < data.length; i++) { datum[0] = data[i]; - System.out.println(i +" : "+ DatatypeConverter.printHexBinary(datum)); + System.out.println(i + " : " + DatatypeConverter.printHexBinary(datum)); } } public static int findOffset(byte[] classBytes, byte[] originalSeq) { int offset = 0; - for (int i=415; i < classBytes.length; i++) { + for (int i = 415; i < classBytes.length; i++) { if (classBytes[i] != originalSeq[offset]) { offset = 0; continue; @@ -110,7 +110,7 @@ public class TestUtils { } offset++; } - + return -1; } @@ -119,7 +119,7 @@ public class TestUtils { return wlcl.instrumentWithCosts(basic, hashSet); } - + public static final class MyClassloader extends ClassLoader { public Class byPath(Path p) throws IOException { diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java b/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java index 91d455b0d2..bcc5e94172 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java @@ -1,10 +1,13 @@ package net.corda.sandbox; import static org.junit.Assert.*; + import org.junit.Test; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.*; public class WhitelistClassLoaderTest { @@ -123,8 +126,8 @@ public class WhitelistClassLoaderTest { final Class clz = wlcl.loadClass("resource.ThrowExceptions"); assertNotNull("ThrowExceptions class could not be transformed and loaded", clz); } - - + + // TODO Test cases that terminate when other resource limits are broken @Test public void when_too_much_memory_is_allocated_then_thread_dies() throws Exception { diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java index c276907870..15130086d3 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java @@ -11,7 +11,6 @@ import java.util.*; import static org.junit.Assert.*; /** - * * @author ben */ public class DeterministicClassInstrumenterTest { @@ -66,7 +65,7 @@ public class DeterministicClassInstrumenterTest { // TestUtils.printBytes(basic); final int origOffset = TestUtils.findOffset(basic, originalSeq); final int tmfdOffset = TestUtils.findOffset(tfmd, tfmdSeq); - + for (int i = 0; i < originalSeq.length; i++) { assertEquals(originalSeq[i], basic[origOffset + i]); assertEquals(tfmdSeq[i], tfmd[tmfdOffset + i]); diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java index 15bd7dafe2..16b3972390 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java @@ -1,19 +1,24 @@ package net.corda.sandbox.costing; import net.corda.sandbox.TestUtils; + import static net.corda.sandbox.TestUtils.*; + import net.corda.sandbox.Utils; + import java.io.IOException; import java.lang.reflect.Method; import java.net.URISyntaxException; + import org.junit.AfterClass; import org.junit.Test; + import static org.junit.Assert.*; + import org.junit.Before; import org.junit.BeforeClass; /** - * * @author ben */ public class SandboxedRewritingTest { diff --git a/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java b/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java index e0aee3b576..d34132570e 100644 --- a/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java +++ b/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java @@ -1,7 +1,6 @@ package sandbox.greymalkin; /** - * * @author ben */ // Simple hack for now, generalise to lambdas later... diff --git a/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt index 6f12d53844..7abb5ed97d 100644 --- a/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt @@ -3,6 +3,7 @@ package net.corda.finance.contracts.universal import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.finance.contracts.BusinessCalendar @@ -124,19 +125,18 @@ class UniversalContract : Contract { } } - @Suppress("UNCHECKED_CAST") fun replaceStartEnd(p: Perceivable, start: Instant, end: Instant): Perceivable = when (p) { is Const -> p - is TimePerceivable -> TimePerceivable(p.cmp, replaceStartEnd(p.instant, start, end)) as Perceivable - is EndDate -> const(end) as Perceivable - is StartDate -> const(start) as Perceivable + is TimePerceivable -> uncheckedCast(TimePerceivable(p.cmp, replaceStartEnd(p.instant, start, end))) + is EndDate -> uncheckedCast(const(end)) + is StartDate -> uncheckedCast(const(start)) is UnaryPlus -> UnaryPlus(replaceStartEnd(p.arg, start, end)) is PerceivableOperation -> PerceivableOperation(replaceStartEnd(p.left, start, end), p.op, replaceStartEnd(p.right, start, end)) - is Interest -> Interest(replaceStartEnd(p.amount, start, end), p.dayCountConvention, replaceStartEnd(p.interest, start, end), replaceStartEnd(p.start, start, end), replaceStartEnd(p.end, start, end)) as Perceivable - is Fixing -> Fixing(p.source, replaceStartEnd(p.date, start, end), p.tenor) as Perceivable - is PerceivableAnd -> (replaceStartEnd(p.left, start, end) and replaceStartEnd(p.right, start, end)) as Perceivable - is PerceivableOr -> (replaceStartEnd(p.left, start, end) or replaceStartEnd(p.right, start, end)) as Perceivable + is Interest -> uncheckedCast(Interest(replaceStartEnd(p.amount, start, end), p.dayCountConvention, replaceStartEnd(p.interest, start, end), replaceStartEnd(p.start, start, end), replaceStartEnd(p.end, start, end))) + is Fixing -> uncheckedCast(Fixing(p.source, replaceStartEnd(p.date, start, end), p.tenor)) + is PerceivableAnd -> uncheckedCast(replaceStartEnd(p.left, start, end) and replaceStartEnd(p.right, start, end)) + is PerceivableOr -> uncheckedCast(replaceStartEnd(p.left, start, end) or replaceStartEnd(p.right, start, end)) is ActorPerceivable -> p else -> throw NotImplementedError("replaceStartEnd " + p.javaClass.name) } @@ -276,7 +276,6 @@ class UniversalContract : Contract { } } - @Suppress("UNCHECKED_CAST") fun replaceFixing(tx: LedgerTransaction, perceivable: Perceivable, fixings: Map, unusedFixings: MutableSet): Perceivable = when (perceivable) { @@ -284,14 +283,14 @@ class UniversalContract : Contract { is UnaryPlus -> UnaryPlus(replaceFixing(tx, perceivable.arg, fixings, unusedFixings)) is PerceivableOperation -> PerceivableOperation(replaceFixing(tx, perceivable.left, fixings, unusedFixings), perceivable.op, replaceFixing(tx, perceivable.right, fixings, unusedFixings)) - is Interest -> Interest(replaceFixing(tx, perceivable.amount, fixings, unusedFixings), + is Interest -> uncheckedCast(Interest(replaceFixing(tx, perceivable.amount, fixings, unusedFixings), perceivable.dayCountConvention, replaceFixing(tx, perceivable.interest, fixings, unusedFixings), - perceivable.start, perceivable.end) as Perceivable + perceivable.start, perceivable.end)) is Fixing -> { val dt = eval(tx, perceivable.date) if (dt != null && fixings.containsKey(FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor))) { unusedFixings.remove(FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor)) - Const(fixings[FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor)]!!) as Perceivable + uncheckedCast(Const(fixings[FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor)]!!)) } else perceivable } else -> throw NotImplementedError("replaceFixing - " + perceivable.javaClass.name) diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt index 95c7e983fd..bedc953058 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt @@ -5,16 +5,18 @@ import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before +import net.corda.testing.EnforceVerifyOrFail +import net.corda.testing.TransactionDSL +import net.corda.testing.TransactionDSLInterpreter import org.junit.Ignore import org.junit.Test import java.time.Instant import java.time.LocalDate +fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { + net.corda.testing.transaction(cordappPackages = listOf("net.corda.finance.contracts.universal"), dsl = script) +} + class Cap { val TEST_TX_TIME_1: Instant get() = Instant.parse("2017-09-02T12:00:00.00Z") @@ -167,16 +169,6 @@ class Cap { } } - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt index 8793dd65b9..390ea97154 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt @@ -3,11 +3,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -53,17 +48,6 @@ class Caplet { val stateFixed = UniversalContract.State(listOf(DUMMY_NOTARY), contractFixed) val stateFinal = UniversalContract.State(listOf(DUMMY_NOTARY), contractFinal) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt index ec78fc1b0c..7117b22b74 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -50,17 +45,6 @@ class FXFwdTimeOption val inState = UniversalContract.State(listOf(DUMMY_NOTARY), initialContract) val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract1) val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract2) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `issue - signature`() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt index 26189d2424..67983a6c0e 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -41,17 +36,6 @@ class FXSwap { val outStateBad3 = UniversalContract.State(listOf(DUMMY_NOTARY), transferBad3) val inState = UniversalContract.State(listOf(DUMMY_NOTARY), contract) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `issue - signature`() { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt index ae55271902..9239146e8c 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt @@ -4,11 +4,6 @@ import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -132,17 +127,6 @@ class IRS { val stateAfterExecutionFirst = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterExecutionFirst) val statePaymentFirst = UniversalContract.State(listOf(DUMMY_NOTARY), paymentFirst) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt index 40d4b7bb94..3a86f41cfa 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt @@ -2,11 +2,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.Frequency import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test import java.time.Instant import kotlin.test.assertEquals @@ -122,16 +117,6 @@ class RollOutTests { next() } - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `arrangement equality transfer`() { assertEquals(contract_transfer1, contract_transfer2) diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt index 590e7a47ac..c0b464af33 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt @@ -3,11 +3,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -59,17 +54,6 @@ class Swaption { } val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY), contractInitial) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt index 3d79d0df1b..8692518724 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test import java.time.Instant import kotlin.test.assertEquals @@ -43,17 +38,6 @@ class ZeroCouponBond { val outStateWrong = UniversalContract.State(listOf(DUMMY_NOTARY), transferWrong) val outStateMove = UniversalContract.State(listOf(DUMMY_NOTARY), contractMove) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun basic() { assertEquals(Zero(), Zero()) diff --git a/finance/build.gradle b/finance/build.gradle index 7ded74b45d..662e7c9aa5 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -5,24 +5,42 @@ apply plugin: 'kotlin-jpa' apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'com.jfrog.artifactory' description 'Corda finance modules' +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } +} + dependencies { // Note the :finance module is a CorDapp in its own right // and CorDapps using :finance features should use 'cordapp' not 'compile' linkage. cordaCompile project(':core') cordaCompile project(':confidential-identities') + // TODO Remove this once we have app configs + compile "com.typesafe:config:$typesafe_config_version" + testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" + + // AssertJ: for fluent assertions for testing + testCompile "org.assertj:assertj-core:$assertj_version" } configurations { testArtifacts.extendsFrom testRuntime + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime } task testJar(type: Jar) { @@ -30,6 +48,11 @@ task testJar(type: Jar) { from sourceSets.test.output } +task integrationTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + artifacts { testArtifacts testJar } diff --git a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt index 855c06cea4..a065b49c43 100644 --- a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt +++ b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt @@ -9,6 +9,7 @@ import net.corda.core.identity.Party * loaded or blocked. */ class IsolatedDummyFlow { + @StartableByRPC @InitiatingFlow class Initiator(val toWhom: Party) : FlowLogic() { @Suspendable @@ -31,4 +32,4 @@ class IsolatedDummyFlow { stx.verify(serviceHub) } } -} \ No newline at end of file +} diff --git a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedPlugin.kt b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedPlugin.kt deleted file mode 100644 index 61e0288d88..0000000000 --- a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedPlugin.kt +++ /dev/null @@ -1,5 +0,0 @@ -package net.corda.finance.contracts.isolated - -import net.corda.core.node.CordaPluginRegistry - -class IsolatedPlugin : CordaPluginRegistry() \ No newline at end of file diff --git a/finance/isolated/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/finance/isolated/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 759dc08195..0000000000 --- a/finance/isolated/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1 +0,0 @@ -net.corda.finance.contracts.isolated.IsolatedPlugin \ No newline at end of file diff --git a/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt new file mode 100644 index 0000000000..49351f1e13 --- /dev/null +++ b/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt @@ -0,0 +1,20 @@ +package net.corda.finance.flows + +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.finance.EUR +import net.corda.finance.USD +import net.corda.testing.driver.driver +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class CashConfigDataFlowTest { + @Test + fun `issuable currencies are read in from node config`() { + driver { + val node = startNode(customOverrides = mapOf("issuableCurrencies" to listOf("EUR", "USD"))).getOrThrow() + val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow() + assertThat(config.issuableCurrencies).containsExactly(EUR, USD) + } + } +} \ No newline at end of file diff --git a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java index 468d1919c8..221e02427c 100644 --- a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java @@ -32,7 +32,7 @@ import static net.corda.core.contracts.ContractsDSL.requireThat; */ @SuppressWarnings("unused") public class JavaCommercialPaper implements Contract { - static final String JCP_PROGRAM_ID = "net.corda.finance.contracts.JavaCommercialPaper"; + public static final String JCP_PROGRAM_ID = "net.corda.finance.contracts.JavaCommercialPaper"; @SuppressWarnings("unused") public static class State implements OwnableState, ICommercialPaperState { @@ -101,9 +101,7 @@ public class JavaCommercialPaper implements Contract { if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - if (maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null) - return false; - return true; + return maturityDate != null ? maturityDate.equals(state.maturityDate) : state.maturityDate == null; } @Override diff --git a/finance/src/main/kotlin/net/corda/finance/Currencies.kt b/finance/src/main/kotlin/net/corda/finance/Currencies.kt index 5e1b997aed..75bb50cd83 100644 --- a/finance/src/main/kotlin/net/corda/finance/Currencies.kt +++ b/finance/src/main/kotlin/net/corda/finance/Currencies.kt @@ -8,24 +8,41 @@ import net.corda.core.contracts.PartyAndReference import java.math.BigDecimal import java.util.* -@JvmField val USD: Currency = Currency.getInstance("USD") -@JvmField val GBP: Currency = Currency.getInstance("GBP") -@JvmField val EUR: Currency = Currency.getInstance("EUR") -@JvmField val CHF: Currency = Currency.getInstance("CHF") -@JvmField val JPY: Currency = Currency.getInstance("JPY") -@JvmField val RUB: Currency = Currency.getInstance("RUB") +@JvmField +val USD: Currency = Currency.getInstance("USD") +@JvmField +val GBP: Currency = Currency.getInstance("GBP") +@JvmField +val EUR: Currency = Currency.getInstance("EUR") +@JvmField +val CHF: Currency = Currency.getInstance("CHF") +@JvmField +val JPY: Currency = Currency.getInstance("JPY") +@JvmField +val RUB: Currency = Currency.getInstance("RUB") -fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) +fun AMOUNT(amount: Int, token: T): Amount = AMOUNT(amount.toLong(), token) +fun AMOUNT(amount: Long, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) +fun DOLLARS(amount: Long): Amount = AMOUNT(amount, USD) fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) +fun POUNDS(amount: Long): Amount = AMOUNT(amount, GBP) +fun POUNDS(amount: Double): Amount = AMOUNT(amount, GBP) fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) +fun SWISS_FRANCS(amount: Long): Amount = AMOUNT(amount, CHF) +fun SWISS_FRANCS(amount: Double): Amount = AMOUNT(amount, CHF) val Int.DOLLARS: Amount get() = DOLLARS(this) +val Long.DOLLARS: Amount get() = DOLLARS(this) val Double.DOLLARS: Amount get() = DOLLARS(this) val Int.POUNDS: Amount get() = POUNDS(this) +val Long.POUNDS: Amount get() = POUNDS(this) +val Double.POUNDS: Amount get() = POUNDS(this) val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) +val Long.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) +val Double.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt index f7fcdc3e69..1c302480f3 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt @@ -49,6 +49,7 @@ class CommercialPaper : Contract { companion object { const val CP_PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.CommercialPaper" } + data class State( val issuance: PartyAndReference, override val owner: AbstractParty, @@ -89,7 +90,8 @@ class CommercialPaper : Contract { } } - /** @suppress */ infix fun `owned by`(owner: AbstractParty) = copy(owner = owner) + /** @suppress */ + infix fun `owned by`(owner: AbstractParty) = copy(owner = owner) } interface Commands : CommandData { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index 2a1c02a11e..be6ada5492 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -12,6 +12,7 @@ import net.corda.core.contracts.CommandData import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TokenizableAssetInfo +import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder @@ -28,12 +29,16 @@ import java.util.* // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// DOCSTART 1 /** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */ @CordaSerializable data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) +// DOCEND 1 +// DOCSTART 2 /** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ data class Fix(val of: FixOf, val value: BigDecimal) : CommandData +// DOCEND 2 /** Represents a textual expression of e.g. a formula */ @CordaSerializable @@ -160,7 +165,7 @@ enum class DayCountBasisDay { enum class DayCountBasisYear { // Ditto above comment for years. Y360, - Y365F, Y365L, Y365Q, Y366, YActual, YActualA, Y365B, Y365, YISMA, YICMA, Y252; + Y365F, Y365L, Y365Q, Y366, YActual, YActualA, Y365B, Y365, YISMA, YISDA, YICMA, Y252; override fun toString(): String { return super.toString().drop(1) @@ -197,9 +202,9 @@ enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) * no staff are around to handle problems. */ @CordaSerializable -open class BusinessCalendar (val holidayDates: List) { +open class BusinessCalendar(val holidayDates: List) { @CordaSerializable - class UnknownCalendar(name: String) : Exception("$name not found") + class UnknownCalendar(name: String) : FlowException("$name not found") companion object { val calendars = listOf("London", "NewYork") diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt index 7f6c84010d..d96b5b5c09 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt @@ -39,7 +39,6 @@ private fun rowsToAmount(currency: Currency, rows: Vault.Page>) } else { require(rows.otherResults.size == 2) require(rows.otherResults[1] == currency.currencyCode) - @Suppress("UNCHECKED_CAST") val quantity = rows.otherResults[0] as Long Amount(quantity, currency) } @@ -60,7 +59,7 @@ fun CordaRPCOps.getCashBalance(currency: Currency): Amount { } fun ServiceHub.getCashBalance(currency: Currency): Amount { - val results = this.vaultQueryService.queryBy>(generateCashSumCriteria(currency)) + val results = this.vaultService.queryBy>(generateCashSumCriteria(currency)) return rowsToAmount(currency, results) } @@ -70,7 +69,7 @@ fun CordaRPCOps.getCashBalances(): Map> { } fun ServiceHub.getCashBalances(): Map> { - val sums = this.vaultQueryService.queryBy>(generateCashSumsCriteria()).otherResults + val sums = this.vaultService.queryBy>(generateCashSumsCriteria()).otherResults return rowsToBalances(sums) } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 9a578b7ff2..d8ead23d9d 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -19,79 +19,21 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String -import net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl +import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero import java.math.BigInteger import java.security.PublicKey -import java.sql.DatabaseMetaData import java.util.* -import java.util.concurrent.atomic.AtomicReference ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Cash // -/** - * Pluggable interface to allow for different cash selection provider implementations - * Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation. - * Custom implementations must implement this interface and declare their implementation in - * META-INF/services/net.corda.contracts.asset.CashSelection - */ -interface CashSelection { - companion object { - val instance = AtomicReference() - - fun getInstance(metadata: () -> java.sql.DatabaseMetaData): CashSelection { - return instance.get() ?: { - val _metadata = metadata() - val cashSelectionAlgos = ServiceLoader.load(CashSelection::class.java).toList() - val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) } - cashSelectionAlgo?.let { - instance.set(cashSelectionAlgo) - cashSelectionAlgo - } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + - "\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection") - }.invoke() - } - } - - /** - * Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services - * this method determines whether the loaded implementation is compatible and usable with the currently - * loaded JDBC driver. - * Note: the first loaded implementation to pass this check will be used at run-time. - */ - fun isCompatible(metadata: DatabaseMetaData): Boolean - - /** - * Query to gather Cash states that are available - * @param services The service hub to allow access to the database session - * @param amount The amount of currency desired (ignoring issues, but specifying the currency) - * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, - * otherwise the set of eligible states wil be filtered to only include those from these issuers. - * @param notary If null the notary source is ignored, if specified then only states marked - * with this notary are included. - * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. - * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. - * @param withIssuerRefs If not empty the specific set of issuer references to match against. - * @return The matching states that were found. If sufficient funds were found these will be locked, - * otherwise what is available is returned unlocked for informational purposes. - */ - @Suspendable - fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set = emptySet(), - notary: Party? = null, - lockId: UUID, - withIssuerRefs: Set = emptySet()): List> -} - /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour @@ -129,10 +71,10 @@ class Cash : OnLedgerAsset() { override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)" override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner)) - fun ownedBy(owner: AbstractParty) = copy(owner = owner) - fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) - fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) - fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) + infix fun ownedBy(owner: AbstractParty) = copy(owner = owner) + infix fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) + infix fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) + infix fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) /** Object Relational Mapping support. */ override fun generateMappedObject(schema: MappedSchema): PersistentState { @@ -384,7 +326,7 @@ class Cash : OnLedgerAsset() { // Retrieve unspent and unlocked cash states that meet our spending criteria. val totalAmount = payments.map { it.amount }.sumOrThrow() - val cashSelection = CashSelection.getInstance({ services.jdbcSession().metaData }) + val cashSelection = AbstractCashSelection.getInstance({ services.jdbcSession().metaData }) val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId) val revocationEnabled = false // Revocation is currently unsupported // Generate a new identity that change will be sent to for confidentiality purposes. This means that a @@ -399,13 +341,6 @@ class Cash : OnLedgerAsset() { } } -// Small DSL extensions. - -/** @suppress */ infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) -/** @suppress */ infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) -/** @suppress */ infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -/** @suppress */ infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) - // Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. /** A randomly generated key. */ diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt index bf31292c69..677728874b 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt @@ -44,6 +44,7 @@ class CommodityContract : OnLedgerAsset { constructor(deposit: PartyAndReference, amount: Amount, owner: AbstractParty) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) + override val exitKeys: Set = Collections.singleton(owner.owningKey) override val participants = listOf(owner) @@ -91,7 +92,7 @@ class CommodityContract : OnLedgerAsset().firstOrNull() @@ -107,7 +108,7 @@ class CommodityContract : OnLedgerAsset() requireThat { - "output deposits are owned by a command signer" using (issuer.party in issueCommand.signingParties) + "output deposits are ownedBy a command signer" using (issuer.party in issueCommand.signingParties) "output values sum to more than the inputs" using (outputAmount > inputAmount) "there is only a single issue command" using (commodityCommands.count() == 1) } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt index d499c08233..c30ddc0875 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt @@ -73,6 +73,7 @@ class Obligation

: Contract { companion object { const val PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.asset.Obligation" } + /** * Represents where in its lifecycle a contract state is, which in turn controls the commands that can be applied * to the state. Most states will not leave the [NORMAL] lifecycle. Note that settled (as an end lifecycle) is @@ -191,7 +192,7 @@ class Obligation

: Contract { */ data class Move(override val contract: Class? = null) : MoveCommand - /** + /** * Allows new obligation states to be issued into existence. */ class Issue : TypeOnlyCommandData() @@ -199,7 +200,8 @@ class Obligation

: Contract { /** * A command stating that the obligor is settling some or all of the amount owed by transferring a suitable * state object to the beneficiary. If this reduces the balance to zero, the state object is destroyed. - * @see [MoveCommand]. + * + * @see MoveCommand */ data class Settle

(val amount: Amount>>) : CommandData @@ -452,7 +454,7 @@ class Obligation

: Contract { requireThat { "there is a time-window from the authority" using (timeWindow != null) - "the due date has passed" using (timeWindow!!.fromTime?.isAfter(deadline) ?: false) + "the due date has passed" using (timeWindow!!.fromTime?.isAfter(deadline) == true) "input state lifecycle is correct" using (input.lifecycle == expectedInputLifecycle) "output state corresponds exactly to input state, with lifecycle changed" using (expectedOutput == actualOutput) } @@ -784,9 +786,11 @@ infix fun Obligation.State.between(parties: Pair Obligation.State.`owned by`(owner: AbstractParty) = copy(beneficiary = owner) infix fun Obligation.State.`issued by`(party: AbstractParty) = copy(obligor = party) // For Java users: -@Suppress("unused") fun Obligation.State.ownedBy(owner: AbstractParty) = copy(beneficiary = owner) +@Suppress("unused") +fun Obligation.State.ownedBy(owner: AbstractParty) = copy(beneficiary = owner) -@Suppress("unused") fun Obligation.State.issuedBy(party: AnonymousParty) = copy(obligor = party) +@Suppress("unused") +fun Obligation.State.issuedBy(party: AnonymousParty) = copy(obligor = party) /** A randomly generated key. */ val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt index 7042f3a94f..d64bb817b2 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt @@ -55,13 +55,13 @@ abstract class OnLedgerAsset> : C */ @Throws(InsufficientBalanceException::class) @JvmStatic - fun , T: Any> generateSpend(tx: TransactionBuilder, - amount: Amount, - to: AbstractParty, - acceptableStates: List>, - payChangeTo: AbstractParty, - deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, - generateMoveCommand: () -> CommandData): Pair> { + fun , T : Any> generateSpend(tx: TransactionBuilder, + amount: Amount, + to: AbstractParty, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { return generateSpend(tx, listOf(PartyAndAmount(to, amount)), acceptableStates, payChangeTo, deriveState, generateMoveCommand) } @@ -91,12 +91,12 @@ abstract class OnLedgerAsset> : C */ @Throws(InsufficientBalanceException::class) @JvmStatic - fun , T: Any> generateSpend(tx: TransactionBuilder, - payments: List>, - acceptableStates: List>, - payChangeTo: AbstractParty, - deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, - generateMoveCommand: () -> CommandData): Pair> { + fun , T : Any> generateSpend(tx: TransactionBuilder, + payments: List>, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { // Discussion // // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. @@ -230,12 +230,36 @@ abstract class OnLedgerAsset> : C */ @Throws(InsufficientBalanceException::class) @JvmStatic + @Deprecated("Replaced with generateExit() which takes in a party to pay change to") fun , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, assetStates: List>, deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, generateMoveCommand: () -> CommandData, generateExitCommand: (Amount>) -> CommandData): Set { - val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued) + val owner = assetStates.map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amountIssued) + return generateExit(tx, amountIssued, assetStates, owner, deriveState, generateMoveCommand, generateExitCommand) + } + + /** + * Generate an transaction exiting fungible assets from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not attempt to exit funds held by others. + * @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling + * party. + * @return the public keys which must sign the transaction for it to be valid. + */ + @Throws(InsufficientBalanceException::class) + @JvmStatic + fun , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): Set { + require(assetStates.isNotEmpty()) { "List of states to exit cannot be empty." } val currency = amountIssued.token.product val amount = Amount(amountIssued.quantity, currency) var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } @@ -255,7 +279,7 @@ abstract class OnLedgerAsset> : C val outputs = if (change != null) { // Add a change output and adjust the last output downwards. - listOf(deriveState(gathered.last().state, change, owner)) + listOf(deriveState(gathered.last().state, change, payChangeTo)) } else emptyList() for (state in gathered) tx.addInputState(state) @@ -272,9 +296,9 @@ abstract class OnLedgerAsset> : C * wrappers around this function, which build the state for you, and those should be used in preference. */ @JvmStatic - fun , T: Any> generateIssue(tx: TransactionBuilder, - transactionState: TransactionState, - issueCommand: CommandData): Set { + fun , T : Any> generateIssue(tx: TransactionBuilder, + transactionState: TransactionState, + issueCommand: CommandData): Set { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty()) require(transactionState.data.amount.quantity > 0) @@ -295,9 +319,12 @@ abstract class OnLedgerAsset> : C * @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not exit funds held by others. + * @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling + * party. * @return the public keys which must sign the transaction for it to be valid. */ @Throws(InsufficientBalanceException::class) + @Deprecated("Replaced with generateExit() which takes in a party to pay change to") fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, assetStates: List>): Set { return generateExit( @@ -310,6 +337,30 @@ abstract class OnLedgerAsset> : C ) } + /** + * Generate an transaction exiting assets from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not exit funds held by others. + * @return the public keys which must sign the transaction for it to be valid. + */ + @Throws(InsufficientBalanceException::class) + fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + payChangeTo: AbstractParty): Set { + return generateExit( + tx, + amountIssued, + assetStates, + payChangeTo, + deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, + generateMoveCommand = { -> generateMoveCommand() }, + generateExitCommand = { amount -> generateExitCommand(amount) } + ) + } + abstract fun generateExitCommand(amount: Amount>): CommandData abstract fun generateMoveCommand(): MoveCommand diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt new file mode 100644 index 0000000000..fdc3d0d5a7 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt @@ -0,0 +1,168 @@ +package net.corda.finance.contracts.asset.cash.selection + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.utilities.* +import net.corda.finance.contracts.asset.Cash +import java.sql.* +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Pluggable interface to allow for different cash selection provider implementations + * Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation. + * Custom implementations must implement this interface and declare their implementation in + * META-INF/services/net.corda.contracts.asset.CashSelection + */ +abstract class AbstractCashSelection { + companion object { + val instance = AtomicReference() + + fun getInstance(metadata: () -> java.sql.DatabaseMetaData): AbstractCashSelection { + return instance.get() ?: { + val _metadata = metadata() + val cashSelectionAlgos = ServiceLoader.load(AbstractCashSelection::class.java).toList() + val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) } + cashSelectionAlgo?.let { + instance.set(cashSelectionAlgo) + cashSelectionAlgo + } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + + "\nPlease specify an implementation in META-INF/services/${AbstractCashSelection::class.java}") + }.invoke() + } + + val log = loggerFor() + } + + // coin selection retry loop counter, sleep (msecs) and lock for selecting states + // TODO: make parameters configurable when we get CorDapp configuration. + private val MAX_RETRIES = 8 + private val RETRY_SLEEP = 100 + private val RETRY_CAP = 2000 + private val spendLock: ReentrantLock = ReentrantLock() + + /** + * Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services + * this method determines whether the loaded implementation is compatible and usable with the currently + * loaded JDBC driver. + * Note: the first loaded implementation to pass this check will be used at run-time. + */ + abstract fun isCompatible(metadata: DatabaseMetaData): Boolean + + /** + * A vendor specific query(ies) to gather Cash states that are available. + * @param statement The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param onlyFromIssuerParties Optional issuer parties to match against. + * @param withIssuerRefs Optional issuer references to match against. + * @return JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + abstract fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet + + override abstract fun toString() : String + + /** + * Query to gather Cash states that are available and retry if they are temporarily unavailable. + * @param services The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, + * otherwise the set of eligible states wil be filtered to only include those from these issuers. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param withIssuerRefs If not empty the specific set of issuer references to match against. + * @return The matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + @Suspendable + fun unconsumedCashStatesForSpending(services: ServiceHub, + amount: Amount, + onlyFromIssuerParties: Set = emptySet(), + notary: Party? = null, + lockId: UUID, + withIssuerRefs: Set = emptySet()): List> { + val stateAndRefs = mutableListOf>() + + for (retryCount in 1..MAX_RETRIES) { + if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) { + log.warn("Coin selection failed on attempt $retryCount") + // TODO: revisit the back off strategy for contended spending. + if (retryCount != MAX_RETRIES) { + stateAndRefs.clear() + val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt() + FlowLogic.sleep(durationMillis.millis) + } else { + log.warn("Insufficient spendable states identified for $amount") + } + } else { + break + } + } + return stateAndRefs + } + + private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, stateAndRefs: MutableList>): Boolean { + spendLock.withLock { + val connection = services.jdbcSession() + try { + // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) + // the softLockReserve update will detect whether we try to lock states locked by others + val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) + stateAndRefs.clear() + + var totalPennies = 0L + while (rs.next()) { + val txHash = SecureHash.parse(rs.getString(1)) + val index = rs.getInt(2) + val stateRef = StateRef(txHash, index) + val state = rs.getBlob(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) + val pennies = rs.getLong(4) + totalPennies = rs.getLong(5) + val rowLockId = rs.getString(6) + stateAndRefs.add(StateAndRef(state, stateRef)) + log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } + } + + if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { + // we should have a minimum number of states to satisfy our selection `amount` criteria + log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") + + // With the current single threaded state machine available states are guaranteed to lock. + // TODO However, we will have to revisit these methods in the future multi-threaded. + services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) + return true + } + log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") + // retry as more states may become available + } catch (e: SQLException) { + log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] + $e. + """) + } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine + log.warn(e.message) + // retry only if there are locked states that may become available again (or consumed with change) + } + } + return false + } +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt index fb3a84371b..5894eff73f 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -1,28 +1,15 @@ package net.corda.finance.contracts.asset.cash.selection -import co.paralleluniverse.fibers.Suspendable -import co.paralleluniverse.strands.Strand import net.corda.core.contracts.Amount -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub -import net.corda.core.node.services.StatesNotAvailableException -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.deserialize import net.corda.core.utilities.* -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import java.sql.Connection import java.sql.DatabaseMetaData -import java.sql.SQLException +import java.sql.ResultSet import java.util.* -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock -class CashSelectionH2Impl : CashSelection { +class CashSelectionH2Impl : AbstractCashSelection() { companion object { const val JDBC_DRIVER_NAME = "H2 JDBC Driver" @@ -33,118 +20,48 @@ class CashSelectionH2Impl : CashSelection { return metadata.driverName == JDBC_DRIVER_NAME } - // coin selection retry loop counter, sleep (msecs) and lock for selecting states - private val MAX_RETRIES = 5 - private val RETRY_SLEEP = 100 - private val spendLock: ReentrantLock = ReentrantLock() + override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" - /** - * An optimised query to gather Cash states that are available and retry if they are temporarily unavailable. - * @param services The service hub to allow access to the database session - * @param amount The amount of currency desired (ignoring issues, but specifying the currency) - * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, - * otherwise the set of eligible states wil be filtered to only include those from these issuers. - * @param notary If null the notary source is ignored, if specified then only states marked - * with this notary are included. - * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. - * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. - * @param withIssuerRefs If not empty the specific set of issuer references to match against. - * @return The matching states that were found. If sufficient funds were found these will be locked, - * otherwise what is available is returned unlocked for informational purposes. - */ - @Suspendable - override fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set, - notary: Party?, - lockId: UUID, - withIssuerRefs: Set): List> { - val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1) - val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1) + // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: + // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the + // running total of such an accumulator + // 2) H2 uses session variables to perform this accumulator function: + // http://www.h2database.com/html/functions.html#set + // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) + override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet { + connection.createStatement().execute("CALL SET(@t, 0);") - val stateAndRefs = mutableListOf>() - - // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: - // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the - // running total of such an accumulator - // 2) H2 uses session variables to perform this accumulator function: - // http://www.h2database.com/html/functions.html#set - // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) - - for (retryCount in 1..MAX_RETRIES) { - - spendLock.withLock { - val statement = services.jdbcSession().createStatement() - try { - statement.execute("CALL SET(@t, 0);") - - // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) - // the softLockReserve update will detect whether we try to lock states locked by others - val selectJoin = """ + val selectJoin = """ SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 - AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} - AND (vs.lock_id = '$lockId' OR vs.lock_id is null) + AND ccs.ccy_code = ? and @t < ? + AND (vs.lock_id = ? OR vs.lock_id is null) """ + - (if (notary != null) - " AND vs.notary_name = '${notary.name}'" else "") + - (if (onlyFromIssuerParties.isNotEmpty()) - " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + - (if (withIssuerRefs.isNotEmpty()) - " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") + (if (notary != null) + " AND vs.notary_name = ?" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key IN (?)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref IN (?)" else "") - // Retrieve spendable state refs - val rs = statement.executeQuery(selectJoin) - stateAndRefs.clear() - log.debug(selectJoin) - var totalPennies = 0L - while (rs.next()) { - val txHash = SecureHash.parse(rs.getString(1)) - val index = rs.getInt(2) - val stateRef = StateRef(txHash, index) - val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) - val pennies = rs.getLong(4) - totalPennies = rs.getLong(5) - val rowLockId = rs.getString(6) - stateAndRefs.add(StateAndRef(state, stateRef)) - log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } - } + // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection) + val psSelectJoin = connection.prepareStatement(selectJoin) + var pIndex = 0 + psSelectJoin.setString(++pIndex, amount.token.currencyCode) + psSelectJoin.setLong(++pIndex, amount.quantity) + psSelectJoin.setString(++pIndex, lockId.toString()) + if (notary != null) + psSelectJoin.setString(++pIndex, notary.name.toString()) + if (onlyFromIssuerParties.isNotEmpty()) + psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() ) + if (withIssuerRefs.isNotEmpty()) + psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray()) + log.debug { psSelectJoin.toString() } - if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { - // we should have a minimum number of states to satisfy our selection `amount` criteria - log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") - - // With the current single threaded state machine available states are guaranteed to lock. - // TODO However, we will have to revisit these methods in the future multi-threaded. - services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) - return stateAndRefs - } - log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") - // retry as more states may become available - } catch (e: SQLException) { - log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] - $e. - """) - } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine - stateAndRefs.clear() - log.warn(e.message) - // retry only if there are locked states that may become available again (or consumed with change) - } finally { - statement.close() - } - } - - log.warn("Coin selection failed on attempt $retryCount") - // TODO: revisit the back off strategy for contended spending. - if (retryCount != MAX_RETRIES) { - Strand.sleep(RETRY_SLEEP * retryCount.toLong()) - } - } - - log.warn("Insufficient spendable states identified for $amount") - return stateAndRefs + return psSelectJoin.executeQuery() } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt index 73a49e8a18..853ba23d07 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt @@ -1,17 +1,15 @@ package net.corda.finance.contracts.asset.cash.selection import net.corda.core.contracts.Amount -import net.corda.core.contracts.StateAndRef import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub import net.corda.core.utilities.OpaqueBytes -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import java.sql.Connection import java.sql.DatabaseMetaData +import java.sql.ResultSet import java.util.* -class CashSelectionMySQLImpl : CashSelection { +class CashSelectionMySQLImpl : AbstractCashSelection() { companion object { const val JDBC_DRIVER_NAME = "MySQL JDBC Driver" @@ -21,12 +19,9 @@ class CashSelectionMySQLImpl : CashSelection { return metadata.driverName == JDBC_DRIVER_NAME } - override fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set, - notary: Party?, - lockId: UUID, - withIssuerRefs: Set): List> { + override fun executeQuery(statement: Connection, amount: Amount, lockId: UUID, notary: Party?, issuerKeysStr: Set, issuerRefsStr: Set): ResultSet { TODO("MySQL cash selection not implemented") } - } \ No newline at end of file + + override fun toString() = "${this::class.java} for ${CashSelectionH2Impl.JDBC_DRIVER_NAME}" +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt index 59d50403ca..c202d5633c 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt @@ -1,38 +1,59 @@ package net.corda.finance.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowException +import com.typesafe.config.ConfigFactory import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.declaredField +import net.corda.core.internal.div +import net.corda.core.internal.read +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.finance.CHF import net.corda.finance.EUR import net.corda.finance.GBP import net.corda.finance.USD +import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies +import java.nio.file.Path import java.util.* +// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf +// again to get our config and store it here for access by our flow +@CordaService +class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() { + companion object { + val supportedCurrencies = listOf(USD, GBP, CHF, EUR) + } + + val issuableCurrencies: List + + init { + // Warning!! You are about to see a major hack! + val baseDirectory = services.declaredField("serviceHub").value + .let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) } + .declaredField("baseDirectory").value + val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) } + if (config.hasPath("issuableCurrencies")) { + issuableCurrencies = config.getStringList("issuableCurrencies").map { Currency.getInstance(it) } + require(supportedCurrencies.containsAll(issuableCurrencies)) + } else { + issuableCurrencies = emptyList() + } + } +} + + /** * Flow to obtain cash cordapp app configuration. */ @StartableByRPC class CashConfigDataFlow : FlowLogic() { - companion object { - private val supportedCurrencies = listOf(USD, GBP, CHF, EUR) - } - @Suspendable override fun call(): CashConfiguration { - val issuableCurrencies = supportedCurrencies.mapNotNull { - try { - // Currently it uses checkFlowPermission to determine the list of issuable currency as a temporary hack. - // TODO: get the config from proper configuration source. - checkFlowPermission("corda.issuer.$it", emptyMap()) - it - } catch (e: FlowException) { - null - } - } - return CashConfiguration(issuableCurrencies, supportedCurrencies) + val configHolder = serviceHub.cordaService(ConfigHolder::class.java) + return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies) } } diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index 763f0d4cff..23b05a1e8f 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -14,7 +14,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.issuedBy import java.util.* @@ -46,7 +46,7 @@ class CashExitFlow(private val amount: Amount, progressTracker.currentStep = GENERATING_TX val builder = TransactionBuilder(notary = null) val issuer = ourIdentity.ref(issuerRef) - val exitStates = CashSelection + val exitStates = AbstractCashSelection .getInstance { serviceHub.jdbcSession().metaData } .unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) val signers = try { @@ -59,8 +59,8 @@ class CashExitFlow(private val amount: Amount, } // Work out who the owners of the burnt states were (specify page size so we don't silently drop any if > DEFAULT_PAGE_SIZE) - val inputStates = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = builder.inputStates()), - PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states + val inputStates = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = builder.inputStates()), + PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them // count as a reason to fail? diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt index 48f0f43c4a..95a58e8cfb 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt @@ -32,6 +32,7 @@ class CashIssueAndPaymentFlow(val amount: Amount, recipient: Party, anonymous: Boolean, notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker()) + constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker()) @Suspendable diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt index 56f071783a..39ef76823b 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt @@ -31,6 +31,7 @@ class CashIssueFlow(private val amount: Amount, constructor(amount: Amount, issuerBankPartyRef: OpaqueBytes, notary: Party) : this(amount, issuerBankPartyRef, notary, tracker()) + constructor(request: IssueRequest) : this(request.amount, request.issueRef, request.notary, tracker()) @Suspendable diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt index 8857835da2..dc71e54884 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt @@ -31,8 +31,10 @@ open class CashPaymentFlow( val issuerConstraint: Set = emptySet()) : AbstractCashFlow(progressTracker) { /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) + /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker()) + constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint) @Suspendable diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt index f87bfb2390..b1dd0137aa 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt @@ -43,6 +43,7 @@ object TwoPartyDealFlow { companion object { object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities") object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.") + fun tracker() = ProgressTracker(GENERATING_ID, SENDING_PROPOSAL) } @@ -50,12 +51,14 @@ object TwoPartyDealFlow { abstract val notaryParty: Party abstract val otherSideSession: FlowSession + // DOCSTART 2 @Suspendable override fun call(): SignedTransaction { progressTracker.currentStep = GENERATING_ID val txIdentities = subFlow(SwapIdentitiesFlow(otherSideSession.counterparty)) val anonymousMe = txIdentities[ourIdentity] ?: ourIdentity.anonymise() val anonymousCounterparty = txIdentities[otherSideSession.counterparty] ?: otherSideSession.counterparty.anonymise() + // DOCEND 2 progressTracker.currentStep = SENDING_PROPOSAL // Make the first message we'll send to kick off the flow. val hello = Handshake(payload, anonymousMe, anonymousCounterparty) @@ -146,6 +149,7 @@ object TwoPartyDealFlow { @Suspendable protected abstract fun validateHandshake(handshake: Handshake): Handshake + @Suspendable protected abstract fun assembleSharedTX(handshake: Handshake): Triple, List> } diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index 47da4a403a..2d5093a583 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -136,6 +136,7 @@ object TwoPartyTradeFlow { private val anonymous: Boolean) : FlowLogic() { constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount, typeToBuy: Class) : this(otherSideSession, notary, acceptablePrice, typeToBuy, true) + // DOCSTART 2 object RECEIVING : ProgressTracker.Step("Waiting for seller trading info") @@ -169,6 +170,7 @@ object TwoPartyTradeFlow { progressTracker.currentStep = SIGNING val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest, buyerAnonymousIdentity) + // DOCSTART 6 // Now sign the transaction with whatever keys we need to move the cash. val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys) @@ -180,6 +182,7 @@ object TwoPartyTradeFlow { progressTracker.currentStep = COLLECTING_SIGNATURES val sellerSignature = subFlow(CollectSignatureFlow(partSignedTx, sellerSession, sellerSession.counterparty.owningKey)) val twiceSignedTx = partSignedTx + sellerSignature + // DOCEND 6 // Notarise and record the transaction. progressTracker.currentStep = RECORDING @@ -202,8 +205,8 @@ object TwoPartyTradeFlow { // Register the identity we're about to send payment to. This shouldn't be the same as the asset owner // identity, so that anonymity is enforced. - val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity) - require(wellKnownPayToIdentity?.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" } + val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity) ?: it.payToIdentity + require(wellKnownPayToIdentity.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" } if (it.price > acceptablePrice) throw UnacceptablePriceException(it.price) diff --git a/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt index 6ae8d9a9a0..5fdfc58492 100644 --- a/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt @@ -22,8 +22,8 @@ object CashSchema object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "contract_cash_states", - indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), - Index(name = "pennies_idx", columnList = "pennies"))) + indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), + Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( /** X500Name of owner party **/ @Column(name = "owner_name") diff --git a/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt b/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt index b1d58a5c8c..f8c1b46b0e 100644 --- a/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt @@ -22,9 +22,9 @@ object CommercialPaperSchema object CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states", - indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), - Index(name = "maturity_index", columnList = "maturity_instant"), - Index(name = "face_value_index", columnList = "face_value"))) + indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), + Index(name = "maturity_index", columnList = "maturity_instant"), + Index(name = "face_value_index", columnList = "face_value"))) class PersistentCommercialPaperState( @Column(name = "issuance_key") var issuanceParty: String, diff --git a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt index 75d6c072f9..5f3d7c8f37 100644 --- a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt +++ b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt @@ -1,4 +1,5 @@ @file:JvmName("StateSumming") + package net.corda.finance.utils import net.corda.core.contracts.Amount diff --git a/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection b/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection similarity index 100% rename from finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection rename to finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection diff --git a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java index 59ba1ad99c..455472503f 100644 --- a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java @@ -34,7 +34,7 @@ public class CashTestsJava { }); tx.tweak(tw -> { - tw.output(Cash.PROGRAM_ID, () -> outState ); + tw.output(Cash.PROGRAM_ID, () -> outState); tw.command(getMEGA_CORP_PUBKEY(), DummyCommandData.INSTANCE); // Invalid command return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command"); diff --git a/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java b/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java deleted file mode 100644 index b753141ac5..0000000000 --- a/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.finance.flows; - -import net.corda.core.flows.AbstractStateReplacementFlow; -import net.corda.core.flows.FlowSession; -import net.corda.core.transactions.SignedTransaction; -import net.corda.core.utilities.ProgressTracker; -import org.jetbrains.annotations.NotNull; - -@SuppressWarnings("unused") -public class AbstractStateReplacementFlowTest { - - // Acceptor used to have a type parameter of Unit which prevented Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964). - private static class TestAcceptorCanBeInheritedInJava extends AbstractStateReplacementFlow.Acceptor { - public TestAcceptorCanBeInheritedInJava(@NotNull FlowSession otherSideSession, @NotNull ProgressTracker progressTracker) { - super(otherSideSession, progressTracker); - } - - @Override - protected void verifyProposal(@NotNull SignedTransaction stx, @NotNull AbstractStateReplacementFlow.Proposal proposal) { - } - } -} diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 73bdd6d4aa..d1d6751aa1 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -80,7 +80,8 @@ class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate { @RunWith(Parameterized::class) class CommercialPaperTestsGeneric { companion object { - @Parameterized.Parameters @JvmStatic + @Parameterized.Parameters + @JvmStatic fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest()) } @@ -95,8 +96,8 @@ class CommercialPaperTestsGeneric { ledger { unverifiedTransaction { attachment(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) - output(Cash.PROGRAM_ID, "some profits", someProfits.STATE `owned by` MEGA_CORP) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy MEGA_CORP) } // Some CP is issued onto the ledger by MegaCorp. @@ -114,7 +115,7 @@ class CommercialPaperTestsGeneric { attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID) input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP } output(thisTest.getContract(), "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } @@ -129,8 +130,8 @@ class CommercialPaperTestsGeneric { input("some profits") fun TransactionDSL.outputs(aliceGetsBack: Amount>) { - output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE `owned by` ALICE } - output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP } + output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE ownedBy ALICE } + output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP } } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } @@ -227,10 +228,9 @@ class CommercialPaperTestsGeneric { private lateinit var moveTX: SignedTransaction -// @Test + // @Test @Ignore fun `issue move and then redeem`() { - setCordappPackages("net.corda.finance.contracts") initialiseTestSerialization() val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY)) val databaseAlice = aliceDatabaseAndServices.first diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt new file mode 100644 index 0000000000..ef85dead0a --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt @@ -0,0 +1,40 @@ +package net.corda.finance.contracts.asset + +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashException +import net.corda.finance.flows.CashPaymentFlow +import net.corda.testing.chooseIdentity +import net.corda.testing.node.MockNetwork +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test + + +class CashSelectionH2Test { + + @Test + fun `check does not hold connection over retries`() { + val mockNet = MockNetwork(threadPerNode = true) + try { + val notaryNode = mockNet.createNotaryNode() + val bankA = mockNet.createNode(configOverrides = { existingConfig -> + // Tweak connections to be minimal to make this easier (1 results in a hung node during start up, so use 2 connections). + existingConfig.dataSourceProperties.setProperty("maximumPoolSize", "2") + existingConfig + }) + + mockNet.startNodes() + + // Start more cash spends than we have connections. If spend leaks a connection on retry, we will run out of connections. + val flow1 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + val flow2 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + val flow3 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + + assertThatThrownBy { flow1.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + assertThatThrownBy { flow2.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + assertThatThrownBy { flow3.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + } finally { + mockNet.stopNodes() + } + } +} \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 770e0ae5a2..df14ca6843 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -3,9 +3,8 @@ package net.corda.finance.contracts.asset import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party +import net.corda.core.identity.* +import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.transactions.TransactionBuilder @@ -31,25 +30,25 @@ import java.util.* import kotlin.test.* class CashTests : TestDependencyInjectionBase() { - val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) - val defaultIssuer = MEGA_CORP.ref(defaultRef) - val inState = Cash.State( + private val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) + private val defaultIssuer = MEGA_CORP.ref(defaultRef) + private val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, owner = AnonymousParty(ALICE_PUBKEY) ) // Input state held by the issuer - val issuerInState = inState.copy(owner = defaultIssuer.party) - val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY)) + private val issuerInState = inState.copy(owner = defaultIssuer.party) + private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY)) - fun Cash.State.editDepositRef(ref: Byte) = copy( + private fun Cash.State.editDepositRef(ref: Byte) = copy( amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) ) - lateinit var miniCorpServices: MockServices - lateinit var megaCorpServices: MockServices + private lateinit var miniCorpServices: MockServices + private lateinit var megaCorpServices: MockServices val vault: VaultService get() = miniCorpServices.vaultService lateinit var database: CordaPersistence - lateinit var vaultStatesUnconsumed: List> + private lateinit var vaultStatesUnconsumed: List> @Before fun setUp() { @@ -71,7 +70,7 @@ class CashTests : TestDependencyInjectionBase() { ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices) } database.transaction { - vaultStatesUnconsumed = miniCorpServices.vaultQueryService.queryBy().states + vaultStatesUnconsumed = miniCorpServices.vaultService.queryBy().states } resetTestSerialization() } @@ -105,7 +104,7 @@ class CashTests : TestDependencyInjectionBase() { } tweak { output(Cash.PROGRAM_ID) { outState } - output(Cash.PROGRAM_ID) { outState `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP } command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "at least one cash input" } @@ -309,7 +308,7 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID) { inState } - output(Cash.PROGRAM_ID) { outState `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP } command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" } @@ -348,7 +347,7 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID) { inState } - input(Cash.PROGRAM_ID) { inState `issued by` MINI_CORP } + input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP } output(Cash.PROGRAM_ID) { outState } command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" @@ -396,9 +395,9 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID) { issuerInState } - input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) `issued by` MINI_CORP } + input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP } - output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP } output(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) } command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() } @@ -432,7 +431,7 @@ class CashTests : TestDependencyInjectionBase() { attachment(Cash.PROGRAM_ID) // Gather 2000 dollars from two different issuers. input(Cash.PROGRAM_ID) { inState } - input(Cash.PROGRAM_ID) { inState `issued by` MINI_CORP } + input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP } command(ALICE_PUBKEY) { Cash.Commands.Move() } // Can't merge them together. @@ -449,7 +448,7 @@ class CashTests : TestDependencyInjectionBase() { // This works. output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) } - output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP } this.verifies() } } @@ -460,10 +459,10 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY)) - input(Cash.PROGRAM_ID) { inState `owned by` AnonymousParty(ALICE_PUBKEY) } + input(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(ALICE_PUBKEY) } input(Cash.PROGRAM_ID) { pounds } - output(Cash.PROGRAM_ID) { inState `owned by` AnonymousParty(BOB_PUBKEY) } - output(Cash.PROGRAM_ID) { pounds `owned by` AnonymousParty(ALICE_PUBKEY) } + output(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(BOB_PUBKEY) } + output(Cash.PROGRAM_ID) { pounds ownedBy AnonymousParty(ALICE_PUBKEY) } command(ALICE_PUBKEY, BOB_PUBKEY) { Cash.Commands.Move() } this.verifies() @@ -474,19 +473,20 @@ class CashTests : TestDependencyInjectionBase() { // // Spend tx generation - val OUR_KEY: KeyPair by lazy { generateKeyPair() } - val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) + private val OUR_KEY: KeyPair by lazy { generateKeyPair() } + private val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) + private val OUR_IDENTITY_AND_CERT = getTestPartyAndCertificate(CordaX500Name(organisation = "Me", locality = "London", country = "GB"), OUR_KEY.public) - val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) - val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) + private val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) + private val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) - fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = + private fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = StateAndRef( - TransactionState(Cash.State(amount `issued by` corp.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) - val WALLET = listOf( + private val WALLET = listOf( makeCash(100.DOLLARS, MEGA_CORP), makeCash(400.DOLLARS, MEGA_CORP), makeCash(80.DOLLARS, MINI_CORP), @@ -496,16 +496,17 @@ class CashTests : TestDependencyInjectionBase() { /** * Generate an exit transaction, removing some amount of cash from the ledger. */ - private fun makeExit(amount: Amount, corp: Party, depositRef: Byte = 1): WireTransaction { + private fun makeExit(serviceHub: ServiceHub, amount: Amount, issuer: Party, depositRef: Byte = 1): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), WALLET) - return tx.toWireTransaction(miniCorpServices) + val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(MINI_CORP_IDENTITY, false).party + Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), WALLET, payChangeTo) + return tx.toWireTransaction(serviceHub) } private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - Cash.generateSpend(miniCorpServices, tx, amount, dest) + Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest) } return tx.toWireTransaction(miniCorpServices) } @@ -516,7 +517,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateSimpleExit() { initialiseTestSerialization() - val wtx = makeExit(100.DOLLARS, MEGA_CORP, 1) + val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(0, wtx.outputs.size) @@ -532,10 +533,16 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generatePartialExit() { initialiseTestSerialization() - val wtx = makeExit(50.DOLLARS, MEGA_CORP, 1) - assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(1, wtx.outputs.size) - assertEquals(WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.splitEvenly(2).first()), wtx.getOutput(0)) + val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1) + val actualInput = wtx.inputs.single() + // Filter the available inputs and confirm exactly one has been used + val expectedInputs = WALLET.filter { it.ref == actualInput } + assertEquals(1, expectedInputs.size) + val inputState = expectedInputs.single() + val actualChange = wtx.outputs.single().data as Cash.State + val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity + val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner) + assertEquals(expectedChange, wtx.getOutput(0)) } /** @@ -544,7 +551,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateAbsentExit() { initialiseTestSerialization() - assertFailsWith { makeExit(100.POUNDS, MEGA_CORP, 1) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) } } /** @@ -553,7 +560,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateInvalidReferenceExit() { initialiseTestSerialization() - assertFailsWith { makeExit(100.POUNDS, MEGA_CORP, 2) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) } } /** @@ -562,7 +569,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateInsufficientExit() { initialiseTestSerialization() - assertFailsWith { makeExit(1000.DOLLARS, MEGA_CORP, 1) } + assertFailsWith { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) } } /** @@ -571,7 +578,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateOwnerWithNoStatesExit() { initialiseTestSerialization() - assertFailsWith { makeExit(100.POUNDS, CHARLIE, 1) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) } } /** @@ -580,9 +587,9 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateExitWithEmptyVault() { initialiseTestSerialization() - assertFailsWith { + assertFailsWith { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList()) + Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1) } } @@ -590,11 +597,10 @@ class CashTests : TestDependencyInjectionBase() { fun generateSimpleDirectSpend() { initialiseTestSerialization() val wtx = - database.transaction { - makeSpend(100.DOLLARS, THEIR_IDENTITY_1) - } + database.transaction { + makeSpend(100.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { - @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1), wtx.getOutput(0)) @@ -608,7 +614,7 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) + Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -618,20 +624,19 @@ class CashTests : TestDependencyInjectionBase() { fun generateSimpleSpendWithChange() { initialiseTestSerialization() val wtx = - database.transaction { - makeSpend(10.DOLLARS, THEIR_IDENTITY_1) - } + database.transaction { + makeSpend(10.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { - @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) val changeAmount = 90.DOLLARS `issued by` defaultIssuer - val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).filter { state -> + val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).single { state -> if (state is Cash.State) { state.amount == changeAmount } else { false } - }.single() + } val changeOwner = (likelyChangeState as Cash.State).owner assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size) assertEquals(vaultState.ref, wtx.inputs[0]) @@ -645,11 +650,10 @@ class CashTests : TestDependencyInjectionBase() { fun generateSpendWithTwoInputs() { initialiseTestSerialization() val wtx = - database.transaction { - makeSpend(500.DOLLARS, THEIR_IDENTITY_1) - } + database.transaction { + makeSpend(500.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { - @Suppress("UNCHECKED_CAST") val vaultState0 = vaultStatesUnconsumed.elementAt(0) val vaultState1 = vaultStatesUnconsumed.elementAt(1) assertEquals(vaultState0.ref, wtx.inputs[0]) @@ -663,11 +667,11 @@ class CashTests : TestDependencyInjectionBase() { fun generateSpendMixedDeposits() { initialiseTestSerialization() val wtx = - database.transaction { - val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) - assertEquals(3, wtx.inputs.size) - wtx - } + database.transaction { + val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) + assertEquals(3, wtx.inputs.size) + wtx + } database.transaction { val vaultState0: StateAndRef = vaultStatesUnconsumed.elementAt(0) val vaultState1: StateAndRef = vaultStatesUnconsumed.elementAt(1) @@ -721,8 +725,8 @@ class CashTests : TestDependencyInjectionBase() { Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token) // States cannot be aggregated if the reference differs - assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token) - assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token) + assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token) + assertNotEquals((fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token) } @Test @@ -799,7 +803,7 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input("MEGA_CORP cash") - output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output().copy(owner = AnonymousParty(ALICE_PUBKEY)) ) + output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output().copy(owner = AnonymousParty(ALICE_PUBKEY))) command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } this.verifies() } 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 3b053c212b..c2d683a74b 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 @@ -60,7 +60,7 @@ class ObligationTests { output(Obligation.PROGRAM_ID, "Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) output(Obligation.PROGRAM_ID, "Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB)) - output(Obligation.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) + output(Obligation.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) } } @@ -286,7 +286,7 @@ class ObligationTests { assertEquals(expected, actual) } - private inline fun getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef { + private inline fun getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef { val txState = TransactionState(state, contractClassName, DUMMY_NOTARY) return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0)) @@ -418,7 +418,7 @@ class ObligationTests { @Test fun `payment netting`() { // Try netting out two obligations - ledger(mockService) { + ledger(mockService) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) @@ -484,7 +484,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } attachment(attachment(cashContractBytes.inputStream())) @@ -498,9 +498,9 @@ class ObligationTests { transaction("Settlement") { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) - input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) + input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB) } - output(Obligation.PROGRAM_ID, "Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + output(Obligation.PROGRAM_ID, "Bob's $500,000") { 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } attachment(attachment(cashContractBytes.inputStream())) @@ -514,8 +514,8 @@ class ObligationTests { transaction("Settlement") { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob - input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) - output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) + output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } this `fails with` "all inputs are in the normal state" @@ -529,7 +529,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } attachment(attachment(cashContractBytes.inputStream())) @@ -593,7 +593,6 @@ class ObligationTests { } // Try defaulting an obligation that is now in the past - unsetCordappPackages() ledger { transaction("Settlement") { attachments(Obligation.PROGRAM_ID) diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt index 1180a73ff0..94b3f83658 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt @@ -7,13 +7,10 @@ import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.node.internal.StartedNode -import net.corda.testing.chooseIdentity -import net.corda.testing.getDefaultNotary +import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -21,7 +18,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashExitFlowTests { - private lateinit var mockNet : MockNetwork + private lateinit var mockNet: MockNetwork private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) private lateinit var bankOfCordaNode: StartedNode @@ -31,11 +28,10 @@ class CashExitFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - val nodes = mockNet.createSomeNodes(1) - notaryNode = nodes.notaryNode - bankOfCordaNode = nodes.partyNodes[0] + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) + notaryNode = mockNet.createNotaryNode() + bankOfCordaNode = mockNet.createPartyNode(BOC.name) + notary = notaryNode.services.getDefaultNotary() bankOfCorda = bankOfCordaNode.info.chooseIdentity() mockNet.runNetwork() @@ -48,7 +44,6 @@ class CashExitFlowTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt index b5901c1c30..911a014d5f 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -9,10 +9,10 @@ import net.corda.finance.contracts.asset.Cash import net.corda.node.internal.StartedNode import net.corda.testing.chooseIdentity import net.corda.testing.getDefaultNotary +import net.corda.testing.BOC import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.setCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -20,7 +20,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashIssueFlowTests { - private lateinit var mockNet : MockNetwork + private lateinit var mockNet: MockNetwork private lateinit var bankOfCordaNode: StartedNode private lateinit var bankOfCorda: Party private lateinit var notaryNode: StartedNode @@ -28,15 +28,12 @@ class CashIssueFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - val nodes = mockNet.createSomeNodes(1) - notaryNode = nodes.notaryNode - bankOfCordaNode = nodes.partyNodes[0] + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) + notaryNode = mockNet.createNotaryNode() + bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() - + notary = notaryNode.services.getDefaultNotary() mockNet.runNetwork() - notary = bankOfCordaNode.services.getDefaultNotary() } @After diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 5fe62e9d3c..0c5b1e5efa 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -10,14 +10,10 @@ import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.node.internal.StartedNode -import net.corda.testing.chooseIdentity -import net.corda.testing.expect -import net.corda.testing.expectEvents -import net.corda.testing.getDefaultNotary +import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.setCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -25,7 +21,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashPaymentFlowTests { - private lateinit var mockNet : MockNetwork + private lateinit var mockNet: MockNetwork private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) private lateinit var bankOfCordaNode: StartedNode @@ -35,11 +31,9 @@ class CashPaymentFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - val nodes = mockNet.createSomeNodes(1) - notaryNode = nodes.notaryNode - bankOfCordaNode = nodes.partyNodes[0] + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) + notaryNode = mockNet.createNotaryNode() + bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() notary = notaryNode.services.getDefaultNotary() val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture @@ -61,8 +55,8 @@ class CashPaymentFlowTests { bankOfCordaNode.database.transaction { // Register for vault updates val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) - val (_, vaultUpdatesBankClient) = notaryNode.services.vaultQueryService.trackBy(criteria) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = notaryNode.services.vaultService.trackBy(criteria) val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, payTo)).resultFuture diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt index f9395c1adb..b94d6ac700 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt @@ -19,8 +19,8 @@ object CashSchema object SampleCashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "contract_cash_states", - indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), - Index(name = "pennies_idx", columnList = "pennies"))) + indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), + Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( @Column(name = "owner_key") var owner: String, diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt index 7a5e0da89c..95108eddfa 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt @@ -13,25 +13,25 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 2, - mappedTypes = listOf(PersistentCashState::class.java)) { + mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "cash_states_v2", - indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code"))) - class PersistentCashState ( - /** product type */ - @Column(name = "ccy_code", length = 3) - var currency: String, + indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code"))) + class PersistentCashState( + /** product type */ + @Column(name = "ccy_code", length = 3) + var currency: String, - /** parent attributes */ - @Transient - val _participants: Set, - @Transient - val _owner: AbstractParty, - @Transient - val _quantity: Long, - @Transient - val _issuerParty: AbstractParty, - @Transient - val _issuerRef: ByteArray + /** parent attributes */ + @Transient + val _participants: Set, + @Transient + val _owner: AbstractParty, + @Transient + val _quantity: Long, + @Transient + val _issuerParty: AbstractParty, + @Transient + val _issuerRef: ByteArray ) : CommonSchemaV1.FungibleState(_participants.toMutableSet(), _owner, _quantity, _issuerParty, _issuerRef) } diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt index f6288c0591..eaae33410f 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt @@ -13,7 +13,7 @@ import javax.persistence.Table * at the time of writing. */ object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 3, - mappedTypes = listOf(PersistentCashState::class.java)) { + mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "cash_states_v3") class PersistentCashState( diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt index 9b4c91b179..4da37b7143 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt @@ -20,9 +20,9 @@ object CommercialPaperSchema object SampleCommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states", - indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), - Index(name = "maturity_index", columnList = "maturity_instant"), - Index(name = "face_value_index", columnList = "face_value"))) + indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), + Index(name = "maturity_index", columnList = "maturity_instant"), + Index(name = "face_value_index", columnList = "face_value"))) class PersistentCommercialPaperState( @Column(name = "issuance_key") var issuanceParty: String, diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt index ead55b4b1e..a3b855fe25 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt @@ -14,11 +14,11 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, - mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { + mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states_v2", - indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"), - Index(name = "maturity_index2", columnList = "maturity_instant"))) + indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"), + Index(name = "maturity_index2", columnList = "maturity_instant"))) class PersistentCommercialPaperState( @Column(name = "maturity_instant") var maturity: Instant, diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gradle-plugins/api-scanner/README.md b/gradle-plugins/api-scanner/README.md new file mode 100644 index 0000000000..9f8fc4f674 --- /dev/null +++ b/gradle-plugins/api-scanner/README.md @@ -0,0 +1,79 @@ +# API Scanner + +Generates a text summary of Corda's public API that we can check for API-breaking changes. + +```bash +$ gradlew generateApi +``` + +See [here](../../docs/source/api-index.rst) for Corda's public API strategy. We will need to +apply this plugin to other modules in future Corda releases as those modules' APIs stabilise. + +Basically, this plugin will document a module's `public` and `protected` classes/methods/fields, +excluding those from our `*.internal.*` packgages, any synthetic methods, bridge methods, or methods +identified as having Kotlin's `internal` scope. (Kotlin doesn't seem to have implemented `internal` +scope for classes or fields yet as these are currently `public` inside the `.class` file.) + +## Usage +Include this line in the `build.gradle` file of every Corda module that exports public API: + +```gradle +apply plugin: 'net.corda.plugins.api-scanner' +``` + +This will create a Gradle task called `scanApi` which will analyse that module's Jar artifacts. More precisely, +it will analyse all of the Jar artifacts that have not been assigned a Maven classifier, on the basis +that these should be the module's main artifacts. + +The `scanApi` task supports the following configuration options: +```gradle +scanApi { + // Make the classpath-scanning phase more verbose. + verbose = {true|false} + + // Enable / disable the task within this module. + enabled = {true|false} + + // Names of classes that should be excluded from the output. + excludeClasses = [ + ... + ] +} +``` + +All of the `ScanApi` tasks write their output files to their own `$buildDir/api` directory, where they +are collated into a single output file by the `GenerateApi` task. The `GenerateApi` task is declared +in the root project's `build.gradle` file: + +```gradle +task generateApi(type: net.corda.plugins.GenerateApi){ + baseName = "api-corda" +} +``` + +The final API file is written to `$buildDir/api/$baseName-$project.version.txt` + +### Sample Output +``` +public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash + public abstract void extractFile(String, java.io.OutputStream) + @org.jetbrains.annotations.NotNull public abstract List getSigners() + @org.jetbrains.annotations.NotNull public abstract java.io.InputStream open() + @org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR() +## +public interface net.corda.core.contracts.AttachmentConstraint + public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) +## +public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +``` + +#### Notes +The `GenerateApi` task will collate the output of every `ScanApi` task found either in the same project, +or in any of that project's subprojects. So it is _theoretically_ possible also to collate the API output +from subtrees of modules simply by defining a new `GenerateApi` task at the root of that subtree. + +## Plugin Installation +See [here](../README.rst) for full installation instructions. diff --git a/gradle-plugins/api-scanner/build.gradle b/gradle-plugins/api-scanner/build.gradle new file mode 100644 index 0000000000..85f7b4d0f9 --- /dev/null +++ b/gradle-plugins/api-scanner/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'java' +apply plugin: 'net.corda.plugins.publish-utils' + +description "Generates a summary of the artifact's public API" + +repositories { + mavenCentral() +} + +dependencies { + compile gradleApi() + compile "io.github.lukehutch:fast-classpath-scanner:2.7.0" + testCompile "junit:junit:4.12" +} + +publish { + name project.name +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java new file mode 100644 index 0000000000..0861605258 --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java @@ -0,0 +1,70 @@ +package net.corda.plugins; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.TaskCollection; +import org.gradle.jvm.tasks.Jar; + +public class ApiScanner implements Plugin { + + /** + * Identify the Gradle Jar tasks creating jars + * without Maven classifiers, and generate API + * documentation for them. + * @param p Current project. + */ + @Override + public void apply(Project p) { + p.getLogger().info("Applying API scanner to {}", p.getName()); + + ScannerExtension extension = p.getExtensions().create("scanApi", ScannerExtension.class); + + p.afterEvaluate(project -> { + TaskCollection jarTasks = project.getTasks() + .withType(Jar.class) + .matching(jarTask -> jarTask.getClassifier().isEmpty() && jarTask.isEnabled()); + if (jarTasks.isEmpty()) { + return; + } + + project.getLogger().info("Adding scanApi task to {}", project.getName()); + project.getTasks().create("scanApi", ScanApi.class, scanTask -> { + scanTask.setClasspath(compilationClasspath(project.getConfigurations())); + scanTask.setSources(project.files(jarTasks)); + scanTask.setExcludeClasses(extension.getExcludeClasses()); + scanTask.setVerbose(extension.isVerbose()); + scanTask.setEnabled(extension.isEnabled()); + scanTask.dependsOn(jarTasks); + + // Declare this ScanApi task to be a dependency of any + // GenerateApi tasks belonging to any of our ancestors. + project.getRootProject().getTasks() + .withType(GenerateApi.class) + .matching(generateTask -> isAncestorOf(generateTask.getProject(), project)) + .forEach(generateTask -> generateTask.dependsOn(scanTask)); + }); + }); + } + + /* + * Recurse through a child project's parents until we reach the root, + * and return true iff we find our target project along the way. + */ + private static boolean isAncestorOf(Project target, Project child) { + Project p = child; + while (p != null) { + if (p == target) { + return true; + } + p = p.getParent(); + } + return false; + } + + private static FileCollection compilationClasspath(ConfigurationContainer configurations) { + return configurations.getByName("compile") + .plus(configurations.getByName("compileOnly")); + } +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java new file mode 100644 index 0000000000..9c87224075 --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java @@ -0,0 +1,61 @@ +package net.corda.plugins; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import java.io.*; +import java.nio.file.Files; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +@SuppressWarnings("unused") +public class GenerateApi extends DefaultTask { + + private final File outputDir; + private String baseName; + + public GenerateApi() { + outputDir = new File(getProject().getBuildDir(), "api"); + baseName = "api-" + getProject().getName(); + } + + public void setBaseName(String baseName) { + this.baseName = baseName; + } + + @InputFiles + public FileCollection getSources() { + return getProject().files(getProject().getAllprojects().stream() + .flatMap(project -> project.getTasks() + .withType(ScanApi.class) + .matching(ScanApi::isEnabled) + .stream()) + .flatMap(scanTask -> scanTask.getTargets().getFiles().stream()) + .sorted(comparing(File::getName)) + .collect(toList()) + ); + } + + @OutputFile + public File getTarget() { + return new File(outputDir, String.format("%s-%s.txt", baseName, getProject().getVersion())); + } + + @TaskAction + public void generate() { + FileCollection apiFiles = getSources(); + if (!apiFiles.isEmpty()) { + try (OutputStream output = new BufferedOutputStream(new FileOutputStream(getTarget()))) { + for (File apiFile : apiFiles) { + Files.copy(apiFile.toPath(), output); + } + } catch (IOException e) { + getLogger().error("Failed to generate API file", e); + } + } + } +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java new file mode 100644 index 0000000000..f0220add42 --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java @@ -0,0 +1,333 @@ +package net.corda.plugins; + +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; +import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo; +import io.github.lukehutch.fastclasspathscanner.scanner.FieldInfo; +import io.github.lukehutch.fastclasspathscanner.scanner.MethodInfo; +import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.CompileClasspath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputFiles; +import org.gradle.api.tasks.TaskAction; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.stream.StreamSupport; + +import static java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.*; + +@SuppressWarnings("unused") +public class ScanApi extends DefaultTask { + private static final int CLASS_MASK = Modifier.classModifiers(); + private static final int INTERFACE_MASK = Modifier.interfaceModifiers() & ~Modifier.ABSTRACT; + private static final int METHOD_MASK = Modifier.methodModifiers(); + private static final int FIELD_MASK = Modifier.fieldModifiers(); + private static final int VISIBILITY_MASK = Modifier.PUBLIC | Modifier.PROTECTED; + + private static final Set ANNOTATION_BLACKLIST; + static { + Set blacklist = new LinkedHashSet<>(); + blacklist.add("kotlin.jvm.JvmOverloads"); + ANNOTATION_BLACKLIST = unmodifiableSet(blacklist); + } + + /** + * This information has been lifted from: + * @link Metadata.kt + */ + private static final String KOTLIN_METADATA = "kotlin.Metadata"; + private static final String KOTLIN_CLASSTYPE_METHOD = "k"; + private static final int KOTLIN_SYNTHETIC = 3; + + private final ConfigurableFileCollection sources; + private final ConfigurableFileCollection classpath; + private final Set excludeClasses; + private final File outputDir; + private boolean verbose; + + public ScanApi() { + sources = getProject().files(); + classpath = getProject().files(); + excludeClasses = new LinkedHashSet<>(); + outputDir = new File(getProject().getBuildDir(), "api"); + } + + @InputFiles + public FileCollection getSources() { + return sources; + } + + void setSources(FileCollection sources) { + this.sources.setFrom(sources); + } + + @CompileClasspath + @InputFiles + public FileCollection getClasspath() { + return classpath; + } + + void setClasspath(FileCollection classpath) { + this.classpath.setFrom(classpath); + } + + @Input + public Collection getExcludeClasses() { + return unmodifiableSet(excludeClasses); + } + + void setExcludeClasses(Collection excludeClasses) { + this.excludeClasses.clear(); + this.excludeClasses.addAll(excludeClasses); + } + + @OutputFiles + public FileCollection getTargets() { + return getProject().files( + StreamSupport.stream(sources.spliterator(), false) + .map(this::toTarget) + .collect(toList()) + ); + } + + public boolean isVerbose() { + return verbose; + } + + void setVerbose(boolean verbose) { + this.verbose = verbose; + } + + private File toTarget(File source) { + return new File(outputDir, source.getName().replaceAll(".jar$", ".txt")); + } + + @TaskAction + public void scan() { + try (Scanner scanner = new Scanner(classpath)) { + for (File source : sources) { + scanner.scan(source); + } + } catch (IOException e) { + getLogger().error("Failed to write API file", e); + } + } + + class Scanner implements Closeable { + private final URLClassLoader classpathLoader; + private final Class metadataClass; + private final Method classTypeMethod; + + @SuppressWarnings("unchecked") + Scanner(URLClassLoader classpathLoader) { + this.classpathLoader = classpathLoader; + + Class kClass; + Method kMethod; + try { + kClass = (Class) Class.forName(KOTLIN_METADATA, true, classpathLoader); + kMethod = kClass.getDeclaredMethod(KOTLIN_CLASSTYPE_METHOD); + } catch (ClassNotFoundException | NoSuchMethodException e) { + kClass = null; + kMethod = null; + } + + metadataClass = kClass; + classTypeMethod = kMethod; + } + + Scanner(FileCollection classpath) throws MalformedURLException { + this(new URLClassLoader(toURLs(classpath))); + } + + @Override + public void close() throws IOException { + classpathLoader.close(); + } + + void scan(File source) { + File target = toTarget(source); + try ( + URLClassLoader appLoader = new URLClassLoader(new URL[]{ toURL(source) }, classpathLoader); + PrintWriter writer = new PrintWriter(target, "UTF-8") + ) { + scan(writer, appLoader); + } catch (IOException e) { + getLogger().error("API scan has failed", e); + } + } + + void scan(PrintWriter writer, ClassLoader appLoader) { + ScanResult result = new FastClasspathScanner(getScanSpecification()) + .overrideClassLoaders(appLoader) + .ignoreParentClassLoaders() + .ignoreMethodVisibility() + .ignoreFieldVisibility() + .enableMethodInfo() + .enableFieldInfo() + .verbose(verbose) + .scan(); + writeApis(writer, result); + } + + private String[] getScanSpecification() { + String[] spec = new String[2 + excludeClasses.size()]; + spec[0] = "!"; // Don't blacklist system classes from the output. + spec[1] = "-dir:"; // Ignore classes on the filesystem. + + int i = 2; + for (String excludeClass : excludeClasses) { + spec[i++] = '-' + excludeClass; + } + return spec; + } + + private void writeApis(PrintWriter writer, ScanResult result) { + Map allInfo = result.getClassNameToClassInfo(); + result.getNamesOfAllClasses().forEach(className -> { + if (className.contains(".internal.")) { + // These classes belong to internal Corda packages. + return; + } + ClassInfo classInfo = allInfo.get(className); + if (classInfo.getClassLoaders() == null) { + // Ignore classes that belong to one of our target ClassLoader's parents. + return; + } + + Class javaClass = result.classNameToClassRef(className); + if (!isVisible(javaClass.getModifiers())) { + // Excludes private and package-protected classes + return; + } + + int kotlinClassType = getKotlinClassType(javaClass); + if (kotlinClassType == KOTLIN_SYNTHETIC) { + // Exclude classes synthesised by the Kotlin compiler. + return; + } + + writeClass(writer, classInfo, javaClass.getModifiers()); + writeMethods(writer, classInfo.getMethodAndConstructorInfo()); + writeFields(writer, classInfo.getFieldInfo()); + writer.println("##"); + }); + } + + private void writeClass(PrintWriter writer, ClassInfo classInfo, int modifiers) { + if (classInfo.isAnnotation()) { + writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); + writer.append(" @interface ").print(classInfo); + } else if (classInfo.isStandardClass()) { + writer.append(Modifier.toString(modifiers & CLASS_MASK)); + writer.append(" class ").print(classInfo); + Set superclasses = classInfo.getDirectSuperclasses(); + if (!superclasses.isEmpty()) { + writer.append(" extends ").print(stringOf(superclasses)); + } + Set interfaces = classInfo.getDirectlyImplementedInterfaces(); + if (!interfaces.isEmpty()) { + writer.append(" implements ").print(stringOf(interfaces)); + } + } else { + writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); + writer.append(" interface ").print(classInfo); + Set superinterfaces = classInfo.getDirectSuperinterfaces(); + if (!superinterfaces.isEmpty()) { + writer.append(" extends ").print(stringOf(superinterfaces)); + } + } + writer.println(); + } + + private void writeMethods(PrintWriter writer, List methods) { + Collections.sort(methods); + for (MethodInfo method : methods) { + if (isVisible(method.getAccessFlags()) // Only public and protected methods + && isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods + && !isKotlinInternalScope(method)) { + writer.append(" ").println(filterAnnotationsFor(method)); + } + } + } + + private void writeFields(PrintWriter output, List fields) { + Collections.sort(fields); + for (FieldInfo field : fields) { + if (isVisible(field.getAccessFlags()) && isValid(field.getAccessFlags(), FIELD_MASK)) { + output.append(" ").println(field); + } + } + } + + private int getKotlinClassType(Class javaClass) { + if (metadataClass != null) { + Annotation metadata = javaClass.getAnnotation(metadataClass); + if (metadata != null) { + try { + return (int) classTypeMethod.invoke(metadata); + } catch (IllegalAccessException | InvocationTargetException e) { + getLogger().error("Failed to read Kotlin annotation", e); + } + } + } + return 0; + } + + private MethodInfo filterAnnotationsFor(MethodInfo method) { + return new MethodInfo( + method.getClassName(), + method.getMethodName(), + method.getAccessFlags(), + method.getTypeDescriptor(), + method.getAnnotationNames().stream() + .filter(ScanApi::isVisibleAnnotation) + .collect(toList()) + ); + } + } + + private static boolean isVisibleAnnotation(String annotationName) { + return !ANNOTATION_BLACKLIST.contains(annotationName); + } + + private static boolean isKotlinInternalScope(MethodInfo method) { + return method.getMethodName().indexOf('$') >= 0; + } + + private static boolean isValid(int modifiers, int mask) { + return (modifiers & mask) == modifiers; + } + + private static boolean isVisible(int accessFlags) { + return (accessFlags & VISIBILITY_MASK) != 0; + } + + private static String stringOf(Collection items) { + return items.stream().map(ClassInfo::toString).collect(joining(", ")); + } + + private static URL toURL(File file) throws MalformedURLException { + return file.toURI().toURL(); + } + + private static URL[] toURLs(Iterable files) throws MalformedURLException { + List urls = new LinkedList<>(); + for (File file : files) { + urls.add(toURL(file)); + } + return urls.toArray(new URL[urls.size()]); + } +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java new file mode 100644 index 0000000000..4e77437cfc --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java @@ -0,0 +1,37 @@ +package net.corda.plugins; + +import java.util.List; + +import static java.util.Collections.emptyList; + +@SuppressWarnings("unused") +public class ScannerExtension { + + private boolean verbose; + private boolean enabled = true; + private List excludeClasses = emptyList(); + + public boolean isVerbose() { + return verbose; + } + + public void setVerbose(boolean verbose) { + this.verbose = verbose; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getExcludeClasses() { + return excludeClasses; + } + + public void setExcludeClasses(List excludeClasses) { + this.excludeClasses = excludeClasses; + } +} diff --git a/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties b/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties new file mode 100644 index 0000000000..fc9e2277a5 --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties @@ -0,0 +1 @@ +implementation-class=net.corda.plugins.ApiScanner diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle index 9825dc1106..88169c215c 100644 --- a/gradle-plugins/build.gradle +++ b/gradle-plugins/build.gradle @@ -10,6 +10,7 @@ buildscript { ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") + ext.kotlin_version = constants.getProperty("kotlinVersion") repositories { mavenLocal() @@ -19,13 +20,14 @@ buildscript { dependencies { classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'net.corda.plugins.publish-utils' allprojects { - version "$gradle_plugins_version" + version gradle_plugins_version group 'net.corda.plugins' } @@ -39,7 +41,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['cordformation', 'quasar-utils', 'cordform-common'] + publications = ['cordformation', 'quasar-utils', 'cordform-common', 'api-scanner', 'cordapp'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/gradle-plugins/cordapp/README.md b/gradle-plugins/cordapp/README.md new file mode 100644 index 0000000000..6b0cebe690 --- /dev/null +++ b/gradle-plugins/cordapp/README.md @@ -0,0 +1,10 @@ +# Cordapp Gradle Plugin + +## Purpose + +To transform any project this plugin is applied to into a cordapp project that generates a cordapp JAR. + +## Effects + +Will modify the default JAR task to create a CorDapp format JAR instead [see here](https://docs.corda.net/cordapp-build-systems.html) +for more information. \ No newline at end of file diff --git a/gradle-plugins/cordapp/build.gradle b/gradle-plugins/cordapp/build.gradle new file mode 100644 index 0000000000..dc284faca1 --- /dev/null +++ b/gradle-plugins/cordapp/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.publish-utils' + +description 'Turns a project into a cordapp project that produces cordapp fat JARs' + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + compile gradleApi() + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" +} + +publish { + name project.name +} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt new file mode 100644 index 0000000000..ce65e18e5f --- /dev/null +++ b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt @@ -0,0 +1,76 @@ +package net.corda.plugins + +import org.gradle.api.* +import org.gradle.api.artifacts.* +import org.gradle.jvm.tasks.Jar +import java.io.File + +/** + * The Cordapp plugin will turn a project into a cordapp project which builds cordapp JARs with the correct format + * and with the information needed to run on Corda. + */ +class CordappPlugin : Plugin { + override fun apply(project: Project) { + project.logger.info("Configuring ${project.name} as a cordapp") + + Utils.createCompileConfiguration("cordapp", project) + Utils.createCompileConfiguration("cordaCompile", project) + + val configuration: Configuration = project.configurations.create("cordaRuntime") + configuration.isTransitive = false + project.configurations.single { it.name == "runtime" }.extendsFrom(configuration) + + configureCordappJar(project) + } + + /** + * Configures this project's JAR as a Cordapp JAR + */ + private fun configureCordappJar(project: Project) { + // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead + val task = project.task("configureCordappFatJar") + val jarTask = project.tasks.single { it.name == "jar" } as Jar + task.doLast { + jarTask.from(getDirectNonCordaDependencies(project).map { project.zipTree(it)}).apply { + exclude("META-INF/*.SF") + exclude("META-INF/*.DSA") + exclude("META-INF/*.RSA") + } + } + jarTask.dependsOn(task) + } + + private fun getDirectNonCordaDependencies(project: Project): Set { + project.logger.info("Finding direct non-corda dependencies for inclusion in CorDapp JAR") + val excludes = listOf( + mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib"), + mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib-jre8"), + mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-reflect"), + mapOf("group" to "co.paralleluniverse", "name" to "quasar-core") + ) + + val runtimeConfiguration = project.configuration("runtime") + // The direct dependencies of this project + val excludeDeps = project.configuration("cordapp").allDependencies + + project.configuration("cordaCompile").allDependencies + + project.configuration("cordaRuntime").allDependencies + val directDeps = runtimeConfiguration.allDependencies - excludeDeps + // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar + val filteredDeps = directDeps.filter { dep -> + excludes.none { exclude -> (exclude["group"] == dep.group) && (exclude["name"] == dep.name) } + } + filteredDeps.forEach { + // net.corda or com.r3.corda.enterprise may be a core dependency which shouldn't be included in this cordapp so give a warning + if ((it.group.startsWith("net.corda.") || it.group.startsWith("com.r3.corda.enterprise."))) { + project.logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." + + "This can cause node stability problems. Please use 'corda' instead." + + "See http://docs.corda.net/cordapp-build-systems.html") + } else { + project.logger.info("Including dependency in CorDapp JAR: $it") + } + } + return filteredDeps.map { runtimeConfiguration.files(it) }.flatten().toSet() + } + + private fun Project.configuration(name: String): Configuration = configurations.single { it.name == name } +} diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt new file mode 100644 index 0000000000..33e552ca7d --- /dev/null +++ b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt @@ -0,0 +1,17 @@ +package net.corda.plugins + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration + +class Utils { + companion object { + @JvmStatic + fun createCompileConfiguration(name: String, project: Project) { + if(!project.configurations.any { it.name == name }) { + val configuration = project.configurations.create(name) + configuration.isTransitive = false + project.configurations.single { it.name == "compile" }.extendsFrom(configuration) + } + } + } +} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties b/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties new file mode 100644 index 0000000000..90871e27c8 --- /dev/null +++ b/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties @@ -0,0 +1 @@ +implementation-class=net.corda.plugins.CordappPlugin diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java index 51d44def93..047d7e6289 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java @@ -7,11 +7,9 @@ import java.util.function.Consumer; public abstract class CordformDefinition { public final Path driverDirectory; public final ArrayList> nodeConfigurers = new ArrayList<>(); - public final String networkMapNodeName; - public CordformDefinition(Path driverDirectory, String networkMapNodeName) { + public CordformDefinition(Path driverDirectory) { this.driverDirectory = driverDirectory; - this.networkMapNodeName = networkMapNodeName; } public void addNode(Consumer configurer) { diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index 9175bead2f..7421fee251 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -1,14 +1,17 @@ package net.corda.cordform; import static java.util.Collections.emptyList; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; +import com.typesafe.config.*; import java.util.Collections; import java.util.List; import java.util.Map; public class CordformNode implements NodeDefinition { + /** + * Path relative to the running node where the serialized NodeInfos are stored. + */ + public static final String NODE_INFO_DIRECTORY = "additional-node-infos"; + protected static final String DEFAULT_HOST = "localhost"; /** @@ -20,16 +23,6 @@ public class CordformNode implements NodeDefinition { return name; } - /** - * A list of advertised services ID strings. - */ - public List advertisedServices = emptyList(); - - /** - * If running a Raft notary cluster, the address of at least one node in the cluster, or leave blank to start a new cluster. - * If running a BFT notary cluster, the addresses of all nodes in the cluster. - */ - public List notaryClusterAddresses = emptyList(); /** * Set the RPC users for this node. This configuration block allows arbitrary configuration. * The recommended current structure is: @@ -40,6 +33,14 @@ public class CordformNode implements NodeDefinition { */ public List> rpcUsers = emptyList(); + /** + * Apply the notary configuration if this node is a notary. The map is the config structure of + * net.corda.node.services.config.NotaryConfig + */ + public Map notary = null; + + public Map extraConfig = null; + protected Config config = ConfigFactory.empty(); public Config getConfig() { @@ -75,18 +76,11 @@ public class CordformNode implements NodeDefinition { } /** - * Set the port which to bind the Copycat (Raft) node to. + * Set the path to a file with optional properties, which are appended to the generated node.conf file. * - * @param notaryPort The Raft port. + * @param configFile The file path. */ - public void notaryNodePort(Integer notaryPort) { - config = config.withValue("notaryNodeAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + notaryPort)); - } - - /** - * @param id The (0-based) BFT replica ID. - */ - public void bftReplicaId(Integer id) { - config = config.withValue("bftSMaRt", ConfigValueFactory.fromMap(Collections.singletonMap("replicaId", id))); + public void configFile(String configFile) { + config = config.withValue("configFile", ConfigValueFactory.fromAnyRef(configFile)); } } diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 390ea7c8ba..828647d434 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -1,10 +1,4 @@ buildscript { - // For sharing constants between builds - Properties constants = new Properties() - file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } - - ext.kotlin_version = constants.getProperty("kotlinVersion") - repositories { mavenCentral() } @@ -41,6 +35,7 @@ sourceSets { dependencies { compile gradleApi() compile localGroovy() + compile project(":cordapp") noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" @@ -51,7 +46,7 @@ task createNodeRunner(type: Jar, dependsOn: [classes]) { manifest { attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt') } - baseName = project.name + '-fatjar' + classifier = 'fatjar' from { configurations.noderunner.collect { it.isDirectory() ? it : zipTree(it) } } from sourceSets.runnodes.output } diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy index 98668d85af..7baf20a45a 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy @@ -4,7 +4,6 @@ import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import org.apache.tools.ant.filters.FixCrLfFilter -import org.bouncycastle.asn1.x500.X500Name import org.gradle.api.DefaultTask import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.TaskAction @@ -23,7 +22,6 @@ class Cordform extends DefaultTask { String definitionClass protected def directory = Paths.get("build", "nodes") private def nodes = new ArrayList() - protected String networkMapNodeName /** * Set the directory to install nodes into. @@ -35,16 +33,6 @@ class Cordform extends DefaultTask { this.directory = Paths.get(directory) } - /** - * Set the network map node. - * - * @warning Ensure the node name is one of the configured nodes. - * @param nodeName The name of the node that will host the network map. - */ - void networkMap(String nodeName) { - networkMapNodeName = nodeName - } - /** * Add a node configuration. * @@ -61,8 +49,8 @@ class Cordform extends DefaultTask { * @return A node instance. */ private Node getNodeByName(String name) { - for(Node node : nodes) { - if(node.name == name) { + for (Node node : nodes) { + if (node.name == name) { return node } } @@ -109,10 +97,17 @@ class Cordform extends DefaultTask { */ @TaskAction void build() { - String networkMapNodeName + initializeConfiguration() + installRunScript() + nodes.each { + it.build() + } + generateNodeInfos() + } + + private initializeConfiguration() { if (null != definitionClass) { def cd = loadCordformDefinition() - networkMapNodeName = cd.networkMapNodeName.toString() cd.nodeConfigurers.each { nc -> node { Node it -> nc.accept it @@ -125,20 +120,34 @@ class Cordform extends DefaultTask { } } } else { - networkMapNodeName = this.networkMapNodeName nodes.each { it.rootDir directory } } - installRunScript() - def networkMapNode = getNodeByName(networkMapNodeName) - if (networkMapNode == null) - throw new IllegalStateException("The networkMap property refers to a node that isn't configured ($networkMapNodeName)") - nodes.each { - if(it != networkMapNode) { - it.networkMapAddress(networkMapNode.getP2PAddress(), networkMapNodeName) + } + + Path fullNodePath(Node node) { + return project.projectDir.toPath().resolve(node.nodeDir.toPath()) + } + + private generateNodeInfos() { + nodes.each { Node node -> + def process = new ProcessBuilder("java", "-jar", Node.NODEJAR_NAME, "--just-generate-node-info") + .directory(fullNodePath(node).toFile()) + .redirectErrorStream(true) + .start() + .waitFor() + } + for (source in nodes) { + for (destination in nodes) { + if (source.nodeDir != destination.nodeDir) { + project.copy { + from fullNodePath(source).toString() + include 'nodeInfo-*' + into fullNodePath(destination).resolve(Node.NODE_INFO_DIRECTORY).toString() + } + } } - it.build() } } } diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 2fe24a7933..eeb4443801 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -9,40 +9,6 @@ import org.gradle.api.artifacts.Configuration * testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner. */ class Cordformation implements Plugin { - void apply(Project project) { - createCompileConfiguration("cordapp", project) - createCompileConfiguration("cordaCompile", project) - - Configuration configuration = project.configurations.create("cordaRuntime") - configuration.transitive = false - project.configurations.runtime.extendsFrom configuration - - configureCordappJar(project) - } - - private void createCompileConfiguration(String name, Project project) { - Configuration configuration = project.configurations.create(name) - configuration.transitive = false - project.configurations.compile.extendsFrom configuration - } - - /** - * Configures this project's JAR as a Cordapp JAR - */ - private void configureCordappJar(Project project) { - // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead - def task = project.task('configureCordappFatJar') { - doLast { - project.tasks.jar.from(getDirectNonCordaDependencies(project).collect { project.zipTree(it)}) { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } - } - } - project.tasks.jar.dependsOn task - } - /** * Gets a resource file from this plugin's JAR file. * @@ -56,31 +22,7 @@ class Cordformation implements Plugin { }, filePathInJar).asFile() } - private static Set getDirectNonCordaDependencies(Project project) { - def excludes = [ - [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib'], - [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8'], - [group: 'org.jetbrains.kotlin', name: 'kotlin-reflect'], - [group: 'co.paralleluniverse', name: 'quasar-core'] - ] - - project.with { - // The direct dependencies of this project - def excludeDeps = configurations.cordapp.allDependencies + configurations.cordaCompile.allDependencies + configurations.cordaRuntime.allDependencies - def directDeps = configurations.runtime.allDependencies - excludeDeps - // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar - def filteredDeps = directDeps.findAll { excludes.collect { exclude -> (exclude.group == it.group) && (exclude.name == it.name) }.findAll { it }.isEmpty() } - filteredDeps.each { - // net.corda or com.r3.corda.enterprise may be a core dependency which shouldn't be included in this cordapp so give a warning - if (it.group && (it.group.startsWith('net.corda.') || it.group.startsWith('com.r3.corda.enterprise.'))) { - logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." + - "This can cause node stability problems. Please use 'corda' instead." + - "See http://docs.corda.net/cordapp-build-systems.html") - } else { - logger.info("Including dependency in CorDapp JAR: $it") - } - } - return filteredDeps.collect { configurations.runtime.files it }.flatten().toSet() - } + void apply(Project project) { + Utils.createCompileConfiguration("cordapp", project) } } diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index 19e3a6b9b3..8f6dcea295 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -17,7 +17,7 @@ class Node extends CordformNode { static final String WEBJAR_NAME = 'corda-webserver.jar' /** - * Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven + * Set the list of CorDapps to install to the cordapps directory. Each cordapp is a fully qualified Maven * dependency name, eg: com.example:product-name:0.1 * * @note Your app will be installed by default and does not need to be included here. @@ -99,14 +99,15 @@ class Node extends CordformNode { } protected void build() { - configureRpcUsers() + configureProperties() installCordaJar() if (config.hasPath("webAddress")) { installWebserverJar() } - installBuiltPlugin() + installBuiltCordapp() installCordapps() installConfig() + appendOptionalConfig() } /** @@ -118,11 +119,14 @@ class Node extends CordformNode { return config.getString("p2pAddress") } - /** - * Write the RPC users to the config - */ - private void configureRpcUsers() { + private void configureProperties() { config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers)) + if (notary) { + config = config.withValue("notary", ConfigValueFactory.fromMap(notary)) + } + if (extraConfig) { + config = config.withFallback(ConfigFactory.parseMap(extraConfig)) + } } /** @@ -153,23 +157,23 @@ class Node extends CordformNode { /** * Installs this project's cordapp to this directory. */ - private void installBuiltPlugin() { - def pluginsDir = new File(nodeDir, "plugins") + private void installBuiltCordapp() { + def cordappsDir = new File(nodeDir, "cordapps") project.copy { from project.jar - into pluginsDir + into cordappsDir } } /** - * Installs other cordapps to this node's plugins directory. + * Installs other cordapps to this node's cordapps directory. */ private void installCordapps() { - def pluginsDir = new File(nodeDir, "plugins") + def cordappsDir = new File(nodeDir, "cordapps") def cordapps = getCordappList() project.copy { from cordapps - into pluginsDir + into cordappsDir } } @@ -177,11 +181,6 @@ class Node extends CordformNode { * Installs the configuration file to this node's directory and detokenises it. */ private void installConfig() { - // Adding required default values - config = config.withValue('extraAdvertisedServiceIds', ConfigValueFactory.fromIterable(advertisedServices*.toString())) - if (notaryClusterAddresses.size() > 0) { - config = config.withValue('notaryClusterAddresses', ConfigValueFactory.fromIterable(notaryClusterAddresses*.toString())) - } def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList() // Need to write a temporary file first to use the project.copy, which resolves directories correctly. @@ -195,6 +194,29 @@ class Node extends CordformNode { } } + /** + * Appends installed config file with properties from an optional file. + */ + private void appendOptionalConfig() { + final configFileProperty = "configFile" + File optionalConfig + if (project.findProperty(configFileProperty)) { //provided by -PconfigFile command line property when running Gradle task + optionalConfig = new File(project.findProperty(configFileProperty)) + } else if (config.hasPath(configFileProperty)) { + optionalConfig = new File(config.getString(configFileProperty)) + } + if (optionalConfig) { + if (!optionalConfig.exists()) { + println "$configFileProperty '$optionalConfig' not found" + } else { + def confFile = new File(project.buildDir.getPath() + "/../" + nodeDir, 'node.conf') + optionalConfig.withInputStream { + input -> confFile << input + } + } + } + } + /** * Find the corda JAR amongst the dependencies. * diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy index ee978bdbb8..97029028e3 100644 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy @@ -15,6 +15,13 @@ class ProjectPublishExtension { task.setPublishName(name) } + /** + * Get the publishing name for this project. + */ + String name() { + return task.getPublishName() + } + /** * True when we do not want to publish default Java components */ diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle index f71f2da269..995cd8c899 100644 --- a/gradle-plugins/settings.gradle +++ b/gradle-plugins/settings.gradle @@ -2,4 +2,6 @@ rootProject.name = 'corda-gradle-plugins' include 'publish-utils' include 'quasar-utils' include 'cordformation' -include 'cordform-common' \ No newline at end of file +include 'cordform-common' +include 'api-scanner' +include 'cordapp' \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 8fc01b0e7d..aa062e3195 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -68,7 +68,7 @@ object RPCApi { val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " + - "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" val RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_ADDED.name}' AND " + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" @@ -97,20 +97,20 @@ object RPCApi { * @param clientAddress return address to contact the client at. * @param id a unique ID for the request, which the server will use to identify its response with. * @param methodName name of the method (procedure) to be called. - * @param arguments arguments to pass to the method, if any. + * @param serialisedArguments Serialised arguments to pass to the method, if any. */ data class RpcRequest( val clientAddress: SimpleString, val id: RpcRequestId, val methodName: String, - val arguments: List + val serialisedArguments: ByteArray ) : ClientToServer() { - fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { + fun writeToClientMessage(message: ClientMessage) { MessageUtil.setJMSReplyTo(message, clientAddress) message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REQUEST.ordinal) message.putLongProperty(RPC_ID_FIELD_NAME, id.toLong) message.putStringProperty(METHOD_NAME_FIELD_NAME, methodName) - message.bodyBuffer.writeBytes(arguments.serialize(context = context).bytes) + message.bodyBuffer.writeBytes(serialisedArguments) } } @@ -128,20 +128,20 @@ object RPCApi { } companion object { - fun fromClientMessage(context: SerializationContext, message: ClientMessage): ClientToServer { + fun fromClientMessage(message: ClientMessage): ClientToServer { val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)] return when (tag) { RPCApi.ClientToServer.Tag.RPC_REQUEST -> RpcRequest( clientAddress = MessageUtil.getJMSReplyTo(message), id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME)), methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME), - arguments = message.getBodyAsByteArray().deserialize(context = context) + serialisedArguments = message.getBodyAsByteArray() ) RPCApi.ClientToServer.Tag.OBSERVABLES_CLOSED -> { val ids = ArrayList() val buffer = message.bodyBuffer val numberOfIds = buffer.readInt() - for (i in 1 .. numberOfIds) { + for (i in 1..numberOfIds) { ids.add(ObservableId(buffer.readLong())) } ObservablesClosed(ids) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/User.kt b/node-api/src/main/kotlin/net/corda/nodeapi/User.kt index 0c0d259ab2..aa86e64414 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/User.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/User.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi import net.corda.nodeapi.config.OldConfig +import net.corda.nodeapi.config.toConfig data class User( @OldConfig("user") @@ -8,9 +9,6 @@ data class User( val password: String, val permissions: Set) { override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)" - fun toMap() = mapOf( - "username" to username, - "password" to password, - "permissions" to permissions - ) + @Deprecated("Use toConfig().root().unwrapped() instead") + fun toMap(): Map = toConfig().root().unwrapped() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index 052a361267..cd1118928c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -1,18 +1,26 @@ @file:JvmName("ConfigUtilities") + package net.corda.nodeapi.config import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigUtil +import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.internal.noneOrSingle +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.NetworkHostAndPort import org.slf4j.LoggerFactory +import java.lang.reflect.Field +import java.lang.reflect.Modifier.isStatic +import java.lang.reflect.ParameterizedType import java.net.Proxy import java.net.URL import java.nio.file.Path import java.nio.file.Paths import java.time.Instant import java.time.LocalDate +import java.time.temporal.Temporal import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -25,7 +33,7 @@ import kotlin.reflect.jvm.jvmErasure annotation class OldConfig(val value: String) // TODO Move other config parsing to use parseAs and remove this -operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { +operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { return getValueInternal(metadata.name, metadata.returnType) } @@ -52,9 +60,8 @@ fun Config.toProperties(): Properties { { it.value.unwrapped().toString() }) } -@Suppress("UNCHECKED_CAST") -private fun Config.getValueInternal(path: String, type: KType): T { - return (if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type)) as T +private fun Config.getValueInternal(path: String, type: KType): T { + return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type)) } private fun Config.getSingleValue(path: String, type: KType): Any? { @@ -71,8 +78,8 @@ private fun Config.getSingleValue(path: String, type: KType): Any? { NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path)) Path::class -> Paths.get(getString(path)) URL::class -> URL(getString(path)) - Properties::class -> getConfig(path).toProperties() CordaX500Name::class -> CordaX500Name.parse(getString(path)) + Properties::class -> getConfig(path).toProperties() else -> if (typeClass.java.isEnum) { parseEnum(typeClass.java, getString(path)) } else { @@ -122,9 +129,69 @@ private fun Config.defaultToOldPath(property: KProperty<*>): String { return property.name } -@Suppress("UNCHECKED_CAST") -private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(enumType as Class, name) // Any enum will do +private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(uncheckedCast(enumType), name) // Any enum will do private fun > enumBridge(clazz: Class, name: String): T = java.lang.Enum.valueOf(clazz, name) +/** + * Convert the receiver object into a [Config]. This does the inverse action of [parseAs]. + */ +fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig() + +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +// Reflect over the fields of the receiver and generate a value Map that can use to create Config object. +private fun Any.toConfigMap(): Map { + val values = HashMap() + for (field in javaClass.declaredFields) { + if (isStatic(field.modifiers) || field.isSynthetic) continue + field.isAccessible = true + val value = field.get(this) ?: continue + val configValue = if (value is String || value is Boolean || value is Number) { + // These types are supported by Config as use as is + value + } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL) { + // These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs + value.toString() + } else if (value is Enum<*>) { + // Expicitly use the Enum's name in case the toString is overridden, which would make parsing problematic. + value.name + } else if (value is Properties) { + // For Properties we treat keys with . as nested configs + ConfigFactory.parseMap(uncheckedCast(value)).root() + } else if (value is Iterable<*>) { + value.toConfigIterable(field) + } else { + // Else this is a custom object recursed over + value.toConfigMap() + } + values[field.name] = configValue + } + return values +} + +// For Iterables figure out the type parameter and apply the same logic as above on the individual elements. +private fun Iterable<*>.toConfigIterable(field: Field): Iterable { + val elementType = (field.genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + return when (elementType) { + // For the types already supported by Config we can use the Iterable as is + String::class.java -> this + Integer::class.java -> this + java.lang.Long::class.java -> this + java.lang.Double::class.java -> this + java.lang.Boolean::class.java -> this + LocalDate::class.java -> map(Any?::toString) + Instant::class.java -> map(Any?::toString) + NetworkHostAndPort::class.java -> map(Any?::toString) + Path::class.java -> map(Any?::toString) + URL::class.java -> map(Any?::toString) + CordaX500Name::class.java -> map(Any?::toString) + Properties::class.java -> map { ConfigFactory.parseMap(uncheckedCast(it)).root() } + else -> if (elementType.isEnum) { + map { (it as Enum<*>).name } + } else { + map { it?.toConfigMap() } + } + } +} + private val logger = LoggerFactory.getLogger("net.corda.nodeapi.config") 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 749c82b7ef..20d2efca63 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 @@ -72,7 +72,7 @@ class AttachmentsClassLoader(attachments: List, parent: ClassLoader val stream = ByteArrayOutputStream() try { attachment.extractFile(path, stream) - } catch(e: FileNotFoundException) { + } catch (e: FileNotFoundException) { throw ClassNotFoundException(name) } val bytes = stream.toByteArray() @@ -99,7 +99,7 @@ class AttachmentsClassLoader(attachments: List, parent: ClassLoader val stream = ByteArrayOutputStream() attachment.extractFile(path, stream) return ByteArrayInputStream(stream.toByteArray()) - } catch(e: FileNotFoundException) { + } catch (e: FileNotFoundException) { return null } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt index 65cdf86820..7ec0a1ce8f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt @@ -27,7 +27,6 @@ class ServiceType private constructor(val id: String) { } val notary: ServiceType = corda.getSubType("notary") - val networkMap: ServiceType = corda.getSubType("network_map") fun parse(id: String): ServiceType = ServiceType(id) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index 6368a416a0..501840e3df 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.serialization -import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 @@ -14,12 +13,6 @@ import java.util.concurrent.ConcurrentHashMap val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0 -class AMQPSerializationCustomization(val factory: SerializerFactory) : SerializationCustomization { - override fun addToWhitelist(vararg types: Class<*>) { - factory.addToWhitelist(*types) - } -} - fun SerializerFactory.addToWhitelist(vararg types: Class<*>) { require(types.toSet().size == types.size) { val duplicates = types.toMutableList() @@ -33,13 +26,14 @@ fun SerializerFactory.addToWhitelist(vararg types: Class<*>) { abstract class AbstractAMQPSerializationScheme : SerializationScheme { internal companion object { - private val pluginRegistries: List by lazy { - ServiceLoader.load(CordaPluginRegistry::class.java, this::class.java.classLoader).toList() + private val serializationWhitelists: List by lazy { + ServiceLoader.load(SerializationWhitelist::class.java, this::class.java.classLoader).toList() + DefaultWhitelist } fun registerCustomSerializers(factory: SerializerFactory) { - factory.apply { + with(factory) { register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) + register(net.corda.nodeapi.internal.serialization.amqp.custom.PrivateKeySerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(this)) register(net.corda.nodeapi.internal.serialization.amqp.custom.X500NameSerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) @@ -67,8 +61,8 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { register(net.corda.nodeapi.internal.serialization.amqp.custom.BitSetSerializer(this)) register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(this)) } - val customizer = AMQPSerializationCustomization(factory) - pluginRegistries.forEach { it.customizeSerialization(customizer) } + for (whitelistProvider in serializationWhitelists) + factory.addToWhitelist(*whitelistProvider.whitelist.toTypedArray()) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt index eadd539414..4bc36e9a8b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt @@ -30,6 +30,9 @@ import kotlin.collections.LinkedHashSet * we can often end up pulling in a lot of objects that do not make sense to put in a checkpoint. * Thus, by blacklisting classes/interfaces we don't expect to be serialised, we can better handle/monitor the aforementioned behaviour. * Inheritance works for blacklisted items, but one can specifically exclude classes from blacklisting as well. + * Note: Custom serializer registration trumps white/black lists. So if a given type has a custom serializer and has its name + * in the blacklist - it will still be serialized as specified by custom serializer. + * For more details, see [net.corda.nodeapi.internal.serialization.CordaClassResolver.getRegistration] */ object AllButBlacklisted : ClassWhitelist { 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 add6b1465e..bd6135522a 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 @@ -1,4 +1,5 @@ @file:JvmName("ClientContexts") + package net.corda.nodeapi.internal.serialization import net.corda.core.serialization.SerializationContext diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 09983c8c68..97c50dcd67 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -11,6 +11,7 @@ import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.loggerFor +import net.corda.nodeapi.internal.serialization.amqp.hasAnnotationInHierarchy import java.io.PrintWriter import java.lang.reflect.Modifier.isAbstract import java.nio.charset.StandardCharsets @@ -30,9 +31,9 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl * The point is that we do not want to send Kotlin types "over the wire" via RPC. */ private val javaAliases: Map, Class<*>> = mapOf( - listOf().javaClass to Collections.emptyList().javaClass, - setOf().javaClass to Collections.emptySet().javaClass, - mapOf().javaClass to Collections.emptyMap().javaClass + listOf().javaClass to Collections.emptyList().javaClass, + setOf().javaClass to Collections.emptySet().javaClass, + mapOf().javaClass to Collections.emptyMap().javaClass ) private fun typeForSerializationOf(type: Class<*>): Class<*> = javaAliases[type] ?: type @@ -56,14 +57,13 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl private fun checkClass(type: Class<*>): Registration? { // If call path has disabled whitelisting (see [CordaKryo.register]), just return without checking. if (!whitelistEnabled) return null - // Allow primitives, abstracts and interfaces - if (type.isPrimitive || type == Any::class.java || isAbstract(type.modifiers) || type == String::class.java) return null // If array, recurse on element type if (type.isArray) return checkClass(type.componentType) // Specialised enum entry, so just resolve the parent Enum type since cannot annotate the specialised entry. if (!type.isEnum && Enum::class.java.isAssignableFrom(type)) return checkClass(type.superclass) - // Kotlin lambdas require some special treatment - if (kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type)) return null + // Allow primitives, abstracts and interfaces. Note that we can also create abstract Enum types, + // but we don't want to whitelist those here. + if (type.isPrimitive || type == Any::class.java || type == String::class.java || (!type.isEnum && isAbstract(type.modifiers))) return null // It's safe to have the Class already, since Kryo loads it with initialisation off. // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw an IllegalStateException if input class is blacklisted. // Thus, blacklisting precedes annotation checking. @@ -115,13 +115,7 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl return (type.classLoader !is AttachmentsClassLoader) && !KryoSerializable::class.java.isAssignableFrom(type) && !type.isAnnotationPresent(DefaultSerializer::class.java) - && (type.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(type)) - } - - // Recursively check interfaces for our annotation. - private fun hasInheritedAnnotation(type: Class<*>): Boolean { - return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(it) } - || (type.superclass != null && hasInheritedAnnotation(type.superclass)) + && (type.isAnnotationPresent(CordaSerializable::class.java) || whitelist.hasAnnotationInHierarchy(type)) } // Need to clear out class names from attachments. @@ -157,14 +151,15 @@ object AllWhitelist : ClassWhitelist { override fun hasListed(type: Class<*>): Boolean = true } -// TODO: Need some concept of from which class loader -class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { - companion object { - val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) - } +sealed class AbstractMutableClassWhitelist(private val whitelist: MutableSet, private val delegate: ClassWhitelist) : MutableClassWhitelist { override fun hasListed(type: Class<*>): Boolean { - return (type.name in whitelist) || delegate.hasListed(type) + /** + * There are certain delegates like [net.corda.nodeapi.internal.serialization.AllButBlacklisted] + * which may throw when asked whether the type is listed. + * In such situations - it may be a good idea to ask [delegate] first before making a check against own [whitelist]. + */ + return delegate.hasListed(type) || (type.name in whitelist) } override fun add(entry: Class<*>) { @@ -172,21 +167,17 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass } } +// TODO: Need some concept of from which class loader +class GlobalTransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableClassWhitelist(GlobalTransientClassWhiteList.whitelist, delegate) { + companion object { + private val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) + } +} + /** - * A whitelist that can be customised via the [net.corda.core.node.CordaPluginRegistry], since implements [MutableClassWhitelist]. + * A whitelist that can be customised via the [net.corda.core.node.SerializationWhitelist], since it implements [MutableClassWhitelist]. */ -class TransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { - val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) - - override fun hasListed(type: Class<*>): Boolean { - return (type.name in whitelist) || delegate.hasListed(type) - } - - override fun add(entry: Class<*>) { - whitelist += entry.name - } -} - +class TransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableClassWhitelist(Collections.synchronizedSet(mutableSetOf()), delegate) /** * This class is not currently used, but can be installed to log a large number of missing entries from the whitelist @@ -204,7 +195,7 @@ class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) if (fileName != null && fileName.isNotEmpty()) { try { return PrintWriter(Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE), true) - } catch(ioEx: Exception) { + } catch (ioEx: Exception) { log.error("Could not open/create whitelist journal file for append: $fileName", ioEx) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index 75274a810d..7532ecffe9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -15,7 +15,7 @@ import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.CompositeKey import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializedBytes import net.corda.core.transactions.NotaryChangeWireTransaction @@ -49,8 +49,8 @@ import java.util.* import kotlin.collections.ArrayList object DefaultKryoCustomizer { - private val pluginRegistries: List by lazy { - ServiceLoader.load(CordaPluginRegistry::class.java, this.javaClass.classLoader).toList() + private val serializationWhitelists: List by lazy { + ServiceLoader.load(SerializationWhitelist::class.java, this.javaClass.classLoader).toList() + DefaultWhitelist } fun customize(kryo: Kryo): Kryo { @@ -88,10 +88,10 @@ object DefaultKryoCustomizer { register(BufferedInputStream::class.java, InputStreamSerializer) register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer) noReferencesWithin() - register(ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer) - register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) - register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) - register(CompositeKey::class.java, CompositeKeySerializer) // Using a custom serializer for compactness + register(ECPublicKeyImpl::class.java, PublicKeySerializer) + register(EdDSAPublicKey::class.java, PublicKeySerializer) + register(EdDSAPrivateKey::class.java, PrivateKeySerializer) + register(CompositeKey::class.java, PublicKeySerializer) // Using a custom serializer for compactness // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. register(Array::class, read = { _, _ -> emptyArray() }, write = { _, _, _ -> }) // This ensures a NonEmptySetSerializer is constructed with an initial value. @@ -109,7 +109,6 @@ object DefaultKryoCustomizer { register(BCRSAPublicKey::class.java, PublicKeySerializer) register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer) register(BCSphincs256PublicKey::class.java, PublicKeySerializer) - register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer) register(PartyAndCertificate::class.java, PartyAndCertificateSerializer) @@ -122,8 +121,17 @@ object DefaultKryoCustomizer { register(java.lang.invoke.SerializedLambda::class.java) register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer) - val customization = KryoSerializationCustomization(this) - pluginRegistries.forEach { it.customizeSerialization(customization) } + for (whitelistProvider in serializationWhitelists) { + val types = whitelistProvider.whitelist + require(types.toSet().size == types.size) { + val duplicates = types.toMutableList() + types.toSet().forEach { duplicates -= it } + "Cannot add duplicate classes to the whitelist ($duplicates)." + } + for (type in types) { + ((kryo.classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type) + } + } } } @@ -132,6 +140,7 @@ object DefaultKryoCustomizer { // Use this to allow construction of objects using a JVM backdoor that skips invoking the constructors, if there // is no no-arg constructor available. private val defaultStrategy = Kryo.DefaultInstantiatorStrategy(fallbackStrategy) + override fun newInstantiatorOf(type: Class): ObjectInstantiator { // However this doesn't work for non-public classes in the java. namespace val strat = if (type.name.startsWith("java.") && !isPublic(type.modifiers)) fallbackStrategy else defaultStrategy @@ -143,6 +152,7 @@ object DefaultKryoCustomizer { override fun write(kryo: Kryo, output: Output, obj: PartyAndCertificate) { kryo.writeClassAndObject(output, obj.certPath) } + override fun read(kryo: Kryo, input: Input, type: Class): PartyAndCertificate { return PartyAndCertificate(kryo.readClassAndObject(input) as CertPath) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt index c602698ade..8a5cdbcaa3 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt @@ -1,8 +1,7 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.KryoException -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.SerializationCustomization +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.SimpleString import rx.Notification @@ -12,10 +11,9 @@ import java.util.* /** * NOTE: We do not whitelist [HashMap] or [HashSet] since they are unstable under serialization. */ -class DefaultWhitelist : CordaPluginRegistry() { - override fun customizeSerialization(custom: SerializationCustomization): Boolean { - custom.apply { - addToWhitelist(Array(0, {}).javaClass, +object DefaultWhitelist : SerializationWhitelist { + override val whitelist = + listOf(Array(0, {}).javaClass, Notification::class.java, Notification.Kind::class.java, ArrayList::class.java, @@ -60,8 +58,6 @@ class DefaultWhitelist : CordaPluginRegistry() { java.util.LinkedHashMap::class.java, BitSet::class.java, OnErrorNotImplementedException::class.java, - StackTraceElement::class.java) - } - return true - } + StackTraceElement::class.java + ) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 1db93d78d0..9e3192859f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -10,11 +10,12 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.StateRef -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Crypto import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationContext.UseCase.* import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializedBytes import net.corda.core.toFuture @@ -24,20 +25,12 @@ import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.SgxSupport -import net.i2p.crypto.eddsa.EdDSAPrivateKey -import net.i2p.crypto.eddsa.EdDSAPublicKey -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec -import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec -import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import org.slf4j.LoggerFactory import rx.Observable -import sun.security.ec.ECPublicKeyImpl -import sun.security.util.DerValue import java.io.ByteArrayInputStream import java.io.InputStream import java.lang.reflect.InvocationTargetException @@ -253,9 +246,8 @@ object WireTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.privacySalt) } - @Suppress("UNCHECKED_CAST") override fun read(kryo: Kryo, input: Input, type: Class): WireTransaction { - val componentGroups = kryo.readClassAndObject(input) as List + val componentGroups: List = uncheckedCast(kryo.readClassAndObject(input)) val privacySalt = kryo.readClassAndObject(input) as PrivacySalt return WireTransaction(componentGroups, privacySalt) } @@ -269,9 +261,8 @@ object NotaryChangeWireTransactionSerializer : Serializer): NotaryChangeWireTransaction { - val inputs = kryo.readClassAndObject(input) as List + val inputs: List = uncheckedCast(kryo.readClassAndObject(input)) val notary = kryo.readClassAndObject(input) as Party val newNotary = kryo.readClassAndObject(input) as Party @@ -286,78 +277,24 @@ object SignedTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.sigs) } - @Suppress("UNCHECKED_CAST") override fun read(kryo: Kryo, input: Input, type: Class): SignedTransaction { return SignedTransaction( - kryo.readClassAndObject(input) as SerializedBytes, - kryo.readClassAndObject(input) as List + uncheckedCast>(kryo.readClassAndObject(input)), + uncheckedCast>(kryo.readClassAndObject(input)) ) } } -/** For serialising an ed25519 private key */ -@ThreadSafe -object Ed25519PrivateKeySerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: EdDSAPrivateKey) { - check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) - output.writeBytesWithLength(obj.seed) - } - - override fun read(kryo: Kryo, input: Input, type: Class): EdDSAPrivateKey { - val seed = input.readBytesWithLength() - return EdDSAPrivateKey(EdDSAPrivateKeySpec(seed, Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec)) - } -} - -/** For serialising an ed25519 public key */ -@ThreadSafe -object Ed25519PublicKeySerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: EdDSAPublicKey) { - check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) - output.writeBytesWithLength(obj.abyte) - } - - override fun read(kryo: Kryo, input: Input, type: Class): EdDSAPublicKey { - val A = input.readBytesWithLength() - return EdDSAPublicKey(EdDSAPublicKeySpec(A, Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec)) - } -} - -/** For serialising an ed25519 public key */ -@ThreadSafe -object ECPublicKeyImplSerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: ECPublicKeyImpl) { - output.writeBytesWithLength(obj.encoded) - } - - override fun read(kryo: Kryo, input: Input, type: Class): ECPublicKeyImpl { - val A = input.readBytesWithLength() - val der = DerValue(A) - return ECPublicKeyImpl.parse(der) as ECPublicKeyImpl - } -} - -// TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249. -@ThreadSafe -object CompositeKeySerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: CompositeKey) { - output.writeInt(obj.threshold) - output.writeInt(obj.children.size) - obj.children.forEach { kryo.writeClassAndObject(output, it) } - } - - override fun read(kryo: Kryo, input: Input, type: Class): CompositeKey { - val threshold = input.readInt() - val children = readListOfLength(kryo, input, minLen = 2) - val builder = CompositeKey.Builder() - children.forEach { builder.addKey(it.node, it.weight) } - return builder.build(threshold) as CompositeKey +sealed class UseCaseSerializer(private val allowedUseCases: EnumSet) : Serializer() { + protected fun checkUseCase() { + checkUseCase(allowedUseCases) } } @ThreadSafe -object PrivateKeySerializer : Serializer() { +object PrivateKeySerializer : UseCaseSerializer(EnumSet.of(Storage, Checkpoint)) { override fun write(kryo: Kryo, output: Output, obj: PrivateKey) { + checkUseCase() output.writeBytesWithLength(obj.encoded) } @@ -531,7 +468,7 @@ object LoggerSerializer : Serializer() { object ClassSerializer : Serializer>() { override fun read(kryo: Kryo, input: Input, type: Class>): Class<*> { val className = input.readString() - return Class.forName(className) + return Class.forName(className, true, kryo.classLoader) } override fun write(kryo: Kryo, output: Output, clazz: Class<*>) { @@ -608,8 +545,7 @@ class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer } } - @Suppress("UNCHECKED_CAST") - private val delegate: Serializer = ReflectionSerializerFactory.makeSerializer(kryo, FieldSerializer::class.java, type) as Serializer + private val delegate: Serializer = uncheckedCast(ReflectionSerializerFactory.makeSerializer(kryo, FieldSerializer::class.java, type)) override fun write(kryo: Kryo, output: Output, throwable: Throwable) { delegate.write(kryo, output, throwable) @@ -617,7 +553,7 @@ class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer override fun read(kryo: Kryo, input: Input, type: Class): Throwable { val throwableRead = delegate.read(kryo, input, type) - if(throwableRead.suppressed.isEmpty()) { + if (throwableRead.suppressed.isEmpty()) { throwableRead.setSuppressedToSentinel() } return throwableRead diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt deleted file mode 100644 index 993a2dad02..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.nodeapi.internal.serialization - -import com.esotericsoftware.kryo.Kryo -import net.corda.core.serialization.SerializationCustomization - -class KryoSerializationCustomization(val kryo: Kryo) : SerializationCustomization { - override fun addToWhitelist(vararg types: Class<*>) { - require(types.toSet().size == types.size) { - val duplicates = types.toMutableList() - types.toSet().forEach { duplicates -= it } - "Cannot add duplicate classes to the whitelist ($duplicates)." - } - for (type in types) { - ((kryo.classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type) - } - } -} \ No newline at end of file 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 299b77ae84..1e7c257a1d 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 @@ -14,6 +14,7 @@ import com.google.common.cache.CacheBuilder import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.internal.LazyPool +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes @@ -26,7 +27,7 @@ import java.util.concurrent.ExecutionException val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled" -object NotSupportedSeralizationScheme : SerializationScheme { +object NotSupportedSerializationScheme : SerializationScheme { private fun doThrow(): Nothing = throw UnsupportedOperationException("Serialization scheme not supported.") override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean = doThrow() @@ -51,7 +52,7 @@ data class SerializationContextImpl(override val preferredSerializationVersion: * We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context. */ override fun withAttachmentsClassLoader(attachmentHashes: List): SerializationContext { - properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return this + properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContextImpl ?: return this // Some tests don't set one. try { return withClassLoader(cache.get(attachmentHashes) { @@ -106,7 +107,7 @@ open class SerializationFactoryImpl : SerializationFactory() { registeredSchemes .filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) } .forEach { return@computeIfAbsent it } - NotSupportedSeralizationScheme + NotSupportedSerializationScheme } } @@ -141,8 +142,8 @@ open class SerializationFactoryImpl : SerializationFactory() { private object AutoCloseableSerialisationDetector : Serializer() { override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) { val message = "${closeable.javaClass.name}, which is a closeable resource, has been detected during flow checkpointing. " + - "Restoring such resources across node restarts is not supported. Make sure code accessing it is " + - "confined to a private method or the reference is nulled out." + "Restoring such resources across node restarts is not supported. Make sure code accessing it is " + + "confined to a private method or the reference is nulled out." throw UnsupportedOperationException(message) } @@ -204,11 +205,10 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { Input(byteSequence.bytes, byteSequence.offset + headerSize, byteSequence.size - headerSize).use { input -> return pool.run { kryo -> withContext(kryo, context) { - @Suppress("UNCHECKED_CAST") if (context.objectReferencesEnabled) { - kryo.readClassAndObject(input) as T + uncheckedCast(kryo.readClassAndObject(input)) } else { - kryo.withoutReferences { kryo.readClassAndObject(input) as T } + kryo.withoutReferences { uncheckedCast(kryo.readClassAndObject(input)) } } } } 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 89dd6b524d..baab649669 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 @@ -1,4 +1,5 @@ @file:JvmName("ServerContexts") + package net.corda.nodeapi.internal.serialization import net.corda.core.serialization.ClassWhitelist diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt new file mode 100644 index 0000000000..7f1d8c17b4 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt @@ -0,0 +1,12 @@ +package net.corda.nodeapi.internal.serialization + +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationFactory +import java.util.* + +internal fun checkUseCase(allowedUseCases: EnumSet) { + val currentContext: SerializationContext = SerializationFactory.currentFactory?.currentContext ?: throw IllegalStateException("Current context is not set") + if (!allowedUseCases.contains(currentContext.useCase)) { + throw IllegalStateException("UseCase '${currentContext.useCase}' is not within '$allowedUseCases'") + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt index e49eafaa96..10f320a55a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt @@ -23,14 +23,13 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) // // We *need* to retain knowledge for AMQP deserialisation weather that lowest primitive // was boxed or unboxed so just infer it recursively - private fun calcTypeName(type: Type) : String = - if (type.componentType().isArray()) { - val typeName = calcTypeName(type.componentType()); "$typeName[]" - } - else { - val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]" - "${type.componentType().typeName}$arrayType" - } + private fun calcTypeName(type: Type): String = + if (type.componentType().isArray()) { + val typeName = calcTypeName(type.componentType()); "$typeName[]" + } else { + val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]" + "${type.componentType().typeName}$arrayType" + } override val typeDescriptor by lazy { Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") } internal val elementType: Type by lazy { type.componentType() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt index 615cea26af..fc176cfda7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.NonEmptySet import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data @@ -34,12 +35,10 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { - if(supportedTypes.containsKey(declaredClass)) { + if (supportedTypes.containsKey(declaredClass)) { // Simple case - it is already known to be a collection. - @Suppress("UNCHECKED_CAST") - return deriveParametrizedType(declaredType, declaredClass as Class>) - } - else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) { + return deriveParametrizedType(declaredType, uncheckedCast(declaredClass)) + } else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) { // Declared class is not collection, but [actualClass] is - represent it accordingly. val collectionClass = findMostSuitableCollectionType(actualClass) return deriveParametrizedType(declaredType, collectionClass) @@ -49,11 +48,11 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } private fun deriveParametrizedType(declaredType: Type, collectionClass: Class>): ParameterizedType = - (declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType)) + (declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType)) private fun findMostSuitableCollectionType(actualClass: Class<*>): Class> = - supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!! + supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!! } @@ -61,13 +60,13 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList()) - override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { if (output.writeTypeNotations(typeNotation)) { output.requireSerializer(declaredType.actualTypeArguments[0]) } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { // Write described data.withDescribed(typeNotation.descriptor) { withList { @@ -78,7 +77,7 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) { // TODO: Can we verify the entries in the list? concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) }) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 7188a99b50..e039911ec0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data @@ -9,7 +10,7 @@ import java.lang.reflect.Type * Base class for serializers of core platform types that do not conform to the usual serialization rules and thus * cannot be automatically serialized. */ -abstract class CustomSerializer : AMQPSerializer { +abstract class CustomSerializer : AMQPSerializer { /** * This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects * that refer to other custom types etc. @@ -36,8 +37,7 @@ abstract class CustomSerializer : AMQPSerializer { override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { data.withDescribed(descriptor) { - @Suppress("UNCHECKED_CAST") - writeDescribedObject(obj as T, data, type, output) + writeDescribedObject(uncheckedCast(obj), data, type, output) } } @@ -49,7 +49,7 @@ abstract class CustomSerializer : AMQPSerializer { * subclass in the schema, so that we can distinguish between subclasses. */ // TODO: should this be a custom serializer at all, or should it just be a plain AMQPSerializer? - class SubClass(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer) : CustomSerializer() { + class SubClass(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer) : CustomSerializer() { // TODO: should this be empty or contain the schema of the super? override val schemaForDocumentation = Schema(emptyList()) @@ -76,7 +76,7 @@ abstract class CustomSerializer : AMQPSerializer { * Additional base features for a custom serializer for a particular class [withInheritance] is false * or super class / interfaces [withInheritance] is true */ - abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { + abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { override val type: Type get() = clazz override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(clazz)}") override fun writeClassInfo(output: SerializationOutput) {} @@ -87,12 +87,12 @@ abstract class CustomSerializer : AMQPSerializer { /** * Additional base features for a custom serializer for a particular class, that excludes subclasses. */ - abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) + abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) /** * Additional base features for a custom serializer for all implementations of a particular interface or super class. */ - abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) + abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) /** * Additional base features over and above [Implements] or [Is] custom serializer for when the serialized form should be @@ -101,10 +101,10 @@ abstract class CustomSerializer : AMQPSerializer { * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered * custom serializers. */ - abstract class Proxy(clazz: Class, - protected val proxyClass: Class

, - protected val factory: SerializerFactory, - withInheritance: Boolean = true) : CustomSerializerImp(clazz, withInheritance) { + abstract class Proxy(clazz: Class, + protected val proxyClass: Class

, + protected val factory: SerializerFactory, + withInheritance: Boolean = true) : CustomSerializerImp(clazz, withInheritance) { override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) } @@ -134,8 +134,7 @@ abstract class CustomSerializer : AMQPSerializer { } override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { - @Suppress("UNCHECKED_CAST") - val proxy = proxySerializer.readObject(obj, schema, input) as P + val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schema, input)) return fromProxy(proxy) } } @@ -151,12 +150,11 @@ abstract class CustomSerializer : AMQPSerializer { * @param make A lambda for constructing an instance, that defaults to calling a constructor that expects a string. * @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ - abstract class ToString(clazz: Class, withInheritance: Boolean = false, - private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { - `constructor` -> - { string -> `constructor`.newInstance(string) } - }, - private val unmaker: (T) -> String = { obj -> obj.toString() }) + abstract class ToString(clazz: Class, withInheritance: Boolean = false, + private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> + { string -> `constructor`.newInstance(string) } + }, + private val unmaker: (T) -> String = { obj -> obj.toString() }) : CustomSerializerImp(clazz, withInheritance) { override val schemaForDocumentation = Schema( 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 7522844db0..7269aa8c40 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 @@ -11,6 +11,8 @@ import org.apache.qpid.proton.codec.Data 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) @@ -58,7 +60,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { deserializeAndReturnEnvelope(bytes, T::class.java) @Throws(NotSerializableException::class) - private fun getEnvelope(bytes: ByteSequence): Envelope { + internal fun getEnvelope(bytes: ByteSequence): Envelope { // Check that the lead bytes match expected header val headerSize = AmqpHeaderV1_0.size if (bytes.take(headerSize) != AmqpHeaderV1_0) { @@ -78,9 +80,9 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { private fun des(generator: () -> R): R { try { return generator() - } catch(nse: NotSerializableException) { + } catch (nse: NotSerializableException) { throw nse - } catch(t: Throwable) { + } catch (t: Throwable) { throw NotSerializableException("Unexpected throwable: ${t.message} ${t.getStackTraceAsString()}") } finally { objectHistory.clear() @@ -142,10 +144,18 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { } /** - * TODO: Currently performs rather basic checks aimed in particular at [java.util.List>] and - * [java.lang.Class] + * Currently performs checks aimed at: + * * [java.util.List>] and [java.lang.Class] + * * [T : Parent] and [Parent] + * * [? extends Parent] and [Parent] + * * In the future tighter control might be needed */ private fun Type.materiallyEquivalentTo(that: Type): Boolean = - asClass() == that.asClass() && that is ParameterizedType + when (that) { + is ParameterizedType -> asClass() == that.asClass() + is TypeVariable<*> -> isSubClassOf(that.bounds.first()) + is WildcardType -> isSubClassOf(that.upperBounds.first()) + else -> false + } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt index 6ac728783c..a9e6d04cd4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt @@ -30,9 +30,9 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { val enumName = (obj as List<*>)[0] as String val enumOrd = obj[1] as Int - val fromOrd = type.asClass()!!.enumConstants[enumOrd] + val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>? - if (enumName != fromOrd?.toString()) { + if (enumName != fromOrd?.name) { throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but " + "ordinality has changed") } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index b343e96469..932b641ad2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException @@ -31,8 +32,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial LinkedHashMap::class.java to { map -> LinkedHashMap(map) }, TreeMap::class.java to { map -> TreeMap(map) }, EnumMap::class.java to { map -> - @Suppress("UNCHECKED_CAST") - EnumMap(map as Map) + EnumMap(uncheckedCast, Map>(map)) } )) @@ -42,12 +42,10 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { declaredClass.checkSupportedMapType() - if(supportedTypes.containsKey(declaredClass)) { + if (supportedTypes.containsKey(declaredClass)) { // Simple case - it is already known to be a map. - @Suppress("UNCHECKED_CAST") - return deriveParametrizedType(declaredType, declaredClass as Class>) - } - else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) { + return deriveParametrizedType(declaredType, uncheckedCast(declaredClass)) + } else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) { // Declared class is not map, but [actualClass] is - represent it accordingly. val mapClass = findMostSuitableMapType(actualClass) return deriveParametrizedType(declaredType, mapClass) @@ -68,14 +66,14 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList()) - override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { if (output.writeTypeNotations(typeNotation)) { output.requireSerializer(declaredType.actualTypeArguments[0]) output.requireSerializer(declaredType.actualTypeArguments[1]) } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { obj.javaClass.checkSupportedMapType() // Write described data.withDescribed(typeNotation.descriptor) { @@ -90,7 +88,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) { // TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole? val entries: Iterable> = (obj as Map<*, *>).map { readEntry(schema, input, it) } concreteBuilder(entries.toMap()) @@ -109,13 +107,11 @@ internal fun Class<*>.checkSupportedMapType() { if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) { throw IllegalArgumentException( "Map type $this is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.") - } - else if (WeakHashMap::class.java.isAssignableFrom(this)) { - throw IllegalArgumentException ("Weak references with map types not supported. Suggested fix: " - + "use java.util.LinkedHashMap instead.") - } - else if (Dictionary::class.java.isAssignableFrom(this)) { - throw IllegalArgumentException ( + } else if (WeakHashMap::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException("Weak references with map types not supported. Suggested fix: " + + "use java.util.LinkedHashMap instead.") + } else if (Dictionary::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException( "Unable to serialise deprecated type $this. Suggested fix: prefer java.util.map implementations") } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt index c483e8e126..7dcb450803 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt @@ -41,7 +41,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({clazz.typeName}) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ clazz.typeName }) { // Write described data.withDescribed(typeNotation.descriptor) { // Write list @@ -53,7 +53,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({clazz.typeName}) { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) { if (obj is List<*>) { if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName") val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt index 2decd00d37..3d9fa8b0a5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.utilities.loggerFor import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.codec.Data import java.lang.reflect.Method @@ -20,8 +21,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r val default: String? = generateDefault() val mandatory: Boolean = generateMandatory() - private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface ?: false - private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive ?: false + private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface == true + private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive == true private fun generateType(): String { return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType) @@ -44,19 +45,28 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r } private fun generateMandatory(): Boolean { - return isJVMPrimitive || !(readMethod?.returnsNullable() ?: true) + return isJVMPrimitive || readMethod?.returnsNullable() == false } private fun Method.returnsNullable(): Boolean { - val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?" - return returnTypeString.endsWith('?') || returnTypeString.endsWith('!') + try { + val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?" + return returnTypeString.endsWith('?') || returnTypeString.endsWith('!') + } catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) { + // This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077 + // TODO: Revisit this when Kotlin issue is fixed. + logger.error("Unexpected internal Kotlin error", e) + return true + } } companion object { + private val logger = loggerFor() + fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer { readMethod?.isAccessible = true if (SerializerFactory.isPrimitive(resolvedType)) { - return when(resolvedType) { + return when (resolvedType) { Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod) else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType) } @@ -76,17 +86,17 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r // This is lazy so we don't get an infinite loop when a method returns an instance of the class. private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() } - override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({nameForDebug}) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) { if (resolvedType != Any::class.java) { typeSerializer.writeClassInfo(output) } } - override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({nameForDebug}) { + override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) { input.readObjectOrNull(obj, schema, resolvedType) } - override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({nameForDebug}) { + override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) { output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType) } @@ -123,7 +133,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r override fun writeClassInfo(output: SerializationOutput) {} override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { - return if(obj == null) null else (obj as Short).toChar() + return if (obj == null) null else (obj as Short).toChar() } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { 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 4ec6bb32cf..adb0c44f0c 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 @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toBase64 @@ -94,8 +95,7 @@ data class Schema(val types: List) : DescribedType { override fun newInstance(described: Any?): Schema { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return Schema(list[0] as List) + return Schema(uncheckedCast(list[0])) } } @@ -162,8 +162,7 @@ data class Field(val name: String, val type: String, val requires: List, override fun newInstance(described: Any?): Field { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return Field(list[0] as String, list[1] as String, list[2] as List, list[3] as? String, list[4] as? String, list[5] as Boolean, list[6] as Boolean) + return Field(list[0] as String, list[1] as String, uncheckedCast(list[2]), list[3] as? String, list[4] as? String, list[5] as Boolean, list[6] as Boolean) } } @@ -223,8 +222,7 @@ data class CompositeType(override val name: String, override val label: String?, override fun newInstance(described: Any?): CompositeType { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return CompositeType(list[0] as String, list[1] as? String, list[2] as List, list[3] as Descriptor, list[4] as List) + return CompositeType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as Descriptor, uncheckedCast(list[4])) } } @@ -273,8 +271,7 @@ data class RestrictedType(override val name: String, override fun newInstance(described: Any?): RestrictedType { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return RestrictedType(list[0] as String, list[1] as? String, list[2] as List, list[3] as String, list[4] as Descriptor, list[5] as List) + return RestrictedType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as String, list[4] as Descriptor, uncheckedCast(list[5])) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 2cc91ecb88..6a1377d083 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -1,5 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.CordaSerializable import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken import net.corda.core.serialization.SerializationContext @@ -51,7 +53,7 @@ internal fun constructorForDeserialization(type: Type): KFunction? { } } - return preferredCandidate?.apply { isAccessible = true} + return preferredCandidate?.apply { isAccessible = true } ?: throw NotSerializableException("No constructor for deserialization found for $clazz.") } else { return null @@ -80,8 +82,8 @@ private fun propertiesForSerializationFromConstructor(kotlinConstructo for (param in kotlinConstructor.parameters) { val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.") val matchingProperty = properties[name] ?: - throw NotSerializableException("No property matching constructor parameter named $name of $clazz." + - " If using Java, check that you have the -parameters option specified in the Java compiler.") + throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'." + + " If using Java, check that you have the -parameters option specified in the Java compiler.") // Check that the method has a getter in java. val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." + " If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.") @@ -123,7 +125,7 @@ private fun exploreType(type: Type?, interfaces: MutableSet, serializerFac val clazz = type?.asClass() if (clazz != null) { if (clazz.isInterface) { - if(serializerFactory.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting. + if (serializerFactory.whitelist.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting. else interfaces += type } for (newInterface in clazz.genericInterfaces) { @@ -189,6 +191,8 @@ internal fun Type.asClass(): Class<*>? { this is Class<*> -> this this is ParameterizedType -> this.rawType.asClass() this is GenericArrayType -> this.genericComponentType.asClass()?.arrayClass() + this is TypeVariable<*> -> this.bounds.first().asClass() + this is WildcardType -> this.upperBounds.first().asClass() else -> null } } @@ -261,3 +265,19 @@ private fun Throwable.setMessage(newMsg: String) { detailMessageField.isAccessible = true detailMessageField.set(this, newMsg) } + +fun ClassWhitelist.requireWhitelisted(type: Type) { + if (!this.isWhitelisted(type.asClass()!!)) { + throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.") + } +} + +fun ClassWhitelist.isWhitelisted(clazz: Class<*>) = (hasListed(clazz) || hasAnnotationInHierarchy(clazz)) +fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = !(this.isWhitelisted(clazz)) + +// Recursively check the class, interfaces and superclasses for our annotation. +fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean { + return type.isAnnotationPresent(CordaSerializable::class.java) + || type.interfaces.any { hasAnnotationInHierarchy(it) } + || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) +} 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 5379b5ab84..87a19376c9 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 @@ -82,14 +82,13 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory } val retrievedRefCount = objectHistory[obj] - if(retrievedRefCount == null) { + if (retrievedRefCount == null) { serializer.writeObject(obj, data, type, this) // Important to do it after serialization such that dependent object will have preceding reference numbers // assigned to them first as they will be first read from the stream on receiving end. // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content if (suitableForObjectReference(obj.javaClass)) objectHistory.put(obj, objectHistory.size) - } - else { + } else { data.writeReferencedObject(ReferencedObject(retrievedRefCount)) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index e7a59f8d5c..da8ae1afc6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -2,8 +2,8 @@ package net.corda.nodeapi.internal.serialization.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeResolver +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.CordaSerializable import net.corda.nodeapi.internal.serialization.carpenter.* import org.apache.qpid.proton.amqp.* import java.io.NotSerializableException @@ -30,11 +30,11 @@ data class FactorySchemaAndDescriptor(val schema: Schema, val typeDescriptor: An // TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc. // TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact? @ThreadSafe -class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { +open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() private val customSerializers = CopyOnWriteArrayList>() - val classCarpenter = ClassCarpenter(cl, whitelist) + open val classCarpenter = ClassCarpenter(cl, whitelist) val classloader: ClassLoader get() = classCarpenter.classloader @@ -81,6 +81,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { } } Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> serializersByType.computeIfAbsent(actualClass ?: declaredClass) { + whitelist.requireWhitelisted(actualType) EnumSerializer(actualType, actualClass ?: declaredClass, this) } else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) @@ -105,6 +106,8 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { val declaredComponent = declaredType.genericComponentType inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray() } + is TypeVariable<*> -> actualClass + is WildcardType -> actualClass else -> null } @@ -239,10 +242,10 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this) else ArraySerializer.make(type, this) } else if (clazz.kotlin.objectInstance != null) { - whitelisted(clazz) + whitelist.requireWhitelisted(clazz) SingletonSerializer(clazz, clazz.kotlin.objectInstance!!, this) } else { - whitelisted(type) + whitelist.requireWhitelisted(type) ObjectSerializer(type, this) } } @@ -260,32 +263,13 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { return customSerializer } else { // Make a subclass serializer for the subclass and return that... - @Suppress("UNCHECKED_CAST") - return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer) + return CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer)) } } } return null } - private fun whitelisted(type: Type) { - val clazz = type.asClass()!! - if (isNotWhitelisted(clazz)) { - throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.") - } - } - - // Ignore SimpleFieldAccess as we add it to anything we build in the carpenter. - internal fun isNotWhitelisted(clazz: Class<*>): Boolean = clazz == SimpleFieldAccess::class.java || - (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) - - // Recursively check the class, interfaces and superclasses for our annotation. - private fun hasAnnotationInHierarchy(type: Class<*>): Boolean { - return type.isAnnotationPresent(CordaSerializable::class.java) || - type.interfaces.any { hasAnnotationInHierarchy(it) } - || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) - } - private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer { val rawType = declaredType.rawType as Class<*> rawType.checkSupportedMapType() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt index 29527d21dd..5f86857ffe 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt @@ -1,11 +1,11 @@ package net.corda.nodeapi.internal.serialization.amqp.custom +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer import net.corda.nodeapi.internal.serialization.amqp.MapSerializer import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import java.util.* -@Suppress("UNCHECKED_CAST") /** * A serializer that writes out an [EnumSet] as a type, plus list of instances in the set. */ @@ -16,7 +16,7 @@ class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy): Class<*> { return if (set.isEmpty()) { - EnumSet.complementOf(set as EnumSet).first().javaClass + EnumSet.complementOf(uncheckedCast, EnumSet>(set)).first().javaClass } else { set.first().javaClass } @@ -24,9 +24,9 @@ class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy { return if (proxy.elements.isEmpty()) { - EnumSet.noneOf(proxy.clazz as Class) + EnumSet.noneOf(uncheckedCast, Class>(proxy.clazz)) } else { - EnumSet.copyOf(proxy.elements as List) + EnumSet.copyOf(uncheckedCast, List>(proxy.elements)) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PrivateKeySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PrivateKeySerializer.kt new file mode 100644 index 0000000000..f9a2d1817d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PrivateKeySerializer.kt @@ -0,0 +1,27 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.core.crypto.Crypto +import net.corda.core.serialization.SerializationContext.UseCase.* +import net.corda.nodeapi.internal.serialization.amqp.* +import net.corda.nodeapi.internal.serialization.checkUseCase +import org.apache.qpid.proton.codec.Data +import java.lang.reflect.Type +import java.security.PrivateKey +import java.util.* + +object PrivateKeySerializer : CustomSerializer.Implements(PrivateKey::class.java) { + + private val allowedUseCases = EnumSet.of(Storage, Checkpoint) + + override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList()))) + + override fun writeDescribedObject(obj: PrivateKey, data: Data, type: Type, output: SerializationOutput) { + checkUseCase(allowedUseCases) + output.writeObject(obj.encoded, data, clazz) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PrivateKey { + val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray + return Crypto.decodePrivateKey(bits) + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt index 696b2616b9..8697cd98c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt @@ -27,7 +27,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy> = listOf(LocalDateTimeSerializer(factory), ZoneIdSerializer(factory)) override fun toProxy(obj: ZonedDateTime): ZonedDateTimeProxy = ZonedDateTimeProxy(obj.toLocalDateTime(), obj.offset, obj.zone) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt index 611dbb9788..0bbdf01033 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt @@ -132,5 +132,10 @@ fun AMQPField.validateType(classloader: ClassLoader) = when (type) { } private fun ClassLoader.exists(clazz: String) = run { - try { this.loadClass(clazz); true } catch (e: ClassNotFoundException) { false } } + try { + this.loadClass(clazz); true + } catch (e: ClassNotFoundException) { + false + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt index d47bc1bf52..2c98ebbc07 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt @@ -6,10 +6,8 @@ import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* import org.objectweb.asm.Type - import java.lang.Character.isJavaIdentifierPart import java.lang.Character.isJavaIdentifierStart - import java.util.* /** @@ -17,6 +15,7 @@ import java.util.* * as if `this.class.getMethod("get" + name.capitalize()).invoke(this)` had been called. It is intended as a more * convenient alternative to reflection. */ +@CordaSerializable interface SimpleFieldAccess { operator fun get(name: String): Any? } @@ -134,7 +133,10 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader visit(TARGET_VERSION, ACC_PUBLIC + ACC_FINAL + ACC_SUPER + ACC_ENUM, schema.jvmName, "L$jlEnum;", jlEnum, null) - visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + if (schema.flags.cordaSerializable()) { + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + } + generateFields(schema) generateStaticEnumConstructor(schema) generateEnumConstructor() @@ -151,8 +153,10 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader cw.apply { visit(TARGET_VERSION, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, jlObject, interfaces) - visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + if (schema.flags.cordaSerializable()) { + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + } generateAbstractGetters(schema) }.visitEnd() } @@ -163,20 +167,25 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader val superName = schema.superclass?.jvmName ?: jlObject val interfaces = schema.interfaces.map { it.name.jvm }.toMutableList() - if (SimpleFieldAccess::class.java !in schema.interfaces) { + if (SimpleFieldAccess::class.java !in schema.interfaces + && schema.flags.cordaSerializable() + && schema.flags.simpleFieldAccess()) { interfaces.add(SimpleFieldAccess::class.java.name.jvm) } cw.apply { visit(TARGET_VERSION, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces.toTypedArray()) - visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + if (schema.flags.cordaSerializable()) { + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + } generateFields(schema) generateClassConstructor(schema) generateGetters(schema) - if (schema.superclass == null) + if (schema.superclass == null) { generateGetMethod() // From SimplePropertyAccess + } generateToString(schema) }.visitEnd() } @@ -237,10 +246,9 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader } private fun ClassWriter.generateGetters(schema: Schema) { - @Suppress("UNCHECKED_CAST") - for ((name, type) in (schema.fields as Map)) { + for ((name, type) in schema.fields) { visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null).apply { - type.addNullabilityAnnotation(this) + (type as ClassField).addNullabilityAnnotation(this) visitCode() visitVarInsn(ALOAD, 0) // Load 'this' visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor) @@ -258,8 +266,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader } private fun ClassWriter.generateAbstractGetters(schema: Schema) { - @Suppress("UNCHECKED_CAST") - for ((name, field) in (schema.fields as Map)) { + for ((name, field) in schema.fields) { val opcodes = ACC_ABSTRACT + ACC_PUBLIC // abstract method doesn't have any implementation so just end visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null).visitEnd() @@ -363,9 +370,8 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader // Assign the fields from parameters. var slot = 1 + superclassFields.size - @Suppress("UNCHECKED_CAST") - for ((name, field) in (schema.fields as Map)) { - field.nullTest(this, slot) + for ((name, field) in schema.fields) { + (field as ClassField).nullTest(this, slot) visitVarInsn(ALOAD, 0) // Load 'this' onto the stack slot += load(slot, field) // Load the contents of the parameter onto the stack. @@ -391,11 +397,21 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader } } + /** + * If a sub element isn't whitelist we will not build a class containing that type as a member. Since, by + * default, classes created by the [ClassCarpenter] are annotated as [CordaSerializable] we will always + * be able to carpent classes generated from our AMQP library as, at a base level, we will either be able to + * create the lowest level in the meta hierarchy because either all members are jvm primitives or + * whitelisted classes + */ private fun validateSchema(schema: Schema) { if (schema.name in _loaded) throw DuplicateNameException() fun isJavaName(n: String) = n.isNotBlank() && isJavaIdentifierStart(n.first()) && n.all(::isJavaIdentifierPart) require(isJavaName(schema.name.split(".").last())) { "Not a valid Java name: ${schema.name}" } - schema.fields.keys.forEach { require(isJavaName(it)) { "Not a valid Java name: $it" } } + schema.fields.forEach { + require(isJavaName(it.key)) { "Not a valid Java name: $it" } + } + // Now check each interface we've been asked to implement, as the JVM will unfortunately only catch the // fact that we didn't implement the interface we said we would at the moment the missing method is // actually called, which is a bit too dynamic for my tastes. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Exceptions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Exceptions.kt index c96ae86e91..a260d82272 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Exceptions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Exceptions.kt @@ -1,11 +1,14 @@ package net.corda.nodeapi.internal.serialization.carpenter -class DuplicateNameException : RuntimeException( +import net.corda.core.CordaException +import net.corda.core.CordaRuntimeException + +class DuplicateNameException : CordaRuntimeException( "An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") -class InterfaceMismatchException(msg: String) : RuntimeException(msg) +class InterfaceMismatchException(msg: String) : CordaRuntimeException(msg) -class NullablePrimitiveException(msg: String) : RuntimeException(msg) +class NullablePrimitiveException(msg: String) : CordaRuntimeException(msg) class UncarpentableException(name: String, field: String, type: String) : - Exception("Class $name is loadable yet contains field $field of unknown type $type") + CordaException("Class $name is loadable yet contains field $field of unknown type $type") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt index e6684756b8..352ad498da 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt @@ -1,8 +1,12 @@ package net.corda.nodeapi.internal.serialization.carpenter -import kotlin.collections.LinkedHashMap import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes.* +import java.util.* + +enum class SchemaFlags { + SimpleFieldAccess, CordaSerializable +} /** * A Schema is the representation of an object the Carpenter can contsruct @@ -20,6 +24,8 @@ abstract class Schema( updater: (String, Field) -> Unit) { private fun Map.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor }) + var flags: EnumMap = EnumMap(SchemaFlags::class.java) + init { fields.forEach { updater(it.key, it.value) } @@ -41,6 +47,18 @@ abstract class Schema( val asArray: String get() = "[L$jvmName;" + + fun unsetCordaSerializable() { + flags.replace(SchemaFlags.CordaSerializable, false) + } +} + +fun EnumMap.cordaSerializable(): Boolean { + return this.getOrDefault(SchemaFlags.CordaSerializable, true) == true +} + +fun EnumMap.simpleFieldAccess(): Boolean { + return this.getOrDefault(SchemaFlags.SimpleFieldAccess, true) == true } /** diff --git a/node-api/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/node-api/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 6a4ca5dcd5..0000000000 --- a/node-api/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.nodeapi.internal.serialization.DefaultWhitelist \ No newline at end of file diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java new file mode 100644 index 0000000000..6085546c37 --- /dev/null +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java @@ -0,0 +1,135 @@ +package net.corda.nodeapi.internal.serialization.amqp; + +import net.corda.core.serialization.CordaSerializable; +import net.corda.core.serialization.SerializedBytes; +import net.corda.nodeapi.internal.serialization.AllWhitelist; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class ListsSerializationJavaTest { + + @CordaSerializable + interface Parent { + } + + public static class Child implements Parent { + private final int value; + + Child(int value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Child child = (Child) o; + + return value == child.value; + } + + @Override + public int hashCode() { + return value; + } + + // Needed to show that there is a property called "value" + @SuppressWarnings("unused") + public int getValue() { + return value; + } + } + + @CordaSerializable + public static class CovariantContainer { + private final List content; + + CovariantContainer(List content) { + this.content = content; + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CovariantContainer that = (CovariantContainer) o; + + return content != null ? content.equals(that.content) : that.content == null; + } + + @Override + public int hashCode() { + return content != null ? content.hashCode() : 0; + } + + // Needed to show that there is a property called "content" + @SuppressWarnings("unused") + public List getContent() { + return content; + } + } + + @CordaSerializable + public static class CovariantContainer2 { + private final List content; + + CovariantContainer2(List content) { + this.content = content; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CovariantContainer2 that = (CovariantContainer2) o; + + return content != null ? content.equals(that.content) : that.content == null; + } + + @Override + public int hashCode() { + return content != null ? content.hashCode() : 0; + } + + // Needed to show that there is a property called "content" + @SuppressWarnings("unused") + public List getContent() { + return content; + } + } + + @Test + public void checkCovariance() throws Exception { + List payload = new ArrayList<>(); + payload.add(new Child(1)); + payload.add(new Child(2)); + CovariantContainer container = new CovariantContainer<>(payload); + assertEqualAfterRoundTripSerialization(container, CovariantContainer.class); + } + + @Test + public void checkCovariance2() throws Exception { + List payload = new ArrayList<>(); + payload.add(new Child(1)); + payload.add(new Child(2)); + CovariantContainer2 container = new CovariantContainer2(payload); + assertEqualAfterRoundTripSerialization(container, CovariantContainer2.class); + } + + // Have to have own version as Kotlin inline functions cannot be easily called from Java + private static void assertEqualAfterRoundTripSerialization(T container, Class clazz) throws Exception { + SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); + SerializationOutput ser = new SerializationOutput(factory1); + SerializedBytes bytes = ser.serialize(container); + DeserializationInput des = new DeserializationInput(factory1); + T deserialized = des.deserialize(bytes, clazz); + Assert.assertEquals(container, deserialized); + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt index e13c47ce05..452e25ce78 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -76,54 +76,66 @@ class ConfigParsingTest { testPropertyType(URL("http://localhost:1234"), URL("http://localhost:1235"), valuesToString = true) } + @Test + fun CordaX500Name() { + testPropertyType( + CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"), + CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"), + valuesToString = true) + } + @Test fun `flat Properties`() { val config = config("value" to mapOf("key" to "prop")) - assertThat(config.parseAs().value).isEqualTo(Properties().apply { this["key"] = "prop" }) + val data = PropertiesData(Properties().apply { this["key"] = "prop" }) + assertThat(config.parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `Properties key with dot`() { val config = config("value" to mapOf("key.key2" to "prop")) - assertThat(config.parseAs().value).isEqualTo(Properties().apply { this["key.key2"] = "prop" }) + val data = PropertiesData(Properties().apply { this["key.key2"] = "prop" }) + assertThat(config.parseAs().value).isEqualTo(data.value) } @Test fun `nested Properties`() { val config = config("value" to mapOf("first" to mapOf("second" to "prop"))) - assertThat(config.parseAs().value).isEqualTo(Properties().apply { this["first.second"] = "prop" }) + val data = PropertiesData(Properties().apply { this["first.second"] = "prop" }) + assertThat(config.parseAs().value).isEqualTo(data.value) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `List of Properties`() { val config = config("values" to listOf(emptyMap(), mapOf("key" to "prop"))) - assertThat(config.parseAs().values).containsExactly( + val data = PropertiesListData(listOf( Properties(), - Properties().apply { this["key"] = "prop" }) + Properties().apply { this["key"] = "prop" })) + assertThat(config.parseAs().values).isEqualTo(data.values) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `Set`() { - val config = config("values" to listOf("a", "a", "b")) - assertThat(config.parseAs().values).containsOnly("a", "b") + val data = StringSetData(setOf("a", "b")) + assertThat(config("values" to listOf("a", "a", "b")).parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config("values" to listOf("a", "b"))) assertThat(empty().parseAs().values).isEmpty() } - @Test - fun x500Name() { - testPropertyType(CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"), CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"), valuesToString = true) - } - @Test fun `multi property data class`() { - val data = config( + val config = config( "b" to true, "i" to 123, "l" to listOf("a", "b")) - .parseAs() + val data = config.parseAs() assertThat(data.i).isEqualTo(123) assertThat(data.b).isTrue() assertThat(data.l).containsExactly("a", "b") + assertThat(data.toConfig()).isEqualTo(config) } @Test @@ -133,6 +145,7 @@ class ConfigParsingTest { "value" to "nested")) val data = NestedData(StringData("nested")) assertThat(config.parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config) } @Test @@ -143,12 +156,14 @@ class ConfigParsingTest { mapOf("value" to "2"))) val data = DataListData(listOf(StringData("1"), StringData("2"))) assertThat(config.parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `default value property`() { assertThat(config("a" to 3).parseAs()).isEqualTo(DefaultData(3, 2)) assertThat(config("a" to 3, "defaultOfTwo" to 3).parseAs()).isEqualTo(DefaultData(3, 3)) + assertThat(DefaultData(3).toConfig()).isEqualTo(config("a" to 3, "defaultOfTwo" to 2)) } @Test @@ -156,12 +171,19 @@ class ConfigParsingTest { assertThat(empty().parseAs().nullable).isNull() assertThat(config("nullable" to null).parseAs().nullable).isNull() assertThat(config("nullable" to "not null").parseAs().nullable).isEqualTo("not null") + assertThat(NullableData(null).toConfig()).isEqualTo(empty()) } @Test fun `old config property`() { assertThat(config("oldValue" to "old").parseAs().newValue).isEqualTo("old") assertThat(config("newValue" to "new").parseAs().newValue).isEqualTo("new") + assertThat(OldData("old").toConfig()).isEqualTo(config("newValue" to "old")) + } + + @Test + fun `static field`() { + assertThat(DataWithCompanion(3).toConfig()).isEqualTo(config("value" to 3)) } private inline fun , reified L : ListData, V : Any> testPropertyType( @@ -177,6 +199,7 @@ class ConfigParsingTest { val config = config("value" to if (valueToString) value.toString() else value) val data = constructor.call(value) assertThat(config.parseAs().value).isEqualTo(data.value) + assertThat(data.toConfig()).isEqualTo(config) } private inline fun , V : Any> testListProperty(value1: V, value2: V, valuesToString: Boolean) { @@ -187,6 +210,7 @@ class ConfigParsingTest { val config = config("values" to configValues.take(n)) val data = constructor.call(rawValues.take(n)) assertThat(config.parseAs().values).isEqualTo(data.values) + assertThat(data.toConfig()).isEqualTo(config) } assertThat(empty().parseAs().values).isEmpty() } @@ -228,8 +252,8 @@ class ConfigParsingTest { data class PathListData(override val values: List) : ListData data class URLData(override val value: URL) : SingleData data class URLListData(override val values: List) : ListData - data class X500NameData(override val value: CordaX500Name) : SingleData - data class X500NameListData(override val values: List) : ListData + data class CordaX500NameData(override val value: CordaX500Name) : SingleData + data class CordaX500NameListData(override val values: List) : ListData data class PropertiesData(override val value: Properties) : SingleData data class PropertiesListData(override val values: List) : ListData data class MultiPropertyData(val i: Int, val b: Boolean, val l: List) @@ -240,7 +264,12 @@ class ConfigParsingTest { data class OldData( @OldConfig("oldValue") val newValue: String) + data class DataWithCompanion(val value: Int) { + companion object { + @Suppress("unused") + val companionValue = 2 + } + } enum class TestEnum { Value1, Value2 } - } \ No newline at end of file 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 77733ec060..3e72b1d0d4 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 @@ -9,7 +9,6 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.testing.* import net.corda.testing.node.MockServices -import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -37,7 +36,7 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase() fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { val state = State(magicNumber) return TransactionBuilder(notary) - .withItems(StateAndContract(state, ATTACHMENT_PROGRAM_ID), Command(Commands.Create(), owner.party.owningKey)) + .withItems(StateAndContract(state, ATTACHMENT_PROGRAM_ID), Command(Commands.Create(), owner.party.owningKey)) } } @@ -45,12 +44,7 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase() @Before fun `create service hub`() { - serviceHub = MockServices(cordappPackages=listOf("net.corda.nodeapi.internal")) - } - - @After - fun `clear packages`() { - unsetCordappPackages() + serviceHub = MockServices(cordappPackages = listOf("net.corda.nodeapi.internal")) } @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 5fc205e666..5fd6b46f21 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 @@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.internal.declaredField +import net.corda.core.internal.toWireTransaction import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* @@ -44,6 +45,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { whenever(serviceHub.attachments).thenReturn(attachmentStorage) return this.withServiceHub(serviceHub) } + private fun SerializationContext.withServiceHub(serviceHub: ServiceHub): SerializationContext { return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(attachmentsClassLoaderEnabledPropertyName, true) } @@ -53,8 +55,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { class DummyServiceHub : MockServices() { override val cordappProvider: CordappProviderImpl - = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH))).start(attachments) - + = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments) private val cordapp get() = cordappProvider.cordapps.first() val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!! val appContext get() = cordappProvider.getAppContext(cordapp) @@ -265,7 +266,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { @Test fun `test serialization of sub-sequence OpaqueBytes`() { - val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3 ,2) + val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3, 2) val bytes = bytesSequence.serialize() val copiedBytesSequence = bytes.deserialize() @@ -300,7 +301,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { @Test fun `test deserialize of WireTransaction where contract cannot be found`() { - kryoSpecific("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") { + kryoSpecific("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") { ClassLoaderForTests().use { child -> val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child) val contract = contractClass.newInstance() as DummyContractBackdoor @@ -309,8 +310,8 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { val attachmentRef = serviceHub.attachmentId val bytes = run { val outboundContext = SerializationFactory.defaultFactory.defaultContext - .withServiceHub(serviceHub) - .withClassLoader(child) + .withServiceHub(serviceHub) + .withClassLoader(child) val wireTransaction = tx.toWireTransaction(serviceHub, outboundContext) wireTransaction.serialize(context = outboundContext) } 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 3a67501191..e684ef1f29 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 @@ -34,6 +34,23 @@ enum class Foo { abstract val value: Int } +enum class BadFood { + Mud { + override val value = -1 + }; + + abstract val value: Int +} + +@CordaSerializable +enum class Simple { + Easy +} + +enum class BadSimple { + Nasty +} + @CordaSerializable open class Element @@ -106,17 +123,36 @@ class CordaClassResolverTests { @Test fun `Annotation on enum works for specialised entries`() { - // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. - @Suppress("UNSUPPORTED_FEATURE") CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) } + @Test(expected = KryoException::class) + fun `Unannotated specialised enum does not work`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(BadFood.Mud::class.java) + } + + @Test + fun `Annotation on simple enum works`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(Simple.Easy::class.java) + } + + @Test(expected = KryoException::class) + fun `Unannotated simple enum does not work`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(BadSimple.Nasty::class.java) + } + @Test fun `Annotation on array element works`() { val values = arrayOf(Element()) CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) } + @Test(expected = KryoException::class) + fun `Unannotated array elements do not work`() { + val values = arrayOf(NotSerializable()) + CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) + } + @Test fun `Annotation not needed on abstract class`() { CordaClassResolver(emptyWhitelistContext).getRegistration(AbstractClass::class.java) @@ -271,6 +307,7 @@ class CordaClassResolverTests { } open class SubHashSet : HashSet() + @Test fun `Check blacklisted subclass`() { expectedEx.expect(IllegalStateException::class.java) @@ -281,6 +318,7 @@ class CordaClassResolverTests { } class SubSubHashSet : SubHashSet() + @Test fun `Check blacklisted subsubclass`() { expectedEx.expect(IllegalStateException::class.java) @@ -291,6 +329,7 @@ class CordaClassResolverTests { } class ConnectionImpl(private val connection: Connection) : Connection by connection + @Test fun `Check blacklisted interface impl`() { expectedEx.expect(IllegalStateException::class.java) @@ -302,6 +341,7 @@ class CordaClassResolverTests { interface SubConnection : Connection class SubConnectionImpl(private val subConnection: SubConnection) : SubConnection by subConnection + @Test fun `Check blacklisted super-interface impl`() { expectedEx.expect(IllegalStateException::class.java) @@ -320,6 +360,7 @@ class CordaClassResolverTests { @CordaSerializable class CordaSerializableHashSet : HashSet() + @Test fun `Check blacklist precedes CordaSerializable`() { expectedEx.expect(IllegalStateException::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 1855a4c8c7..d6cb313f88 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 @@ -43,7 +43,7 @@ class KryoTests : TestDependencyInjectionBase() { AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.P2P) + SerializationContext.UseCase.Storage) } @Test @@ -188,10 +188,10 @@ class KryoTests : TestDependencyInjectionBase() { @Test fun `serialize - deserialize PrivacySalt`() { val expected = PrivacySalt(byteArrayOf( - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, - 31, 32 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32 )) val serializedBytes = expected.serialize(factory, context) val actual = serializedBytes.deserialize(factory, context) @@ -267,7 +267,7 @@ class KryoTests : TestDependencyInjectionBase() { assertEquals(exception.message, exception2.message) assertEquals(1, exception2.suppressed.size) - assertNotNull({ exception2.suppressed.find { it.message == toBeSuppressedOnSenderSide.message }}) + assertNotNull({ exception2.suppressed.find { it.message == toBeSuppressedOnSenderSide.message } }) val toBeSuppressedOnReceiverSide = IllegalStateException("bazz2") exception2.addSuppressed(toBeSuppressedOnReceiverSide) 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 07780c9fd1..d9d0f58e0c 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 @@ -4,19 +4,31 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver import net.corda.core.serialization.* import net.corda.node.services.statemachine.SessionData +import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput +import net.corda.nodeapi.internal.serialization.amqp.Envelope +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific +import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test import java.io.ByteArrayOutputStream import java.io.NotSerializableException -import java.nio.charset.StandardCharsets.* +import java.nio.charset.StandardCharsets.US_ASCII import java.util.* class ListsSerializationTest : TestDependencyInjectionBase() { private companion object { val javaEmptyListClass = Collections.emptyList().javaClass + + fun verifyEnvelope(serBytes: SerializedBytes, envVerBody: (Envelope) -> Unit) = + amqpSpecific("AMQP specific envelope verification") { + val context = SerializationFactory.defaultFactory.defaultContext + val envelope = DeserializationInput(SerializerFactory(context.whitelist, context.deserializationClassLoader)).getEnvelope(serBytes) + envVerBody(envelope) + } } @Test @@ -29,21 +41,24 @@ class ListsSerializationTest : TestDependencyInjectionBase() { @Test fun `check list can be serialized as part of SessionData`() { run { - val sessionData = SessionData(123, listOf(1)) + val sessionData = SessionData(123, listOf(1).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(listOf(1), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, listOf(1, 2)) + val sessionData = SessionData(123, listOf(1, 2).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(listOf(1, 2), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, emptyList()) + val sessionData = SessionData(123, emptyList().serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(emptyList(), sessionData.payload.deserialize()) } } @Test - fun `check empty list serialises as Java emptyList`() { + fun `check empty list serialises as Java emptyList`() = kryoSpecific("Kryo specific test") { val nameID = 0 val serializedForm = emptyList().serialize() val output = ByteArrayOutputStream().apply { @@ -60,7 +75,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() { data class WrongPayloadType(val payload: ArrayList) @Test - fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { + fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { val payload = ArrayList() payload.add(1) payload.add(2) @@ -68,11 +83,34 @@ class ListsSerializationTest : TestDependencyInjectionBase() { Assertions.assertThatThrownBy { wrongPayloadType.serialize() } .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declaredType") } + + @CordaSerializable + interface Parent + + data class Child(val value: Int) : Parent + + @CordaSerializable + data class CovariantContainer(val payload: List) + + @Test + fun `check covariance`() { + val payload = ArrayList() + payload.add(Child(1)) + payload.add(Child(2)) + val container = CovariantContainer(payload) + + fun verifyEnvelopeBody(envelope: Envelope) { + envelope.schema.types.single { typeNotation -> typeNotation.name == java.util.List::class.java.name + "" } + } + + assertEqualAfterRoundTripSerialization(container, { bytes -> verifyEnvelope(bytes, ::verifyEnvelopeBody) }) + } } -internal inline fun assertEqualAfterRoundTripSerialization(obj: T) { +internal inline fun assertEqualAfterRoundTripSerialization(obj: T, noinline streamValidation: ((SerializedBytes) -> Unit)? = null) { val serializedForm: SerializedBytes = obj.serialize() + streamValidation?.invoke(serializedForm) val deserializedInstance = serializedForm.deserialize() assertEquals(obj, deserializedInstance) 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 6f97a39892..4e9f598eab 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 @@ -3,17 +3,19 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions +import org.bouncycastle.asn1.x500.X500Name import org.junit.Assert.assertArrayEquals import org.junit.Test -import org.bouncycastle.asn1.x500.X500Name import java.io.ByteArrayOutputStream import java.util.* +import kotlin.test.assertEquals class MapsSerializationTest : TestDependencyInjectionBase() { private companion object { @@ -22,7 +24,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check EmptyMap serialization`() = amqpSpecific("kotlin.collections.EmptyMap is not enabled for Kryo serialization") { + fun `check EmptyMap serialization`() = amqpSpecific("kotlin.collections.EmptyMap is not enabled for Kryo serialization") { assertEqualAfterRoundTripSerialization(emptyMap()) } @@ -33,15 +35,16 @@ class MapsSerializationTest : TestDependencyInjectionBase() { @Test fun `check list can be serialized as part of SessionData`() { - val sessionData = SessionData(123, smallMap) + val sessionData = SessionData(123, smallMap.serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(smallMap, sessionData.payload.deserialize()) } @CordaSerializable data class WrongPayloadType(val payload: HashMap) @Test - fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { + fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { val payload = HashMap(smallMap) val wrongPayloadType = WrongPayloadType(payload) Assertions.assertThatThrownBy { wrongPayloadType.serialize() } @@ -64,7 +67,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check empty map serialises as Java emptyMap`() = kryoSpecific("Specifically checks Kryo serialization") { + fun `check empty map serialises as Java emptyMap`() = kryoSpecific("Specifically checks Kryo serialization") { val nameID = 0 val serializedForm = emptyMap().serialize() val output = ByteArrayOutputStream().apply { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/PrivateKeySerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/PrivateKeySerializationTest.kt new file mode 100644 index 0000000000..bf9cd8f2e5 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/PrivateKeySerializationTest.kt @@ -0,0 +1,42 @@ +package net.corda.nodeapi.internal.serialization + +import net.corda.core.crypto.Crypto +import net.corda.core.serialization.SerializationContext.UseCase.* +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize +import net.corda.testing.TestDependencyInjectionBase +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.security.PrivateKey +import kotlin.test.assertTrue + +@RunWith(Parameterized::class) +class PrivateKeySerializationTest(private val privateKey: PrivateKey, private val testName: String) : TestDependencyInjectionBase() { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> { + val privateKeys: List = Crypto.supportedSignatureSchemes().filterNot { Crypto.COMPOSITE_KEY === it } + .map { Crypto.generateKeyPair(it).private } + + return privateKeys.map { arrayOf(it, PrivateKeySerializationTest::class.java.simpleName + "-" + it.javaClass.simpleName) } + } + } + + @Test + fun `passed with expected UseCases`() { + assertTrue { privateKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes.isNotEmpty() } + assertTrue { privateKey.serialize(context = SerializationDefaults.CHECKPOINT_CONTEXT).bytes.isNotEmpty() } + } + + @Test + fun `failed with wrong UseCase`() { + assertThatThrownBy { privateKey.serialize(context = SerializationDefaults.P2P_CONTEXT) } + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("UseCase '${P2P}' is not within") + + } +} \ No newline at end of file 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 2c8a337035..909f672fde 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 @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import java.io.ByteArrayOutputStream -class SerializationTokenTest : TestDependencyInjectionBase() { +class SerializationTokenTest : TestDependencyInjectionBase() { private lateinit var factory: SerializationFactory private lateinit var context: SerializationContext @@ -58,7 +58,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { val testContext = this.context.withTokenContext(context) val serializedBytes = tokenizableBefore.serialize(factory, testContext) val tokenizableAfter = serializedBytes.deserialize(factory, testContext) - assertThat(tokenizableAfter).isSameAs(tokenizableBefore) + assertThat(tokenizableAfter).isSameAs(tokenizableBefore) } @Test(expected = UnsupportedOperationException::class) @@ -92,11 +92,11 @@ class SerializationTokenTest : TestDependencyInjectionBase() { val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(this.context))) val stream = ByteArrayOutputStream() - Output(stream).use { - it.write(KryoHeaderV0_1.bytes) - kryo.writeClass(it, SingletonSerializeAsToken::class.java) - kryo.writeObject(it, emptyList()) - } + Output(stream).use { + it.write(KryoHeaderV0_1.bytes) + kryo.writeClass(it, SingletonSerializeAsToken::class.java) + kryo.writeObject(it, emptyList()) + } val serializedBytes = SerializedBytes(stream.toByteArray()) serializedBytes.deserialize(factory, testContext) } @@ -105,6 +105,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { object UnitSerializationToken : SerializationToken { override fun fromToken(context: SerializeAsTokenContext): Any = UnitSerializeAsToken() } + override fun toToken(context: SerializeAsTokenContext): SerializationToken = UnitSerializationToken } 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 c1c5ea05dd..210a0cd800 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 @@ -2,10 +2,13 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver +import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase -import org.junit.Assert.* +import net.corda.testing.kryoSpecific +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test import java.io.ByteArrayOutputStream import java.util.* @@ -25,21 +28,24 @@ class SetsSerializationTest : TestDependencyInjectionBase() { @Test fun `check set can be serialized as part of SessionData`() { run { - val sessionData = SessionData(123, setOf(1)) + val sessionData = SessionData(123, setOf(1).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(setOf(1), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, setOf(1, 2)) + val sessionData = SessionData(123, setOf(1, 2).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(setOf(1, 2), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, emptySet()) + val sessionData = SessionData(123, emptySet().serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(emptySet(), sessionData.payload.deserialize()) } } @Test - fun `check empty set serialises as Java emptySet`() { + fun `check empty set serialises as Java emptySet`() = kryoSpecific("Checks Kryo header properties") { val nameID = 0 val serializedForm = emptySet().serialize() val output = ByteArrayOutputStream().apply { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt index 8314286d12..68e106fd89 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -17,25 +17,28 @@ class DeserializeMapTests { @Test fun mapTest() { data class C(val c: Map) - val c = C (mapOf("A" to 1, "B" to 2)) + + val c = C(mapOf("A" to 1, "B" to 2)) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) } - @Test(expected=java.io.NotSerializableException::class) + @Test(expected = java.io.NotSerializableException::class) fun abstractMapFromMapOf() { data class C(val c: AbstractMap) - val c = C (mapOf("A" to 1, "B" to 2) as AbstractMap) + + val c = C(mapOf("A" to 1, "B" to 2) as AbstractMap) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) } - @Test(expected=java.io.NotSerializableException::class) + @Test(expected = java.io.NotSerializableException::class) fun abstractMapFromTreeMap() { data class C(val c: AbstractMap) - val c = C (TreeMap(mapOf("A" to 1, "B" to 2))) + + val c = C(TreeMap(mapOf("A" to 1, "B" to 2))) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) @@ -44,7 +47,8 @@ class DeserializeMapTests { @Test fun sortedMapTest() { data class C(val c: SortedMap) - val c = C(sortedMapOf ("A" to 1, "B" to 2)) + + val c = C(sortedMapOf("A" to 1, "B" to 2)) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) } @@ -52,7 +56,8 @@ class DeserializeMapTests { @Test fun navigableMapTest() { data class C(val c: NavigableMap) - val c = C(TreeMap (mapOf("A" to 1, "B" to 2)).descendingMap()) + + val c = C(TreeMap(mapOf("A" to 1, "B" to 2)).descendingMap()) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) @@ -61,9 +66,10 @@ class DeserializeMapTests { @Test fun dictionaryTest() { data class C(val c: Dictionary) - val v : Hashtable = Hashtable() - v.put ("a", 1) - v.put ("b", 2) + + val v: Hashtable = Hashtable() + v.put("a", 1) + v.put("b", 2) val c = C(v) // expected to throw @@ -74,9 +80,10 @@ class DeserializeMapTests { @Test fun hashtableTest() { data class C(val c: Hashtable) - val v : Hashtable = Hashtable() - v.put ("a", 1) - v.put ("b", 2) + + val v: Hashtable = Hashtable() + v.put("a", 1) + v.put("b", 2) val c = C(v) // expected to throw @@ -86,8 +93,9 @@ class DeserializeMapTests { @Test fun hashMapTest() { - data class C(val c : HashMap) - val c = C (HashMap (mapOf("A" to 1, "B" to 2))) + data class C(val c: HashMap) + + val c = C(HashMap(mapOf("A" to 1, "B" to 2))) // expect this to throw Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } @@ -96,8 +104,9 @@ class DeserializeMapTests { @Test fun weakHashMapTest() { - data class C(val c : WeakHashMap) - val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2))) + data class C(val c: WeakHashMap) + + val c = C(WeakHashMap(mapOf("A" to 1, "B" to 2))) Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } .isInstanceOf(IllegalArgumentException::class.java).hasMessageContaining("Weak references with map types not supported. Suggested fix: use java.util.LinkedHashMap instead.") @@ -106,7 +115,8 @@ class DeserializeMapTests { @Test fun concreteTreeMapTest() { data class C(val c: TreeMap) - val c = C(TreeMap (mapOf("A" to 1, "B" to 3))) + + val c = C(TreeMap(mapOf("A" to 1, "B" to 3))) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) @@ -114,8 +124,9 @@ class DeserializeMapTests { @Test fun concreteLinkedHashMapTest() { - data class C(val c : LinkedHashMap) - val c = C (LinkedHashMap (mapOf("A" to 1, "B" to 2))) + data class C(val c: LinkedHashMap) + + val c = C(LinkedHashMap(mapOf("A" to 1, "B" to 2))) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt index cc05b9b39f..4c5c9311ec 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt @@ -402,7 +402,8 @@ class DeserializeSimpleTypesTests { @Test fun arrayOfArrayOfInt() { class C(val c: Array>) - val c = C (arrayOf (arrayOf(1,2,3), arrayOf(4,5,6))) + + val c = C(arrayOf(arrayOf(1, 2, 3), arrayOf(4, 5, 6))) val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -421,7 +422,8 @@ class DeserializeSimpleTypesTests { @Test fun arrayOfIntArray() { class C(val c: Array) - val c = C (arrayOf (IntArray(3), IntArray(3))) + + val c = C(arrayOf(IntArray(3), IntArray(3))) c.c[0][0] = 1; c.c[0][1] = 2; c.c[0][2] = 3 c.c[1][0] = 4; c.c[1][1] = 5; c.c[1][2] = 6 @@ -444,22 +446,32 @@ class DeserializeSimpleTypesTests { class C(val c: Array>) val c = C(arrayOf(arrayOf(IntArray(3), IntArray(3), IntArray(3)), - arrayOf(IntArray(3), IntArray(3), IntArray(3)), - arrayOf(IntArray(3), IntArray(3), IntArray(3)))) + arrayOf(IntArray(3), IntArray(3), IntArray(3)), + arrayOf(IntArray(3), IntArray(3), IntArray(3)))) - for (i in 0..2) { for (j in 0..2) { for (k in 0..2) { c.c[i][j][k] = i + j + k } } } + for (i in 0..2) { + for (j in 0..2) { + for (k in 0..2) { + c.c[i][j][k] = i + j + k + } + } + } val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) - for (i in 0..2) { for (j in 0..2) { for (k in 0..2) { - assertEquals(c.c[i][j][k], deserializedC.c[i][j][k]) - }}} + for (i in 0..2) { + for (j in 0..2) { + for (k in 0..2) { + assertEquals(c.c[i][j][k], deserializedC.c[i][j][k]) + } + } + } } @Test fun nestedRepeatedTypes() { - class A(val a : A?, val b: Int) + class A(val a: A?, val b: Int) var a = A(A(A(A(A(null, 1), 2), 3), 4), 5) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt index d46ec93c5a..1f20ac013f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt @@ -1,5 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.CordaSerializable import org.junit.Test import java.time.DayOfWeek @@ -10,12 +12,18 @@ import java.io.File import java.io.NotSerializableException import net.corda.core.serialization.SerializedBytes +import org.assertj.core.api.Assertions class EnumTests { enum class Bras { TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED } + @CordaSerializable + enum class AnnotatedBras { + TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED + } + // The state of the OldBras enum when the tests in changedEnum1 were serialised // - use if the test file needs regenerating //enum class OldBras { @@ -41,7 +49,7 @@ class EnumTests { } - enum class BrasWithInit (val someList: List) { + enum class BrasWithInit(val someList: List) { TSHIRT(emptyList()), UNDERWIRE(listOf(1, 2, 3)), PUSHUP(listOf(100, 200)), @@ -82,7 +90,7 @@ class EnumTests { assertEquals(8, schema_bras.choices.size) Bras.values().forEach { val bra = it - assertNotNull (schema_bras.choices.find { it.name == bra.name }) + assertNotNull(schema_bras.choices.find { it.name == bra.name }) } } @@ -107,7 +115,7 @@ class EnumTests { assertEquals(8, schema_bras.choices.size) Bras.values().forEach { val bra = it - assertNotNull (schema_bras.choices.find { it.name == bra.name }) + assertNotNull(schema_bras.choices.find { it.name == bra.name }) } // Test the actual deserialised object @@ -116,13 +124,13 @@ class EnumTests { @Test fun multiEnum() { - data class Support (val top: Bras, val day : DayOfWeek) - data class WeeklySupport (val tops: List) + data class Support(val top: Bras, val day: DayOfWeek) + data class WeeklySupport(val tops: List) - val week = WeeklySupport (listOf( - Support (Bras.PUSHUP, DayOfWeek.MONDAY), - Support (Bras.UNDERWIRE, DayOfWeek.WEDNESDAY), - Support (Bras.PADDED, DayOfWeek.SUNDAY))) + val week = WeeklySupport(listOf( + Support(Bras.PUSHUP, DayOfWeek.MONDAY), + Support(Bras.UNDERWIRE, DayOfWeek.WEDNESDAY), + Support(Bras.PADDED, DayOfWeek.SUNDAY))) val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(week)) @@ -138,7 +146,7 @@ class EnumTests { fun enumWithInit() { data class C(val c: BrasWithInit) - val c = C (BrasWithInit.PUSHUP) + val c = C(BrasWithInit.PUSHUP) val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(c)) assertEquals(c.c, obj.c) @@ -149,7 +157,7 @@ class EnumTests { val path = EnumTests::class.java.getResource("EnumTests.changedEnum1") val f = File(path.toURI()) - data class C (val a: OldBras) + data class C(val a: OldBras) // Original version of the class for the serialised version of this class // @@ -169,7 +177,7 @@ class EnumTests { val path = EnumTests::class.java.getResource("EnumTests.changedEnum2") val f = File(path.toURI()) - data class C (val a: OldBras2) + data class C(val a: OldBras2) // DO NOT CHANGE THIS, it's important we serialise with a value that doesn't // change position in the upated enum class @@ -186,4 +194,74 @@ class EnumTests { // we expect this to throw DeserializationInput(sf1).deserialize(SerializedBytes(sc2)) } + + @Test + fun enumNotWhitelistedFails() { + data class C(val c: Bras) + + class WL(val allowed: String) : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean { + return type.name == allowed + } + } + + val factory = SerializerFactory(WL(classTestName("C")), ClassLoader.getSystemClassLoader()) + + Assertions.assertThatThrownBy { + TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE)) + }.isInstanceOf(NotSerializableException::class.java) + } + + @Test + fun enumWhitelisted() { + data class C(val c: Bras) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean { + return type.name == "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$enumWhitelisted\$C" || + type.name == "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$Bras" + } + } + + val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + + // if it all works, this won't explode + TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE)) + } + + @Test + fun enumAnnotated() { + @CordaSerializable data class C(val c: AnnotatedBras) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = false + } + + val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + + // if it all works, this won't explode + TestSerializationOutput(VERBOSE, factory).serialize(C(AnnotatedBras.UNDERWIRE)) + } + + @Test + fun deserializeNonWhitlistedEnum() { + data class C(val c: Bras) + + class WL(val allowed: List) : ClassWhitelist { + override fun hasListed(type: Class<*>) = type.name in allowed + } + + // first serialise the class using a context in which Bras are whitelisted + val factory = SerializerFactory(WL(listOf(classTestName("C"), + "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$Bras")), + ClassLoader.getSystemClassLoader()) + val bytes = TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE)) + + // then take that serialised object and attempt to deserialize it in a context that + // disallows the Bras enum + val factory2 = SerializerFactory(WL(listOf(classTestName("C"))), ClassLoader.getSystemClassLoader()) + Assertions.assertThatThrownBy { + DeserializationInput(factory2).deserialize(bytes) + }.isInstanceOf(NotSerializableException::class.java) + } } \ No newline at end of file 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 0bf23ae85d..cdf690029c 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 @@ -32,7 +32,7 @@ class EvolvabilityTests { // f.writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped - data class C (val b: Int, val a: Int) + data class C(val b: Int, val a: Int) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -56,7 +56,7 @@ class EvolvabilityTests { // f.writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped - data class C (val b: String, val a: Int) + data class C(val b: String, val a: Int) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -79,13 +79,13 @@ class EvolvabilityTests { // f.writeBytes(sc.bytes) // println ("Path = $path") - data class C (val a: Int, val b: Int?) + data class C(val a: Int, val b: Int?) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedC.a) - assertEquals (null, deserializedC.b) + assertEquals(A, deserializedC.a) + assertEquals(null, deserializedC.b) } @Test(expected = NotSerializableException::class) @@ -104,7 +104,7 @@ class EvolvabilityTests { // println ("Path = $path") // new version of the class, in this case a new parameter has been added (b) - data class C (val a: Int, val b: Int) + data class C(val a: Int, val b: Int) val sc2 = f.readBytes() @@ -132,13 +132,13 @@ class EvolvabilityTests { // f.writeBytes(scc.bytes) // println ("Path = $path") - data class CC (val b: String, val d: Int) + data class CC(val b: String, val d: Int) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (B, deserializedCC.b) - assertEquals (D, deserializedCC.d) + assertEquals(B, deserializedCC.b) + assertEquals(D, deserializedCC.d) } @Suppress("UNUSED_VARIABLE") @@ -185,16 +185,16 @@ class EvolvabilityTests { // println ("Path = $path") @Suppress("UNUSED") - data class CC (val a: Int, val b: String) { + data class CC(val a: Int, val b: String) { @DeprecatedConstructorForDeserialization(1) - constructor (a: Int) : this (a, "hello") + constructor (a: Int) : this(a, "hello") } val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedCC.a) - assertEquals ("hello", deserializedCC.b) + assertEquals(A, deserializedCC.a) + assertEquals("hello", deserializedCC.b) } @Test(expected = NotSerializableException::class) @@ -214,9 +214,9 @@ class EvolvabilityTests { // f.writeBytes(scc.bytes) // println ("Path = $path") - data class CC (val a: Int, val b: String) { + data class CC(val a: Int, val b: String) { // constructor annotation purposefully omitted - constructor (a: Int) : this (a, "hello") + constructor (a: Int) : this(a, "hello") } // we expect this to throw as we should not find any constructors @@ -242,20 +242,20 @@ class EvolvabilityTests { // println ("Path = $path") @Suppress("UNUSED") - data class CC (val a: Int, val b: Int, val c: String, val d: String) { + data class CC(val a: Int, val b: Int, val c: String, val d: String) { // ensure none of the original parameters align with the initial // construction order @DeprecatedConstructorForDeserialization(1) - constructor (c: String, a: Int, b: Int) : this (a, b, c, "wibble") + constructor (c: String, a: Int, b: Int) : this(a, b, c, "wibble") } val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedCC.a) - assertEquals (B, deserializedCC.b) - assertEquals (C, deserializedCC.c) - assertEquals ("wibble", deserializedCC.d) + assertEquals(A, deserializedCC.a) + assertEquals(B, deserializedCC.b) + assertEquals(C, deserializedCC.c) + assertEquals("wibble", deserializedCC.d) } @Test @@ -277,20 +277,20 @@ class EvolvabilityTests { // println ("Path = $path") // b is removed, d is added - data class CC (val a: Int, val c: String, val d: String) { + data class CC(val a: Int, val c: String, val d: String) { // ensure none of the original parameters align with the initial // construction order @Suppress("UNUSED") @DeprecatedConstructorForDeserialization(1) - constructor (c: String, a: Int) : this (a, c, "wibble") + constructor (c: String, a: Int) : this(a, c, "wibble") } val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedCC.a) - assertEquals (C, deserializedCC.c) - assertEquals ("wibble", deserializedCC.d) + assertEquals(A, deserializedCC.a) + assertEquals(C, deserializedCC.c) + assertEquals("wibble", deserializedCC.d) } @Test @@ -322,13 +322,15 @@ class EvolvabilityTests { // println ("Path = $path1") @Suppress("UNUSED") - data class C (val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) { + data class C(val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) { @DeprecatedConstructorForDeserialization(1) - constructor (b: Int, a: Int) : this (-1, -1, b, a, -1) + constructor (b: Int, a: Int) : this(-1, -1, b, a, -1) + @DeprecatedConstructorForDeserialization(2) - constructor (a: Int, c: Int, b: Int) : this (-1, c, b, a, -1) + constructor (a: Int, c: Int, b: Int) : this(-1, c, b, a, -1) + @DeprecatedConstructorForDeserialization(3) - constructor (a: Int, b: Int, c: Int, d: Int) : this (-1, c, b, a, d) + constructor (a: Int, b: Int, c: Int, d: Int) : this(-1, c, b, a, d) } val sb1 = File(path1.toURI()).readBytes() @@ -376,15 +378,16 @@ class EvolvabilityTests { // println ("Path = $path") // Add a parameter to inner but keep outer unchanged - data class Inner (val a: Int, val b: String?) - data class Outer (val a: Int, val b: Inner) + data class Inner(val a: Int, val b: String?) + + data class Outer(val a: Int, val b: Inner) val sc2 = f.readBytes() val outer = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (oa, outer.a) - assertEquals (ia, outer.b.a) - assertEquals (null, outer.b.b) + assertEquals(oa, outer.a) + assertEquals(ia, outer.b.a) + assertEquals(null, outer.b.b) } @Test @@ -416,15 +419,18 @@ class EvolvabilityTests { // println ("Path = $path1") @Suppress("UNUSED") - data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) { + data class C(val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) { @DeprecatedConstructorForDeserialization(1) - constructor (b: Int, c: Int) : this (b, c, -1, -1, -1, -1) + constructor (b: Int, c: Int) : this(b, c, -1, -1, -1, -1) + @DeprecatedConstructorForDeserialization(2) - constructor (b: Int, c: Int, d: Int) : this (b, c, d, -1, -1, -1) + constructor (b: Int, c: Int, d: Int) : this(b, c, d, -1, -1, -1) + @DeprecatedConstructorForDeserialization(3) - constructor (b: Int, c: Int, d: Int, e: Int) : this (b, c, d, e, -1, -1) + constructor (b: Int, c: Int, d: Int, e: Int) : this(b, c, d, e, -1, -1) + @DeprecatedConstructorForDeserialization(4) - constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this (b, c, d, e, f, -1) + constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this(b, c, d, e, f, -1) } val sb1 = File(path1.toURI()).readBytes() 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 05cd84c934..31136f0723 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 @@ -50,7 +50,7 @@ class SerializationOutputTests { data class testShort(val s: Short) - data class testBoolean(val b : Boolean) + data class testBoolean(val b: Boolean) interface FooInterface { val pub: Int @@ -145,13 +145,13 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) - private inline fun serdes(obj: T, - factory: SerializerFactory = SerializerFactory ( - AllWhitelist, ClassLoader.getSystemClassLoader()), - freshDeserializationFactory: SerializerFactory = SerializerFactory( - AllWhitelist, ClassLoader.getSystemClassLoader()), - expectedEqual: Boolean = true, - expectDeserializedEqual: Boolean = true): T { + private inline fun serdes(obj: T, + factory: SerializerFactory = SerializerFactory( + AllWhitelist, ClassLoader.getSystemClassLoader()), + freshDeserializationFactory: SerializerFactory = SerializerFactory( + AllWhitelist, ClassLoader.getSystemClassLoader()), + expectedEqual: Boolean = true, + expectDeserializedEqual: Boolean = true): T { val ser = SerializationOutput(factory) val bytes = ser.serialize(obj) @@ -446,10 +446,10 @@ class SerializationOutputTests { try { try { throw IOException("Layer 1") - } catch(t: Throwable) { + } catch (t: Throwable) { throw IllegalStateException("Layer 2", t) } - } catch(t: Throwable) { + } catch (t: Throwable) { val desThrowable = serdesThrowableWithInternalInfo(t, factory, factory2, false) assertSerializedThrowableEquivalent(t, desThrowable) } @@ -476,12 +476,12 @@ class SerializationOutputTests { try { try { throw IOException("Layer 1") - } catch(t: Throwable) { + } catch (t: Throwable) { val e = IllegalStateException("Layer 2") e.addSuppressed(t) throw e } - } catch(t: Throwable) { + } catch (t: Throwable) { val desThrowable = serdesThrowableWithInternalInfo(t, factory, factory2, false) assertSerializedThrowableEquivalent(t, desThrowable) } @@ -534,7 +534,22 @@ class SerializationOutputTests { } } - val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract" + @Test + fun `test custom object`() { + serdes(FooContract) + } + + @Test + @Ignore("Cannot serialize due to known Kotlin/serialization limitation") + fun `test custom anonymous object`() { + val anonymous: Contract = object : Contract { + override fun verify(tx: LedgerTransaction) { + } + } + serdes(anonymous) + } + + private val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract" class FooState : ContractState { override val participants: List = emptyList() } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt index 86b769a14f..acd8c5b8fe 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt @@ -19,6 +19,7 @@ class SerializeAndReturnSchemaTest { @Test fun getSchema() { data class C(val a: Int, val b: Int) + val a = 1 val b = 2 diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt new file mode 100644 index 0000000000..2533bdf182 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt @@ -0,0 +1,144 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializedBytes +import net.corda.nodeapi.internal.serialization.AllWhitelist +import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter +import org.assertj.core.api.Assertions +import org.junit.Test +import java.io.File +import java.io.NotSerializableException +import java.lang.reflect.Type +import java.util.concurrent.ConcurrentHashMap +import kotlin.test.assertEquals + +class InStatic : Exception("Help!, help!, I'm being repressed") + +class C { + companion object { + init { + throw InStatic() + } + } +} + +// To re-setup the resource file for the tests +// * deserializeTest +// * deserializeTest2 +// comment out the companion object from here, comment out the test code and uncomment +// the generation code, then re-run the test and copy the file shown in the output print +// to the resource directory +class C2(var b: Int) { + /* + companion object { + init { + throw InStatic() + } + } + */ +} + +class StaticInitialisationOfSerializedObjectTest { + @Test(expected = java.lang.ExceptionInInitializerError::class) + fun itBlowsUp() { + C() + } + + @Test + fun KotlinObjectWithCompanionObject() { + data class D(val c: C) + + val sf = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + + val typeMap = sf::class.java.getDeclaredField("serializersByType") + typeMap.isAccessible = true + + @Suppress("UNCHECKED_CAST") + val serialisersByType = typeMap.get(sf) as ConcurrentHashMap> + + // pre building a serializer, we shouldn't have anything registered + assertEquals(0, serialisersByType.size) + + // build a serializer for type D without an instance of it to serialise, since + // we can't actually construct one + sf.get(null, D::class.java) + + // post creation of the serializer we should have one element in the map, this + // proves we didn't statically construct an instance of C when building the serializer + assertEquals(1, serialisersByType.size) + } + + + @Test + fun deserializeTest() { + data class D(val c: C2) + + val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest") + val f = File(path.toURI()) + + // Original version of the class for the serialised version of this class + // + //val sf1 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + //val sc = SerializationOutput(sf1).serialize(D(C2(20))) + //f.writeBytes(sc.bytes) + //println (path) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = + type.name == "net.corda.nodeapi.internal.serialization.amqp" + + ".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D" + } + + val sf2 = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + val bytes = f.readBytes() + + Assertions.assertThatThrownBy { + DeserializationInput(sf2).deserialize(SerializedBytes(bytes)) + }.isInstanceOf(NotSerializableException::class.java) + } + + // Version of a serializer factory that will allow the class carpenter living on the + // factory to have a different whitelist applied to it than the factory + class TestSerializerFactory(wl1: ClassWhitelist, wl2: ClassWhitelist) : + SerializerFactory(wl1, ClassLoader.getSystemClassLoader()) { + override val classCarpenter = ClassCarpenter(ClassLoader.getSystemClassLoader(), wl2) + } + + // This time have the serilization factory and the carpenter use different whitelists + @Test + fun deserializeTest2() { + data class D(val c: C2) + + val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest2") + val f = File(path.toURI()) + + // Original version of the class for the serialised version of this class + // + //val sf1 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + //val sc = SerializationOutput(sf1).serialize(D(C2(20))) + //f.writeBytes(sc.bytes) + //println (path) + + // whitelist to be used by the serialisation factory + class WL1 : ClassWhitelist { + override fun hasListed(type: Class<*>) = + type.name == "net.corda.nodeapi.internal.serialization.amqp" + + ".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D" + } + + // whitelist to be used by the carpenter + class WL2 : ClassWhitelist { + override fun hasListed(type: Class<*>) = true + } + + val sf2 = TestSerializerFactory(WL1(), WL2()) + val bytes = f.readBytes() + + // Deserializing should throw because C is not on the whitelist NOT because + // we ever went anywhere near statically constructing it prior to not actually + // creating an instance of it + Assertions.assertThatThrownBy { + DeserializationInput(sf2).deserialize(SerializedBytes(bytes)) + }.isInstanceOf(NotSerializableException::class.java) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt index 913ea207c1..0c1c610c5a 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.carpenter +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.serialization.AllWhitelist import org.junit.Test import java.beans.Introspector @@ -323,7 +324,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `int array`() { val className = "iEnjoyPotato" val schema = ClassSchema( @@ -356,7 +356,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `integer array`() { val className = "iEnjoyFlan" val schema = ClassSchema( @@ -366,16 +365,15 @@ class ClassCarpenterTest { val clazz = cc.build(schema) val i = clazz.constructors[0].newInstance(arrayOf(1, 2, 3)) as SimpleFieldAccess - val arr = clazz.getMethod("getA").invoke(i) + val arr: Array = uncheckedCast(clazz.getMethod("getA").invoke(i)) - assertEquals(1, (arr as Array)[0]) + assertEquals(1, arr[0]) assertEquals(2, arr[1]) assertEquals(3, arr[2]) assertEquals("$className{a=[1, 2, 3]}", i.toString()) } @Test - @Suppress("UNCHECKED_CAST") fun `int array with ints`() { val className = "iEnjoyCrumble" val schema = ClassSchema( @@ -395,7 +393,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `multiple int arrays`() { val className = "iEnjoyJam" val schema = ClassSchema( @@ -417,7 +414,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `string array`() { val className = "iEnjoyToast" val schema = ClassSchema( @@ -427,7 +423,7 @@ class ClassCarpenterTest { val clazz = cc.build(schema) val i = clazz.constructors[0].newInstance(arrayOf("toast", "butter", "jam")) - val arr = clazz.getMethod("getA").invoke(i) as Array + val arr: Array = uncheckedCast(clazz.getMethod("getA").invoke(i)) assertEquals("toast", arr[0]) assertEquals("butter", arr[1]) @@ -435,7 +431,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `string arrays`() { val className = "iEnjoyToast" val schema = ClassSchema( @@ -452,8 +447,8 @@ class ClassCarpenterTest { "and on the side", arrayOf("some pickles", "some fries")) - val arr1 = clazz.getMethod("getA").invoke(i) as Array - val arr2 = clazz.getMethod("getC").invoke(i) as Array + val arr1: Array = uncheckedCast(clazz.getMethod("getA").invoke(i)) + val arr2: Array = uncheckedCast(clazz.getMethod("getC").invoke(i)) assertEquals("bread", arr1[0]) assertEquals("spread", arr1[1]) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt new file mode 100644 index 0000000000..deb51da65f --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt @@ -0,0 +1,103 @@ +package net.corda.nodeapi.internal.serialization.carpenter + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.CordaSerializable +import org.assertj.core.api.Assertions +import org.junit.Ignore +import org.junit.Test +import java.io.NotSerializableException + +class ClassCarpenterWhitelistTest { + + // whitelisting a class on the class path will mean we will carpente up a class that + // contains it as a member + @Test + fun whitelisted() { + data class A(val a: Int) + + class WL : ClassWhitelist { + private val allowedClasses = hashSetOf( + A::class.java.name + ) + + override fun hasListed(type: Class<*>): Boolean = type.name in allowedClasses + } + + val cc = ClassCarpenter(whitelist = WL()) + + // if this works, the test works, if it throws then we're in a world of pain, we could + // go further but there are a lot of other tests that test weather we can build + // carpented objects + cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java)))) + } + + @Test + @Ignore("Currently the carpenter doesn't inspect it's whitelist so will carpent anything" + + "it's asked relying on the serializer factory to not ask for anything") + fun notWhitelisted() { + data class A(val a: Int) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = false + } + + val cc = ClassCarpenter(whitelist = WL()) + + // Class A isn't on the whitelist, so we should fail to carpent it + Assertions.assertThatThrownBy { + cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java)))) + }.isInstanceOf(NotSerializableException::class.java) + } + + // despite now being whitelisted and on the class path, we will carpent this because + // it's marked as CordaSerializable + @Test + fun notWhitelistedButAnnotated() { + @CordaSerializable data class A(val a: Int) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = false + } + + val cc = ClassCarpenter(whitelist = WL()) + + // again, simply not throwing here is enough to show the test worked and the carpenter + // didn't reject the type even though it wasn't on the whitelist because it was + // annotated properly + cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java)))) + } + + @Test + @Ignore("Currently the carpenter doesn't inspect it's whitelist so will carpent anything" + + "it's asked relying on the serializer factory to not ask for anything") + fun notWhitelistedButCarpented() { + // just have the white list reject *Everything* except ints + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = type.name == "int" + } + + val cc = ClassCarpenter(whitelist = WL()) + + val schema1a = ClassSchema("thing1a", mapOf("a" to NonNullableField(Int::class.java))) + + // thing 1 won't be set as corda serializable, meaning we won't build schema 2 + schema1a.unsetCordaSerializable() + + val clazz1a = cc.build(schema1a) + val schema2 = ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1a))) + + // thing 2 references thing 1 which wasn't carpented as corda s erializable and thus + // this will fail + Assertions.assertThatThrownBy { + cc.build(schema2) + }.isInstanceOf(NotSerializableException::class.java) + + // create a second type of schema1, this time leave it as corda serialzable + val schema1b = ClassSchema("thing1b", mapOf("a" to NonNullableField(Int::class.java))) + + val clazz1b = cc.build(schema1b) + + // since schema 1b was created as CordaSerializable this will work + ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1b))) + } +} \ No newline at end of file diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest new file mode 100644 index 0000000000..0ba0299a3f Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest2 new file mode 100644 index 0000000000..6423985eaf Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest2 differ diff --git a/node/build.gradle b/node/build.gradle index 76073747d9..3d6c5d4e0f 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -108,11 +108,6 @@ dependencies { // Manifests: for reading stuff from the manifest file compile "com.jcabi:jcabi-manifests:1.1" - // GraphStream: For visualisation - testCompile "org.graphstream:gs-core:1.3" - testCompile("org.graphstream:gs-ui:1.3") { - exclude group: "bouncycastle" - } compile("com.intellij:forms_rt:7.0.3") { exclude group: "asm" } @@ -162,7 +157,7 @@ dependencies { compile "io.netty:netty-all:$netty_version" // CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy. - compile("com.github.corda.crash:crash.shell:9d242da2a10e686f33a3aefc69e4768824ad0716") { + compile("com.github.corda.crash:crash.shell:d5da86ba1b38e9c33af2a621dd15ba286307bec4") { exclude group: "org.slf4j", module: "slf4j-jdk14" } @@ -195,7 +190,7 @@ task integrationTest(type: Test) { } task smokeTestJar(type: Jar) { - baseName = project.name + '-smoke-test' + classifier 'smokeTests' from sourceSets.smokeTest.output } diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index c61ad1cca9..525790ea22 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -1,20 +1,20 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable -import net.corda.core.internal.div import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.div import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.testing.ALICE import net.corda.node.internal.NodeStartup import net.corda.node.services.FlowPermissions.Companion.startFlowPermission +import net.corda.nodeapi.User import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.ServiceType -import net.corda.nodeapi.User +import net.corda.testing.ALICE +import net.corda.testing.ProjectStructure.projectRootDir import net.corda.testing.driver.ListenProcessDeathException import net.corda.testing.driver.NetworkMapStartStrategy -import net.corda.testing.ProjectStructure.projectRootDir import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -32,7 +32,7 @@ class BootTests { driver { val user = User("u", "p", setOf(startFlowPermission())) val future = startNode(rpcUsers = listOf(user)).getOrThrow().rpcClientToNode(). - start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue + start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue assertThatThrownBy { future.getOrThrow() }.isInstanceOf(InvalidClassException::class.java).hasMessage("filter status: REJECTED") } } @@ -60,7 +60,7 @@ class BootTests { fun `node quits on failure to register with network map`() { val tooManyAdvertisedServices = (1..100).map { ServiceInfo(ServiceType.notary.getSubType("$it")) }.toSet() driver(networkMapStartStrategy = NetworkMapStartStrategy.Nominated(ALICE.name)) { - val future = startNode(providedName = ALICE.name, advertisedServices = tooManyAdvertisedServices) + val future = startNode(providedName = ALICE.name) assertFailsWith(ListenProcessDeathException::class) { future.getOrThrow() } } } diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index a46856b2ce..50788582b5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -36,7 +36,7 @@ class CordappScanningDriverTest { @StartableByRPC @InitiatingFlow - class ReceiveFlow(val otherParty: Party) :FlowLogic() { + class ReceiveFlow(val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): String = initiateFlow(otherParty).receive().unwrap { it } } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index e2d9648556..211f0e1f15 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -7,14 +7,14 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.minutes import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver @@ -103,10 +103,10 @@ class NodePerformanceTests { @Test fun `self pay rate`() { driver(startNodesInProcess = true) { - val a = startNode( - rpcUsers = listOf(User("A", "A", setOf(startFlowPermission(), startFlowPermission()))), - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)) - ).get() + val a = startNotaryNode( + DUMMY_NOTARY.name, + rpcUsers = listOf(User("A", "A", setOf(startFlowPermission(), startFlowPermission()))) + ).getOrThrow() a as NodeHandle.InProcess val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics) a.rpcClientToNode().use("A", "A") { connection -> diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt index a6aef9b4fd..2f29d8df6d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt @@ -17,7 +17,7 @@ class NodeStartupPerformanceTests { driver(networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false)) { startDedicatedNetworkMapService().get() val times = ArrayList() - for (i in 1 .. 10) { + for (i in 1..10) { val time = Stopwatch.createStarted().apply { startNode().get() }.stop().elapsed(TimeUnit.MICROSECONDS) 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 03b9a9435e..9d4abec634 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 @@ -4,24 +4,31 @@ import net.corda.core.contracts.Contract import net.corda.core.contracts.PartyAndReference import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic +import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.createDirectories import net.corda.core.internal.div +import net.corda.core.internal.toLedgerTransaction import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.seconds import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.nodeapi.User import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase +import net.corda.testing.driver.DriverDSLExposedInterface +import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver +import net.corda.testing.eventually import net.corda.testing.node.MockServices -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import java.net.URLClassLoader @@ -30,16 +37,55 @@ import kotlin.test.assertFailsWith class AttachmentLoadingTests : TestDependencyInjectionBase() { private class Services : MockServices() { - private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR))).start(attachments) + private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments) private val cordapp get() = provider.cordapps.first() val attachmentId get() = provider.getCordappAttachmentId(cordapp)!! val appContext get() = provider.getAppContext(cordapp) override val cordappProvider: CordappProvider = provider } - companion object { - private val isolatedJAR = this::class.java.getResource("isolated.jar")!! - private val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract" + private companion object { + val logger = loggerFor() + val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!! + val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract" + + val bankAName = CordaX500Name("BankA", "Zurich", "CH") + val bankBName = CordaX500Name("BankB", "Zurich", "CH") + val notaryName = CordaX500Name("Notary", "Zurich", "CH") + val flowInitiatorClass = + Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) + .asSubclass(FlowLogic::class.java) + + private fun DriverDSLExposedInterface.createTwoNodesAndNotary(): List { + val adminUser = User("admin", "admin", permissions = setOf("ALL")) + val nodes = listOf( + startNode(providedName = bankAName, rpcUsers = listOf(adminUser)), + startNode(providedName = bankBName, rpcUsers = listOf(adminUser)), + startNotaryNode(providedName = notaryName, rpcUsers = listOf(adminUser), validating = false) + ).transpose().getOrThrow() // Wait for all nodes to start up. + nodes.forEach { it.rpc.waitUntilNetworkReady().getOrThrow() } + return nodes + } + + private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { + // Copy the app jar to the first node. The second won't have it. + val path = (baseDirectory(nodeName.toString()) / "cordapps").createDirectories() / "isolated.jar" + logger.info("Installing isolated jar to $path") + isolatedJAR.openStream().buffered().use { input -> + Files.newOutputStream(path).buffered().use { output -> + input.copyTo(output) + } + } + } + + // Due to cluster instability after nodes been started it may take some time to all the nodes to become available + // *and* discover each other to reliably communicate. Hence, eventual nature of the test. + // TODO: Remove this method and usages of it once NetworkMap service been re-worked + private fun eventuallyPassingTest(block: () -> Unit) { + eventually(30.seconds) { + block() + } + } } private lateinit var services: Services @@ -57,7 +103,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { val contract = contractClass.newInstance() val txBuilder = generateInitialMethod.invoke(contract, PartyAndReference(DUMMY_BANK_A, OpaqueBytes(kotlin.ByteArray(1))), 1, DUMMY_NOTARY) as TransactionBuilder val context = SerializationFactory.defaultFactory.defaultContext - .withClassLoader(appClassLoader) + .withClassLoader(appClassLoader) val ledgerTx = txBuilder.toLedgerTransaction(services, context) contract.verify(ledgerTx) @@ -67,41 +113,27 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { assertEquals(expected, actual) } - // TODO - activate this test - // @Test + @Test fun `test that attachments retrieved over the network are not used for code`() { driver(initialiseSerialization = false) { - val bankAName = CordaX500Name("BankA", "Zurich", "CH") - val bankBName = CordaX500Name("BankB", "Zurich", "CH") - // Copy the app jar to the first node. The second won't have it. - val path = (baseDirectory(bankAName.toString()) / "plugins").createDirectories() / "isolated.jar" - isolatedJAR.openStream().buffered().use { input -> - Files.newOutputStream(path).buffered().use { output -> - input.copyTo(output) + installIsolatedCordappTo(bankAName) + val (bankA, bankB, _) = createTwoNodesAndNotary() + eventuallyPassingTest { + assertFailsWith("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") { + bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() } } - val admin = User("admin", "admin", permissions = setOf("ALL")) - val (bankA, bankB) = listOf( - startNode(providedName = bankAName, rpcUsers = listOf(admin)), - startNode(providedName = bankBName, rpcUsers = listOf(admin)) - ).transpose().getOrThrow() // Wait for all nodes to start up. + } + } - val clazz = - Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) - .asSubclass(FlowLogic::class.java) - - try { - bankA.rpcClientToNode().start("admin", "admin").use { rpc -> - val proxy = rpc.proxy - val party = proxy.wellKnownPartyFromX500Name(bankBName)!! - - assertFailsWith("xxx") { - proxy.startFlowDynamic(clazz, party).returnValue.getOrThrow() - } - } - } finally { - bankA.stop() - bankB.stop() + @Test + fun `tests that if the attachment is loaded on both sides already that a flow can run`() { + driver(initialiseSerialization = false) { + installIsolatedCordappTo(bankAName) + installIsolatedCordappTo(bankBName) + val (bankA, bankB, _) = createTwoNodesAndNotary() + eventuallyPassingTest { + bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() } } } 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 a28f1e3a9c..9a6a5c3112 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 @@ -10,6 +10,7 @@ import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -18,12 +19,11 @@ import net.corda.core.utilities.Try import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.BFTSMaRtConfiguration -import net.corda.node.services.network.NetworkMapService +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.chooseIdentity import net.corda.testing.contracts.DummyContract import net.corda.testing.dummyCommand @@ -31,18 +31,17 @@ import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Test -import java.nio.file.Files +import java.nio.file.Paths import kotlin.test.assertEquals import kotlin.test.assertTrue class BFTNotaryServiceTests { companion object { - private val serviceType = BFTNonValidatingNotaryService.type - private val clusterName = CordaX500Name(serviceType.id, "BFT", "Zurich", "CH") + private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") } private val mockNet = MockNetwork() - private val node = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + private val node = mockNet.createNode() @After fun stopNodes() { @@ -50,21 +49,17 @@ class BFTNotaryServiceTests { } private fun bftNotaryCluster(clusterSize: Int, exposeRaces: Boolean = false) { - Files.deleteIfExists("config" / "currentView") // XXX: Make config object warn if this exists? + (Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) ServiceIdentityGenerator.generateToDisk( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, clusterName) - val bftNotaryService = ServiceInfo(serviceType, clusterName) - val notaryClusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } + val clusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } replicaIds.forEach { replicaId -> - mockNet.createNode( - node.network.myAddress, - advertisedServices = bftNotaryService, - configOverrides = { - whenever(it.bftSMaRt).thenReturn(BFTSMaRtConfiguration(replicaId, false, exposeRaces)) - whenever(it.notaryClusterAddresses).thenReturn(notaryClusterAddresses) - }) + mockNet.createNode(configOverrides = { + val notary = NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces)) + whenever(it.notary).thenReturn(notary) + }) } mockNet.runNetwork() // Exchange initial network map registration messages. } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index 69915138dd..06ed089e16 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -39,10 +39,9 @@ class DistributedServiceTests : DriverBasedTest() { ) val aliceFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(testUser)) val notariesFuture = startNotaryCluster( - DUMMY_NOTARY.name.copy(commonName = RaftValidatingNotaryService.type.id), + DUMMY_NOTARY.name.copy(commonName = RaftValidatingNotaryService.id), rpcUsers = listOf(testUser), - clusterSize = clusterSize, - type = RaftValidatingNotaryService.type + clusterSize = clusterSize ) alice = aliceFuture.get() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index c81b86195c..3ba78ff7bd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -14,29 +14,15 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* -import net.corda.testing.DUMMY_BANK_A -import net.corda.testing.chooseIdentity import net.corda.testing.contracts.DummyContract import net.corda.testing.node.NodeBasedTest -import org.junit.After -import org.junit.Before import org.junit.Test import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class RaftNotaryServiceTests : NodeBasedTest() { - private val notaryName = CordaX500Name(RaftValidatingNotaryService.type.id, "RAFT Notary Service", "London", "GB") - - @Before - fun setup() { - setCordappPackages("net.corda.testing.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } +class RaftNotaryServiceTests : NodeBasedTest(listOf("net.corda.testing.contracts")) { + private val notaryName = CordaX500Name(RaftValidatingNotaryService.id, "RAFT Notary Service", "London", "GB") @Test fun `detect double spend`() { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt new file mode 100644 index 0000000000..97e2deb364 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -0,0 +1,146 @@ +package net.corda.node.services.network + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import net.corda.cordform.CordformNode +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.KeyManagementService +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.testing.ALICE +import net.corda.testing.ALICE_KEY +import net.corda.testing.DEV_TRUST_ROOT +import net.corda.testing.getTestPartyAndCertificate +import net.corda.testing.node.MockKeyManagementService +import net.corda.testing.node.NodeBasedTest +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.contentOf +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import rx.observers.TestSubscriber +import rx.schedulers.TestScheduler +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class NodeInfoWatcherTest : NodeBasedTest() { + + @Rule + @JvmField + var folder = TemporaryFolder() + + lateinit var keyManagementService: KeyManagementService + lateinit var nodeInfoPath: Path + val scheduler = TestScheduler() + val testSubscriber = TestSubscriber() + + // Object under test + lateinit var nodeInfoWatcher: NodeInfoWatcher + + companion object { + val nodeInfoFileRegex = Regex("nodeInfo\\-.*") + val nodeInfo = NodeInfo(listOf(), listOf(getTestPartyAndCertificate(ALICE)), 0, 0) + } + + @Before + fun start() { + val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) + keyManagementService = MockKeyManagementService(identityService, ALICE_KEY) + nodeInfoWatcher = NodeInfoWatcher(folder.root.toPath(), scheduler = scheduler) + nodeInfoPath = folder.root.toPath() / CordformNode.NODE_INFO_DIRECTORY + } + + @Test + fun `save a NodeInfo`() { + assertEquals(0, folder.root.list().filter { it.matches(nodeInfoFileRegex) }.size) + NodeInfoWatcher.saveToFile(folder.root.toPath(), nodeInfo, keyManagementService) + + val nodeInfoFiles = folder.root.list().filter { it.matches(nodeInfoFileRegex) } + assertEquals(1, nodeInfoFiles.size) + val fileName = nodeInfoFiles.first() + assertTrue(fileName.matches(nodeInfoFileRegex)) + val file = (folder.root.path / fileName).toFile() + // Just check that something is written, another tests verifies that the written value can be read back. + assertThat(contentOf(file)).isNotEmpty() + } + + @Test + fun `save a NodeInfo to JimFs`() { + val jimFs = Jimfs.newFileSystem(Configuration.unix()) + val jimFolder = jimFs.getPath("/nodeInfo") + NodeInfoWatcher.saveToFile(jimFolder, nodeInfo, keyManagementService) + } + + @Test + fun `load an empty Directory`() { + nodeInfoPath.createDirectories() + + val subscription = nodeInfoWatcher.nodeInfoUpdates() + .subscribe(testSubscriber) + try { + advanceTime() + + val readNodes = testSubscriber.onNextEvents.distinct() + assertEquals(0, readNodes.size) + } finally { + subscription.unsubscribe() + } + } + + @Test + fun `load a non empty Directory`() { + createNodeInfoFileInPath(nodeInfo) + + val subscription = nodeInfoWatcher.nodeInfoUpdates() + .subscribe(testSubscriber) + advanceTime() + + try { + val readNodes = testSubscriber.onNextEvents.distinct() + + assertEquals(1, readNodes.size) + assertEquals(nodeInfo, readNodes.first()) + } finally { + subscription.unsubscribe() + } + } + + @Test + fun `polling folder`() { + nodeInfoPath.createDirectories() + + // Start polling with an empty folder. + val subscription = nodeInfoWatcher.nodeInfoUpdates() + .subscribe(testSubscriber) + try { + // Ensure the watch service is started. + advanceTime() + // Check no nodeInfos are read. + assertEquals(0, testSubscriber.valueCount) + createNodeInfoFileInPath(nodeInfo) + + advanceTime() + + // We need the WatchService to report a change and that might not happen immediately. + testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS) + // The same folder can be reported more than once, so take unique values. + val readNodes = testSubscriber.onNextEvents.distinct() + assertEquals(nodeInfo, readNodes.first()) + } finally { + subscription.unsubscribe() + } + } + + private fun advanceTime() { + scheduler.advanceTimeBy(1, TimeUnit.MINUTES) + } + + // Write a nodeInfo under the right path. + private fun createNodeInfoFileInPath(nodeInfo: NodeInfo) { + NodeInfoWatcher.saveToFile(nodeInfoPath, nodeInfo, keyManagementService) + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt similarity index 67% rename from node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt rename to node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index da94d376aa..fae4c25c51 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -16,14 +16,21 @@ import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test +import java.time.Duration import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertTrue +private const val BRIDGE_RETRY_MS: Long = 100 + class PersistentNetworkMapCacheTest : NodeBasedTest() { - val partiesList = listOf(DUMMY_NOTARY, ALICE, BOB) - val addressesMap: HashMap = HashMap() - val infos: MutableSet = HashSet() + private val partiesList = listOf(DUMMY_NOTARY, ALICE, BOB) + private val addressesMap: HashMap = HashMap() + private val infos: MutableSet = HashSet() + + companion object { + val logger = loggerFor() + } @Before fun start() { @@ -44,7 +51,7 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { val res = netCache.getNodeByLegalIdentity(alice.info.chooseIdentity()) assertEquals(alice.info, res) val res2 = netCache.getNodeByLegalName(DUMMY_NOTARY.name) - assertEquals(infos.filter { DUMMY_NOTARY.name in it.legalIdentitiesAndCerts.map { it.name } }.singleOrNull(), res2) + assertEquals(infos.singleOrNull { DUMMY_NOTARY.name in it.legalIdentitiesAndCerts.map { it.name } }, res2) } } @@ -107,19 +114,23 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { @Test fun `new node joins network without network map started`() { + + fun customNodesStart(parties: List): List> = + startNodesWithPort(parties, noNetworkMap = false, customRetryIntervalMs = BRIDGE_RETRY_MS) + val parties = partiesList.subList(1, partiesList.size) // Start 2 nodes pointing at network map, but don't start network map service. - val otherNodes = startNodesWithPort(parties, noNetworkMap = false) + val otherNodes = customNodesStart(parties) otherNodes.forEach { node -> assertTrue(infos.any { it.legalIdentitiesAndCerts.toSet() == node.info.legalIdentitiesAndCerts.toSet() }) } // Start node that is not in databases of other nodes. Point to NMS. Which has't started yet. - val charlie = startNodesWithPort(listOf(CHARLIE), noNetworkMap = false)[0] + val charlie = customNodesStart(listOf(CHARLIE)).single() otherNodes.forEach { assertThat(it.services.networkMapCache.allNodes).doesNotContain(charlie.info) } // Start Network Map and see that charlie node appears in caches. - val nms = startNodesWithPort(listOf(DUMMY_NOTARY), noNetworkMap = false)[0] + val nms = customNodesStart(listOf(DUMMY_NOTARY)).single() nms.internals.startupComplete.get() assertTrue(nms.inNodeNetworkMapService != NullNetworkMapService) assertTrue(infos.any { it.legalIdentities.toSet() == nms.info.legalIdentities.toSet() }) @@ -127,25 +138,39 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { assertTrue(nms.info.chooseIdentity() in it.services.networkMapCache.allNodes.map { it.chooseIdentity() }) } charlie.internals.nodeReadyFuture.get() // Finish registration. - checkConnectivity(listOf(otherNodes[0], nms)) // Checks connectivity from A to NMS. - val cacheA = otherNodes[0].services.networkMapCache.allNodes - val cacheB = otherNodes[1].services.networkMapCache.allNodes - val cacheC = charlie.services.networkMapCache.allNodes - assertEquals(4, cacheC.size) // Charlie fetched data from NetworkMap - assertThat(cacheB).contains(charlie.info) - assertEquals(cacheA.toSet(), cacheB.toSet()) - assertEquals(cacheA.toSet(), cacheC.toSet()) + + val allTheStartedNodesPopulation = otherNodes.plus(charlie).plus(nms) + + // This is prediction of the longest time it will take to get the cluster into a stable state such that further + // testing can be performed upon it + val maxInstabilityInterval = BRIDGE_RETRY_MS * allTheStartedNodesPopulation.size * 30 + logger.info("Instability interval is set to: $maxInstabilityInterval ms") + + // TODO: Re-visit this sort of re-try for stable cluster once network map redesign is finished. + eventually(Duration.ofMillis(maxInstabilityInterval)) { + logger.info("Checking connectivity") + checkConnectivity(listOf(otherNodes[0], nms)) // Checks connectivity from A to NMS. + logger.info("Loading caches") + val cacheA = otherNodes[0].services.networkMapCache.allNodes + val cacheB = otherNodes[1].services.networkMapCache.allNodes + val cacheC = charlie.services.networkMapCache.allNodes + logger.info("Performing verification") + assertEquals(4, cacheC.size) // Charlie fetched data from NetworkMap + assertThat(cacheB).contains(charlie.info) + assertEquals(cacheA.toSet(), cacheB.toSet()) + assertEquals(cacheA.toSet(), cacheC.toSet()) + } } // HELPERS // Helper function to restart nodes with the same host and port. - private fun startNodesWithPort(nodesToStart: List, noNetworkMap: Boolean = false): List> { + private fun startNodesWithPort(nodesToStart: List, noNetworkMap: Boolean = false, customRetryIntervalMs: Long? = null): List> { return nodesToStart.map { party -> - val configOverrides = addressesMap[party.name]?.let { mapOf("p2pAddress" to it.toString()) } ?: emptyMap() + val configOverrides = (addressesMap[party.name]?.let { mapOf("p2pAddress" to it.toString()) } ?: emptyMap()) + + (customRetryIntervalMs?.let { mapOf("activeMQServer.bridge.retryIntervalMs" to it.toString()) } ?: emptyMap()) if (party == DUMMY_NOTARY) { startNetworkMapNode(party.name, configOverrides = configOverrides) - } - else { + } else { startNode(party.name, configOverrides = configOverrides, noNetworkMap = noNetworkMap, @@ -158,9 +183,11 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { private fun checkConnectivity(nodes: List>) { nodes.forEach { node1 -> nodes.forEach { node2 -> - node2.internals.registerInitiatedFlow(SendBackFlow::class.java) - val resultFuture = node1.services.startFlow(SendFlow(node2.info.chooseIdentity())).resultFuture - assertThat(resultFuture.getOrThrow()).isEqualTo("Hello!") + if (!(node1 === node2)) { // Do not check connectivity to itself + node2.internals.registerInitiatedFlow(SendBackFlow::class.java) + val resultFuture = node1.services.startFlow(SendFlow(node2.info.chooseIdentity())).resultFuture + assertThat(resultFuture.getOrThrow()).isEqualTo("Hello!") + } } } } @@ -169,8 +196,8 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { private class SendFlow(val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): String { - println("SEND FLOW to $otherParty") - println("Party key ${otherParty.owningKey.toBase58String()}") + logger.info("SEND FLOW to $otherParty") + logger.info("Party key ${otherParty.owningKey.toBase58String()}") val session = initiateFlow(otherParty) return session.sendAndReceive("Hi!").unwrap { it } } @@ -180,8 +207,8 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { private class SendBackFlow(val otherSideSession: FlowSession) : FlowLogic() { @Suspendable override fun call() { - println("SEND BACK FLOW to ${otherSideSession.counterparty}") - println("Party key ${otherSideSession.counterparty.owningKey.toBase58String()}") + logger.info("SEND BACK FLOW to ${otherSideSession.counterparty}") + logger.info("Party key ${otherSideSession.counterparty.owningKey.toBase58String()}") otherSideSession.send("Hello!") } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index 0e66989f10..46ac098355 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -21,7 +21,8 @@ import kotlin.test.assertEquals * transaction size limit (which should only consider the hashes). */ class LargeTransactionsTest { - @StartableByRPC @InitiatingFlow + @StartableByRPC + @InitiatingFlow class SendLargeTransactionFlow(private val hash1: SecureHash, private val hash2: SecureHash, private val hash3: SecureHash, @@ -44,7 +45,8 @@ class LargeTransactionsTest { } } - @InitiatedBy(SendLargeTransactionFlow::class) @Suppress("UNUSED") + @InitiatedBy(SendLargeTransactionFlow::class) + @Suppress("UNUSED") class ReceiveLargeTransactionFlow(private val otherSide: FlowSession) : FlowLogic() { @Suspendable override fun call() { diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index af332fcf21..d6aaeb10a7 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -154,7 +154,7 @@ abstract class MQSecurityTest : NodeBasedTest() { } fun loginToRPC(target: NetworkHostAndPort, rpcUser: User): CordaRPCOps { - return CordaRPCClient(target, initialiseSerialization = false).start(rpcUser.username, rpcUser.password).proxy + return CordaRPCClient(target).start(rpcUser.username, rpcUser.password).proxy } fun loginToRPCAndGetClientQueue(): String { diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index 786e5c0af8..5503d5ca19 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -17,9 +17,6 @@ import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode import net.corda.node.services.messaging.* import net.corda.node.services.transactions.RaftValidatingNotaryService -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat @@ -32,8 +29,7 @@ import java.util.concurrent.atomic.AtomicInteger class P2PMessagingTest : NodeBasedTest() { private companion object { - val DISTRIBUTED_SERVICE_NAME = CordaX500Name(RaftValidatingNotaryService.type.id, "DistributedService", "London", "GB") - val SERVICE_2_NAME = CordaX500Name(organisation = "Service 2", locality = "London", country = "GB") + val DISTRIBUTED_SERVICE_NAME = CordaX500Name(RaftValidatingNotaryService.id, "DistributedService", "London", "GB") } @Test @@ -49,46 +45,6 @@ class P2PMessagingTest : NodeBasedTest() { startNodes().getOrThrow(timeout = startUpDuration * 3) } - // https://github.com/corda/corda/issues/71 - @Test - fun `communicating with a service running on the network map node`() { - startNetworkMapNode(advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) - networkMapNode.respondWith("Hello") - val alice = startNode(ALICE.name).getOrThrow() - val serviceAddress = alice.services.networkMapCache.run { - val notaryParty = notaryIdentities.randomOrNull()!! - alice.network.getAddressOfParty(getPartyInfo(notaryParty)!!) - } - val received = alice.receiveFrom(serviceAddress).getOrThrow(10.seconds) - assertThat(received).isEqualTo("Hello") - } - - // TODO Use a dummy distributed service - @Test - fun `communicating with a distributed service which the network map node is part of`() { - ServiceIdentityGenerator.generateToDisk( - listOf(DUMMY_MAP.name, SERVICE_2_NAME).map { baseDirectory(it) }, - DISTRIBUTED_SERVICE_NAME) - - val distributedService = ServiceInfo(RaftValidatingNotaryService.type, DISTRIBUTED_SERVICE_NAME) - val notaryClusterAddress = freeLocalHostAndPort() - startNetworkMapNode( - DUMMY_MAP.name, - advertisedServices = setOf(distributedService), - configOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString())) - val (serviceNode2, alice) = listOf( - startNode( - SERVICE_2_NAME, - advertisedServices = setOf(distributedService), - configOverrides = mapOf( - "notaryNodeAddress" to freeLocalHostAndPort().toString(), - "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()))), - startNode(ALICE.name) - ).transpose().getOrThrow() - - assertAllNodesAreUsed(listOf(networkMapNode, serviceNode2), DISTRIBUTED_SERVICE_NAME, alice) - } - @Ignore @Test fun `communicating with a distributed service which we're part of`() { diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index 8d4b6fab55..317252e531 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -9,6 +9,8 @@ import net.corda.core.internal.cert import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.NetworkMapInfo +import net.corda.node.services.config.ActiveMqServerConfiguration +import net.corda.node.services.config.BridgeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.NetworkMapService @@ -60,6 +62,7 @@ class P2PSecurityTest : NodeBasedTest() { baseDirectory = baseDirectory(legalName), myLegalName = legalName).also { whenever(it.networkMapService).thenReturn(NetworkMapInfo(networkMapNode.internals.configuration.p2pAddress, networkMapNode.info.chooseIdentity().name)) + whenever(it.activeMQServer).thenReturn(ActiveMqServerConfiguration(BridgeConfiguration(1001, 2, 3.4))) } config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name return SimpleNode(config, trustRoot = trustRoot).apply { start() } diff --git a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt index efd07f1a55..0e8f062c9c 100644 --- a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt @@ -18,8 +18,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.node.services.FlowPermissions -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.User import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity @@ -38,8 +36,7 @@ class NodeStatePersistenceTests { val user = User("mark", "dadada", setOf(FlowPermissions.startFlowPermission())) val message = Message("Hello world!") driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified()) { - - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() + startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() var nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.chooseIdentity().name nodeHandle.rpcClientToNode().start(user.username, user.password).use { diff --git a/node/src/integration-test/resources/net/corda/node/services/isolated.jar b/node/src/integration-test/resources/net/corda/node/services/isolated.jar index b1dfc71dac..05544ab868 100644 Binary files a/node/src/integration-test/resources/net/corda/node/services/isolated.jar and b/node/src/integration-test/resources/net/corda/node/services/isolated.jar differ diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index 83e76ae2ba..fa39580fa7 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -24,26 +24,27 @@ public class CordaCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - return (T) augmentClasspath((List) cp, "plugins"); + + (new File("cordapps")).mkdir(); + augmentClasspath((List) cp, "cordapps"); + augmentClasspath((List) cp, "plugins"); + return cp; } return super.attribute(attr); } // TODO: Make directory configurable via the capsule manifest. // TODO: Add working directory variable to capsules string replacement variables. - private List augmentClasspath(List classpath, String dirName) { + private void augmentClasspath(List classpath, String dirName) { File dir = new File(dirName); - if (!dir.exists()) { - dir.mkdir(); - } - - File[] files = dir.listFiles(); - for (File file : files) { - if (file.isFile() && isJAR(file)) { - classpath.add(file.toPath().toAbsolutePath()); + if (dir.exists()) { + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isFile() && isJAR(file)) { + classpath.add(file.toPath().toAbsolutePath()); + } } } - return classpath; } @Override diff --git a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java b/node/src/main/java/net/corda/node/shell/FlowShellCommand.java index 8f16381e01..4bac0ce5b5 100644 --- a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java +++ b/node/src/main/java/net/corda/node/shell/FlowShellCommand.java @@ -12,11 +12,11 @@ import java.util.*; import static net.corda.node.shell.InteractiveShell.*; @Man( - "Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" + - "Starting flow is the primary way in which you command the node to change the ledger.\n\n" + - "This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + - "command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" + - "flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command." + "Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" + + "Starting flow is the primary way in which you command the node to change the ledger.\n\n" + + "This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + + "command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" + + "flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command." ) public class FlowShellCommand extends InteractiveShellCommand { @Command diff --git a/node/src/main/java/net/corda/node/shell/RunShellCommand.java b/node/src/main/java/net/corda/node/shell/RunShellCommand.java index c388ccfe5f..108b567a9b 100644 --- a/node/src/main/java/net/corda/node/shell/RunShellCommand.java +++ b/node/src/main/java/net/corda/node/shell/RunShellCommand.java @@ -13,10 +13,10 @@ import java.util.*; public class RunShellCommand extends InteractiveShellCommand { @Command @Man( - "Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" + + "Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" + - "You can learn more about what commands are available by typing 'run' just by itself, or by\n" + - "consulting the developer guide at https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html" + "You can learn more about what commands are available by typing 'run' just by itself, or by\n" + + "consulting the developer guide at https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html" ) @Usage("runs a method from the CordaRPCOps interface on the node.") public Object main( diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt index 1527beec44..7139055566 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt @@ -34,6 +34,8 @@ class ArgsParser { private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.") private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") private val isVersionArg = optionParser.accepts("version", "Print the version and exit") + private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info", + "Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit") private val helpArg = optionParser.accepts("help").forHelp() fun parse(vararg args: String): CmdLineOptions { @@ -50,7 +52,9 @@ class ArgsParser { val isVersion = optionSet.has(isVersionArg) val noLocalShell = optionSet.has(noLocalShellArg) val sshdServer = optionSet.has(sshdServerArg) - return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration, isVersion, noLocalShell, sshdServer) + val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg) + return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration, isVersion, + noLocalShell, sshdServer, justGenerateNodeInfo) } fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) @@ -64,8 +68,9 @@ data class CmdLineOptions(val baseDirectory: Path, val isRegistration: Boolean, val isVersion: Boolean, val noLocalShell: Boolean, - val sshdServer: Boolean) { + val sshdServer: Boolean, + val justGenerateNodeInfo: Boolean) { fun loadConfig() = ConfigHelper - .loadConfig(baseDirectory, configFile) - .parseAs() + .loadConfig(baseDirectory, configFile) + .parseAs() } 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 0c6b9cec56..16829873aa 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1,13 +1,12 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry -import com.google.common.collect.Lists import com.google.common.collect.MutableClassToInstanceMap import com.google.common.util.concurrent.MoreExecutors import net.corda.confidential.SwapIdentitiesFlow import net.corda.confidential.SwapIdentitiesHandler +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture -import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.* import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -18,28 +17,32 @@ import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.toX509CertHolder -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.RPCOps -import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.internal.uncheckedCast +import net.corda.core.messaging.* +import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug +import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader -import net.corda.node.services.ContractUpgradeHandler import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.internal.cordapp.CordappProviderInternal +import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.api.* +import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver @@ -56,20 +59,13 @@ import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.statemachine.appName -import net.corda.node.services.statemachine.flowVersionAndInitiatingClass +import net.corda.node.services.statemachine.* import net.corda.node.services.transactions.* import net.corda.node.services.upgrade.ContractUpgradeServiceImpl -import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.utilities.* import net.corda.node.utilities.AddOrRemove.ADD -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType -import net.corda.nodeapi.internal.serialization.DefaultWhitelist import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger import rx.Observable @@ -82,11 +78,9 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.sql.Connection import java.time.Clock -import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS -import kotlin.collections.ArrayList import kotlin.collections.set import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair @@ -101,10 +95,17 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair // TODO: Where this node is the initial network map service, currently no networkMapService is provided. // In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the // AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in. -abstract class AbstractNode(open val configuration: NodeConfiguration, - val advertisedServices: Set, +abstract class AbstractNode(config: NodeConfiguration, val platformClock: Clock, + protected val versionInfo: VersionInfo, + protected val cordappLoader: CordappLoader, @VisibleForTesting val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { + open val configuration = config.apply { + require(minimumPlatformVersion <= versionInfo.platformVersion) { + "minimumPlatformVersion cannot be greater than the node's own version" + } + } + private class StartedNodeImpl( override val internals: N, override val services: ServiceHubInternalImpl, @@ -125,7 +126,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract val log: Logger protected abstract val networkMapAddress: SingleMessageRecipient? - protected abstract val platformVersion: Int // We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the // low-performance prototyping period. @@ -147,9 +147,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected lateinit var network: MessagingService protected val runOnStop = ArrayList<() -> Any?>() protected lateinit var database: CordaPersistence - protected var dbCloser: (() -> Any?)? = null - lateinit var cordappProvider: CordappProviderImpl - protected val _nodeReadyFuture = openFuture() /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ @@ -161,8 +158,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, CordaX500Name.build(cert.subjectX500Principal).copy(commonName = null) } - open val pluginRegistries: List by lazy { - cordappProvider.cordapps.flatMap { it.plugins } + DefaultWhitelist() + open val serializationWhitelists: List by lazy { + cordappLoader.cordapps.flatMap { it.serializationWhitelists } } /** Set to non-null once [start] has been successfully called. */ @@ -174,25 +171,45 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return CordaRPCOpsImpl(services, smm, database) } - open fun start(): StartedNode { - require(started == null) { "Node has already been started" } + private fun saveOwnNodeInfo() { + NodeInfoWatcher.saveToFile(configuration.baseDirectory, info, services.keyManagementService) + } + + private fun initCertificate() { if (configuration.devMode) { log.warn("Corda node is running in dev mode.") configuration.configureWithDevSSLCertificate() } validateKeystore() + } + private fun makeSchemaService() = NodeSchemaService(cordappLoader) + open fun generateNodeInfo() { + check(started == null) { "Node has already been started" } + initCertificate() + log.info("Generating nodeInfo ...") + val schemaService = makeSchemaService() + initialiseDatabasePersistence(schemaService) { + makeServices(schemaService) + saveOwnNodeInfo() + } + } + + open fun start(): StartedNode { + check(started == null) { "Node has already been started" } + initCertificate() log.info("Node starting up ...") - + val schemaService = makeSchemaService() // Do all of this in a database transaction so anything that might need a connection has one. - val startedImpl = initialiseDatabasePersistence { - val tokenizableServices = makeServices() - + val startedImpl = initialiseDatabasePersistence(schemaService) { + val tokenizableServices = makeServices(schemaService) + saveOwnNodeInfo() smm = StateMachineManager(services, checkpointStorage, serverThread, database, - busyNodeLatch) + busyNodeLatch, + cordappLoader.appClassLoader) smm.tokenizableServices.addAll(tokenizableServices) @@ -213,8 +230,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, installCordaServices() registerCordappFlows() - _services.rpcFlows += cordappProvider.cordapps.flatMap { it.rpcFlows } - registerCustomSchemas(cordappProvider.cordapps.flatMap { it.customSchemas }.toSet()) + _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } + FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader runOnStop += network::stop StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, inNodeNetworkMapService, network, database, rpcOps) @@ -232,10 +249,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - private class ServiceInstantiationException(cause: Throwable?) : Exception(cause) + private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause) private fun installCordaServices() { - cordappProvider.cordapps.flatMap { it.services }.forEach { + val loadedServices = cordappLoader.cordapps.flatMap { it.services } + filterServicesToInstall(loadedServices).forEach { try { installCordaService(it) } catch (e: NoSuchMethodException) { @@ -249,6 +267,68 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } + private fun filterServicesToInstall(loadedServices: List>): List> { + val customNotaryServiceList = loadedServices.filter { isNotaryService(it) } + if (customNotaryServiceList.isNotEmpty()) { + if (configuration.notary?.custom == true) { + require(customNotaryServiceList.size == 1) { + "Attempting to install more than one notary service: ${customNotaryServiceList.joinToString()}" + } + } + else return loadedServices - customNotaryServiceList + } + return loadedServices + } + + /** + * If the [serviceClass] is a notary service, it will only be enable if the "custom" flag is set in + * the notary configuration. + */ + private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass) + + /** + * This customizes the ServiceHub for each CordaService that is initiating flows + */ + private class AppServiceHubImpl(val serviceHub: ServiceHubInternal) : AppServiceHub, ServiceHub by serviceHub { + lateinit var serviceInstance: T + override fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle { + val stateMachine = startFlowChecked(flow) + return FlowProgressHandleImpl( + id = stateMachine.id, + returnValue = stateMachine.resultFuture, + progress = stateMachine.logic.track()?.updates ?: Observable.empty() + ) + } + + override fun startFlow(flow: FlowLogic): FlowHandle { + val stateMachine = startFlowChecked(flow) + return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture) + } + + private fun startFlowChecked(flow: FlowLogic): FlowStateMachineImpl { + val logicType = flow.javaClass + require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" } + val currentUser = FlowInitiator.Service(serviceInstance.javaClass.name) + return serviceHub.startFlow(flow, currentUser) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AppServiceHubImpl<*>) return false + + if (serviceHub != other.serviceHub) return false + if (serviceInstance != other.serviceInstance) return false + + return true + } + + override fun hashCode(): Int { + var result = serviceHub.hashCode() + result = 31 * result + serviceInstance.hashCode() + return result + } + } + /** * Use this method to install your Corda services in your tests. This is automatically done by the node when it * starts up for all classes it finds which are annotated with [CordaService]. @@ -256,13 +336,22 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, fun installCordaService(serviceClass: Class): T { serviceClass.requireAnnotation() val service = try { - if (NotaryService::class.java.isAssignableFrom(serviceClass)) { + val serviceContext = AppServiceHubImpl(services) + if (isNotaryService(serviceClass)) { check(myNotaryIdentity != null) { "Trying to install a notary service but no notary identity specified" } - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } - constructor.newInstance(services, myNotaryIdentity!!.owningKey) + val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } + serviceContext.serviceInstance = constructor.newInstance(serviceContext, myNotaryIdentity!!.owningKey) + serviceContext.serviceInstance } else { - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true } - constructor.newInstance(services) + try { + val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } + serviceContext.serviceInstance = extendedServiceConstructor.newInstance(serviceContext) + serviceContext.serviceInstance + } catch (ex: NoSuchMethodException) { + val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true } + log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. Upgrade to an AppServiceHub parameter to enable updated API features.") + constructor.newInstance(services) + } } } catch (e: InvocationTargetException) { throw ServiceInstantiationException(e.cause) @@ -283,7 +372,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } private fun registerCordappFlows() { - cordappProvider.cordapps.flatMap { it.initiatedFlows } + cordappLoader.cordapps.flatMap { it.initiatedFlows } .forEach { try { registerInitiatedFlowInternal(it, track = false) @@ -322,11 +411,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } else { log.warn(deprecatedFlowConstructorMessage(initiatedFlow)) } - @Suppress("UNCHECKED_CAST") - { flowSession: FlowSession -> partyCtor.newInstance(flowSession.counterparty) as F } + { flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) } } else { - @Suppress("UNCHECKED_CAST") - { flowSession: FlowSession -> flowSessionCtor.newInstance(flowSession) as F } + { flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) } } val initiatingFlow = initiatedFlow.requireAnnotation().value.java val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass @@ -380,38 +467,26 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(): MutableList { + private fun makeServices(schemaService: SchemaService): MutableList { checkpointStorage = DBCheckpointStorage() - cordappProvider = CordappProviderImpl(makeCordappLoader()) - _services = ServiceHubInternalImpl() - attachments = NodeAttachmentService(services.monitoringService.metrics) - cordappProvider.start(attachments) - legalIdentity = obtainIdentity() + val transactionStorage = makeTransactionStorage() + val metrics = MetricRegistry() + attachments = NodeAttachmentService(metrics) + val cordappProvider = CordappProviderImpl(cordappLoader, attachments) + _services = ServiceHubInternalImpl(schemaService, transactionStorage, StateLoaderImpl(transactionStorage), MonitoringService(metrics), cordappProvider) + legalIdentity = obtainIdentity(notaryConfig = null) network = makeMessagingService(legalIdentity) info = makeInfo(legalIdentity) - - val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.vaultQueryService, + val networkMapCache = services.networkMapCache + val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.schedulerService, - services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, + services.auditService, services.monitoringService, networkMapCache, services.schemaService, services.transactionVerifierService, services.validatedTransactions, services.contractUpgradeService, services, cordappProvider, this) - makeNetworkServices(tokenizableServices) + makeNetworkServices(network, networkMapCache, tokenizableServices) return tokenizableServices } - private fun makeCordappLoader(): CordappLoader { - val scanPackages = System.getProperty("net.corda.node.cordapp.scan.packages") - return if (CordappLoader.testPackages.isNotEmpty()) { - check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createWithTestPackages(CordappLoader.testPackages) - } else if (scanPackages != null) { - check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createWithTestPackages(scanPackages.split(",")) - } else { - CordappLoader.createDefault(configuration.baseDirectory) - } - } - protected open fun makeTransactionStorage(): WritableTransactionStorage = DBTransactionStorage() private fun makeVaultObservers() { @@ -426,23 +501,14 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val allIdentitiesList = mutableListOf(legalIdentity) myNotaryIdentity?.let { allIdentitiesList.add(it) } val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. - return NodeInfo(addresses, allIdentitiesList, platformVersion, platformClock.instant().toEpochMilli()) + return NodeInfo(addresses, allIdentitiesList, versionInfo.platformVersion, platformClock.instant().toEpochMilli()) } /** - * A service entry contains the advertised [ServiceInfo] along with the service identity. The identity *name* is - * taken from the configuration or, if non specified, generated by combining the node's legal name and the service id. - * Used only for notary identities. + * Obtain the node's notary identity if it's configured to be one. If part of a distributed notary then this will be + * the distributed identity shared across all the nodes of the cluster. */ - protected open fun getNotaryIdentity(): PartyAndCertificate? { - return advertisedServices.singleOrNull { it.type.isNotary() }?.let { - it.name?.let { - require(it.commonName != null) {"Common name in '$it' must not be null for notary service, use service type id as common name."} - require(ServiceType.parse(it.commonName!!).isNotary()) {"Common name for notary service in '$it' must be the notary service type id."} - } - obtainIdentity(it) - } - } + protected fun getNotaryIdentity(): PartyAndCertificate? = configuration.notary?.let { obtainIdentity(it) } @VisibleForTesting protected open fun acceptableLiveFiberCountOnStop(): Int = 0 @@ -468,20 +534,17 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } // Specific class so that MockNode can catch it. - class DatabaseConfigurationException(msg: String) : Exception(msg) + class DatabaseConfigurationException(msg: String) : CordaException(msg) - protected open fun initialiseDatabasePersistence(insideTransaction: () -> T): T { + protected open fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T): T { val props = configuration.dataSourceProperties if (props.isNotEmpty()) { - this.database = configureDatabase(props, configuration.database, { _services.schemaService }, createIdentityService = { _services.identityService }) + this.database = configureDatabase(props, configuration.database, { _services.identityService }, schemaService) // Now log the vendor string as this will also cause a connection to be tested eagerly. database.transaction { log.info("Connected to ${database.dataSource.connection.metaData.databaseProductName} database.") } - this.database::close.let { - dbCloser = it - runOnStop += it - } + runOnStop += database::close return database.transaction { insideTransaction() } @@ -490,23 +553,15 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - private fun makeNetworkServices(tokenizableServices: MutableList) { - val serviceTypes = advertisedServices.map { it.type } - inNodeNetworkMapService = if (NetworkMapService.type in serviceTypes) makeNetworkMapService() else NullNetworkMapService - val notaryServiceType = serviceTypes.singleOrNull { it.isNotary() } - if (notaryServiceType != null) { - val service = makeCoreNotaryService(notaryServiceType) - if (service != null) { - service.apply { - tokenizableServices.add(this) - runOnStop += this::stop - start() - } - installCoreFlow(NotaryFlow.Client::class, service::createServiceFlow) - } else { - log.info("Notary type ${notaryServiceType.id} does not match any built-in notary types. " + - "It is expected to be loaded via a CorDapp") - } + private fun makeNetworkServices(network: MessagingService, networkMapCache: NetworkMapCacheInternal, tokenizableServices: MutableList) { + inNodeNetworkMapService = if (configuration.networkMapService == null) makeNetworkMapService(network, networkMapCache) else NullNetworkMapService + configuration.notary?.let { + val notaryService = makeCoreNotaryService(it) + tokenizableServices.add(notaryService) + runOnStop += notaryService::stop + installCoreFlow(NotaryFlow.Client::class, notaryService::createServiceFlow) + log.info("Running core notary: ${notaryService.javaClass.name}") + notaryService.start() } } @@ -534,9 +589,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * updates) if one has been supplied. */ protected open fun registerWithNetworkMap(): CordaFuture { - require(networkMapAddress != null || NetworkMapService.type in advertisedServices.map { it.type }) { - "Initial network map address must indicate a node that provides a network map service" - } val address: SingleMessageRecipient = networkMapAddress ?: network.getAddressOfParty(PartyInfo.SingleNode(services.myInfo.legalIdentitiesAndCerts.first().party, info.addresses)) as SingleMessageRecipient // Register for updates, even if we're the one running the network map. @@ -574,19 +626,35 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return PersistentKeyManagementService(identityService, partyKeys) } - open protected fun makeNetworkMapService(): NetworkMapService { - return PersistentNetworkMapService(services, configuration.minimumPlatformVersion) + abstract protected fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal): NetworkMapService + + private fun makeCoreNotaryService(notaryConfig: NotaryConfig): NotaryService { + val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service") + return if (notaryConfig.validating) { + if (notaryConfig.raft != null) { + RaftValidatingNotaryService(services, notaryKey, notaryConfig.raft) + } else if (notaryConfig.bftSMaRt != null) { + throw IllegalArgumentException("Validating BFTSMaRt notary not supported") + } else { + ValidatingNotaryService(services, notaryKey) + } + } else { + if (notaryConfig.raft != null) { + RaftNonValidatingNotaryService(services, notaryKey, notaryConfig.raft) + } else if (notaryConfig.bftSMaRt != null) { + val cluster = makeBFTCluster(notaryKey, notaryConfig.bftSMaRt) + BFTNonValidatingNotaryService(services, notaryKey, notaryConfig.bftSMaRt, cluster) + } else { + SimpleNotaryService(services, notaryKey) + } + } } - open protected fun makeCoreNotaryService(type: ServiceType): NotaryService? { - check(myNotaryIdentity != null) { "No notary identity initialized when creating a notary service" } - return when (type) { - SimpleNotaryService.type -> SimpleNotaryService(services, myNotaryIdentity!!.owningKey) - ValidatingNotaryService.type -> ValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - RaftValidatingNotaryService.type -> RaftValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - BFTNonValidatingNotaryService.type -> BFTNonValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - else -> null + protected open fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster { + return object : BFTSMaRt.Cluster { + override fun waitUntilAllReplicasHaveInitialized() { + log.warn("A BFT replica may still be initializing, in which case the upcoming consensus change may cause it to spin.") + } } } @@ -629,29 +697,34 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract fun startMessagingService(rpcOps: RPCOps) - private fun obtainIdentity(serviceInfo: ServiceInfo? = null): PartyAndCertificate { - // Load the private identity key, creating it if necessary. The identity key is a long term well known key that - // is distributed to other peers and we use it (or a key signed by it) when we need to do something - // "permissioned". The identity file is what gets distributed and contains the node's legal name along with - // the public key. Obviously in a real system this would need to be a certificate chain of some kind to ensure - // the legal name is actually validated in some way. + private fun obtainIdentity(notaryConfig: NotaryConfig?): PartyAndCertificate { val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) - val (id, name) = if (serviceInfo == null) { - // Create node identity if service info = null + val (id, singleName) = if (notaryConfig == null) { + // Node's main identity Pair("identity", myLegalName) } else { - val name = serviceInfo.name ?: myLegalName.copy(commonName = serviceInfo.type.id) - Pair(serviceInfo.type.id, name) + val notaryId = notaryConfig.run { + NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom) + } + if (notaryConfig.bftSMaRt == null && notaryConfig.raft == null) { + // Node's notary identity + Pair(notaryId, myLegalName.copy(commonName = notaryId)) + } else { + // The node is part of a distributed notary whose identity must already be generated beforehand + Pair(notaryId, null) + } } // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" if (!keyStore.containsAlias(privateKeyAlias)) { + singleName ?: throw IllegalArgumentException( + "Unable to find in the key store the identity of the distributed notary ($id) the node is part of") // TODO: Remove use of [ServiceIdentityGenerator.generateToDisk]. log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") - keyStore.signAndSaveNewKeyPair(name, privateKeyAlias, generateKeyPair()) + keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair()) } val (x509Cert, keys) = keyStore.certificateAndKeyPair(privateKeyAlias) @@ -664,7 +737,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // We have to create the certificate chain for the composite key manually, this is because we don't have a keystore // provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate + // the tail of the private key certificates, as they are both signed by the same certificate chain. - Lists.asList(certificate, keyStore.getCertificateChain(privateKeyAlias).drop(1).toTypedArray()) + listOf(certificate) + keyStore.getCertificateChain(privateKeyAlias).drop(1) } else { keyStore.getCertificateChain(privateKeyAlias).let { check(it[0].toX509CertHolder() == x509Cert) { "Certificates from key store do not line up!" } @@ -674,8 +747,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val nodeCert = certificates[0] as? X509Certificate ?: throw ConfigurationException("Node certificate must be an X.509 certificate") val subject = CordaX500Name.build(nodeCert.subjectX500Principal) - if (subject != name) - throw ConfigurationException("The name '$name' for $id doesn't match what's in the key store: $subject") + // TODO Include the name of the distributed notary, which the node is part of, in the notary config so that we + // can cross-check the identity we get from the key store + if (singleName != null && subject != singleName) { + throw ConfigurationException("The name '$singleName' for $id doesn't match what's in the key store: $subject") + } partyKeys += keys return PartyAndCertificate(CertificateFactory.getInstance("X509").generateCertPath(certificates)) @@ -683,21 +759,21 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun generateKeyPair() = cryptoGenerateKeyPair() - private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() { - + private inner class ServiceHubInternalImpl( + override val schemaService: SchemaService, + override val validatedTransactions: WritableTransactionStorage, + private val stateLoader: StateLoader, + override val monitoringService: MonitoringService, + override val cordappProvider: CordappProviderInternal + ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by stateLoader { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() - override val monitoringService = MonitoringService(MetricRegistry()) - override val validatedTransactions = makeTransactionStorage() override val transactionVerifierService by lazy { makeTransactionVerifierService() } - override val schemaService by lazy { NodeSchemaService() } override val networkMapCache by lazy { PersistentNetworkMapCache(this) } - override val vaultService by lazy { NodeVaultService(this) } + override val vaultService by lazy { NodeVaultService(platformClock, keyManagementService, stateLoader, this@AbstractNode.database.hibernateConfig) } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } - override val vaultQueryService by lazy { - HibernateVaultQueryImpl(database.hibernateConfig, vaultService) - } + // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. @@ -717,8 +793,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val myInfo: NodeInfo get() = info override val database: CordaPersistence get() = this@AbstractNode.database override val configuration: NodeConfiguration get() = this@AbstractNode.configuration - override val cordappProvider: CordappProvider = this@AbstractNode.cordappProvider - override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") @@ -740,9 +814,4 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override fun jdbcSession(): Connection = database.createSession() } - - fun registerCustomSchemas(schemas: Set) { - database.hibernateConfig.schemaService.registerCustomSchemas(schemas) - } - } 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 ae8afce323..ee685466d3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -56,7 +56,7 @@ class CordaRPCOpsImpl( sorting: Sort, contractStateType: Class): Vault.Page { return database.transaction { - services.vaultQueryService._queryBy(criteria, paging, sorting, contractStateType) + services.vaultService._queryBy(criteria, paging, sorting, contractStateType) } } @@ -66,7 +66,7 @@ class CordaRPCOpsImpl( sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { return database.transaction { - services.vaultQueryService._trackBy(criteria, paging, sorting, contractStateType) + services.vaultService._trackBy(criteria, paging, sorting, contractStateType) } } @@ -176,11 +176,7 @@ class CordaRPCOpsImpl( override fun currentNodeTime(): Instant = Instant.now(services.clock) - override fun waitUntilNetworkReady(): CordaFuture { - return database.transaction { - services.networkMapCache.nodeReady - } - } + override fun waitUntilNetworkReady(): CordaFuture = services.networkMapCache.nodeReady override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? { return database.transaction { @@ -200,6 +196,8 @@ class CordaRPCOpsImpl( } } + override fun notaryPartyFromX500Name(x500Name: CordaX500Name): Party? = services.networkMapCache.getNotary(x500Name) + override fun partiesFromName(query: String, exactMatch: Boolean): Set { return database.transaction { services.identityService.partiesFromName(query, exactMatch) diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index ed34bb5a3a..359a80ecd9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -1,6 +1,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate @@ -14,17 +15,22 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializationDefaults import net.corda.core.utilities.* import net.corda.node.VersionInfo +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.api.SchemaService import net.corda.node.services.config.FullNodeConfiguration +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectRequestProperty import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectResponseProperty import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.NodeMessagingClient +import net.corda.node.services.network.NetworkMapService +import net.corda.node.services.network.PersistentNetworkMapService import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.TestClock @@ -46,6 +52,7 @@ import org.slf4j.LoggerFactory import java.io.IOException import java.time.Clock import java.util.* +import java.util.concurrent.atomic.AtomicInteger import javax.management.ObjectName import kotlin.system.exitProcess @@ -54,14 +61,12 @@ import kotlin.system.exitProcess * loads important data off disk and starts listening for connections. * * @param configuration This is typically loaded from a TypeSafe HOCON configuration file. - * @param advertisedServices The services this node advertises. This must be a subset of the services it runs, - * but nodes are not required to advertise services they run (hence subset). */ -open class Node(override val configuration: FullNodeConfiguration, - advertisedServices: Set, - private val versionInfo: VersionInfo, - val initialiseSerialization: Boolean = true -) : AbstractNode(configuration, advertisedServices, createClock(configuration)) { +open class Node(configuration: FullNodeConfiguration, + versionInfo: VersionInfo, + val initialiseSerialization: Boolean = true, + cordappLoader: CordappLoader = makeCordappLoader(configuration) +) : AbstractNode(configuration, createClock(configuration), versionInfo, cordappLoader) { companion object { private val logger = loggerFor() var renderBasicInfoToConsole = true @@ -82,13 +87,24 @@ open class Node(override val configuration: FullNodeConfiguration, private fun createClock(configuration: FullNodeConfiguration): Clock { return if (configuration.useTestClock) TestClock() else NodeClock() } + + private val sameVmNodeCounter = AtomicInteger() + val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages" + val scanPackagesSeparator = "," + private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader { + return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages -> + CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator)) + } ?: CordappLoader.createDefault(configuration.baseDirectory) + } } override val log: Logger get() = logger - override val platformVersion: Int get() = versionInfo.platformVersion + override val configuration get() = super.configuration as FullNodeConfiguration // Necessary to avoid init order NPE. override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress) override fun makeTransactionVerifierService() = (network as NodeMessagingClient).verifierService + private val sameVmNodeNumber = sameVmNodeCounter.incrementAndGet() // Under normal (non-test execution) it will always be "1" + // DISCUSSION // // We use a single server thread for now, which means all message handling is serialized. @@ -126,7 +142,7 @@ open class Node(override val configuration: FullNodeConfiguration, // // The primary work done by the server thread is execution of flow logics, and related // serialisation/deserialisation work. - override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1) + override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread-$sameVmNodeNumber", 1) private var messageBroker: ArtemisMessagingServer? = null @@ -274,6 +290,10 @@ open class Node(override val configuration: FullNodeConfiguration, return listOf(address.hostAndPort) } + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal): NetworkMapService { + return PersistentNetworkMapService(network, networkMapCache, configuration.minimumPlatformVersion) + } + /** * If the node is persisting to an embedded H2 database, then expose this via TCP with a JDBC URL of the form: * jdbc:h2:tcp://:/node @@ -284,7 +304,7 @@ open class Node(override val configuration: FullNodeConfiguration, * This is not using the H2 "automatic mixed mode" directly but leans on many of the underpinnings. For more details * on H2 URLs and configuration see: http://www.h2database.com/html/features.html#database_url */ - override fun initialiseDatabasePersistence(insideTransaction: () -> T): T { + override fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T): T { val databaseUrl = configuration.dataSourceProperties.getProperty("dataSource.url") val h2Prefix = "jdbc:h2:file:" if (databaseUrl != null && databaseUrl.startsWith(h2Prefix)) { @@ -301,12 +321,17 @@ open class Node(override val configuration: FullNodeConfiguration, printBasicNodeInfo("Database connection url is", "jdbc:h2:$url/node") } } - return super.initialiseDatabasePersistence(insideTransaction) + return super.initialiseDatabasePersistence(schemaService, insideTransaction) } private val _startupComplete = openFuture() val startupComplete: CordaFuture get() = _startupComplete + override fun generateNodeInfo() { + initialiseSerialization() + super.generateNodeInfo() + } + override fun start(): StartedNode { if (initialiseSerialization) { initialiseSerialization() @@ -335,7 +360,7 @@ open class Node(override val configuration: FullNodeConfiguration, _startupComplete.set(Unit) } }, - { th -> logger.error("Unexpected exception", th)} + { th -> logger.error("Unexpected exception", th) } ) shutdownHook = addShutdownHook { stop() @@ -344,14 +369,15 @@ open class Node(override val configuration: FullNodeConfiguration, } private fun initialiseSerialization() { + val classloader = cordappLoader.appClassLoader SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) registerScheme(AMQPServerSerializationScheme()) } - SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT - SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT - SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT - SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT + SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT.withClassLoader(classloader) + SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader) + SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT.withClassLoader(classloader) + SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader) } /** Starts a blocking event loop for message dispatch. */ @@ -381,6 +407,6 @@ open class Node(override val configuration: FullNodeConfiguration, } } -class ConfigurationException(message: String) : Exception(message) +class ConfigurationException(message: String) : CordaException(message) data class NetworkMapInfo(val address: NetworkHostAndPort, val legalName: CordaX500Name) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index b6bbbde83d..3bb63727da 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -9,7 +9,6 @@ import net.corda.core.internal.* import net.corda.core.internal.concurrent.thenMatch import net.corda.core.utilities.loggerFor import net.corda.node.* -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.RelayConfiguration import net.corda.node.services.transactions.bftSMaRtSerialFilter @@ -77,7 +76,7 @@ open class NodeStartup(val args: Array) { cmdlineOptions.baseDirectory.createDirectories() startNode(conf, versionInfo, startTime, cmdlineOptions) } catch (e: Exception) { - if (e.message?.startsWith("Unknown named curve:") ?: false) { + if (e.message?.startsWith("Unknown named curve:") == true) { logger.error("Exception during node startup - ${e.message}. " + "This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.") } else @@ -90,33 +89,36 @@ open class NodeStartup(val args: Array) { open protected fun preNetworkRegistration(conf: FullNodeConfiguration) = Unit - open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, services: Set): Node { - return Node(conf, services, versionInfo) - } + open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) open protected fun startNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) { - val advertisedServices = conf.calculateServices() - val node = createNode(conf, versionInfo, advertisedServices).start() - printPluginsAndServices(node.internals) - node.internals.nodeReadyFuture.thenMatch({ + val node = createNode(conf, versionInfo) + if (cmdlineOptions.justGenerateNodeInfo) { + // Perform the minimum required start-up logic to be able to write a nodeInfo to disk + node.generateNodeInfo() + return + } + val startedNode = node.start() + Node.printBasicNodeInfo("Loaded CorDapps", startedNode.services.cordappProvider.cordapps.joinToString { it.name }) + startedNode.internals.nodeReadyFuture.thenMatch({ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 - val name = node.info.legalIdentitiesAndCerts.first().name.organisation + val name = startedNode.info.legalIdentitiesAndCerts.first().name.organisation Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") // Don't start the shell if there's no console attached. val runShell = !cmdlineOptions.noLocalShell && System.console() != null - node.internals.startupComplete.then { + startedNode.internals.startupComplete.then { try { - InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node) - } catch(e: Throwable) { + InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, startedNode) + } catch (e: Throwable) { logger.error("Shell failed to start", e) } } }, - { - th -> logger.error("Unexpected exception during registration", th) - }) - node.internals.run() + { th -> + logger.error("Unexpected exception during registration", th) + }) + startedNode.internals.run() } open protected fun logStartupInfo(versionInfo: VersionInfo, cmdlineOptions: CmdLineOptions, conf: FullNodeConfiguration) { @@ -154,30 +156,28 @@ open class NodeStartup(val args: Array) { } open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): FullNodeConfiguration { - val conf = try { - cmdlineOptions.loadConfig() + try { + return cmdlineOptions.loadConfig() } catch (e: ConfigException) { println("Unable to load the configuration file: ${e.rootCause.message}") exitProcess(2) } - return conf } open protected fun banJavaSerialisation(conf: FullNodeConfiguration) { - SerialFilter.install(if (conf.bftSMaRt.isValid()) ::bftSMaRtSerialFilter else ::defaultSerialFilter) + SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) } open protected fun getVersionInfo(): VersionInfo { // Manifest properties are only available if running from the corda jar fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null - val versionInfo = VersionInfo( + return VersionInfo( manifestValue("Corda-Platform-Version")?.toInt() ?: 1, manifestValue("Corda-Release-Version") ?: "Unknown", manifestValue("Corda-Revision") ?: "Unknown", manifestValue("Corda-Vendor") ?: "Unknown" ) - return versionInfo } private fun enforceSingleNodeIsRunning(baseDirectory: Path) { @@ -263,18 +263,6 @@ open class NodeStartup(val args: Array) { } } - private fun printPluginsAndServices(node: Node) { - node.configuration.extraAdvertisedServiceIds.let { - if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing network services", it.joinToString()) - } - val plugins = node.pluginRegistries - .map { it.javaClass.name } - .filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") || it.startsWith("net.corda.nodeapi.") } - .map { it.substringBefore('$') } - if (plugins.isNotEmpty()) - Node.printBasicNodeInfo("Loaded plugins", plugins.joinToString()) - } - open fun drawBanner(versionInfo: VersionInfo) { // This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box. AnsiConsole.systemInstall() diff --git a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt index 03ad5b5a23..e0320d9c62 100644 --- a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt @@ -1,8 +1,13 @@ package net.corda.node.internal +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState import net.corda.core.flows.FlowLogic import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo +import net.corda.core.node.StateLoader +import net.corda.core.node.services.TransactionStorage import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.MessagingService @@ -25,3 +30,11 @@ interface StartedNode { fun dispose() = internals.stop() fun > registerInitiatedFlow(initiatedFlowClass: Class) = internals.registerInitiatedFlow(initiatedFlowClass) } + +class StateLoaderImpl(private val validatedTransactions: TransactionStorage) : StateLoader { + @Throws(TransactionResolutionException::class) + override fun loadState(stateRef: StateRef): TransactionState<*> { + val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) + return stx.resolveBaseTransaction(this).outputs[stateRef.index] + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index f29fe98ed4..5154c592b1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -5,18 +5,17 @@ import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.contracts.Contract import net.corda.core.contracts.UpgradedContract import net.corda.core.cordapp.Cordapp -import net.corda.core.flows.ContractUpgradeFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.* import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.internal.serialization.DefaultWhitelist import java.io.File import java.io.FileOutputStream import java.lang.reflect.Modifier @@ -39,34 +38,58 @@ import kotlin.streams.toList * * @property cordappJarPaths The classpath of cordapp JARs */ -class CordappLoader private constructor(private val cordappJarPaths: List) { - val cordapps: List by lazy { loadCordapps() } +class CordappLoader private constructor(private val cordappJarPaths: List) { + val cordapps: List by lazy { loadCordapps() + coreCordapp } - @VisibleForTesting - internal val appClassLoader: ClassLoader = javaClass.classLoader + internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader) + + init { + if (cordappJarPaths.isEmpty()) { + logger.info("No CorDapp paths provided") + } else { + logger.info("Loading CorDapps from ${cordappJarPaths.joinToString()}") + } + } companion object { private val logger = loggerFor() /** - * Creates a default CordappLoader intended to be used in non-dev or non-test environments. - * - * @param baseDir The directory that this node is running in. Will use this to resolve the plugins directory - * for classpath scanning. + * Default cordapp dir name */ - fun createDefault(baseDir: Path): CordappLoader { - val pluginsDir = getPluginsPath(baseDir) - return CordappLoader(if (!pluginsDir.exists()) emptyList() else pluginsDir.list { - it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() - }) - } - - fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins" + val CORDAPPS_DIR_NAME = "cordapps" /** - * Create a dev mode CordappLoader for test environments + * Creates a default CordappLoader intended to be used in non-dev or non-test environments. + * + * @param baseDir The directory that this node is running in. Will use this to resolve the cordapps directory + * for classpath scanning. */ - fun createWithTestPackages(testPackages: List = CordappLoader.testPackages) = CordappLoader(testPackages.flatMap(this::createScanPackage)) + fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getCordappsPath(baseDir))) + + /** + * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath + * and cordapps directory. This is intended mostly for use by the driver. + * + * @param baseDir See [createDefault.baseDir] + * @param testPackages See [createWithTestPackages.testPackages] + */ + @VisibleForTesting + fun createDefaultWithTestPackages(configuration: NodeConfiguration, testPackages: List): CordappLoader { + check(configuration.devMode) { "Package scanning can only occur in dev mode" } + return CordappLoader(getCordappsInDirectory(getCordappsPath(configuration.baseDirectory)) + testPackages.flatMap(this::createScanPackage)) + } + + /** + * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath. + * This is intended for use in unit and integration tests. + * + * @param testPackages List of package names that contain CorDapp classes that can be automatically turned into + * CorDapps. + */ + @VisibleForTesting + fun createWithTestPackages(testPackages: List) + = CordappLoader(testPackages.flatMap(this::createScanPackage)) /** * Creates a dev mode CordappLoader intended only to be used in test environments @@ -74,25 +97,29 @@ class CordappLoader private constructor(private val cordappJarPaths: List) * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection */ @VisibleForTesting - fun createDevMode(scanJars: List) = CordappLoader(scanJars) + fun createDevMode(scanJars: List) = CordappLoader(scanJars.map { RestrictedURL(it, null) }) - private fun createScanPackage(scanPackage: String): List { + private fun getCordappsPath(baseDir: Path): Path = baseDir / CORDAPPS_DIR_NAME + + private fun createScanPackage(scanPackage: String): List { val resource = scanPackage.replace('.', '/') return this::class.java.classLoader.getResources(resource) .asSequence() .map { path -> if (path.protocol == "jar") { - (path.openConnection() as JarURLConnection).jarFileURL.toURI() + // When running tests from gradle this may be a corda module jar, so restrict to scanPackage: + RestrictedURL((path.openConnection() as JarURLConnection).jarFileURL, scanPackage) } else { - createDevCordappJar(scanPackage, path, resource) - }.toURL() + // No need to restrict as createDevCordappJar has already done that: + RestrictedURL(createDevCordappJar(scanPackage, path, resource).toURL(), null) + } } .toList() } - /** Takes a package of classes and creates a JAR from them - only use in tests */ + /** Takes a package of classes and creates a JAR from them - only use in tests. */ private fun createDevCordappJar(scanPackage: String, path: URL, jarPackageName: String): URI { - if(!generatedCordapps.contains(path)) { + if (!generatedCordapps.contains(path)) { val cordappDir = File("build/tmp/generated-test-cordapps") cordappDir.mkdirs() val cordappJAR = File(cordappDir, "$scanPackage-${UUID.randomUUID()}.jar") @@ -118,12 +145,37 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return generatedCordapps[path]!! } - /** - * A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only - */ - @VisibleForTesting - var testPackages: List = emptyList() + private fun getCordappsInDirectory(cordappsDir: Path): List { + return if (!cordappsDir.exists()) { + emptyList() + } else { + cordappsDir.list { + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList() + } + } + } + private val generatedCordapps = mutableMapOf() + + /** A list of the core RPC flows present in Corda */ + private val coreRPCFlows = listOf( + ContractUpgradeFlow.Initiate::class.java, + ContractUpgradeFlow.Authorise::class.java, + ContractUpgradeFlow.Deauthorise::class.java) + + /** A Cordapp representing the core package which is not scanned automatically. */ + @VisibleForTesting + internal val coreCordapp = CordappImpl( + listOf(), + listOf(), + coreRPCFlows, + listOf(), + listOf(), + listOf(), + listOf(), + setOf(), + ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location // Core JAR location + ) } private fun loadCordapps(): List { @@ -132,18 +184,20 @@ class CordappLoader private constructor(private val cordappJarPaths: List) CordappImpl(findContractClassNames(scanResult), findInitiatedFlows(scanResult), findRPCFlows(scanResult), + findServiceFlows(scanResult), + findSchedulableFlows(scanResult), findServices(scanResult), findPlugins(it), findCustomSchemas(scanResult), - it) + it.url) } } - private fun findServices(scanResult: ScanResult): List> { + private fun findServices(scanResult: RestrictedScanResult): List> { return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) } - private fun findInitiatedFlows(scanResult: ScanResult): List>> { + private fun findInitiatedFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) // First group by the initiating flow class in case there are multiple mappings .groupBy { it.requireAnnotation().value.java } @@ -158,37 +212,39 @@ class CordappLoader private constructor(private val cordappJarPaths: List) } } - private fun findRPCFlows(scanResult: ScanResult): List>> { - fun Class>.isUserInvokable(): Boolean { - return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) - } - - val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } - val coreFlows = listOf( - ContractUpgradeFlow.Initiate::class.java, - ContractUpgradeFlow.Authorise::class.java, - ContractUpgradeFlow.Deauthorise::class.java - ) - return found + coreFlows + private fun Class>.isUserInvokable(): Boolean { + return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) } - private fun findContractClassNames(scanResult: ScanResult): List { - return (scanResult.getNamesOfClassesImplementing(Contract::class.java) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class.java)).distinct() + private fun findRPCFlows(scanResult: RestrictedScanResult): List>> { + return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } } - private fun findPlugins(cordappJarPath: URL): List { - return ServiceLoader.load(CordaPluginRegistry::class.java, URLClassLoader(arrayOf(cordappJarPath), appClassLoader)).toList().filter { - cordappJarPath == it.javaClass.protectionDomain.codeSource.location - } + private fun findServiceFlows(scanResult: RestrictedScanResult): List>> { + return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByService::class) } - private fun findCustomSchemas(scanResult: ScanResult): Set { + private fun findSchedulableFlows(scanResult: RestrictedScanResult): List>> { + return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class) + } + + private fun findContractClassNames(scanResult: RestrictedScanResult): List { + return (scanResult.getNamesOfClassesImplementing(Contract::class) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class)).distinct() + } + + private fun findPlugins(cordappJarPath: RestrictedURL): List { + return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath.url), appClassLoader)).toList().filter { + it.javaClass.protectionDomain.codeSource.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) + } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. + } + + private fun findCustomSchemas(scanResult: RestrictedScanResult): Set { return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet() } - private fun scanCordapp(cordappJarPath: URL): ScanResult { + private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult { logger.info("Scanning CorDapp in $cordappJarPath") - return FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath).scan() + return RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix) } private class FlowTypeHierarchyComparator(val initiatingFlow: Class>) : Comparator>> { @@ -215,16 +271,30 @@ class CordappLoader private constructor(private val cordappJarPaths: List) } } - private fun ScanResult.getClassesWithSuperclass(type: KClass): List { - return getNamesOfSubclassesOf(type.java) - .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } - .map { it.kotlin.objectOrNewInstance() } + /** @param rootPackageName only this package and subpackages may be extracted from [url], or null to allow all packages. */ + private class RestrictedURL(val url: URL, rootPackageName: String?) { + val qualifiedNamePrefix = rootPackageName?.let { it + '.' } ?: "" } - private fun ScanResult.getClassesWithAnnotation(type: KClass, annotation: KClass): List> { - return getNamesOfClassesWithAnnotation(annotation.java) - .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } + private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) { + fun getNamesOfClassesImplementing(type: KClass<*>): List { + return scanResult.getNamesOfClassesImplementing(type.java) + .filter { it.startsWith(qualifiedNamePrefix) } + } + + fun getClassesWithSuperclass(type: KClass): List { + return scanResult.getNamesOfSubclassesOf(type.java) + .filter { it.startsWith(qualifiedNamePrefix) } + .mapNotNull { loadClass(it, type) } + .filterNot { Modifier.isAbstract(it.modifiers) } + .map { it.kotlin.objectOrNewInstance() } + } + + fun getClassesWithAnnotation(type: KClass, annotation: KClass): List> { + return scanResult.getNamesOfClassesWithAnnotation(annotation.java) + .filter { it.startsWith(qualifiedNamePrefix) } + .mapNotNull { loadClass(it, type) } + .filterNot { Modifier.isAbstract(it.modifiers) } + } } } 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 3eb485787f..ba305283f2 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 @@ -6,20 +6,19 @@ import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext -import net.corda.core.cordapp.CordappProvider import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.SingletonSerializeAsToken -import java.net.URLClassLoader +import java.net.URL /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. */ -open class CordappProviderImpl(private val cordappLoader: CordappLoader) : SingletonSerializeAsToken(), CordappProvider { +open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { override fun getAppContext(): CordappContext { // TODO: Use better supported APIs in Java 9 Exception().stackTrace.forEach { stackFrame -> val cordapp = getCordappForClass(stackFrame.className) - if(cordapp != null) { + if (cordapp != null) { return getAppContext(cordapp) } } @@ -34,28 +33,19 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl /** * Current known CorDapps loaded on this node */ - val cordapps get() = cordappLoader.cordapps - private lateinit var cordappAttachments: HashBiMap - - /** - * Should only be called once from the initialisation routine of the node or tests - */ - fun start(attachmentStorage: AttachmentStorage): CordappProviderImpl { - cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage)) - return this - } - + 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 * * @param cordapp The cordapp to get the attachment ID * @return An attachment ID if it exists, otherwise nothing */ - fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp) + fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp.jarPath) - private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { - val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() } - val attachmentIds = cordappsWithAttachments.map { it.jarPath.openStream().use { attachmentStorage.importAttachment(it) } } + private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { + val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath } + val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importAttachment(it) } } return attachmentIds.zip(cordappsWithAttachments).toMap() } @@ -66,7 +56,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl * @return A cordapp context for the given CorDapp */ fun getAppContext(cordapp: Cordapp): CordappContext { - return CordappContext(cordapp, getCordappAttachmentId(cordapp), URLClassLoader(arrayOf(cordapp.jarPath), cordappLoader.appClassLoader)) + return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader) } /** diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt new file mode 100644 index 0000000000..a29d8bab25 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt @@ -0,0 +1,8 @@ +package net.corda.node.internal.cordapp + +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappProvider + +interface CordappProviderInternal : CordappProvider { + val cordapps: List +} diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index e3819b661d..16d240eecc 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -2,7 +2,6 @@ package net.corda.node.services import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.ContractState -import net.corda.core.contracts.UpgradeCommand import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.requireThat import net.corda.core.flows.* @@ -64,9 +63,6 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF "The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade) "The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx) } - ContractUpgradeFlow.verify( - oldStateAndRef.state, - expectedTx.outRef(0).state, - expectedTx.toLedgerTransaction(serviceHub).commandsOfType().single()) + proposedTx.toLedgerTransaction(serviceHub).verify() } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt index f14f0af01a..42b12629c4 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt @@ -10,10 +10,7 @@ import javax.annotation.concurrent.ThreadSafe * Abstract superclass for services that a node can host, which provides helper functions. */ @ThreadSafe -abstract class AbstractNodeService(val services: ServiceHubInternal) : SingletonSerializeAsToken() { - - val network: MessagingService get() = services.networkService - +abstract class AbstractNodeService(val network: MessagingService) : SingletonSerializeAsToken() { /** * Register a handler for a message topic. In comparison to using net.addMessageHandler() this manages a lot of * common boilerplate code. Exceptions are caught and passed to the provided consumer. If you just want a simple @@ -36,7 +33,7 @@ abstract class AbstractNodeService(val services: ServiceHubInternal) : Singleton val msg = network.createMessage(topic, request.sessionID, response.serialize().bytes) network.send(msg, request.replyTo) } - } catch(e: Exception) { + } catch (e: Exception) { exceptionConsumer(message, e) } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt b/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt index b9f53f9831..127f69e3d6 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt @@ -118,6 +118,7 @@ data class FlowPermissionAuditEvent(override val timestamp: Instant, override val flowId: StateMachineRunId, val permissionRequested: String, val permissionGranted: Boolean) : AuditEvent(), FlowAuditInfo + /** * Minimal interface for recording audit information within the system. The AuditService is assumed to be available only * to trusted internal components via ServiceHubInternal. diff --git a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt index 7428a2e90a..4a4d815708 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt @@ -30,11 +30,5 @@ interface SchemaService { * or via custom logic in this service. */ fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState - - /** - * Registration mechanism to add custom contract schemas that extend the [MappedSchema] class. - */ - fun registerCustomSchemas(customSchemas: Set) - } //DOCEND SchemaService diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 1e2625ffb4..5fdb7993a0 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -1,5 +1,6 @@ package net.corda.node.services.api +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator @@ -8,6 +9,7 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.StateMachineTransactionMapping @@ -19,11 +21,11 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence interface NetworkMapCacheInternal : NetworkMapCache { @@ -61,9 +63,9 @@ interface NetworkMapCacheInternal : NetworkMapCache { } @CordaSerializable -sealed class NetworkCacheError : Exception() { +sealed class NetworkCacheException : CordaException("Network Cache Error") { /** Indicates a failure to deregister, because of a rejected request from the remote node */ - class DeregistrationFailed : NetworkCacheError() + class DeregistrationFailed : NetworkCacheException() } interface ServiceHubInternal : ServiceHub { @@ -71,6 +73,7 @@ interface ServiceHubInternal : ServiceHub { private val log = loggerFor() } + override val vaultService: VaultServiceInternal /** * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. * The signatures aren't technically needed after that point, but we keep them around so that we can relay @@ -87,7 +90,7 @@ interface ServiceHubInternal : ServiceHub { val networkService: MessagingService val database: CordaPersistence val configuration: NodeConfiguration - + override val cordappProvider: CordappProviderInternal override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { require(txs.any()) { "No transactions passed in for recording" } val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } @@ -102,7 +105,7 @@ interface ServiceHubInternal : ServiceHub { if (notifyVault) { val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } - (vaultService as NodeVaultService).notifyAll(toNotify) + vaultService.notifyAll(toNotify) } } @@ -132,8 +135,7 @@ interface ServiceHubInternal : ServiceHub { flowInitiator: FlowInitiator, vararg args: Any?): FlowStateMachineImpl { val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args) - @Suppress("UNCHECKED_CAST") - val logic = FlowLogicRefFactoryImpl.toFlowLogic(logicRef) as FlowLogic + val logic: FlowLogic = uncheckedCast(FlowLogicRefFactoryImpl.toFlowLogic(logicRef)) return startFlow(logic, flowInitiator, ourIdentity = null) } diff --git a/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt new file mode 100644 index 0000000000..fde38dfe05 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt @@ -0,0 +1,19 @@ +package net.corda.node.services.api + +import net.corda.core.node.services.VaultService +import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.WireTransaction + +interface VaultServiceInternal : VaultService { + /** + * Splits the provided [txns] into batches of [WireTransaction] and [NotaryChangeWireTransaction]. + * This is required because the batches get aggregated into single updates, and we want to be able to + * indicate whether an update consists entirely of regular or notary change transactions, which may require + * different processing logic. + */ + fun notifyAll(txns: Iterable) + + /** Same as notifyAll but with a single transaction. */ + fun notify(tx: CoreTransaction) = notifyAll(listOf(tx)) +} 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 9dfcc92694..7e52c0361c 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 @@ -2,26 +2,23 @@ package net.corda.node.services.config import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.seconds import net.corda.node.internal.NetworkMapInfo -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.messaging.CertificateChainCheckPolicy -import net.corda.node.services.network.NetworkMapService import net.corda.nodeapi.User import net.corda.nodeapi.config.NodeSSLConfiguration -import net.corda.nodeapi.config.OldConfig import java.net.URL import java.nio.file.Path import java.util.* -/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ -data class BFTSMaRtConfiguration(val replicaId: Int, val debug: Boolean, val exposeRaces: Boolean = false) { - fun isValid() = replicaId >= 0 -} - interface NodeConfiguration : NodeSSLConfiguration { // myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this. // TODO: Remove this so we don't accidentally use this identity in the code? val myLegalName: CordaX500Name + /** + * If null then configure the node to run as the netwok map service, otherwise use this to connect to the network map + * service. + */ val networkMapService: NetworkMapInfo? val minimumPlatformVersion: Int val emailAddress: String @@ -34,11 +31,42 @@ interface NodeConfiguration : NodeSSLConfiguration { val certificateChainCheckPolicies: List val verifierType: VerifierType val messageRedeliveryDelaySeconds: Int - val bftSMaRt: BFTSMaRtConfiguration - val notaryNodeAddress: NetworkHostAndPort? - val notaryClusterAddresses: List + val notary: NotaryConfig? + val activeMQServer: ActiveMqServerConfiguration + val additionalNodeInfoPollingFrequencyMsec: Long } +data class NotaryConfig(val validating: Boolean, + val raft: RaftConfig? = null, + val bftSMaRt: BFTSMaRtConfiguration? = null, + val custom: Boolean = false +) { + init { + require(raft == null || bftSMaRt == null || !custom) { + "raft, bftSMaRt, and custom configs cannot be specified together" + } + } +} + +data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List) + +/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ +data class BFTSMaRtConfiguration constructor(val replicaId: Int, + val clusterAddresses: List, + val debug: Boolean = false, + val exposeRaces: Boolean = false +) { + init { + require(replicaId >= 0) { "replicaId cannot be negative" } + } +} + +data class BridgeConfiguration(val retryIntervalMs: Long, + val maxRetryIntervalMin: Long, + val retryIntervalMultiplier: Double) + +data class ActiveMqServerConfiguration(val bridge: BridgeConfiguration) + data class FullNodeConfiguration( /** This is not retrieved from the config file but rather from a command line argument. */ override val baseDirectory: Path, @@ -55,21 +83,19 @@ data class FullNodeConfiguration( override val verifierType: VerifierType, override val messageRedeliveryDelaySeconds: Int = 30, val useHTTPS: Boolean, - @OldConfig("artemisAddress") val p2pAddress: NetworkHostAndPort, val rpcAddress: NetworkHostAndPort?, val relay: RelayConfiguration?, // TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker. // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one val messagingServerAddress: NetworkHostAndPort?, - val extraAdvertisedServiceIds: List, - override val bftSMaRt: BFTSMaRtConfiguration, - override val notaryNodeAddress: NetworkHostAndPort?, - override val notaryClusterAddresses: List, + override val notary: NotaryConfig?, override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, val useTestClock: Boolean = false, - val detectPublicIp: Boolean = true + val detectPublicIp: Boolean = true, + override val activeMQServer: ActiveMqServerConfiguration, + override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis() ) : NodeConfiguration { override val exportJMXto: String get() = "http" @@ -81,15 +107,7 @@ data class FullNodeConfiguration( require(it.username.matches("\\w+".toRegex())) { "Username ${it.username} contains invalid characters" } } require(myLegalName.commonName == null) { "Common name must be null: $myLegalName" } - } - - fun calculateServices(): Set { - val advertisedServices = extraAdvertisedServiceIds - .filter(String::isNotBlank) - .map { ServiceInfo.parse(it) } - .toMutableSet() - if (networkMapService == null) advertisedServices += ServiceInfo(NetworkMapService.type) - return advertisedServices + require(minimumPlatformVersion >= 1) { "minimumPlatformVersion cannot be less than 1" } } } @@ -105,15 +123,16 @@ enum class CertChainPolicyType { MustContainOneOf } -data class CertChainPolicyConfig(val role: String, val policy: CertChainPolicyType, val trustedAliases: Set) { - val certificateChainCheckPolicy: CertificateChainCheckPolicy get() { - return when (policy) { - CertChainPolicyType.Any -> CertificateChainCheckPolicy.Any - CertChainPolicyType.RootMustMatch -> CertificateChainCheckPolicy.RootMustMatch - CertChainPolicyType.LeafMustMatch -> CertificateChainCheckPolicy.LeafMustMatch - CertChainPolicyType.MustContainOneOf -> CertificateChainCheckPolicy.MustContainOneOf(trustedAliases) +data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set) { + val certificateChainCheckPolicy: CertificateChainCheckPolicy + get() { + return when (policy) { + CertChainPolicyType.Any -> CertificateChainCheckPolicy.Any + CertChainPolicyType.RootMustMatch -> CertificateChainCheckPolicy.RootMustMatch + CertChainPolicyType.LeafMustMatch -> CertificateChainCheckPolicy.LeafMustMatch + CertChainPolicyType.MustContainOneOf -> CertificateChainCheckPolicy.MustContainOneOf(trustedAliases) + } } - } } data class RelayConfiguration(val relayHost: String, diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 1d0b5e3d54..26a8322b80 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -72,7 +72,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, // to wait in our code, rather than Thread.sleep() or other time-based pauses. @Suspendable @VisibleForTesting - // We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name + // We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name fun awaitWithDeadline(clock: Clock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create()): Boolean { var nanos: Long do { @@ -89,11 +89,11 @@ class NodeSchedulerService(private val services: ServiceHubInternal, try { // This will return when it times out, or when the clock mutates or when when the original future completes. originalFutureCompleted.get(nanos, TimeUnit.NANOSECONDS) - } catch(e: ExecutionException) { + } catch (e: ExecutionException) { // No need to take action as will fall out of the loop due to future.isDone - } catch(e: CancellationException) { + } catch (e: CancellationException) { // No need to take action as will fall out of the loop due to future.isDone - } catch(e: TimeoutException) { + } catch (e: TimeoutException) { // No need to take action as will fall out of the loop due to future.isDone } } @@ -111,7 +111,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, var txId = it.output.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") var index = it.output.index ?: throw IllegalStateException("DB returned null SecureHash index") Pair(StateRef(SecureHash.parse(txId), index), - ScheduledStateRef(StateRef(SecureHash.parse(txId), index), it.scheduledAt)) + ScheduledStateRef(StateRef(SecureHash.parse(txId), index), it.scheduledAt)) }, toPersistentEntity = { key: StateRef, value: ScheduledStateRef -> PersistentScheduledState().apply { @@ -152,7 +152,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, private class InnerState { var scheduledStates = createMap() - var scheduledStatesQueue: PriorityQueue = PriorityQueue( { a, b -> a.scheduledAt.compareTo(b.scheduledAt) } ) + var scheduledStatesQueue: PriorityQueue = PriorityQueue({ a, b -> a.scheduledAt.compareTo(b.scheduledAt) }) var rescheduled: GuavaSettableFuture? = null } @@ -162,7 +162,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, // We need the [StateMachineManager] to be constructed before this is called in case it schedules a flow. fun start() { mutex.locked { - scheduledStatesQueue.addAll(scheduledStates.all().map { it.second } .toMutableList()) + scheduledStatesQueue.addAll(scheduledStates.all().map { it.second }.toMutableList()) rescheduleWakeUp() } } @@ -182,7 +182,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, if (action.scheduledAt.isBefore(previousEarliest?.scheduledAt ?: Instant.MAX)) { // We are earliest rescheduleWakeUp() - } else if(previousEarliest?.ref == action.ref && previousEarliest.scheduledAt != action.scheduledAt) { + } else if (previousEarliest?.ref == action.ref && previousEarliest.scheduledAt != action.scheduledAt) { // We were earliest but might not be any more rescheduleWakeUp() } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index 9ee5707a57..616e4bd8c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -30,6 +30,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS constructor(wellKnownIdentities: Iterable = emptySet(), confidentialIdentities: Iterable = emptySet(), trustRoot: X509CertificateHolder) : this(wellKnownIdentities, confidentialIdentities, trustRoot.cert) + companion object { private val log = loggerFor() } @@ -45,7 +46,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS init { val caCertificatesWithRoot: Set = caCertificates.toSet() + trustRoot caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(caCertificatesWithRoot)) - keyToParties.putAll(identities.associateBy { it.owningKey } ) + keyToParties.putAll(identities.associateBy { it.owningKey }) principalToParties.putAll(identities.associateBy { it.name }) confidentialIdentities.forEach { identity -> principalToParties.computeIfAbsent(identity.name) { identity } @@ -94,6 +95,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS null } } + override fun wellKnownPartyFromAnonymous(partyRef: PartyAndReference) = wellKnownPartyFromAnonymous(partyRef.party) override fun requireWellKnownPartyFromAnonymous(party: AbstractParty): Party { return wellKnownPartyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}") diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index de1b479c1f..4645c1d8e0 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -48,8 +48,10 @@ class PersistentKeyManagementService(val identityService: IdentityService, fun createKeyMap(): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( toPersistentEntityKey = { it.toBase58String() }, - fromPersistentEntity = { Pair(parsePublicKeyBase58(it.publicKey), - it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, + fromPersistentEntity = { + Pair(parsePublicKeyBase58(it.publicKey), + it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + }, toPersistentEntity = { key: PublicKey, value: PrivateKey -> PersistentKey().apply { publicKey = key.toBase58String() @@ -81,7 +83,7 @@ class PersistentKeyManagementService(val identityService: IdentityService, override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate = freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) - private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) + private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) //It looks for the PublicKey in the (potentially) CompositeKey that is ours, and then returns the associated PrivateKey to use in signing private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 784776820c..52421eb6d1 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle +import net.corda.core.internal.uncheckedCast import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange @@ -56,7 +57,7 @@ import java.math.BigInteger import java.security.KeyStore import java.security.KeyStoreException import java.security.Principal -import java.security.cert.X509Certificate +import java.time.Duration import java.util.* import java.util.concurrent.Executor import java.util.concurrent.ScheduledExecutorService @@ -96,7 +97,8 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, companion object { private val log = loggerFor() /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ - @JvmStatic val MAX_FILE_SIZE = 10485760 + @JvmStatic + val MAX_FILE_SIZE = 10485760 val ipDetectRequestProperty = "ip-request-id" val ipDetectResponseProperty = "ip-address" @@ -390,10 +392,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic // We keep trying until the network map deems the node unreachable and tells us it's been removed at which // point we destroy the bridge - // TODO Give some thought to the retry settings - retryInterval = 5.seconds.toMillis() - retryIntervalMultiplier = 1.5 // Exponential backoff - maxRetryInterval = 3.minutes.toMillis() + retryInterval = config.activeMQServer.bridge.retryIntervalMs + retryIntervalMultiplier = config.activeMQServer.bridge.retryIntervalMultiplier + maxRetryInterval = Duration.ofMinutes(config.activeMQServer.bridge.maxRetryIntervalMin).toMillis() // As a peer of the target node we must connect to it using the peer user. Actual authentication is done using // our TLS certificate. user = PEER_USER @@ -478,8 +479,7 @@ private class VerifyingNettyConnector(configuration: MutableMap, override fun createConnection(): Connection? { val connection = super.createConnection() as? NettyConnection if (sslEnabled && connection != null) { - @Suppress("UNCHECKED_CAST") - val expectedLegalNames = (configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet()) as Set + val expectedLegalNames: Set = uncheckedCast(configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet()) try { val session = connection.channel .pipeline() @@ -608,12 +608,11 @@ class NodeLoginModule : LoginModule { private lateinit var verifierCertCheck: CertificateChainCheckPolicy.Check private val principals = ArrayList() - @Suppress("UNCHECKED_CAST") override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map, options: Map) { this.subject = subject this.callbackHandler = callbackHandler userService = options[RPCUserService::class.java.name] as RPCUserService - val certChainChecks = options[CERT_CHAIN_CHECKS_OPTION_NAME] as Map + val certChainChecks: Map = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME]) peerCertCheck = certChainChecks[PEER_ROLE]!! nodeCertCheck = certChainChecks[NODE_ROLE]!! verifierCertCheck = certChainChecks[VERIFIER_ROLE]!! diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index 062919084e..a708166239 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.PartyInfo @@ -161,8 +162,7 @@ fun MessagingService.onNext(topic: String, sessionId: Long): CordaFutu val messageFuture = openFuture() runOnNextMessage(topic, sessionId) { message -> messageFuture.capture { - @Suppress("UNCHECKED_CAST") - message.data.deserialize() as M + uncheckedCast(message.data.deserialize()) } } return messageFuture @@ -213,6 +213,7 @@ data class TopicSession(val topic: String, val sessionID: Long = MessagingServic * These IDs and timestamps should not be assumed to be globally unique, although due to the nanosecond precision of * the timestamp field they probably will be, even if an implementation just uses a hash prefix as the message id. */ +@CordaSerializable interface Message { val topicSession: TopicSession val data: ByteArray diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 410a753f10..5a9cd04730 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -117,10 +117,12 @@ class NodeMessagingClient(override val config: NodeConfiguration, fun createMessageToRedeliver(): PersistentMap, RetryMessage, Long> { return PersistentMap( toPersistentEntityKey = { it }, - fromPersistentEntity = { Pair(it.key, - Pair(it.message.deserialize( context = SerializationDefaults.STORAGE_CONTEXT), - it.recipients.deserialize( context = SerializationDefaults.STORAGE_CONTEXT)) - ) }, + fromPersistentEntity = { + Pair(it.key, + Pair(it.message.deserialize(context = SerializationDefaults.STORAGE_CONTEXT), + it.recipients.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + ) + }, toPersistentEntity = { _key: Long, (_message: Message, _recipient: MessageRecipients): Pair -> RetryMessage().apply { key = _key @@ -131,6 +133,11 @@ class NodeMessagingClient(override val config: NodeConfiguration, persistentEntityClass = RetryMessage::class.java ) } + + private class NodeClientMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID) : Message { + override val debugTimestamp: Instant = Instant.now() + override fun toString() = "$topicSession#${String(data)}" + } } private class InnerState { @@ -241,7 +248,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, log.info("Network map is complete, so removing filter from P2P consumer.") try { p2pConsumer!!.close() - } catch(e: ActiveMQObjectClosedException) { + } catch (e: ActiveMQObjectClosedException) { // Ignore it: this can happen if the server has gone away before we do. } p2pConsumer = makeP2PConsumer(session, false) @@ -283,8 +290,8 @@ class NodeMessagingClient(override val config: NodeConfiguration, } private fun resumeMessageRedelivery() { - messagesToRedeliver.forEach { - retryId, (message, target) -> send(message, target, retryId) + messagesToRedeliver.forEach { retryId, (message, target) -> + send(message, target, retryId) } } @@ -301,7 +308,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, // It's safe to call into receive simultaneous with other threads calling send on a producer. val artemisMessage: ClientMessage = try { consumer.receive() - } catch(e: ActiveMQObjectClosedException) { + } catch (e: ActiveMQObjectClosedException) { null } ?: return false @@ -433,7 +440,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, } } } - } catch(e: Exception) { + } catch (e: Exception) { log.error("Caught exception whilst executing message handler for ${msg.topicSession}", e) } return true @@ -454,7 +461,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, val c = p2pConsumer ?: throw IllegalStateException("stop can't be called twice") try { c.close() - } catch(e: ActiveMQObjectClosedException) { + } catch (e: ActiveMQObjectClosedException) { // Ignore it: this can happen if the server has gone away before we do. } p2pConsumer = null @@ -597,13 +604,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, override fun createMessage(topicSession: TopicSession, data: ByteArray, uuid: UUID): Message { // TODO: We could write an object that proxies directly to an underlying MQ message here and avoid copying. - return object : Message { - override val topicSession: TopicSession = topicSession - override val data: ByteArray = data - override val debugTimestamp: Instant = Instant.now() - override val uniqueMessageId: UUID = uuid - override fun toString() = "$topicSession#${String(data)}" - } + return NodeClientMessage(topicSession, data, uuid) } private fun createOutOfProcessVerifierService(): TransactionVerifierService { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index b4c61c2574..ae4f328378 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -19,6 +19,7 @@ import net.corda.core.internal.LifeCycle import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT +import net.corda.core.serialization.deserialize import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor @@ -85,6 +86,7 @@ class RPCServer( private companion object { val log = loggerFor() } + private enum class State { UNSTARTED, STARTED, @@ -260,16 +262,26 @@ class RPCServer( private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { lifeCycle.requireState(State.STARTED) - val clientToServer = RPCApi.ClientToServer.fromClientMessage(RPC_SERVER_CONTEXT, artemisMessage) + val clientToServer = RPCApi.ClientToServer.fromClientMessage(artemisMessage) log.debug { "-> RPC -> $clientToServer" } when (clientToServer) { is RPCApi.ClientToServer.RpcRequest -> { - val rpcContext = RpcContext( - currentUser = getUser(artemisMessage) - ) - rpcExecutor!!.submit { - val result = invokeRpc(rpcContext, clientToServer.methodName, clientToServer.arguments) - sendReply(clientToServer.id, clientToServer.clientAddress, result) + val arguments = Try.on { + clientToServer.serialisedArguments.deserialize>(context = RPC_SERVER_CONTEXT) + } + when (arguments) { + is Try.Success -> { + val rpcContext = RpcContext(currentUser = getUser(artemisMessage)) + rpcExecutor!!.submit { + val result = invokeRpc(rpcContext, clientToServer.methodName, arguments.value) + sendReply(clientToServer.id, clientToServer.clientAddress, result) + } + } + is Try.Failure -> { + // We failed to deserialise the arguments, route back the error + log.warn("Inbound RPC failed", arguments.exception) + sendReply(clientToServer.id, clientToServer.clientAddress, arguments) + } } } is RPCApi.ClientToServer.ObservablesClosed -> { @@ -339,6 +351,7 @@ class RPCServer( // TODO remove this User once webserver doesn't need it private val nodeUser = User(NODE_USER, NODE_USER, setOf()) + private fun getUser(message: ClientMessage): User { val validatedUser = message.getStringProperty(Message.HDR_VALIDATED_USER) ?: throw IllegalArgumentException("Missing validated user from the Artemis message") val rpcUser = userService.getUser(validatedUser) @@ -354,6 +367,7 @@ class RPCServer( @JvmField internal val CURRENT_RPC_CONTEXT: ThreadLocal = ThreadLocal() + /** * Returns a context specific to the current RPC call. Note that trying to call this function outside of an RPC will * throw. If you'd like to use the context outside of the call (e.g. in another thread) then pass the returned reference @@ -411,6 +425,7 @@ class ObservableContext( object RpcServerObservableSerializer : Serializer>() { private object RpcObservableContextKey + private val log = loggerFor() fun createContext(observableContext: ObservableContext): SerializationContext { @@ -437,9 +452,11 @@ object RpcServerObservableSerializer : Serializer>() { } } } + override fun onError(exception: Throwable) { log.error("onError called in materialize()d RPC Observable", exception) } + override fun onCompleted() { } } @@ -448,4 +465,4 @@ object RpcServerObservableSerializer : Serializer>() { observableContext.clientAddressToObservables.put(observableContext.clientAddress, observableId) observableContext.observableMap.put(observableId, observableWithSubscription) } -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index 81448e1469..06b04e55a7 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -1,5 +1,6 @@ package net.corda.node.services.network +import net.corda.core.CordaException import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData import net.corda.core.crypto.isFulfilledBy @@ -18,10 +19,10 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.AbstractNodeService -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.messaging.MessageHandlerRegistration +import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.ServiceRequestMessage import net.corda.node.services.messaging.createMessage import net.corda.node.services.network.NetworkMapService.* @@ -71,8 +72,6 @@ interface NetworkMapService { const val PUSH_TOPIC = "platform.network_map.push" // Base topic for messages acknowledging pushed updates const val PUSH_ACK_TOPIC = "platform.network_map.push_ack" - - val type = ServiceType.networkMap } data class FetchMapRequest(val subscribe: Boolean, @@ -116,8 +115,8 @@ interface NetworkMapService { object NullNetworkMapService : NetworkMapService @ThreadSafe -class InMemoryNetworkMapService(services: ServiceHubInternal, minimumPlatformVersion: Int) - : AbstractNetworkMapService(services, minimumPlatformVersion) { +class InMemoryNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal, minimumPlatformVersion: Int) + : AbstractNetworkMapService(network, networkMapCache, minimumPlatformVersion) { override val nodeRegistrations: MutableMap = ConcurrentHashMap() override val subscribers = ThreadBox(mutableMapOf()) @@ -134,8 +133,9 @@ class InMemoryNetworkMapService(services: ServiceHubInternal, minimumPlatformVer * subscriber clean up and is simpler to persist than the previous implementation based on a set of missing messages acks. */ @ThreadSafe -abstract class AbstractNetworkMapService(services: ServiceHubInternal, - val minimumPlatformVersion: Int) : NetworkMapService, AbstractNodeService(services) { +abstract class AbstractNetworkMapService(network: MessagingService, + private val networkMapCache: NetworkMapCacheInternal, + private val minimumPlatformVersion: Int) : NetworkMapService, AbstractNodeService(network) { companion object { /** * Maximum credible size for a registration request. Generally requests are around 2000-6000 bytes, so this gives a @@ -160,14 +160,6 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, val maxUnacknowledgedUpdates = 10 private val handlers = ArrayList() - - init { - require(minimumPlatformVersion >= 1) { "minimumPlatformVersion cannot be less than 1" } - require(minimumPlatformVersion <= services.myInfo.platformVersion) { - "minimumPlatformVersion cannot be greater than the node's own version" - } - } - protected fun setup() { // Register message handlers handlers += addMessageHandler(FETCH_TOPIC) { req: FetchMapRequest -> processFetchAllRequest(req) } @@ -189,7 +181,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, } private fun addSubscriber(subscriber: MessageRecipients) { - if (subscriber !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber() + if (subscriber !is SingleMessageRecipient) throw NodeMapException.InvalidSubscriber() subscribers.locked { if (!containsKey(subscriber)) { put(subscriber, LastAcknowledgeInfo(mapVersion)) @@ -198,12 +190,12 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, } private fun removeSubscriber(subscriber: MessageRecipients) { - if (subscriber !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber() + if (subscriber !is SingleMessageRecipient) throw NodeMapException.InvalidSubscriber() subscribers.locked { remove(subscriber) } } - private fun processAcknowledge(request: UpdateAcknowledge): Unit { - if (request.replyTo !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber() + private fun processAcknowledge(request: UpdateAcknowledge) { + if (request.replyTo !is SingleMessageRecipient) throw NodeMapException.InvalidSubscriber() subscribers.locked { val lastVersionAcked = this[request.replyTo]?.mapVersion if ((lastVersionAcked ?: 0) < request.mapVersion) { @@ -282,11 +274,11 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, when (change.type) { ADD -> { logger.info("Added node ${node.addresses} to network map") - services.networkMapCache.addNode(change.node) + networkMapCache.addNode(change.node) } REMOVE -> { logger.info("Removed node ${node.addresses} from network map") - services.networkMapCache.removeNode(change.node) + networkMapCache.removeNode(change.node) } } @@ -360,13 +352,13 @@ class WireNodeRegistration(raw: SerializedBytes, sig: DigitalS } @CordaSerializable -sealed class NodeMapError : Exception() { +sealed class NodeMapException : CordaException("Network Map Protocol Error") { /** Thrown if the signature on the node info does not match the public key for the identity */ - class InvalidSignature : NodeMapError() + class InvalidSignature : NodeMapException() /** Thrown if the replyTo of a subscription change message is not a single message recipient */ - class InvalidSubscriber : NodeMapError() + class InvalidSubscriber : NodeMapException() } @CordaSerializable 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 new file mode 100644 index 0000000000..65ef358c6d --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -0,0 +1,124 @@ +package net.corda.node.services.network + +import net.corda.cordform.CordformNode +import net.corda.core.crypto.SignedData +import net.corda.core.internal.* +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.KeyManagementService +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.seconds +import rx.Observable +import rx.Scheduler +import rx.schedulers.Schedulers +import java.io.IOException +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.streams.toList + +/** + * Class containing the logic to + * - Serialize and de-serialize a [NodeInfo] to disk and reading it back. + * - Poll a directory for new serialized [NodeInfo] + * + * @param path the base path of a node. + * @param pollFrequencyMsec how often to poll the filesystem in milliseconds. Any value smaller than 5 seconds will + * be treated as 5 seconds. + * @param scheduler a [Scheduler] for the rx [Observable] returned by [nodeInfoUpdates], this is mainly useful for + * testing. It defaults to the io scheduler which is the appropriate value for production uses. + */ +class NodeInfoWatcher(private val nodePath: Path, + pollFrequencyMsec: Long = 5.seconds.toMillis(), + private val scheduler: Scheduler = Schedulers.io()) { + + private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY + private val pollFrequencyMsec: Long = maxOf(pollFrequencyMsec, 5.seconds.toMillis()) + private val successfullyProcessedFiles = mutableSetOf() + + companion object { + private val logger = loggerFor() + + /** + * Saves the given [NodeInfo] to a path. + * The node is 'encoded' as a SignedData, signed with the owning key of its first identity. + * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename + * is used so that one can freely copy these files without fearing to overwrite another one. + * + * @param path the path where to write the file, if non-existent it will be created. + * @param nodeInfo the NodeInfo to serialize. + * @param keyManager a KeyManagementService used to sign the NodeInfo data. + */ + fun saveToFile(path: Path, nodeInfo: NodeInfo, keyManager: KeyManagementService) { + try { + path.createDirectories() + val serializedBytes = nodeInfo.serialize() + val regSig = keyManager.sign(serializedBytes.bytes, nodeInfo.legalIdentities.first().owningKey) + val signedData = SignedData(serializedBytes, regSig) + signedData.serialize().open().copyTo(path / "nodeInfo-${serializedBytes.hash}") + } catch (e: Exception) { + logger.warn("Couldn't write node info to file", e) + } + } + } + + init { + if (!nodeInfoDirectory.isDirectory()) { + try { + nodeInfoDirectory.createDirectories() + } catch (e: IOException) { + logger.info("Failed to create $nodeInfoDirectory", e) + } + } + } + + /** + * Read all the files contained in [nodePath] / [CordformNode.NODE_INFO_DIRECTORY] and keep watching + * the folder for further updates. + * + * We simply list the directory content every 5 seconds, the Java implementation of WatchService has been proven to + * be unreliable on MacOs and given the fairly simple use case we have, this simple implementation should do. + * + * @return an [Observable] returning [NodeInfo]s, at most one [NodeInfo] is returned for each processed file. + */ + fun nodeInfoUpdates(): Observable { + return Observable.interval(pollFrequencyMsec, TimeUnit.MILLISECONDS, scheduler) + .flatMapIterable { loadFromDirectory() } + } + + /** + * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. + * Signatures are checked before returning a value. + * + * @return a list of [NodeInfo]s + */ + private fun loadFromDirectory(): List { + if (!nodeInfoDirectory.isDirectory()) { + return emptyList() + } + val result = nodeInfoDirectory.list { paths -> + paths.filter { it !in successfullyProcessedFiles } + .filter { it.isRegularFile() } + .map { path -> + processFile(path)?.apply { successfullyProcessedFiles.add(path) } + } + .toList() + .filterNotNull() + } + if (result.isNotEmpty()) { + logger.info("Successfully read ${result.size} NodeInfo files from disk.") + } + return result + } + + private fun processFile(file: Path): NodeInfo? { + try { + logger.info("Reading NodeInfo from file: $file") + val signedData = file.readAll().deserialize>() + return signedData.verified() + } catch (e: Exception) { + logger.warn("Exception parsing NodeInfo from file. $file", e) + return null + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 45e2b6f9b0..eb45e7a0df 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -4,6 +4,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.map @@ -12,6 +13,7 @@ import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache.MapChange +import net.corda.core.node.services.NotaryService import net.corda.core.node.services.PartyInfo import net.corda.core.schemas.NodeInfoSchemaV1 import net.corda.core.serialization.SingletonSerializeAsToken @@ -19,9 +21,8 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.core.utilities.toBase58String -import net.corda.node.services.api.NetworkCacheError +import net.corda.node.services.api.NetworkCacheException import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.MessagingService @@ -30,7 +31,6 @@ import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.NetworkMapService.FetchMapResponse import net.corda.node.services.network.NetworkMapService.SubscribeResponse import net.corda.node.utilities.AddOrRemove -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction import org.hibernate.Session @@ -40,6 +40,7 @@ import java.security.PublicKey import java.security.SignatureException import java.util.* import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.HashMap /** * Extremely simple in-memory cache of the network map. @@ -80,19 +81,26 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) // Notary certificates have to be signed by the doorman directly it.legalIdentities } - .filter { - it.name.toString().contains("corda.notary", true) - } - .distinct() // Distinct, because of distributed service nodes + .filter { it.name.commonName?.startsWith(NotaryService.ID_PREFIX) ?: false } + .toSet() // Distinct, because of distributed service nodes .sortedBy { it.name.toString() } } + private val nodeInfoSerializer = NodeInfoWatcher(serviceHub.configuration.baseDirectory, + serviceHub.configuration.additionalNodeInfoPollingFrequencyMsec) + init { - serviceHub.database.transaction { loadFromDB() } + loadFromFiles() + serviceHub.database.transaction { loadFromDB(session) } + } + + private fun loadFromFiles() { + logger.info("Loading network map from files..") + nodeInfoSerializer.nodeInfoUpdates().subscribe { node -> addNode(node) } } override fun getPartyInfo(party: Party): PartyInfo? { - val nodes = serviceHub.database.transaction { queryByIdentityKey(party.owningKey) } + val nodes = serviceHub.database.transaction { queryByIdentityKey(session, party.owningKey) } if (nodes.size == 1 && nodes[0].isLegalIdentity(party)) { return PartyInfo.SingleNode(party, nodes[0].addresses) } @@ -106,17 +114,21 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) return null } - override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = serviceHub.database.transaction { queryByLegalName(name).firstOrNull() } + override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = getNodesByLegalName(name).firstOrNull() + override fun getNodesByLegalName(name: CordaX500Name): List = serviceHub.database.transaction { queryByLegalName(session, name) } override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = - serviceHub.database.transaction { queryByIdentityKey(identityKey) } + serviceHub.database.transaction { queryByIdentityKey(session, identityKey) } + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { val wellKnownParty = serviceHub.identityService.wellKnownPartyFromAnonymous(party) return wellKnownParty?.let { - getNodesByLegalIdentityKey(it.owningKey).singleOrNull() + getNodesByLegalIdentityKey(it.owningKey).firstOrNull() } } - override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = serviceHub.database.transaction { queryByAddress(address) } + override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = serviceHub.database.transaction { queryByAddress(session, address) } + + override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = serviceHub.database.transaction { queryIdentityByLegalName(session, name) } override fun track(): DataFeed, MapChange> { synchronized(_changed) { @@ -135,7 +147,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) data = NetworkMapService.UpdateAcknowledge(req.mapVersion, network.myAddress).serialize().bytes) network.send(ackMessage, req.replyTo) processUpdatePush(req) - } catch (e: NodeMapError) { + } catch (e: NodeMapException) { logger.warn("Failure during node map update due to bad update: ${e.javaClass.name}") } catch (e: Exception) { logger.error("Exception processing update from network map service", e) @@ -157,30 +169,44 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override fun addNode(node: NodeInfo) { + logger.info("Adding node with info: $node") synchronized(_changed) { + registeredNodes[node.legalIdentities.first().owningKey]?.let { + if (it.serial > node.serial) { + logger.info("Discarding older nodeInfo for ${node.legalIdentities.first().name}") + return + } + } val previousNode = registeredNodes.put(node.legalIdentities.first().owningKey, node) // TODO hack... we left the first one as special one if (previousNode == null) { + logger.info("No previous node found") serviceHub.database.transaction { updateInfoDB(node) changePublisher.onNext(MapChange.Added(node)) } } else if (previousNode != node) { + logger.info("Previous node was found as: $previousNode") serviceHub.database.transaction { updateInfoDB(node) changePublisher.onNext(MapChange.Modified(node, previousNode)) } + } else { + logger.info("Previous node was identical to incoming one - doing nothing") } } + logger.info("Done adding node with info: $node") } override fun removeNode(node: NodeInfo) { + logger.info("Removing node with info: $node") synchronized(_changed) { registeredNodes.remove(node.legalIdentities.first().owningKey) serviceHub.database.transaction { - removeInfoDB(node) + removeInfoDB(session, node) changePublisher.onNext(MapChange.Removed(node)) } } + logger.info("Done removing node with info: $node") } /** @@ -194,7 +220,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) val address = getPartyInfo(mapParty)?.let { network.getAddressOfParty(it) } ?: throw IllegalArgumentException("Can't deregister for updates, don't know the party: $mapParty") val future = network.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, address).map { - if (it.confirmed) Unit else throw NetworkCacheError.DeregistrationFailed() + if (it.confirmed) Unit else throw NetworkCacheException.DeregistrationFailed() } _registrationFuture.captureLater(future.map { null }) return future @@ -205,20 +231,16 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) val reg = req.wireReg.verified() processRegistration(reg) } catch (e: SignatureException) { - throw NodeMapError.InvalidSignature() + throw NodeMapException.InvalidSignature() } } override val allNodes: List - get () = serviceHub.database.transaction { - createSession { - getAllInfos(it).map { it.toNodeInfo() } - } + get() = serviceHub.database.transaction { + getAllInfos(session).map { it.toNodeInfo() } } private fun processRegistration(reg: NodeRegistration) { - // TODO: Implement filtering by sequence number, so we only accept changes that are - // more recent than the latest change we've processed. when (reg.type) { AddOrRemove.ADD -> addNode(reg.node) AddOrRemove.REMOVE -> removeNode(reg.node) @@ -233,10 +255,6 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) // Changes related to NetworkMap redesign // TODO It will be properly merged into network map cache after services removal. - private inline fun createSession(block: (Session) -> T): T { - return DatabaseTransactionManager.current().session.let { block(it) } - } - private fun getAllInfos(session: Session): List { val criteria = session.criteriaBuilder.createQuery(NodeInfoSchemaV1.PersistentNodeInfo::class.java) criteria.select(criteria.from(NodeInfoSchemaV1.PersistentNodeInfo::class.java)) @@ -246,26 +264,22 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) /** * Load NetworkMap data from the database if present. Node can start without having NetworkMapService configured. */ - private fun loadFromDB() { + private fun loadFromDB(session: Session) { logger.info("Loading network map from database...") - createSession { - val result = getAllInfos(it) - for (nodeInfo in result) { - try { - logger.info("Loaded node info: $nodeInfo") - val publicKey = parsePublicKeyBase58(nodeInfo.legalIdentitiesAndCerts.single { it.isMain }.owningKey) - val node = nodeInfo.toNodeInfo() - registeredNodes.put(publicKey, node) - changePublisher.onNext(MapChange.Added(node)) // Redeploy bridges after reading from DB on startup. - _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. - } catch (e: Exception) { - logger.warn("Exception parsing network map from the database.", e) - } - } - if (loadDBSuccess) { - _registrationFuture.set(null) // Useful only if we don't have NetworkMapService configured so StateMachineManager can start. + val result = getAllInfos(session) + for (nodeInfo in result) { + try { + logger.info("Loaded node info: $nodeInfo") + val node = nodeInfo.toNodeInfo() + addNode(node) + _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. + } catch (e: Exception) { + logger.warn("Exception parsing network map from the database.", e) } } + if (loadDBSuccess) { + _registrationFuture.set(null) // Useful only if we don't have NetworkMapService configured so StateMachineManager can start. + } } private fun updateInfoDB(nodeInfo: NodeInfo) { @@ -289,11 +303,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } } - private fun removeInfoDB(nodeInfo: NodeInfo) { - createSession { - val info = findByIdentityKey(it, nodeInfo.legalIdentitiesAndCerts.first().owningKey).single() - it.remove(info) - } + private fun removeInfoDB(session: Session, nodeInfo: NodeInfo) { + val info = findByIdentityKey(session, nodeInfo.legalIdentitiesAndCerts.first().owningKey).single() + session.remove(info) } private fun findByIdentityKey(session: Session, identityKey: PublicKey): List { @@ -304,35 +316,40 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) return query.resultList } - private fun queryByIdentityKey(identityKey: PublicKey): List { - createSession { - val result = findByIdentityKey(it, identityKey) - return result.map { it.toNodeInfo() } - } + private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List { + val result = findByIdentityKey(session, identityKey) + return result.map { it.toNodeInfo() } } - private fun queryByLegalName(name: CordaX500Name): List { - createSession { - val query = it.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", - NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("name", name.toString()) - val result = query.resultList - return result.map { it.toNodeInfo() } - } + private fun queryIdentityByLegalName(session: Session, name: CordaX500Name): PartyAndCertificate? { + val query = session.createQuery( + // We do the JOIN here to restrict results to those present in the network map + "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.DBPartyAndCertificate::class.java) + query.setParameter("name", name.toString()) + val candidates = query.resultList.map { it.toLegalIdentityAndCert() } + // The map is restricted to holding a single identity for any X.500 name, so firstOrNull() is correct here. + return candidates.firstOrNull() } - private fun queryByAddress(hostAndPort: NetworkHostAndPort): NodeInfo? { - createSession { - val query = it.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", - NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("host", hostAndPort.host) - query.setParameter("port", hostAndPort.port) - val result = query.resultList - return if (result.isEmpty()) null - else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") - } + private fun queryByLegalName(session: Session, name: CordaX500Name): List { + val query = session.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("name", name.toString()) + val result = query.resultList + return result.map { it.toNodeInfo() } + } + + private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? { + val query = session.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("host", hostAndPort.host) + query.setParameter("port", hostAndPort.port) + val result = query.resultList + return if (result.isEmpty()) null + else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") } /** Object Relational Mapping support. */ @@ -351,10 +368,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) override fun clearNetworkMapCache() { serviceHub.database.transaction { - createSession { - val result = getAllInfos(it) - for (nodeInfo in result) it.remove(nodeInfo) - } + val result = getAllInfos(session) + for (nodeInfo in result) session.remove(nodeInfo) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index f62748dcba..6d3600d398 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -7,7 +7,8 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.messaging.MessagingService import net.corda.node.utilities.* import net.corda.nodeapi.ArtemisMessagingComponent import java.io.ByteArrayInputStream @@ -23,8 +24,8 @@ import java.util.* * This class needs database transactions to be in-flight during method calls and init, otherwise it will throw * exceptions. */ -class PersistentNetworkMapService(services: ServiceHubInternal, minimumPlatformVersion: Int) - : AbstractNetworkMapService(services, minimumPlatformVersion) { +class PersistentNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal, minimumPlatformVersion: Int) + : AbstractNetworkMapService(network, networkMapCache, minimumPlatformVersion) { // Only the node_party_path column is needed to reconstruct a PartyAndCertificate but we have the others for human readability @Entity diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt index ac1d696af7..8dc9c857bd 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt @@ -32,7 +32,7 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic if (dbData != null) { val party = identityService.wellKnownPartyFromX500Name(CordaX500Name.parse(dbData)) if (party != null) return party - log.warn ("Identity service unable to resolve X500name: $dbData") + log.warn("Identity service unable to resolve X500name: $dbData") } return null // non resolvable anonymous parties are stored as nulls } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index a82642adf7..b9b5f0bbdc 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -3,8 +3,8 @@ package net.corda.node.services.persistence import net.corda.core.serialization.SerializedBytes import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.currentDBSession import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id @@ -28,15 +28,14 @@ class DBCheckpointStorage : CheckpointStorage { ) override fun addCheckpoint(checkpoint: Checkpoint) { - val session = DatabaseTransactionManager.current().session - session.save(DBCheckpoint().apply { + currentDBSession().save(DBCheckpoint().apply { checkpointId = checkpoint.id.toString() this.checkpoint = checkpoint.serializedFiber.bytes }) } override fun removeCheckpoint(checkpoint: Checkpoint) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java) val root = delete.from(DBCheckpoint::class.java) @@ -45,7 +44,7 @@ class DBCheckpointStorage : CheckpointStorage { } override fun forEach(block: (Checkpoint) -> Boolean) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(DBCheckpoint::class.java) val root = criteriaQuery.from(DBCheckpoint::class.java) criteriaQuery.select(root) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 0e68c98cce..ff29e4c1da 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -30,8 +30,10 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok fun createTransactionsMap(): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( toPersistentEntityKey = { it.toString() }, - fromPersistentEntity = { Pair(SecureHash.parse(it.txId), - it.transaction.deserialize( context = SerializationDefaults.STORAGE_CONTEXT)) }, + fromPersistentEntity = { + Pair(SecureHash.parse(it.txId), + it.transaction.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + }, toPersistentEntity = { key: SecureHash, value: SignedTransaction -> DBTransaction().apply { txId = key.toString() @@ -46,9 +48,9 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok private val txStorage = createTransactionsMap() override fun addTransaction(transaction: SignedTransaction): Boolean = - txStorage.addWithDuplicatesAllowed(transaction.id, transaction).apply { - updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction) - } + txStorage.addWithDuplicatesAllowed(transaction.id, transaction).apply { + updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction) + } override fun getTransaction(id: SecureHash): SignedTransaction? = txStorage[id] @@ -59,5 +61,6 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok DataFeed(txStorage.allPersisted().map { it.second }.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) @VisibleForTesting - val transactions: Iterable get() = txStorage.allPersisted().map { it.second }.toList() + val transactions: Iterable + get() = txStorage.allPersisted().map { it.second }.toList() } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt index cbec0c793c..8fb0023b3d 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt @@ -24,7 +24,7 @@ import java.sql.Connection import java.util.* import java.util.concurrent.ConcurrentHashMap -class HibernateConfiguration(createSchemaService: () -> SchemaService, private val databaseProperties: Properties, private val createIdentityScervice: () -> IdentityService) { +class HibernateConfiguration(val schemaService: SchemaService, private val databaseProperties: Properties, private val createIdentityService: () -> IdentityService) { companion object { val logger = loggerFor() } @@ -32,27 +32,14 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v // TODO: make this a guava cache or similar to limit ability for this to grow forever. private val sessionFactories = ConcurrentHashMap, SessionFactory>() - private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?:"") - var schemaService = createSchemaService() - - init { - logger.info("Init HibernateConfiguration for schemas: ${schemaService.schemaOptions.keys}") - sessionFactoryForRegisteredSchemas() + private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?: "") + val sessionFactoryForRegisteredSchemas = schemaService.schemaOptions.keys.let { + logger.info("Init HibernateConfiguration for schemas: $it") + sessionFactoryForSchemas(it) } - fun sessionFactoryForRegisteredSchemas(): SessionFactory { - return sessionFactoryForSchemas(*schemaService.schemaOptions.keys.toTypedArray()) - } - - fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory { - return sessionFactoryForSchemas(schema) - } - - //vararg to set conversions left to preserve method signature for now - fun sessionFactoryForSchemas(vararg schemas: MappedSchema): SessionFactory { - val schemaSet: Set = schemas.toSet() - return sessionFactories.computeIfAbsent(schemaSet, { makeSessionFactoryForSchemas(schemaSet) }) - } + /** @param key must be immutable, not just read-only. */ + fun sessionFactoryForSchemas(key: Set) = sessionFactories.computeIfAbsent(key, { makeSessionFactoryForSchemas(key) }) private fun makeSessionFactoryForSchemas(schemas: Set): SessionFactory { logger.info("Creating session factory for schemas: $schemas") @@ -62,7 +49,7 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v // necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though. // TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs. val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name) - .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate") + .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase", "true") == "true") "update" else "validate") .setProperty("hibernate.format_sql", "true") .setProperty("hibernate.connection.isolation", transactionIsolationLevel.toString()) @@ -71,7 +58,7 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v schema.mappedTypes.forEach { config.addAnnotatedClass(it) } } - val sessionFactory = buildSessionFactory(config, metadataSources, databaseProperties.getProperty("serverNameTablePrefix","")) + val sessionFactory = buildSessionFactory(config, metadataSources, databaseProperties.getProperty("serverNameTablePrefix", "")) logger.info("Created session factory for schemas: $schemas") return sessionFactory } @@ -86,7 +73,7 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v } }) // register custom converters - applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(createIdentityScervice)) + applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(createIdentityService)) // Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages. // to avoid OOM when large blobs might get logged. applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt index 740373d3d7..84dd4974dd 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt @@ -22,6 +22,7 @@ class InMemoryStateMachineRecordedTransactionMappingStorage : StateMachineRecord val stateMachineTransactionMap = HashMap>() val updates = PublishSubject.create()!! } + private val mutex = ThreadBox(InnerState()) override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) { 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 b6acddd45e..c95fe2bf94 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 @@ -6,14 +6,15 @@ import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream +import net.corda.core.CordaRuntimeException import net.corda.core.internal.AbstractAttachment import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.currentDBSession import java.io.* import java.nio.file.Paths import java.util.jar.JarInputStream @@ -28,7 +29,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single @Entity @Table(name = "${NODE_DATABASE_PREFIX}attachments", - indexes = arrayOf(Index(name = "att_id_idx", columnList = "att_id"))) + indexes = arrayOf(Index(name = "att_id_idx", columnList = "att_id"))) class DBAttachment( @Id @Column(name = "att_id", length = 65535) @@ -49,7 +50,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single private val attachmentCount = metrics.counter("Attachments") init { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java))) @@ -58,7 +59,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single } @CordaSerializable - class HashMismatchException(val expected: SecureHash, val actual: SecureHash) : RuntimeException("File $expected hashed to $actual: corruption in attachment store?") + class HashMismatchException(val expected: SecureHash, val actual: SecureHash) : CordaRuntimeException("File $expected hashed to $actual: corruption in attachment store?") /** * Wraps a stream and hashes data as it is read: if the entire stream is consumed, then at the end the hash of @@ -68,7 +69,8 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single * around inside it, we haven't read the whole file, so we can't check the hash. But when copying it over the network * this will provide an additional safety check against user error. */ - @VisibleForTesting @CordaSerializable + @VisibleForTesting + @CordaSerializable class HashCheckingStream(val expected: SecureHash.SHA256, val expectedSize: Int, input: InputStream, @@ -109,16 +111,17 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single } private var _hash: HashCode? = null // Backing field for hash property - private val hash: HashCode get() { - var h = _hash - return if (h == null) { - h = stream.hash() - _hash = h - h - } else { - h + private val hash: HashCode + get() { + var h = _hash + return if (h == null) { + h = stream.hash() + _hash = h + h + } else { + h + } } - } } private class AttachmentImpl(override val id: SecureHash, dataLoader: () -> ByteArray, private val checkOnLoad: Boolean) : AbstractAttachment(dataLoader), SerializeAsToken { @@ -137,7 +140,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single } override fun openAttachment(id: SecureHash): Attachment? { - val attachment = DatabaseTransactionManager.current().session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) attachment?.let { return AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad) } @@ -158,7 +161,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single checkIsAValidJAR(ByteArrayInputStream(bytes)) val id = SecureHash.SHA256(hs.hash().asBytes()) - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java) diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index 9afc740949..45a3334ef1 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -38,7 +38,7 @@ class HibernateObserver(vaultUpdates: Observable>, v } fun persistStateWithSchema(state: ContractState, stateRef: StateRef, schema: MappedSchema) { - val sessionFactory = config.sessionFactoryForSchema(schema) + val sessionFactory = config.sessionFactoryForSchemas(setOf(schema)) val session = sessionFactory.withOptions(). connection(DatabaseTransactionManager.current().connection). flushMode(FlushMode.MANUAL). diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 333ecd6152..8436f9459b 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -9,6 +9,7 @@ import net.corda.core.schemas.NodeInfoSchemaV1 import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.SchemaService import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.identity.PersistentIdentityService @@ -27,13 +28,13 @@ import net.corda.node.services.vault.VaultSchemaV1 /** * Most basic implementation of [SchemaService]. - * + * @param cordappLoader if not null, custom schemas will be extracted from its cordapps. * TODO: support loading schema options from node configuration. * TODO: support configuring what schemas are to be selected for persistence. * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { +class NodeSchemaService(cordappLoader: CordappLoader?) : SchemaService, SingletonSerializeAsToken() { // Entities for compulsory services object NodeServices @@ -57,19 +58,22 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS PersistentIdentityService.PersistentIdentity::class.java, PersistentIdentityService.PersistentIdentityNames::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java - )) + )) // Required schemas are those used by internal Corda services // For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future) private val requiredSchemas: Map = mapOf(Pair(CommonSchemaV1, SchemaService.SchemaOptions()), - Pair(VaultSchemaV1, SchemaService.SchemaOptions()), - Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()), - Pair(NodeServicesV1, SchemaService.SchemaOptions())) + Pair(VaultSchemaV1, SchemaService.SchemaOptions()), + Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()), + Pair(NodeServicesV1, SchemaService.SchemaOptions())) - override var schemaOptions: Map = requiredSchemas.plus(customSchemas.map { - mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) - }) + override val schemaOptions: Map = if (cordappLoader == null) { + requiredSchemas + } else { + val customSchemas = cordappLoader.cordapps.flatMap { it.customSchemas }.toSet() + requiredSchemas.plus(customSchemas.map { mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) }) + } // Currently returns all schemas supported by the state, with no filtering or enrichment. override fun selectSchemas(state: ContractState): Iterable { @@ -92,10 +96,4 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants) return (state as QueryableState).generateMappedObject(schema) } - - override fun registerCustomSchemas(_customSchemas: Set) { - schemaOptions = schemaOptions.plus(_customSchemas.map { - mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) - }) - } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt index 748cce9bd8..bd29525072 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt @@ -1,6 +1,8 @@ package net.corda.node.services.statemachine +import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash +import java.time.Instant interface FlowIORequest { // This is used to identify where we suspended, in case of message mismatch errors and other things where we @@ -8,7 +10,9 @@ interface FlowIORequest { val stackTraceInCaseOfProblems: StackSnapshot } -interface WaitingRequest : FlowIORequest +interface WaitingRequest : FlowIORequest { + fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean +} interface SessionedFlowIORequest : FlowIORequest { val session: FlowSessionInternal @@ -21,6 +25,8 @@ interface SendRequest : SessionedFlowIORequest { interface ReceiveRequest : SessionedFlowIORequest, WaitingRequest { val receiveType: Class val userReceiveType: Class<*>? + + override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = this.session === session } data class SendAndReceive(override val session: FlowSessionInternal, @@ -38,6 +44,63 @@ data class ReceiveOnly(override val session: FlowSessionInte override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() } +class ReceiveAll(val requests: List>) : WaitingRequest { + @Transient + override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() + + private fun isComplete(received: LinkedHashMap): Boolean { + return received.keys == requests.map { it.session }.toSet() + } + private fun shouldResumeIfRelevant() = requests.all { hasSuccessfulEndMessage(it) } + + private fun hasSuccessfulEndMessage(it: ReceiveRequest): Boolean { + return it.session.receivedMessages.map { it.message }.any { it is SessionData || it is SessionEnd } + } + + @Suspendable + fun suspendAndExpectReceive(suspend: Suspend): Map { + val receivedMessages = LinkedHashMap() + + poll(receivedMessages) + return if (isComplete(receivedMessages)) { + receivedMessages + } else { + suspend(this) + poll(receivedMessages) + if (isComplete(receivedMessages)) { + receivedMessages + } else { + throw IllegalStateException(requests.filter { it.session !in receivedMessages.keys }.map { "Was expecting a ${it.receiveType.simpleName} but instead got nothing for $it." }.joinToString { "\n" }) + } + } + } + + interface Suspend { + @Suspendable + operator fun invoke(request: FlowIORequest) + } + + @Suspendable + private fun poll(receivedMessages: LinkedHashMap) { + return requests.filter { it.session !in receivedMessages.keys }.forEach { request -> + poll(request)?.let { + receivedMessages[request.session] = RequestMessage(request, it) + } + } + } + + @Suspendable + private fun poll(request: ReceiveRequest): ReceivedSessionMessage<*>? { + return request.session.receivedMessages.poll() + } + + override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = isRelevant(session) && shouldResumeIfRelevant() + + private fun isRelevant(session: FlowSessionInternal) = requests.any { it.session === session } + + data class RequestMessage(val request: ReceiveRequest, val message: ReceivedSessionMessage<*>) +} + data class SendOnly(override val session: FlowSessionInternal, override val message: SessionMessage) : SendRequest { @Transient override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() @@ -46,6 +109,13 @@ data class SendOnly(override val session: FlowSessionInternal, override val mess data class WaitForLedgerCommit(val hash: SecureHash, val fiber: FlowStateMachineImpl<*>) : WaitingRequest { @Transient override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() + + override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = message is ErrorSessionEnd +} + +data class Sleep(val until: Instant, val fiber: FlowStateMachineImpl<*>) : FlowIORequest { + @Transient + override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() } class StackSnapshot : Throwable("This is a stack trace to help identify the source of the underlying problem") diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index 6d16a94db5..a3c36fa3a4 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import net.corda.core.internal.VisibleForTesting import com.google.common.primitives.Primitives +import net.corda.core.cordapp.CordappContext import net.corda.core.flows.* import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -17,7 +18,7 @@ import kotlin.reflect.jvm.javaType * The internal concrete implementation of the FlowLogicRef marker interface. */ @CordaSerializable -data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map) : FlowLogicRef +data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, val args: Map) : FlowLogicRef /** * A class for conversion to and from [FlowLogic] and [FlowLogicRef] instances. @@ -32,6 +33,9 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, * in response to a potential malicious use or buggy update to an app etc. */ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory { + // TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now + var classloader = javaClass.classLoader + override fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef { if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) { throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow") @@ -73,17 +77,14 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor */ @VisibleForTesting internal fun createKotlin(type: Class>, args: Map): FlowLogicRef { - // TODO: we need to capture something about the class loader or "application context" into the ref, - // perhaps as some sort of ThreadLocal style object. For now, just create an empty one. - val appContext = AppContext(emptyList()) // Check we can find a constructor and populate the args to it, but don't call it createConstructor(type, args) - return FlowLogicRefImpl(type.name, appContext, args) + return FlowLogicRefImpl(type.name, args) } fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface") - val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java) + val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java) return createConstructor(klass, ref.args)() } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt index 4f2d1ba5fc..dc5b39c6f5 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import net.corda.core.flows.FlowInfo import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession import net.corda.core.identity.Party import net.corda.node.services.statemachine.FlowSessionState.Initiated import net.corda.node.services.statemachine.FlowSessionState.Initiating @@ -15,6 +16,7 @@ import java.util.concurrent.ConcurrentLinkedQueue // TODO rename this class FlowSessionInternal( val flow: FlowLogic<*>, + val flowSession : FlowSession, val ourSessionId: Long, val initiatingParty: Party?, var state: FlowSessionState, diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 1556ef31c3..89f0bf6dc6 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -12,18 +12,16 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.abbreviate +import net.corda.core.internal.* import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.isRegularFile -import net.corda.core.internal.staticField +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent import net.corda.node.services.api.FlowPermissionAuditEvent import net.corda.node.services.api.ServiceHubInternal -import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.statemachine.FlowSessionState.Initiating import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.DatabaseTransaction @@ -32,16 +30,18 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.nio.file.Paths import java.sql.SQLException +import java.time.Duration +import java.time.Instant import java.util.* import java.util.concurrent.TimeUnit class FlowPermissionException(message: String) : FlowException(message) class FlowStateMachineImpl(override val id: StateMachineRunId, - val logic: FlowLogic, + override val logic: FlowLogic, scheduler: FiberScheduler, override val flowInitiator: FlowInitiator, - // Store the Party rather than the full cert path with PartyAndCertificate + // Store the Party rather than the full cert path with PartyAndCertificate val ourIdentity: Party) : Fiber(id.toString(), scheduler), FlowStateMachine { companion object { @@ -52,23 +52,6 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, * Return the current [FlowStateMachineImpl] or null if executing outside of one. */ fun currentStateMachine(): FlowStateMachineImpl<*>? = Strand.currentStrand() as? FlowStateMachineImpl<*> - - /** - * Provide a mechanism to sleep within a Strand without locking any transactional state - */ - // TODO: inlined due to an intermittent Quasar error (to be fully investigated) - @Suppress("NOTHING_TO_INLINE") - @Suspendable - inline fun sleep(millis: Long) { - if (currentStateMachine() != null) { - val db = DatabaseTransactionManager.dataSource - DatabaseTransactionManager.current().commit() - DatabaseTransactionManager.current().close() - Strand.sleep(millis) - DatabaseTransactionManager.dataSource = db - DatabaseTransactionManager.newTransaction() - } else Strand.sleep(millis) - } } // These fields shouldn't be serialised, so they are marked @Transient. @@ -174,8 +157,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, "@${InitiatingFlow::class.java.simpleName} sub-flow." ) } - createNewSession(otherParty, sessionFlow) val flowSession = FlowSessionImpl(otherParty) + createNewSession(otherParty, flowSession, sessionFlow) flowSession.stateMachine = this flowSession.sessionFlow = sessionFlow return flowSession @@ -259,12 +242,16 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, throw IllegalStateException("We were resumed after waiting for $hash but it wasn't found in our local storage") } + // Provide a mechanism to sleep within a Strand without locking any transactional state. + // This checkpoints, since we cannot undo any database writes up to this point. + @Suspendable + override fun sleepUntil(until: Instant) { + suspend(Sleep(until, this)) + } + // TODO Dummy implementation of access to application specific permission controls and audit logging override fun checkFlowPermission(permissionName: String, extraAuditData: Map) { - // This is a hack to allow cash app access list of permitted issuer currency. - // TODO: replace this with cordapp configuration. - val config = serviceHub.configuration as? FullNodeConfiguration - val permissionGranted = config?.extraAdvertisedServiceIds?.contains(permissionName) ?: true + val permissionGranted = true // TODO define permission control service on ServiceHubInternal and actually check authorization. val checkPermissionEvent = FlowPermissionAuditEvent( serviceHub.clock.instant(), flowInitiator, @@ -275,13 +262,14 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, permissionName, permissionGranted) serviceHub.auditService.recordAuditEvent(checkPermissionEvent) + @Suppress("ConstantConditionIf") if (!permissionGranted) { throw FlowPermissionException("User $flowInitiator not permissioned for $permissionName on flow $id") } } // TODO Dummy implementation of access to application specific audit logging - override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map): Unit { + override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) { val flowAuditEvent = FlowAppAuditEvent( serviceHub.clock.instant(), flowInitiator, @@ -302,6 +290,22 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, FlowStackSnapshotFactory.instance.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id) } + @Suspendable + override fun receiveAll(sessions: Map>, sessionFlow: FlowLogic<*>): Map> { + val requests = ArrayList>() + for ((session, receiveType) in sessions) { + val sessionInternal = getConfirmedSession(session.counterparty, sessionFlow) + requests.add(ReceiveOnly(sessionInternal, SessionData::class.java, receiveType)) + } + val receivedMessages = ReceiveAll(requests).suspendAndExpectReceive(suspend) + val result = LinkedHashMap>() + for ((sessionInternal, requestAndMessage) in receivedMessages) { + val message = requestAndMessage.message.confirmReceiveType(requestAndMessage.request) + result[sessionInternal.flowSession] = message.checkPayloadIs(requestAndMessage.request.userReceiveType as Class) + } + return result + } + /** * This method will suspend the state machine and wait for incoming session init response from other party. */ @@ -325,7 +329,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, is FlowSessionState.Initiated -> sessionState.peerSessionId else -> throw IllegalStateException("We've somehow held onto a non-initiated session: $session") } - return SessionData(peerSessionId, payload) + return SessionData(peerSessionId, payload.serialize(context = SerializationDefaults.P2P_CONTEXT)) } @Suspendable @@ -365,10 +369,11 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun createNewSession( otherParty: Party, + flowSession: FlowSession, sessionFlow: FlowLogic<*> ) { logger.trace { "Creating a new session with $otherParty" } - val session = FlowSessionInternal(sessionFlow, random63BitValue(), null, FlowSessionState.Uninitiated(otherParty)) + val session = FlowSessionInternal(sessionFlow, flowSession, random63BitValue(), null, FlowSessionState.Uninitiated(otherParty)) openSessions[Pair(sessionFlow, otherParty)] = session } @@ -380,19 +385,14 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, waitForConfirmation: Boolean, retryable: Boolean = false ): FlowSessionInternal { - val session = openSessions[Pair(sessionFlow, otherParty)] - if (session == null) { - throw IllegalStateException("Expected an Uninitiated session for $otherParty") - } - val state = session.state - if (state !is FlowSessionState.Uninitiated) { - throw IllegalStateException("Tried to initiate a session $session, but it's already initiating/initiated") - } + val session = openSessions[Pair(sessionFlow, otherParty)] ?: throw IllegalStateException("Expected an Uninitiated session for $otherParty") + val state = session.state as? FlowSessionState.Uninitiated ?: throw IllegalStateException("Tried to initiate a session $session, but it's already initiating/initiated") logger.trace { "Initiating a new session with ${state.otherParty}" } session.state = FlowSessionState.Initiating(state.otherParty) session.retryable = retryable val (version, initiatingFlowClass) = session.flow.javaClass.flowVersionAndInitiatingClass - val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, session.flow.javaClass.appName, firstPayload) + val payloadBytes = firstPayload?.serialize(context = SerializationDefaults.P2P_CONTEXT) + val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, session.flow.javaClass.appName, payloadBytes) sendInternal(session, sessionInit) if (waitForConfirmation) { session.waitForConfirmation() @@ -405,11 +405,16 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest) } + private val suspend : ReceiveAll.Suspend = object : ReceiveAll.Suspend { + @Suspendable + override fun invoke(request: FlowIORequest) { + suspend(request) + } + } + @Suspendable private fun ReceiveRequest<*>.suspendAndExpectReceive(): ReceivedSessionMessage<*> { - fun pollForMessage() = session.receivedMessages.poll() - - val polledMessage = pollForMessage() + val polledMessage = session.receivedMessages.poll() return if (polledMessage != null) { if (this is SendAndReceive) { // Since we've already received the message, we downgrade to a send only to get the payload out and not @@ -420,7 +425,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } else { // Suspend while we wait for a receive suspend(this) - pollForMessage() ?: + session.receivedMessages.poll() ?: throw IllegalStateException("Was expecting a ${receiveType.simpleName} but instead got nothing for $this") } } @@ -430,8 +435,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, val session = receiveRequest.session val receiveType = receiveRequest.receiveType if (receiveType.isInstance(message)) { - @Suppress("UNCHECKED_CAST") - return this as ReceivedSessionMessage + return uncheckedCast(this) } else if (message is SessionEnd) { openSessions.values.remove(session) if (message is ErrorSessionEnd) { @@ -460,7 +464,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun suspend(ioRequest: FlowIORequest) { // We have to pass the thread local database transaction across via a transient field as the fiber park // swaps them out. - txTrampoline = DatabaseTransactionManager.setThreadLocalTx(null) + txTrampoline = DatabaseTransactionManager.setThreadLocalTx(null) if (ioRequest is WaitingRequest) waitingForResponse = ioRequest @@ -481,6 +485,10 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } + if (exceptionDuringSuspend == null && ioRequest is Sleep) { + // Sleep on the fiber. This will not sleep if it's in the past. + Strand.sleep(Duration.between(Instant.now(), ioRequest.until).toNanos(), TimeUnit.NANOSECONDS) + } createTransaction() // TODO Now that we're throwing outside of the suspend the FlowLogic can catch it. We need Quasar to terminate // the fiber when exceptions occur inside a suspend. @@ -519,29 +527,30 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } -@Suppress("UNCHECKED_CAST") -val Class>.flowVersionAndInitiatingClass: Pair>> get() { - var current: Class<*> = this - var found: Pair>>? = null - while (true) { - val annotation = current.getDeclaredAnnotation(InitiatingFlow::class.java) - if (annotation != null) { - if (found != null) throw IllegalArgumentException("${InitiatingFlow::class.java.name} can only be annotated once") - require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" } - found = annotation.version to (current as Class>) +val Class>.flowVersionAndInitiatingClass: Pair>> + get() { + var current: Class<*> = this + var found: Pair>>? = null + while (true) { + val annotation = current.getDeclaredAnnotation(InitiatingFlow::class.java) + if (annotation != null) { + if (found != null) throw IllegalArgumentException("${InitiatingFlow::class.java.name} can only be annotated once") + require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" } + found = annotation.version to uncheckedCast(current) + } + current = current.superclass + ?: return found + ?: throw IllegalArgumentException("$name, as a flow that initiates other flows, must be annotated with " + + "${InitiatingFlow::class.java.name}. See https://docs.corda.net/api-flows.html#flowlogic-annotations.") } - current = current.superclass - ?: return found - ?: throw IllegalArgumentException("$name, as a flow that initiates other flows, must be annotated with " + - "${InitiatingFlow::class.java.name}. See https://docs.corda.net/api-flows.html#flowlogic-annotations.") } -} -val Class>.appName: String get() { - val jarFile = Paths.get(protectionDomain.codeSource.location.toURI()) - return if (jarFile.isRegularFile() && jarFile.toString().endsWith(".jar")) { - jarFile.fileName.toString().removeSuffix(".jar") - } else { - "" +val Class>.appName: String + get() { + val jarFile = Paths.get(protectionDomain.codeSource.location.toURI()) + return if (jarFile.isRegularFile() && jarFile.toString().endsWith(".jar")) { + jarFile.fileName.toString().removeSuffix(".jar") + } else { + "" + } } -} diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt index fc103e6dca..c321d3768a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt @@ -5,7 +5,10 @@ import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.Party import net.corda.core.internal.castIfPossible import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.UntrustworthyData +import java.io.IOException @CordaSerializable interface SessionMessage @@ -25,7 +28,7 @@ data class SessionInit(val initiatorSessionId: Long, val initiatingFlowClass: String, val flowVersion: Int, val appName: String, - val firstPayload: Any?) : SessionMessage + val firstPayload: SerializedBytes?) : SessionMessage data class SessionConfirm(override val initiatorSessionId: Long, val initiatedSessionId: Long, @@ -34,7 +37,7 @@ data class SessionConfirm(override val initiatorSessionId: Long, data class SessionReject(override val initiatorSessionId: Long, val errorMessage: String) : SessionInitResponse -data class SessionData(override val recipientSessionId: Long, val payload: Any) : ExistingSessionMessage +data class SessionData(override val recipientSessionId: Long, val payload: SerializedBytes) : ExistingSessionMessage data class NormalSessionEnd(override val recipientSessionId: Long) : SessionEnd @@ -42,8 +45,15 @@ data class ErrorSessionEnd(override val recipientSessionId: Long, val errorRespo data class ReceivedSessionMessage(val sender: Party, val message: M) -fun ReceivedSessionMessage.checkPayloadIs(type: Class): UntrustworthyData { - return type.castIfPossible(message.payload)?.let { UntrustworthyData(it) } ?: +fun ReceivedSessionMessage.checkPayloadIs(type: Class): UntrustworthyData { + val payloadData: T = try { + val serializer = SerializationDefaults.SERIALIZATION_FACTORY + serializer.deserialize(message.payload, type, SerializationDefaults.P2P_CONTEXT) + } catch (ex: Exception) { + throw IOException("Payload invalid", ex) + } + return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) } ?: throw UnexpectedFlowEndException("We were expecting a ${type.name} from $sender but we instead got a " + - "${message.payload.javaClass.name} (${message.payload})") + "${payloadData.javaClass.name} (${payloadData})") + } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index dc2a8342cb..74697821e9 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -9,14 +9,13 @@ import com.codahale.metrics.Gauge import com.esotericsoftware.kryo.KryoException import com.google.common.collect.HashMultimap import com.google.common.util.concurrent.MoreExecutors +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.internal.castIfPossible +import net.corda.core.internal.* import net.corda.core.messaging.DataFeed import net.corda.core.serialization.SerializationDefaults.CHECKPOINT_CONTEXT import net.corda.core.serialization.SerializationDefaults.SERIALIZATION_FACTORY @@ -78,7 +77,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStorage: CheckpointStorage, val executor: AffinityExecutor, val database: CordaPersistence, - private val unfinishedFibers: ReusableLatch = ReusableLatch()) { + private val unfinishedFibers: ReusableLatch = ReusableLatch(), + private val classloader: ClassLoader = javaClass.classLoader) { inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor) @@ -149,10 +149,9 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, /** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */ fun

, T> findStateMachines(flowClass: Class

): List>> { - @Suppress("UNCHECKED_CAST") return mutex.locked { stateMachines.keys.mapNotNull { - flowClass.castIfPossible(it.logic)?.let { it to (it.stateMachine as FlowStateMachineImpl).resultFuture } + flowClass.castIfPossible(it.logic)?.let { it to uncheckedCast, FlowStateMachineImpl>(it.stateMachine).resultFuture } } } } @@ -287,7 +286,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } private fun onSessionMessage(message: ReceivedMessage) { - val sessionMessage = message.data.deserialize() + val sessionMessage = try { + message.data.deserialize() + } catch (ex: Exception) { + logger.error("Received corrupt SessionMessage data from ${message.peer}") + return + } val sender = serviceHub.networkMapCache.getPeerByLegalName(message.peer) if (sender != null) { when (sessionMessage) { @@ -343,8 +347,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, // commit but a counterparty flow has ended with an error (in which case our flow also has to end) private fun resumeOnMessage(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean { val waitingForResponse = session.fiber.waitingForResponse - return (waitingForResponse as? ReceiveRequest<*>)?.session === session || - waitingForResponse is WaitForLedgerCommit && message is ErrorSessionEnd + return waitingForResponse?.shouldResume(message, session) ?: false } private fun onSessionInit(sessionInit: SessionInit, receivedMessage: ReceivedMessage, sender: Party) { @@ -363,6 +366,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } val session = FlowSessionInternal( flow, + flowSession, random63BitValue(), sender, FlowSessionState.Initiated(sender, senderSessionId, FlowInfo(senderFlowVersion, sessionInit.appName))) @@ -389,7 +393,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } val (ourFlowVersion, appName) = when (initiatedFlowFactory) { - // The flow version for the core flows is the platform version + // The flow version for the core flows is the platform version is InitiatedFlowFactory.Core -> serviceHub.myInfo.platformVersion to "corda" is InitiatedFlowFactory.CorDapp -> initiatedFlowFactory.flowVersion to initiatedFlowFactory.appName } @@ -402,7 +406,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private fun getInitiatedFlowFactory(sessionInit: SessionInit): InitiatedFlowFactory<*> { val initiatingFlowClass = try { - Class.forName(sessionInit.initiatingFlowClass).asSubclass(FlowLogic::class.java) + Class.forName(sessionInit.initiatingFlowClass, true, classloader).asSubclass(FlowLogic::class.java) } catch (e: ClassNotFoundException) { throw SessionRejectException("Don't know ${sessionInit.initiatingFlowClass}") } catch (e: ClassCastException) { @@ -576,6 +580,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, when (ioRequest) { is SendRequest -> processSendRequest(ioRequest) is WaitForLedgerCommit -> processWaitForCommitRequest(ioRequest) + is Sleep -> processSleepRequest(ioRequest) } } @@ -613,6 +618,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } + private fun processSleepRequest(ioRequest: Sleep) { + // Resume the fiber now we have checkpointed, so we can sleep on the Fiber. + resumeFiber(ioRequest.fiber) + } + private fun sendSessionMessage(party: Party, message: SessionMessage, fiber: FlowStateMachineImpl<*>? = null, retryId: Long? = null) { val partyInfo = serviceHub.networkMapCache.getPartyInfo(party) ?: throw IllegalArgumentException("Don't know about party $party") @@ -623,8 +633,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val serialized = try { message.serialize() } catch (e: Exception) { - when(e) { - // Handling Kryo and AMQP serialization problems. Unfortunately the two exception types do not share much of a common exception interface. + when (e) { + // Handling Kryo and AMQP serialization problems. Unfortunately the two exception types do not share much of a common exception interface. is KryoException, is NotSerializableException -> { if (message !is ErrorSessionEnd || message.errorResponse == null) throw e @@ -644,6 +654,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } -class SessionRejectException(val rejectMessage: String, val logMessage: String) : Exception() { +class SessionRejectException(val rejectMessage: String, val logMessage: String) : CordaException(rejectMessage) { constructor(message: String) : this(message, message) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index 807bcc9415..f0c1e17823 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -20,10 +20,12 @@ import net.corda.core.serialization.serialize import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.* import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.NODE_DATABASE_PREFIX import java.security.PublicKey import javax.persistence.Entity +import javax.persistence.Table import kotlin.concurrent.thread /** @@ -33,24 +35,19 @@ import kotlin.concurrent.thread */ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey, - cluster: BFTSMaRt.Cluster = distributedCluster) : NotaryService() { + private val bftSMaRtConfig: BFTSMaRtConfiguration, + cluster: BFTSMaRt.Cluster) : NotaryService() { companion object { - val type = SimpleNotaryService.type.getSubType("bft") + val id = constructId(validating = false, bft = true) private val log = loggerFor() - private val distributedCluster = object : BFTSMaRt.Cluster { - override fun waitUntilAllReplicasHaveInitialized() { - log.warn("A replica may still be initializing, in which case the upcoming consensus change may cause it to spin.") - } - } } private val client: BFTSMaRt.Client private val replicaHolder = SettableFuture.create() init { - require(services.configuration.bftSMaRt.isValid()) { "bftSMaRt replicaId must be specified in the configuration" } - client = BFTSMaRtConfig(services.configuration.notaryClusterAddresses, services.configuration.bftSMaRt.debug, services.configuration.bftSMaRt.exposeRaces).use { - val replicaId = services.configuration.bftSMaRt.replicaId + client = BFTSMaRtConfig(bftSMaRtConfig.clusterAddresses, bftSMaRtConfig.debug, bftSMaRtConfig.exposeRaces).use { + val replicaId = bftSMaRtConfig.replicaId val configHandle = it.handle() // Replica startup must be in parallel with other replicas, otherwise the constructor may not return: thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { @@ -66,7 +63,7 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, } fun waitUntilReplicaHasInitialized() { - log.debug { "Waiting for replica ${services.configuration.bftSMaRt.replicaId} to initialize." } + log.debug { "Waiting for replica ${bftSMaRtConfig.replicaId} to initialize." } replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return. } @@ -96,36 +93,37 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, } @Entity - @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}bft_smart_notary_committed_states") + @Table(name = "${NODE_DATABASE_PREFIX}bft_smart_notary_committed_states") class PersistedCommittedState(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentUniquenessProvider.PersistentParty) : PersistentUniquenessProvider.PersistentUniqueness(id, consumingTxHash, consumingIndex, party) - fun createMap(): AppendOnlyPersistentMap = - AppendOnlyPersistentMap( - toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, - fromPersistentEntity = { - //TODO null check will become obsolete after making DB/JPA columns not nullable - val txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") - val index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index") - Pair(StateRef(txhash = SecureHash.parse(txId), index = index), + private fun createMap(): AppendOnlyPersistentMap { + return AppendOnlyPersistentMap( + toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, + fromPersistentEntity = { + //TODO null check will become obsolete after making DB/JPA columns not nullable + val txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") + val index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index") + Pair(StateRef(txhash = SecureHash.parse(txId), index = index), UniquenessProvider.ConsumingTx( id = SecureHash.parse(it.consumingTxHash), inputIndex = it.consumingIndex, requestingParty = Party( name = CordaX500Name.parse(it.party.name), owningKey = parsePublicKeyBase58(it.party.owningKey)))) - }, - toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> - PersistedCommittedState( - id = PersistentStateRef(txHash.toString(), index), - consumingTxHash = id.toString(), - consumingIndex = inputIndex, - party = PersistentUniquenessProvider.PersistentParty(requestingParty.name.toString(), - requestingParty.owningKey.toBase58String()) - ) - }, - persistentEntityClass = PersistedCommittedState::class.java - ) + }, + toPersistentEntity = { (txHash, index): StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> + PersistedCommittedState( + id = PersistentStateRef(txHash.toString(), index), + consumingTxHash = id.toString(), + consumingIndex = inputIndex, + party = PersistentUniquenessProvider.PersistentParty(requestingParty.name.toString(), + requestingParty.owningKey.toBase58String()) + ) + }, + persistentEntityClass = PersistedCommittedState::class.java + ) + } private class Replica(config: BFTSMaRtConfig, replicaId: Int, diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt index 7a031540f9..20e69d5aae 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt @@ -1,5 +1,6 @@ package net.corda.node.services.transactions +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.addShutdownHook import java.io.Closeable import java.nio.file.Path @@ -31,8 +32,7 @@ open class PathManager>(path: Path) : Closeable { fun handle(): T { handleCounter.incrementAndGet() - @Suppress("UNCHECKED_CAST") - return this as T + return uncheckedCast(this) } override fun close() { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index b17b9902fc..f669ae3426 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -24,7 +24,7 @@ import javax.persistence.* class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsToken() { @MappedSuperclass - open class PersistentUniqueness ( + open class PersistentUniqueness( @EmbeddedId var id: PersistentStateRef = PersistentStateRef(), @@ -45,11 +45,11 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok @Column(name = "requesting_party_key", length = 255) var owningKey: String = "" - ): Serializable + ) : Serializable @Entity @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_commit_log") - class PersistentNotaryCommit(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentParty): + class PersistentNotaryCommit(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentParty) : PersistentUniqueness(id, consumingTxHash, consumingIndex, party) @@ -77,37 +77,37 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok name = CordaX500Name.parse(it.party.name), owningKey = parsePublicKeyBase58(it.party.owningKey)))) }, - toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty) : UniquenessProvider.ConsumingTx -> + toPersistentEntity = { (txHash, index): StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> PersistentNotaryCommit( id = PersistentStateRef(txHash.toString(), index), consumingTxHash = id.toString(), consumingIndex = inputIndex, party = PersistentParty(requestingParty.name.toString(), requestingParty.owningKey.toBase58String()) ) - }, - persistentEntityClass = PersistentNotaryCommit::class.java - ) - } + }, + persistentEntityClass = PersistentNotaryCommit::class.java + ) + } override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { val conflict = mutex.locked { - val conflictingStates = LinkedHashMap() - for (inputState in states) { - val consumingTx = committedStates.get(inputState) - if (consumingTx != null) conflictingStates[inputState] = consumingTx - } - if (conflictingStates.isNotEmpty()) { - log.debug("Failure, input states already committed: ${conflictingStates.keys}") - UniquenessProvider.Conflict(conflictingStates) - } else { - states.forEachIndexed { i, stateRef -> - committedStates[stateRef] = UniquenessProvider.ConsumingTx(txId, i, callerIdentity) - } - log.debug("Successfully committed all input states: $states") - null - } + val conflictingStates = LinkedHashMap() + for (inputState in states) { + val consumingTx = committedStates.get(inputState) + if (consumingTx != null) conflictingStates[inputState] = consumingTx + } + if (conflictingStates.isNotEmpty()) { + log.debug("Failure, input states already committed: ${conflictingStates.keys}") + UniquenessProvider.Conflict(conflictingStates) + } else { + states.forEachIndexed { i, stateRef -> + committedStates[stateRef] = UniquenessProvider.ConsumingTx(txId, i, callerIdentity) } + log.debug("Successfully committed all input states: $states") + null + } + } if (conflict != null) throw UniquenessException(conflict) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index 6e0916e3cc..797f9aa7de 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -5,18 +5,23 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.RaftConfig import java.security.PublicKey /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { +class RaftNonValidatingNotaryService(override val services: ServiceHubInternal, + override val notaryIdentityKey: PublicKey, + raftConfig: RaftConfig) : TrustedAuthorityNotaryService() { companion object { - val type = SimpleNotaryService.type.getSubType("raft") + val id = constructId(validating = false, raft = true) } override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) - override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services, raftConfig) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = NonValidatingNotaryFlow(otherPartySession, this) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { + return NonValidatingNotaryFlow(otherPartySession, this) + } override fun start() { uniquenessProvider.start() diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index ed349f1cc6..130f954a50 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -26,16 +26,14 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.RaftConfig import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.CordaPersistence import net.corda.nodeapi.config.SSLConfiguration import java.nio.file.Path import java.util.concurrent.CompletableFuture import javax.annotation.concurrent.ThreadSafe -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Id -import javax.persistence.Lob +import javax.persistence.* /** * A uniqueness provider that records committed input states in a distributed collection replicated and @@ -46,7 +44,7 @@ import javax.persistence.Lob * to the cluster leader to be actioned. */ @ThreadSafe -class RaftUniquenessProvider(private val services: ServiceHubInternal) : UniquenessProvider, SingletonSerializeAsToken() { +class RaftUniquenessProvider(private val services: ServiceHubInternal, private val raftConfig: RaftConfig) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = loggerFor() @@ -67,7 +65,7 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen } @Entity - @javax.persistence.Table(name = "notary_committed_states") + @Table(name = "notary_committed_states") class RaftState( @Id @Column @@ -81,13 +79,6 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen /** Directory storing the Raft log and state machine snapshots */ private val storagePath: Path = services.configuration.baseDirectory /** Address of the Copycat node run by this Corda node */ - private val myAddress = services.configuration.notaryNodeAddress - ?: throw IllegalArgumentException("notaryNodeAddress must be specified in configuration") - /** - * List of node addresses in the existing Copycat cluster. At least one active node must be - * provided to join the cluster. If empty, a new cluster will be bootstrapped. - */ - private val clusterAddresses = services.configuration.notaryClusterAddresses /** The database to store the state machine state in */ private val db: CordaPersistence = services.database /** SSL configuration */ @@ -96,7 +87,6 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen private lateinit var _clientFuture: CompletableFuture private lateinit var server: CopycatServer - /** * Copycat clients are responsible for connecting to the cluster and submitting commands and queries that operate * on the cluster's replicated state machine. @@ -107,8 +97,9 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen fun start() { log.info("Creating Copycat server, log stored in: ${storagePath.toFile()}") val stateMachineFactory = { - DistributedImmutableMap(db, RaftUniquenessProvider.Companion::createMap) } - val address = Address(myAddress.host, myAddress.port) + DistributedImmutableMap(db, RaftUniquenessProvider.Companion::createMap) + } + val address = raftConfig.nodeAddress.let { Address(it.host, it.port) } val storage = buildStorage(storagePath) val transport = buildTransport(transportConfiguration) val serializer = Serializer().apply { @@ -120,6 +111,7 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen serializer: Serializer) { writeMap(obj.entries, buffer, serializer) } + override fun read(type: Class>, buffer: BufferInput>, serializer: Serializer): DistributedImmutableMap.Commands.PutAll { @@ -142,9 +134,9 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen .withSerializer(serializer) .build() - val serverFuture = if (clusterAddresses.isNotEmpty()) { - log.info("Joining an existing Copycat cluster at $clusterAddresses") - val cluster = clusterAddresses.map { Address(it.host, it.port) } + val serverFuture = if (raftConfig.clusterAddresses.isNotEmpty()) { + log.info("Joining an existing Copycat cluster at ${raftConfig.clusterAddresses}") + val cluster = raftConfig.clusterAddresses.map { Address(it.host, it.port) } server.join(cluster) } else { log.info("Bootstrapping a Copycat cluster at $address") diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index 10da5581fe..4af9e2be74 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -5,18 +5,23 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.RaftConfig import java.security.PublicKey /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { +class RaftValidatingNotaryService(override val services: ServiceHubInternal, + override val notaryIdentityKey: PublicKey, + raftConfig: RaftConfig) : TrustedAuthorityNotaryService() { companion object { - val type = ValidatingNotaryService.type.getSubType("raft") + val id = constructId(validating = true, raft = true) } override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) - override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services, raftConfig) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = ValidatingNotaryFlow(otherPartySession, this) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { + return ValidatingNotaryFlow(otherPartySession, this) + } override fun start() { uniquenessProvider.start() diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index e62bcecb85..cb4401cae5 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -4,16 +4,11 @@ import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.ServiceHubInternal import java.security.PublicKey /** A simple Notary service that does not perform transaction validation */ class SimpleNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { - companion object { - val type = ServiceType.notary.getSubType("simple") - } - override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = PersistentUniquenessProvider() diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index cf871f7f0f..8ce7ba6365 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -1,10 +1,11 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionWithSignatures import java.security.SignatureException /** @@ -15,9 +16,8 @@ import java.security.SignatureException */ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) { /** - * The received transaction is checked for contract-validity, which requires fully resolving it into a - * [TransactionForVerification], for which the caller also has to to reveal the whole transaction - * dependency chain. + * Fully resolves the received transaction and its dependencies, runs contract verification logic and checks that + * the transaction in question has all required signatures apart from the notary's. */ @Suspendable override fun receiveAndVerifyTx(): TransactionParts { @@ -25,9 +25,13 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) val notary = stx.notary checkNotary(notary) - checkSignatures(stx) - val wtx = stx.tx - return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow, notary!!) + val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) + null + else + stx.tx.timeWindow + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + checkSignatures(transactionWithSignatures) + return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { is TransactionVerificationException, @@ -37,10 +41,10 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor } } - private fun checkSignatures(stx: SignedTransaction) { + private fun checkSignatures(tx: TransactionWithSignatures) { try { - stx.verifySignaturesExcept(service.notaryIdentityKey) - } catch(e: SignatureException) { + tx.verifySignaturesExcept(service.notaryIdentityKey) + } catch (e: SignatureException) { throw NotaryException(NotaryError.TransactionInvalid(e)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index c13b9186f1..6c7e36046b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -4,16 +4,14 @@ import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.ServiceHubInternal import java.security.PublicKey /** A Notary service that validates the transaction chain of the submitted transaction before committing it */ class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { companion object { - val type = ServiceType.notary.getSubType("validating") + val id = constructId(validating = true) } - override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = PersistentUniquenessProvider() diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 575d1cbfc9..7b8816b361 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -3,6 +3,7 @@ package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.identity.AbstractParty +import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* @@ -35,11 +36,11 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() - private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate + private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED - override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria) : Collection { + override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection { log.trace { "Parsing VaultQueryCriteria: $criteria" } val predicateSet = mutableSetOf() @@ -47,7 +48,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class predicateSet.add(criteriaBuilder.and(vaultStates.get("lockId").isNull)) QueryCriteria.SoftLockingType.LOCKED_ONLY -> @@ -55,7 +56,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { require(softLocking.lockIds.isNotEmpty()) { "Must specify one or more lockIds" } predicateSet.add(criteriaBuilder.or(vaultStates.get("lockId").isNull, - vaultStates.get("lockId").`in`(softLocking.lockIds.map { it.toString() }))) + vaultStates.get("lockId").`in`(softLocking.lockIds.map { it.toString() }))) } QueryCriteria.SoftLockingType.SPECIFIED -> { require(softLocking.lockIds.isNotEmpty()) { "Must specify one or more lockIds" } @@ -111,8 +112,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { - @Suppress("UNCHECKED_CAST") - val literal = columnPredicate.rightLiteral as Comparable? + val literal: Comparable? = uncheckedCast(columnPredicate.rightLiteral) @Suppress("UNCHECKED_CAST") column as Path?> when (columnPredicate.operator) { @@ -139,10 +139,8 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { @Suppress("UNCHECKED_CAST") column as Path?> - @Suppress("UNCHECKED_CAST") - val fromLiteral = columnPredicate.rightFromLiteral as Comparable? - @Suppress("UNCHECKED_CAST") - val toLiteral = columnPredicate.rightToLiteral as Comparable? + val fromLiteral: Comparable? = uncheckedCast(columnPredicate.rightFromLiteral) + val toLiteral: Comparable? = uncheckedCast(columnPredicate.rightToLiteral) criteriaBuilder.between(column, fromLiteral, toLiteral) } is ColumnPredicate.NullExpression -> { @@ -156,7 +154,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class parseExpression(entityRoot: Root, expression: CriteriaExpression, predicateSet: MutableSet) { - if (expression is CriteriaExpression.AggregateFunctionExpression) { + if (expression is CriteriaExpression.AggregateFunctionExpression) { parseAggregateFunction(entityRoot, expression) } else { predicateSet.add(parseExpression(entityRoot, expression) as Predicate) @@ -223,7 +221,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { + override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection { log.trace { "Parsing FungibleAssetQueryCriteria: $criteria" } val predicateSet = mutableSetOf() @@ -267,7 +265,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { + override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection { log.trace { "Parsing LinearStateQueryCriteria: $criteria" } val predicateSet = mutableSetOf() @@ -316,8 +314,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class if (message.contains("Not an entity")) throw VaultQueryException(""" @@ -388,8 +385,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status)) } - } - else { + } else { commonPredicates.put(predicateID, criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status)) } } @@ -419,7 +415,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class val (entityStateClass, entityStateAttributeParent, entityStateAttributeChild) = - when(sortAttribute) { + when (sortAttribute) { is SortAttribute.Standard -> parse(sortAttribute.attribute) is SortAttribute.Custom -> Triple(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName, null) } @@ -453,8 +449,8 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, String, String?> { - val entityClassAndColumnName : Triple, String, String?> = - when(sortAttribute) { + val entityClassAndColumnName: Triple, String, String?> = + when (sortAttribute) { is Sort.CommonStateAttribute -> { Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeParent, sortAttribute.attributeChild) } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt deleted file mode 100644 index 27afbc2040..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ /dev/null @@ -1,206 +0,0 @@ -package net.corda.node.services.vault - -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.SecureHash -import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.Vault -import net.corda.core.node.services.VaultQueryException -import net.corda.core.node.services.VaultQueryService -import net.corda.core.node.services.VaultService -import net.corda.core.node.services.vault.* -import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria -import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.deserialize -import net.corda.core.utilities.debug -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.trace -import net.corda.node.services.persistence.HibernateConfiguration -import net.corda.node.utilities.DatabaseTransactionManager -import org.hibernate.Session -import rx.Observable -import java.lang.Exception -import java.util.* -import javax.persistence.Tuple - -class HibernateVaultQueryImpl(val hibernateConfig: HibernateConfiguration, - val vault: VaultService) : SingletonSerializeAsToken(), VaultQueryService { - companion object { - val log = loggerFor() - } - - private var sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - private var criteriaBuilder = sessionFactory.criteriaBuilder - - /** - * Maintain a list of contract state interfaces to concrete types stored in the vault - * for usage in generic queries of type queryBy or queryBy> - */ - private val contractStateTypeMappings = bootstrapContractStateTypes() - - init { - vault.rawUpdates.subscribe { update -> - update.produced.forEach { - val concreteType = it.state.data.javaClass - log.trace { "State update of type: $concreteType" } - val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) } - if (!seen) { - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { - val contractInterface = contractStateTypeMappings.getOrPut(it.name, { mutableSetOf() }) - contractInterface.add(concreteType.name) - } - } - } - } - } - - @Throws(VaultQueryException::class) - override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { - log.info("Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting") - - // refresh to include any schemas registered after initial VQ service initialisation - sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - criteriaBuilder = sessionFactory.criteriaBuilder - - // calculate total results where a page specification has been defined - var totalStates = -1L - if (!paging.isDefault) { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) - val results = queryBy(contractStateType, criteria.and(countCriteria)) - totalStates = results.otherResults[0] as Long - } - - val session = getSession() - - session.use { - val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) - val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) - - // TODO: revisit (use single instance of parser for all queries) - val criteriaParser = HibernateQueryCriteriaParser(contractStateType, contractStateTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) - - try { - // parse criteria and build where predicates - criteriaParser.parse(criteria, sorting) - - // prepare query for execution - val query = session.createQuery(criteriaQuery) - - // pagination checks - if (!paging.isDefault) { - // pagination - if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") - if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") - } - - query.firstResult = (paging.pageNumber - 1) * paging.pageSize - query.maxResults = paging.pageSize + 1 // detection too many results - - // execution - val results = query.resultList - - // final pagination check (fail-fast on too many results when no pagination specified) - if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) - throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") - - val statesAndRefs: MutableList> = mutableListOf() - val statesMeta: MutableList = mutableListOf() - val otherResults: MutableList = mutableListOf() - - results.asSequence() - .forEachIndexed { index, result -> - if (result[0] is VaultSchemaV1.VaultStates) { - if (!paging.isDefault && index == paging.pageSize) // skip last result if paged - return@forEachIndexed - val vaultState = result[0] as VaultSchemaV1.VaultStates - val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) - val state = vaultState.contractState.deserialize>(context = STORAGE_CONTEXT) - statesMeta.add(Vault.StateMetadata(stateRef, - vaultState.contractStateClassName, - vaultState.recordedTime, - vaultState.consumedTime, - vaultState.stateStatus, - vaultState.notary, - vaultState.lockId, - vaultState.lockUpdateTime)) - statesAndRefs.add(StateAndRef(state, stateRef)) - } - else { - // TODO: improve typing of returned other results - log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } - otherResults.addAll(result.toArray().asList()) - } - } - - return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) - } catch (e: Exception) { - log.error(e.message) - throw e.cause ?: e - } - } - } - - private val mutex = ThreadBox({ vault.updatesPublisher }) - - @Throws(VaultQueryException::class) - override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { - return mutex.locked { - val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) - @Suppress("UNCHECKED_CAST") - val updates = vault.updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) } as Observable> - DataFeed(snapshotResults, updates) - } - } - - private fun getSession(): Session { - return sessionFactory.withOptions(). - connection(DatabaseTransactionManager.current().connection). - openSession() - } - - /** - * Derive list from existing vault states and then incrementally update using vault observables - */ - fun bootstrapContractStateTypes(): MutableMap> { - val criteria = criteriaBuilder.createQuery(String::class.java) - val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) - criteria.select(vaultStates.get("contractStateClassName")).distinct(true) - val session = getSession() - session.use { - val query = session.createQuery(criteria) - val results = query.resultList - val distinctTypes = results.map { it } - - val contractInterfaceToConcreteTypes = mutableMapOf>() - distinctTypes.forEach { type -> - @Suppress("UNCHECKED_CAST") - val concreteType = Class.forName(type) as Class - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { - val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) - contractInterface.add(concreteType.name) - } - } - return contractInterfaceToConcreteTypes - } - } - - private fun deriveContractInterfaces(clazz: Class): Set> { - val myInterfaces: MutableSet> = mutableSetOf() - clazz.interfaces.forEach { - if (!it.equals(ContractState::class.java)) { - @Suppress("UNCHECKED_CAST") - myInterfaces.add(it as Class) - myInterfaces.addAll(deriveContractInterfaces(it)) - } - } - return myInterfaces - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 196dca299f..d5a189b0ee 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -4,16 +4,17 @@ import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash -import net.corda.core.internal.ThreadBox -import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.tee -import net.corda.core.node.ServiceHub +import net.corda.core.internal.* +import net.corda.core.node.StateLoader +import net.corda.core.node.services.* import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault -import net.corda.core.node.services.VaultService import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.SortAttribute +import net.corda.core.messaging.DataFeed +import net.corda.core.node.services.VaultQueryException +import net.corda.core.node.services.vault.* import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SingletonSerializeAsToken @@ -23,15 +24,30 @@ import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.* +import net.corda.node.services.api.VaultServiceInternal +import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit +import net.corda.node.utilities.currentDBSession import net.corda.node.utilities.wrapWithDatabaseTransaction +import org.hibernate.Session import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey +import java.time.Clock import java.time.Instant import java.util.* +import javax.persistence.Tuple +import javax.persistence.criteria.CriteriaBuilder +import javax.persistence.criteria.CriteriaUpdate +import javax.persistence.criteria.Predicate +import javax.persistence.criteria.Root + +private fun CriteriaBuilder.executeUpdate(session: Session, configure: Root<*>.(CriteriaUpdate<*>) -> Any?) = createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java).let { update -> + update.from(VaultSchemaV1.VaultStates::class.java).run { configure(update) } + session.createQuery(update).executeUpdate() +} /** * Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when @@ -43,7 +59,7 @@ import java.util.* * TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time. * TODO: have transaction storage do some caching. */ -class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsToken(), VaultService { +class NodeVaultService(private val clock: Clock, private val keyManagementService: KeyManagementService, private val stateLoader: StateLoader, hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultServiceInternal { private companion object { val log = loggerFor() @@ -67,14 +83,14 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT val consumedStateRefs = update.consumed.map { it.ref } log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." } - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() producedStateRefsMap.forEach { stateAndRef -> val state = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, contractStateClassName = stateAndRef.value.state.data.javaClass.name, contractState = stateAndRef.value.state.serialize(context = STORAGE_CONTEXT).bytes, stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = services.clock.instant()) + recordedTime = clock.instant()) state.stateRef = PersistentStateRef(stateAndRef.key) session.save(state) } @@ -82,11 +98,11 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) state?.run { stateStatus = Vault.StateStatus.CONSUMED - consumedTime = services.clock.instant() + consumedTime = clock.instant() // remove lock (if held) if (lockId != null) { lockId = null - lockUpdateTime = services.clock.instant() + lockUpdateTime = clock.instant() log.trace("Releasing soft lock on consumed state: $stateRef") } session.save(state) @@ -102,16 +118,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override val updates: Observable> get() = mutex.locked { _updatesInDbTx } - override val updatesPublisher: PublishSubject> - get() = mutex.locked { _updatesPublisher } - - /** - * Splits the provided [txns] into batches of [WireTransaction] and [NotaryChangeWireTransaction]. - * This is required because the batches get aggregated into single updates, and we want to be able to - * indicate whether an update consists entirely of regular or notary change transactions, which may require - * different processing logic. - */ - fun notifyAll(txns: Iterable) { + override fun notifyAll(txns: Iterable) { // It'd be easier to just group by type, but then we'd lose ordering. val regularTxns = mutableListOf() val notaryChangeTxns = mutableListOf() @@ -139,12 +146,9 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT if (notaryChangeTxns.isNotEmpty()) notifyNotaryChange(notaryChangeTxns.toList()) } - /** Same as notifyAll but with a single transaction. */ - fun notify(tx: CoreTransaction) = notifyAll(listOf(tx)) - private fun notifyRegular(txns: Iterable) { fun makeUpdate(tx: WireTransaction): Vault.Update { - val myKeys = services.keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) + val myKeys = keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val ourNewStates = tx.outputs. filter { isRelevant(it.data, myKeys.toSet()) }. map { tx.outRef(it.data) } @@ -170,8 +174,8 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT // We need to resolve the full transaction here because outputs are calculated from inputs // We also can't do filtering beforehand, since output encumbrance pointers get recalculated based on // input positions - val ltx = tx.resolve(services, emptyList()) - val myKeys = services.keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) + val ltx = tx.resolve(stateLoader, emptyList()) + val myKeys = keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val (consumedStateAndRefs, producedStates) = ltx.inputs. zip(ltx.outputs). filter { (_, output) -> isRelevant(output.data, myKeys.toSet()) }. @@ -195,7 +199,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT private fun loadStates(refs: Collection): HashSet> { val states = HashSet>() if (refs.isNotEmpty()) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) @@ -229,11 +233,11 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { val txnNoteEntity = VaultSchemaV1.VaultTxnNote(txnId.toString(), noteText) - DatabaseTransactionManager.current().session.save(txnNoteEntity) + currentDBSession().save(txnNoteEntity) } override fun getTransactionNotes(txnId: SecureHash): Iterable { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultTxnNote::class.java) val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultTxnNote::class.java) @@ -245,37 +249,36 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT @Throws(StatesNotAvailableException::class) override fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet) { - val softLockTimestamp = services.clock.instant() + val softLockTimestamp = clock.instant() try { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.or(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name).isNull, - criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) - val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) - val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + fun execute(configure: Root<*>.(CriteriaUpdate<*>, Array) -> Any?) = criteriaBuilder.executeUpdate(session) { update -> + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + configure(update, arrayOf(stateRefsPredicate)) + } + + val updatedRows = execute { update, commonPredicates -> + val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.or(get(VaultSchemaV1.VaultStates::lockId.name).isNull, + criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.where(stateStatusPredication, lockIdPredicate, *commonPredicates) + } if (updatedRows > 0 && updatedRows == stateRefs.size) { log.trace("Reserving soft lock states for $lockId: $stateRefs") FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true } else { // revert partial soft locks - val criteriaRevertUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStatesRevert = criteriaRevertUpdate.from(VaultSchemaV1.VaultStates::class.java) - val lockIdPredicateRevert = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - val lockUpdateTime = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - val persistentStateRefsRevert = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKeyRevert = vaultStatesRevert.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicateRevert = criteriaBuilder.and(compositeKeyRevert.`in`(persistentStateRefsRevert)) - criteriaRevertUpdate.set(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaRevertUpdate.where(lockUpdateTime, lockIdPredicateRevert, stateRefsPredicateRevert) - val revertUpdatedRows = session.createQuery(criteriaRevertUpdate).executeUpdate() + val revertUpdatedRows = execute { update, commonPredicates -> + val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + val lockUpdateTime = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.where(lockUpdateTime, lockIdPredicate, *commonPredicates) + } if (revertUpdatedRows > 0) { log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId") } @@ -291,34 +294,31 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { - val softLockTimestamp = services.clock.instant() - val session = DatabaseTransactionManager.current().session + val softLockTimestamp = clock.instant() + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder + fun execute(configure: Root<*>.(CriteriaUpdate<*>, Array) -> Any?) = criteriaBuilder.executeUpdate(session) { update -> + val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + configure(update, arrayOf(stateStatusPredication, lockIdPredicate)) + } if (stateRefs == null) { - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate) - val update = session.createQuery(criteriaUpdate).executeUpdate() + val update = execute { update, commonPredicates -> + update.where(*commonPredicates) + } if (update > 0) { log.trace("Releasing $update soft locked states for $lockId") } } else { try { - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) - val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + val updatedRows = execute { update, commonPredicates -> + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + update.where(*commonPredicates, stateRefsPredicate) + } if (updatedRows > 0) { log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs") } @@ -347,7 +347,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT val enrichedCriteria = QueryCriteria.VaultQueryCriteria( contractStateTypes = setOf(contractStateType), softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId))) - val results = services.vaultQueryService.queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter) + val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter) var claimedAmount = 0L val claimedStates = mutableListOf>() @@ -368,8 +368,6 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT return claimedStates } - - @VisibleForTesting internal fun isRelevant(state: ContractState, myKeys: Set): Boolean { val keysToCheck = when (state) { @@ -378,4 +376,162 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } return keysToCheck.any { it in myKeys } } + + private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas + private val criteriaBuilder = sessionFactory.criteriaBuilder + /** + * Maintain a list of contract state interfaces to concrete types stored in the vault + * for usage in generic queries of type queryBy or queryBy> + */ + private val contractStateTypeMappings = bootstrapContractStateTypes() + + init { + rawUpdates.subscribe { update -> + update.produced.forEach { + val concreteType = it.state.data.javaClass + log.trace { "State update of type: $concreteType" } + val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) } + if (!seen) { + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractStateTypeMappings.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } + } + } + } + } + + @Throws(VaultQueryException::class) + override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { + log.info("Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting") + // calculate total results where a page specification has been defined + var totalStates = -1L + if (!paging.isDefault) { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) + val results = queryBy(contractStateType, criteria.and(countCriteria)) + totalStates = results.otherResults[0] as Long + } + + val session = getSession() + + session.use { + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + // TODO: revisit (use single instance of parser for all queries) + val criteriaParser = HibernateQueryCriteriaParser(contractStateType, contractStateTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) + + try { + // parse criteria and build where predicates + criteriaParser.parse(criteria, sorting) + + // prepare query for execution + val query = session.createQuery(criteriaQuery) + + // pagination checks + if (!paging.isDefault) { + // pagination + if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") + if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") + } + + query.firstResult = (paging.pageNumber - 1) * paging.pageSize + query.maxResults = paging.pageSize + 1 // detection too many results + + // execution + val results = query.resultList + + // final pagination check (fail-fast on too many results when no pagination specified) + if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) + throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") + + val statesAndRefs: MutableList> = mutableListOf() + val statesMeta: MutableList = mutableListOf() + val otherResults: MutableList = mutableListOf() + + results.asSequence() + .forEachIndexed { index, result -> + if (result[0] is VaultSchemaV1.VaultStates) { + if (!paging.isDefault && index == paging.pageSize) // skip last result if paged + return@forEachIndexed + val vaultState = result[0] as VaultSchemaV1.VaultStates + val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) + val state = vaultState.contractState.deserialize>(context = STORAGE_CONTEXT) + statesMeta.add(Vault.StateMetadata(stateRef, + vaultState.contractStateClassName, + vaultState.recordedTime, + vaultState.consumedTime, + vaultState.stateStatus, + vaultState.notary, + vaultState.lockId, + vaultState.lockUpdateTime)) + statesAndRefs.add(StateAndRef(state, stateRef)) + } else { + // TODO: improve typing of returned other results + log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + otherResults.addAll(result.toArray().asList()) + } + } + + return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) + } catch (e: java.lang.Exception) { + log.error(e.message) + throw e.cause ?: e + } + } + } + + @Throws(VaultQueryException::class) + override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { + return mutex.locked { + val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) + val updates: Observable> = uncheckedCast(_updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) }) + DataFeed(snapshotResults, updates) + } + } + + private fun getSession(): Session { + return sessionFactory.withOptions(). + connection(DatabaseTransactionManager.current().connection). + openSession() + } + + /** + * Derive list from existing vault states and then incrementally update using vault observables + */ + private fun bootstrapContractStateTypes(): MutableMap> { + val criteria = criteriaBuilder.createQuery(String::class.java) + val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) + criteria.select(vaultStates.get("contractStateClassName")).distinct(true) + val session = getSession() + session.use { + val query = session.createQuery(criteria) + val results = query.resultList + val distinctTypes = results.map { it } + + val contractInterfaceToConcreteTypes = mutableMapOf>() + distinctTypes.forEach { type -> + val concreteType: Class = uncheckedCast(Class.forName(type)) + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } + } + return contractInterfaceToConcreteTypes + } + } + + private fun deriveContractInterfaces(clazz: Class): Set> { + val myInterfaces: MutableSet> = mutableSetOf() + clazz.interfaces.forEach { + if (it != ContractState::class.java) { + myInterfaces.add(uncheckedCast(it)) + myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it))) + } + } + return myInterfaces + } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 8dd74ab76f..909d35925c 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -3,13 +3,12 @@ package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes -import org.hibernate.annotations.Generated -import org.hibernate.annotations.GenerationTime import java.io.Serializable import java.time.Instant import java.util.* @@ -25,14 +24,14 @@ object VaultSchema */ @CordaSerializable object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1, - mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) { + mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) { @Entity @Table(name = "vault_states", indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status"))) class VaultStates( /** refers to the X500Name of the notary a state is attached to */ @Column(name = "notary_name") - var notary: AbstractParty, + var notary: Party, /** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */ @Column(name = "contract_state_class_name") @@ -90,8 +89,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio ) : PersistentState() { constructor(uid: UniqueIdentifier, _participants: List) : this(externalId = uid.externalId, - uuid = uid.id, - participants = _participants.toMutableSet()) + uuid = uid.id, + participants = _participants.toMutableSet()) } @Entity @@ -131,27 +130,27 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio ) : PersistentState() { constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : this(owner = _owner, - quantity = _quantity, - issuer = _issuerParty, - issuerRef = _issuerRef.bytes, - participants = _participants.toMutableSet()) + quantity = _quantity, + issuer = _issuerParty, + issuerRef = _issuerRef.bytes, + participants = _participants.toMutableSet()) } @Entity @Table(name = "vault_transaction_notes", - indexes = arrayOf(Index(name = "seq_no_index", columnList = "seq_no"), - Index(name = "transaction_id_index", columnList = "transaction_id"))) + indexes = arrayOf(Index(name = "seq_no_index", columnList = "seq_no"), + Index(name = "transaction_id_index", columnList = "transaction_id"))) class VaultTxnNote( - @Id - @GeneratedValue - @Column(name = "seq_no") - var seqNo: Int, + @Id + @GeneratedValue + @Column(name = "seq_no") + var seqNo: Int, - @Column(name = "transaction_id", length = 64) - var txId: String, + @Column(name = "transaction_id", length = 64) + var txId: String, - @Column(name = "note") - var note: String + @Column(name = "note") + var note: String ) : Serializable { constructor(txId: String, note: String) : this(0, txId, note) } diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt index 0c6db1a7b8..235c2bcbe9 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -59,7 +59,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub } private fun createStateMachinesTable(): TableElement { - val table = TableElement(1,2,1,2).overflow(Overflow.HIDDEN).rightCellPadding(1) + val table = TableElement(1, 2, 1, 2).overflow(Overflow.HIDDEN).rightCellPadding(1) val header = RowElement(true).add("Id", "Flow name", "Initiator", "Status").style(Decoration.bold.fg(Color.black).bg(Color.white)) table.add(header) return table @@ -102,21 +102,22 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub } private fun formatFlowId(flowId: StateMachineRunId): String { - return flowId.toString().removeSurrounding("[","]") + return flowId.toString().removeSurrounding("[", "]") } private fun formatFlowInitiator(flowInitiator: FlowInitiator): String { return when (flowInitiator) { - is FlowInitiator.Scheduled -> flowInitiator.scheduledState.ref.toString() + is FlowInitiator.Scheduled -> flowInitiator.scheduledState.ref.toString() is FlowInitiator.Shell -> "Shell" // TODO Change when we will have more information on shell user. is FlowInitiator.Peer -> flowInitiator.party.name.organisation is FlowInitiator.RPC -> "RPC: " + flowInitiator.username + is FlowInitiator.Service -> "Service: " + flowInitiator.name } } private fun formatFlowResult(flowResult: Try<*>): String { fun successFormat(value: Any?): String { - return when(value) { + return when (value) { is SignedTransaction -> "Tx ID: " + value.id.toString() is kotlin.Unit -> "No return value" null -> "No return value" diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 668236515a..a671fdb1dd 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -23,6 +23,7 @@ import net.corda.core.messaging.StateMachineUpdate import net.corda.core.utilities.loggerFor import net.corda.client.jackson.JacksonSupport import net.corda.client.jackson.StringToMethodCallParser +import net.corda.core.CordaException import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT @@ -233,8 +234,7 @@ object InteractiveShell { return } - @Suppress("UNCHECKED_CAST") - val clazz = matches.single() as Class> + val clazz: Class> = uncheckedCast(matches.single()) try { // TODO Flow invocation should use startFlowDynamic. val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz) @@ -247,11 +247,11 @@ object InteractiveShell { // Wait for the flow to end and the progress tracker to notice. By the time the latch is released // the tracker is done with the screen. latch.await() - } catch(e: InterruptedException) { + } catch (e: InterruptedException) { ANSIProgressRenderer.progressTracker = null // TODO: When the flow framework allows us to kill flows mid-flight, do so here. } - } catch(e: NoApplicableConstructor) { + } catch (e: NoApplicableConstructor) { output.println("No matching constructor found:", Color.red) e.errors.forEach { output.println("- $it", Color.red) } } finally { @@ -259,7 +259,7 @@ object InteractiveShell { } } - class NoApplicableConstructor(val errors: List) : Exception() { + class NoApplicableConstructor(val errors: List) : CordaException(this.toString()) { override fun toString() = (listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator()) } @@ -305,14 +305,14 @@ object InteractiveShell { continue } return invoke(flow) - } catch(e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { + } catch (e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { errors.add("${getPrototype()}: missing parameter ${e.paramName}") - } catch(e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { + } catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { errors.add("${getPrototype()}: too many parameters") - } catch(e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { + } catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { val argTypes = ctor.parameterTypes.map { it.simpleName } errors.add("$argTypes: ") - } catch(e: StringToMethodCallParser.UnparseableCallException) { + } catch (e: StringToMethodCallParser.UnparseableCallException) { val argTypes = ctor.parameterTypes.map { it.simpleName } errors.add("$argTypes: ${e.message}") } @@ -434,8 +434,6 @@ object InteractiveShell { } } - // Kotlin bug: USELESS_CAST warning is generated below but the IDE won't let us remove it. - @Suppress("USELESS_CAST", "UNCHECKED_CAST") private fun maybeFollow(response: Any?, printerFun: (Any?) -> String, toStream: PrintWriter): OpenFuture? { // Match on a couple of common patterns for "important" observables. It's tough to do this in a generic // way because observables can be embedded anywhere in the object graph, and can emit other arbitrary @@ -454,7 +452,7 @@ object InteractiveShell { } ?: return null val subscriber = PrintingSubscriber(printerFun, toStream) - (observable as Observable).subscribe(subscriber) + uncheckedCast(observable).subscribe(subscriber) return subscriber.future } @@ -508,7 +506,7 @@ object InteractiveShell { } finally { try { value.close() - } catch(e: IOException) { + } catch (e: IOException) { // Ignore. } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index ab9f83e884..a90f77d171 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -9,9 +9,9 @@ import java.util.* * behaviour is unpredictable! There is a best-effort check for double inserts, but this should *not* be relied on, so * ONLY USE THIS IF YOUR TABLE IS APPEND-ONLY */ -class AppendOnlyPersistentMap ( +class AppendOnlyPersistentMap( val toPersistentEntityKey: (K) -> EK, - val fromPersistentEntity: (E) -> Pair, + val fromPersistentEntity: (E) -> Pair, val toPersistentEntity: (key: K, value: V) -> E, val persistentEntityClass: Class, cacheBound: Long = 1024 @@ -40,18 +40,20 @@ class AppendOnlyPersistentMap ( * Returns all key/value pairs from the underlying storage. */ fun allPersisted(): Sequence> { - val criteriaQuery = DatabaseTransactionManager.current().session.criteriaBuilder.createQuery(persistentEntityClass) + val session = currentDBSession() + val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) val root = criteriaQuery.from(persistentEntityClass) criteriaQuery.select(root) - val query = DatabaseTransactionManager.current().session.createQuery(criteriaQuery) + val query = session.createQuery(criteriaQuery) val result = query.resultList return result.map { x -> fromPersistentEntity(x) }.asSequence() } - private tailrec fun set(key: K, value: V, logWarning: Boolean, store: (K,V) -> V?): Boolean { + private tailrec fun set(key: K, value: V, logWarning: Boolean, store: (K, V) -> V?): Boolean { var insertionAttempt = false var isUnique = true - val existingInCache = cache.get(key) { // Thread safe, if multiple threads may wait until the first one has loaded. + val existingInCache = cache.get(key) { + // Thread safe, if multiple threads may wait until the first one has loaded. insertionAttempt = true // Key wasn't in the cache and might be in the underlying storage. // Depending on 'store' method, this may insert without checking key duplication or it may avoid inserting a duplicated key. @@ -85,8 +87,8 @@ class AppendOnlyPersistentMap ( * If the map previously contained a mapping for the key, the behaviour is unpredictable and may throw an error from the underlying storage. */ operator fun set(key: K, value: V) = - set(key, value, logWarning = false) { - k, v -> DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + set(key, value, logWarning = false) { k, v -> + currentDBSession().save(toPersistentEntity(k, v)) null } @@ -96,25 +98,25 @@ class AppendOnlyPersistentMap ( * @return true if added key was unique, otherwise false */ fun addWithDuplicatesAllowed(key: K, value: V, logWarning: Boolean = true): Boolean = - set(key, value, logWarning) { - k, v -> - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) + set(key, value, logWarning) { k, v -> + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + session.save(toPersistentEntity(k, v)) null } else { fromPersistentEntity(existingEntry).second } } - fun putAll(entries: Map) { + fun putAll(entries: Map) { entries.forEach { set(it.key, it.value) } } private fun loadValue(key: K): V? { - val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } @@ -125,7 +127,7 @@ class AppendOnlyPersistentMap ( * WARNING!! The method is not thread safe. */ fun clear() { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val deleteQuery = session.criteriaBuilder.createCriteriaDelete(persistentEntityClass) deleteQuery.from(persistentEntityClass) session.createQuery(deleteQuery).executeUpdate() diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index de78ac1071..77055c6073 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -6,8 +6,6 @@ import net.corda.core.node.services.IdentityService import net.corda.node.services.api.SchemaService import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService -import org.hibernate.SessionFactory - import rx.Observable import rx.Subscriber import rx.subjects.UnicastSubject @@ -23,34 +21,40 @@ import java.util.concurrent.CopyOnWriteArrayList const val NODE_DATABASE_PREFIX = "node_" //HikariDataSource implements Closeable which allows CordaPersistence to be Closeable -class CordaPersistence(var dataSource: HikariDataSource, private var createSchemaService: () -> SchemaService, - private val createIdentityService: ()-> IdentityService, databaseProperties: Properties): Closeable { +class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService, + private val createIdentityService: () -> IdentityService, databaseProperties: Properties) : Closeable { var transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel")) - val hibernateConfig: HibernateConfiguration by lazy { + val hibernateConfig: HibernateConfiguration by lazy { transaction { - HibernateConfiguration(createSchemaService, databaseProperties, createIdentityService) - } - } - - val entityManagerFactory: SessionFactory by lazy { - transaction { - hibernateConfig.sessionFactoryForRegisteredSchemas() + HibernateConfiguration(schemaService, databaseProperties, createIdentityService) } } + val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas companion object { - fun connect(dataSource: HikariDataSource, createSchemaService: () -> SchemaService, createIdentityService: () -> IdentityService, databaseProperties: Properties): CordaPersistence { - return CordaPersistence(dataSource, createSchemaService, createIdentityService, databaseProperties).apply { + fun connect(dataSource: HikariDataSource, schemaService: SchemaService, createIdentityService: () -> IdentityService, databaseProperties: Properties): CordaPersistence { + return CordaPersistence(dataSource, schemaService, createIdentityService, databaseProperties).apply { DatabaseTransactionManager(this) } } } - fun createTransaction(): DatabaseTransaction { + /** + * Creates an instance of [DatabaseTransaction], with the given isolation level. + * @param isolationLevel isolation level for the transaction. If not specified the default (i.e. provided at the creation time) is used. + */ + fun createTransaction(isolationLevel: Int): DatabaseTransaction { // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. DatabaseTransactionManager.dataSource = this - return DatabaseTransactionManager.currentOrNew(transactionIsolationLevel) + return DatabaseTransactionManager.currentOrNew(isolationLevel) + } + + /** + * Creates an instance of [DatabaseTransaction], with the transaction isolation level specified at the creation time. + */ + fun createTransaction(): DatabaseTransaction { + return createTransaction(transactionIsolationLevel) } fun createSession(): Connection { @@ -60,9 +64,22 @@ class CordaPersistence(var dataSource: HikariDataSource, private var createSchem return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.") } - fun transaction(statement: DatabaseTransaction.() -> T): T { + /** + * Executes given statement in the scope of transaction, with the given isolation level. + * @param isolationLevel isolation level for the transaction. + * @param statement to be executed in the scope of this transaction. + */ + fun transaction(isolationLevel: Int, statement: DatabaseTransaction.() -> T): T { DatabaseTransactionManager.dataSource = this - return transaction(transactionIsolationLevel, 3, statement) + return transaction(isolationLevel, 3, statement) + } + + /** + * Executes given statement in the scope of transaction with the transaction level specified at the creation time. + * @param statement to be executed in the scope of this transaction. + */ + fun transaction(statement: DatabaseTransaction.() -> T): T { + return transaction(transactionIsolationLevel, statement) } private fun transaction(transactionIsolation: Int, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T { @@ -70,8 +87,7 @@ class CordaPersistence(var dataSource: HikariDataSource, private var createSchem return if (outer != null) { outer.statement() - } - else { + } else { inTopLevelTransaction(transactionIsolation, repetitionAttempts, statement) } } @@ -84,19 +100,16 @@ class CordaPersistence(var dataSource: HikariDataSource, private var createSchem val answer = transaction.statement() transaction.commit() return answer - } - catch (e: SQLException) { + } catch (e: SQLException) { transaction.rollback() repetitions++ if (repetitions >= repetitionAttempts) { throw e } - } - catch (e: Throwable) { + } catch (e: Throwable) { transaction.rollback() throw e - } - finally { + } finally { transaction.close() } } @@ -107,10 +120,10 @@ class CordaPersistence(var dataSource: HikariDataSource, private var createSchem } } -fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, createSchemaService: () -> SchemaService = { NodeSchemaService() }, createIdentityService: () -> IdentityService): CordaPersistence { +fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, createIdentityService: () -> IdentityService, schemaService: SchemaService = NodeSchemaService(null)): CordaPersistence { val config = HikariConfig(dataSourceProperties) val dataSource = HikariDataSource(config) - val persistence = CordaPersistence.connect(dataSource, createSchemaService, createIdentityService, databaseProperties ?: Properties()) + val persistence = CordaPersistence.connect(dataSource, schemaService, createIdentityService, databaseProperties ?: Properties()) // Check not in read-only mode. persistence.transaction { @@ -170,7 +183,7 @@ private class DatabaseTransactionWrappingSubscriber(val db: CordaPersistence? } // A subscriber that wraps another but does not pass on observations to it. -private class NoOpSubscriber(t: Subscriber): Subscriber(t) { +private class NoOpSubscriber(t: Subscriber) : Subscriber(t) { override fun onCompleted() { } diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt index f847080433..6c810a7005 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt @@ -30,7 +30,7 @@ class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal() diff --git a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt index 5966f88599..0257302d6d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt @@ -147,7 +147,7 @@ fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): Certi */ fun KeyStore.getX509Certificate(alias: String): X509Certificate { val certificate = getCertificate(alias) ?: throw IllegalArgumentException("No certificate under alias \"$alias\".") - return certificate as? X509Certificate ?: throw IllegalArgumentException("Certificate under alias \"$alias\" is not an X.509 certificate.") + return certificate as? X509Certificate ?: throw IllegalArgumentException("Certificate under alias \"$alias\" is not an X.509 certificate.") } /** diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt index c456f8af3b..07dd16b936 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt @@ -8,7 +8,7 @@ import com.google.common.util.concurrent.ListenableFuture class NonInvalidatingCache private constructor( val cache: LoadingCache -): LoadingCache by cache { +) : LoadingCache by cache { constructor(bound: Long, concurrencyLevel: Int, loadFunction: (K) -> V) : this(buildCache(bound, concurrencyLevel, loadFunction)) @@ -25,9 +25,7 @@ class NonInvalidatingCache private constructor( override fun reload(key: K, oldValue: V): ListenableFuture { throw IllegalStateException("Non invalidating cache refreshed") } + override fun load(key: K) = loadFunction(key) - override fun loadAll(keys: Iterable): MutableMap { - return super.loadAll(keys) - } } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt index 44caaa9adb..ea16bfd064 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt @@ -6,10 +6,10 @@ import com.google.common.util.concurrent.ListenableFuture class NonInvalidatingUnboundCache private constructor( val cache: LoadingCache -): LoadingCache by cache { +) : LoadingCache by cache { constructor(concurrencyLevel: Int, loadFunction: (K) -> V, removalListener: RemovalListener = RemovalListener {}, - keysToPreload: () -> Iterable = { emptyList() } ) : + keysToPreload: () -> Iterable = { emptyList() }) : this(buildCache(concurrencyLevel, loadFunction, removalListener, keysToPreload)) private companion object { @@ -27,9 +27,7 @@ class NonInvalidatingUnboundCache private constructor( override fun reload(key: K, oldValue: V): ListenableFuture { throw IllegalStateException("Non invalidating cache refreshed") } + override fun load(key: K) = loadFunction(key) - override fun loadAll(keys: Iterable): MutableMap { - return super.loadAll(keys) - } } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt index 55acb14efc..11ace024ef 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt @@ -11,9 +11,9 @@ import java.util.* /** * Implements an unbound caching layer on top of a table accessed via Hibernate mapping. */ -class PersistentMap ( +class PersistentMap( val toPersistentEntityKey: (K) -> EK, - val fromPersistentEntity: (E) -> Pair, + val fromPersistentEntity: (E) -> Pair, val toPersistentEntity: (key: K, value: V) -> E, val persistentEntityClass: Class ) : MutableMap, AbstractMap() { @@ -28,17 +28,17 @@ class PersistentMap ( removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass) ).apply { //preload to allow all() to take data only from the cache (cache is unbound) - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) } - class ExplicitRemoval(private val toPersistentEntityKey: (K) -> EK, private val persistentEntityClass: Class): RemovalListener { + class ExplicitRemoval(private val toPersistentEntityKey: (K) -> EK, private val persistentEntityClass: Class) : RemovalListener { override fun onRemoval(notification: RemovalNotification?) { when (notification?.cause) { RemovalCause.EXPLICIT -> { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val elem = session.find(persistentEntityClass, toPersistentEntityKey(notification.key)) if (elem != null) { session.remove(elem) @@ -47,7 +47,8 @@ class PersistentMap ( RemovalCause.EXPIRED, RemovalCause.SIZE, RemovalCause.COLLECTED -> { log.error("Entry was removed from cache!!!") } - RemovalCause.REPLACED -> {} + RemovalCause.REPLACED -> { + } } } } @@ -62,10 +63,11 @@ class PersistentMap ( override val size get() = cache.size().toInt() - private tailrec fun set(key: K, value: V, logWarning: Boolean = true, store: (K,V) -> V?, replace: (K, V) -> Unit) : Boolean { + private tailrec fun set(key: K, value: V, logWarning: Boolean = true, store: (K, V) -> V?, replace: (K, V) -> Unit): Boolean { var insertionAttempt = false var isUnique = true - val existingInCache = cache.get(key) { // Thread safe, if multiple threads may wait until the first one has loaded. + val existingInCache = cache.get(key) { + // Thread safe, if multiple threads may wait until the first one has loaded. insertionAttempt = true // Value wasn't in the cache and wasn't in DB (because the cache is unbound). // Store the value, depending on store implementation this may replace existing entry in DB. @@ -98,8 +100,8 @@ class PersistentMap ( operator fun set(key: K, value: V) = set(key, value, logWarning = false, - store = { k: K, v: V -> - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + store = { k: K, v: V -> + currentDBSession().save(toPersistentEntity(k, v)) null }, replace = { _: K, _: V -> Unit } @@ -113,9 +115,10 @@ class PersistentMap ( fun addWithDuplicatesAllowed(key: K, value: V) = set(key, value, store = { k, v -> - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + session.save(toPersistentEntity(k, v)) null } else { fromPersistentEntity(existingEntry).second @@ -143,18 +146,19 @@ class PersistentMap ( } private fun merge(key: K, value: V): V? { - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(key)) return if (existingEntry != null) { - DatabaseTransactionManager.current().session.merge(toPersistentEntity(key,value)) + session.merge(toPersistentEntity(key, value)) fromPersistentEntity(existingEntry).second } else { - DatabaseTransactionManager.current().session.save(toPersistentEntity(key,value)) + session.save(toPersistentEntity(key, value)) null } } private fun loadValue(key: K): V? { - val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } @@ -193,56 +197,59 @@ class PersistentMap ( } } - override val keys: MutableSet get() { - return object : AbstractSet() { - override val size: Int get() = this@PersistentMap.size - override fun iterator(): MutableIterator { - return object : MutableIterator { - private val entryIterator = EntryIterator() + override val keys: MutableSet + get() { + return object : AbstractSet() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val entryIterator = EntryIterator() - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): K = entryIterator.next().key - override fun remove() { - entryIterator.remove() + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): K = entryIterator.next().key + override fun remove() { + entryIterator.remove() + } } } } } - } - override val values: MutableCollection get() { - return object : AbstractCollection() { - override val size: Int get() = this@PersistentMap.size - override fun iterator(): MutableIterator { - return object : MutableIterator { - private val entryIterator = EntryIterator() + override val values: MutableCollection + get() { + return object : AbstractCollection() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val entryIterator = EntryIterator() - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): V = entryIterator.next().value - override fun remove() { - entryIterator.remove() + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): V = entryIterator.next().value + override fun remove() { + entryIterator.remove() + } } } } } - } - override val entries: MutableSet> get() { - return object : AbstractSet>() { - override val size: Int get() = this@PersistentMap.size - override fun iterator(): MutableIterator> { - return object : MutableIterator> { - private val entryIterator = EntryIterator() + override val entries: MutableSet> + get() { + return object : AbstractSet>() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator> { + return object : MutableIterator> { + private val entryIterator = EntryIterator() - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): MutableMap.MutableEntry = entryIterator.next() - override fun remove() { - entryIterator.remove() + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): MutableMap.MutableEntry = entryIterator.next() + override fun remove() { + entryIterator.remove() + } } } } } - } override fun put(key: K, value: V): V? { val old = cache.get(key) @@ -251,7 +258,7 @@ class PersistentMap ( } fun load() { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) cache.getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) diff --git a/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt b/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt index e582f45147..60af87f895 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt @@ -21,7 +21,8 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC override fun toToken(context: SerializeAsTokenContext) = token.registerWithContext(context, this) - @Synchronized fun updateDate(date: LocalDate): Boolean { + @Synchronized + fun updateDate(date: LocalDate): Boolean { val currentDate = LocalDate.now(this) if (currentDate.isBefore(date)) { // It's ok to increment diff --git a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt index a9685fd910..4dfa07ec09 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt @@ -115,7 +115,7 @@ object X509Utilities { subjectPublicKey: PublicKey, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null): X509CertificateHolder - = createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints) + = createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints) /** * Create a X509 v3 certificate for use as a CA or for TLS. This does not require a [CordaX500Name] because the @@ -267,9 +267,9 @@ object X509Utilities { * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. */ internal fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, - subject: X500Name, subjectPublicKey: PublicKey, - validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509CertificateHolder { + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair, + nameConstraints: NameConstraints? = null): X509CertificateHolder { val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) val provider = Crypto.findProvider(signatureScheme.providerName) diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt index e22121b2e2..ffe8bece0f 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt @@ -1,5 +1,6 @@ package net.corda.node.utilities.registration +import net.corda.core.CordaException import net.corda.core.serialization.CordaSerializable import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.Certificate @@ -14,4 +15,4 @@ interface NetworkRegistrationService { } @CordaSerializable -class CertificateRequestException(message: String) : Exception(message) +class CertificateRequestException(message: String) : CordaException(message) diff --git a/node/src/main/resources/net/corda/node/shell/base/login.groovy b/node/src/main/resources/net/corda/node/shell/base/login.groovy index 500704ae69..f6df32b386 100644 --- a/node/src/main/resources/net/corda/node/shell/base/login.groovy +++ b/node/src/main/resources/net/corda/node/shell/base/login.groovy @@ -11,5 +11,5 @@ Useful commands include 'help' to see what is available, and 'bye' to shut down """ prompt = { -> - return "${new Date()}>>> "; + return "${new Date()}>>> " } diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 1b3711c8c4..37ff984da6 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -19,7 +19,10 @@ useHTTPS = false h2port = 0 useTestClock = false verifierType = InMemory -bftSMaRt = { - replicaId = -1 - debug = false +activeMQServer = { + bridge = { + retryIntervalMs = 5000 + retryIntervalMultiplier = 1.5 + maxRetryIntervalMin = 3 + } } diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt index bd2d072d64..82c48bf82e 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.list import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap +import net.corda.node.internal.cordapp.CordappLoader import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess @@ -33,18 +34,18 @@ class CordappSmokeTest { p2pPort = port.andIncrement, rpcPort = port.andIncrement, webPort = port.andIncrement, - extraServices = emptyList(), + isNotary = false, users = listOf(user) ) @Test fun `FlowContent appName returns the filename of the CorDapp jar`() { - val pluginsDir = (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + val cordappsDir = (factory.baseDirectory(aliceConfig) / CordappLoader.CORDAPPS_DIR_NAME).createDirectories() // Find the jar file for the smoke tests of this module val selfCordapp = Paths.get("build", "libs").list { - it.filter { "-smoke-test" in it.toString() }.toList().single() + it.filter { "-smokeTests" in it.toString() }.toList().single() } - selfCordapp.copyToDirectory(pluginsDir) + selfCordapp.copyToDirectory(cordappsDir) factory.create(aliceConfig).use { alice -> alice.connect().use { connectionToAlice -> @@ -59,8 +60,8 @@ class CordappSmokeTest { } @Test - fun `empty plugins directory`() { - (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + fun `empty cordapps directory`() { + (factory.baseDirectory(aliceConfig) / CordappLoader.CORDAPPS_DIR_NAME).createDirectories() factory.create(aliceConfig).close() } diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index f963189d1f..2ad7697d20 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -1,85 +1,63 @@ package net.corda.node.services.vault; -import com.google.common.collect.ImmutableSet; -import kotlin.Pair; -import kotlin.Triple; +import com.google.common.collect.*; +import kotlin.*; import net.corda.core.contracts.*; -import net.corda.core.identity.AbstractParty; -import net.corda.core.messaging.DataFeed; -import net.corda.core.node.services.IdentityService; -import net.corda.core.node.services.Vault; -import net.corda.core.node.services.VaultQueryException; -import net.corda.core.node.services.VaultQueryService; +import net.corda.core.identity.*; +import net.corda.core.messaging.*; +import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; -import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; -import net.corda.core.schemas.MappedSchema; -import net.corda.core.utilities.EncodingUtils; -import net.corda.core.utilities.OpaqueBytes; -import net.corda.finance.contracts.DealState; -import net.corda.finance.contracts.asset.Cash; -import net.corda.finance.contracts.asset.CashUtilities; -import net.corda.finance.schemas.CashSchemaV1; -import net.corda.node.utilities.CordaPersistence; -import net.corda.node.utilities.DatabaseTransaction; -import net.corda.testing.TestConstants; -import net.corda.testing.TestDependencyInjectionBase; -import net.corda.testing.contracts.DummyLinearContract; -import net.corda.testing.contracts.VaultFiller; -import net.corda.testing.node.MockServices; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import net.corda.core.node.services.vault.QueryCriteria.*; +import net.corda.core.utilities.*; +import net.corda.finance.contracts.*; +import net.corda.finance.contracts.asset.*; +import net.corda.finance.schemas.*; +import net.corda.node.utilities.*; +import net.corda.testing.*; +import net.corda.testing.contracts.*; +import net.corda.testing.node.*; +import org.junit.*; import rx.Observable; -import java.io.IOException; -import java.lang.reflect.Field; -import java.security.KeyPair; +import java.io.*; +import java.lang.reflect.*; +import java.security.*; import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.stream.*; -import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM; -import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; -import static net.corda.core.utilities.ByteArrays.toHexString; -import static net.corda.finance.contracts.asset.CashUtilities.getDUMMY_CASH_ISSUER; -import static net.corda.finance.contracts.asset.CashUtilities.getDUMMY_CASH_ISSUER_KEY; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.*; +import static net.corda.core.utilities.ByteArrays.*; +import static net.corda.finance.contracts.asset.CashUtilities.*; import static net.corda.testing.CoreTestUtils.*; -import static net.corda.testing.TestConstants.getDUMMY_NOTARY; -import static net.corda.testing.TestConstants.getDUMMY_NOTARY_KEY; -import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices; -import static net.corda.testing.node.MockServices.makeTestIdentityService; -import static org.assertj.core.api.Assertions.assertThat; +import static net.corda.testing.TestConstants.*; +import static net.corda.testing.node.MockServices.*; +import static org.assertj.core.api.Assertions.*; public class VaultQueryJavaTests extends TestDependencyInjectionBase { private MockServices services; private MockServices issuerServices; - private VaultQueryService vaultQuerySvc; + private VaultService vaultService; private CordaPersistence database; @Before public void setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset"); + List cordappPackages = Arrays.asList("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName()); ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); keys.add(getDUMMY_NOTARY_KEY()); - Set requiredSchemas = Collections.singleton(CashSchemaV1.INSTANCE); IdentityService identitySvc = makeTestIdentityService(); @SuppressWarnings("unchecked") - Pair databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc, Collections.EMPTY_LIST); - issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); + Pair databaseAndServices = makeTestDatabaseAndMockServices(keys, () -> identitySvc, cordappPackages); + issuerServices = new MockServices(cordappPackages, getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); - vaultQuerySvc = services.getVaultQueryService(); + vaultService = services.getVaultService(); } @After public void cleanUp() throws IOException { database.close(); - unsetCordappPackages(); } /** @@ -98,7 +76,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { }); database.transaction(tx -> { // DOCSTART VaultJavaQueryExample0 - Vault.Page results = vaultQuerySvc.queryBy(LinearState.class); + Vault.Page results = vaultService.queryBy(LinearState.class); // DOCEND VaultJavaQueryExample0 assertThat(results.getStates()).hasSize(3); @@ -110,10 +88,10 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Test public void unconsumedStatesForStateRefsSortedByTxnId() { Vault issuedStates = - database.transaction(tx -> { - VaultFiller.fillWithSomeTestLinearStates(services, 8); - return VaultFiller.fillWithSomeTestLinearStates(services, 2); - }); + database.transaction(tx -> { + VaultFiller.fillWithSomeTestLinearStates(services, 8); + return VaultFiller.fillWithSomeTestLinearStates(services, 2); + }); database.transaction(tx -> { Stream stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef); List stateRefs = stateRefsStream.collect(Collectors.toList()); @@ -121,7 +99,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { SortAttribute.Standard sortAttribute = new SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID); Sort sorting = new Sort(Collections.singletonList(new Sort.SortColumn(sortAttribute, Sort.Direction.ASC))); VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, stateRefs); - Vault.Page results = vaultQuerySvc.queryBy(DummyLinearContract.State.class, criteria, sorting); + Vault.Page results = vaultService.queryBy(DummyLinearContract.State.class, criteria, sorting); assertThat(results.getStates()).hasSize(2); @@ -139,7 +117,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { database.transaction(tx -> { VaultFiller.fillWithSomeTestCash(services, new Amount(100, Currency.getInstance("USD")), - issuerServices, + issuerServices, TestConstants.getDUMMY_NOTARY(), 3, 3, @@ -156,7 +134,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { database.transaction(tx -> { // DOCSTART VaultJavaQueryExample1 VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample1 assertThat(results.getStates()).hasSize(3); @@ -170,14 +148,14 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { List dealIds = Arrays.asList("123", "456", "789"); @SuppressWarnings("unchecked") Triple, UniqueIdentifier, Vault> ids = - database.transaction((DatabaseTransaction tx) -> { - Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); - StateAndRef linearState = states.getStates().iterator().next(); - UniqueIdentifier uid = linearState.component1().getData().getLinearId(); + database.transaction((DatabaseTransaction tx) -> { + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); + StateAndRef linearState = states.getStates().iterator().next(); + UniqueIdentifier uid = linearState.component1().getData().getLinearId(); - Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); - return new Triple(linearState,uid,dealStates); - }); + Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); + return new Triple(linearState, uid, dealStates); + }); database.transaction(tx -> { // consume states VaultFiller.consumeDeals(services, (List>) ids.getThird().getStates(), getDUMMY_NOTARY()); @@ -202,7 +180,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - Vault.Page results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); + Vault.Page results = vaultService.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); // DOCEND VaultJavaQueryExample2 assertThat(results.getStates()).hasSize(4); @@ -243,7 +221,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria criteria = generalCriteria.and(customCriteria1).and(customCriteria2); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample3 assertThat(results.getStates()).hasSize(2); @@ -279,7 +257,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); - DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, criteria); + DataFeed, Vault.Update> results = vaultService.trackBy(ContractState.class, criteria); Vault.Page snapshot = results.getSnapshot(); Observable> updates = results.getUpdates(); @@ -295,13 +273,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { public void trackDealStatesPagedSorted() { List dealIds = Arrays.asList("123", "456", "789"); UniqueIdentifier uid = - database.transaction(tx -> { - Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); - UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId(); + database.transaction(tx -> { + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); + UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId(); - VaultFiller.fillWithSomeTestDeals(services, dealIds); - return _uid; - }); + VaultFiller.fillWithSomeTestDeals(services, dealIds); + return _uid; + }); database.transaction(tx -> { // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") @@ -318,7 +296,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); + DataFeed, Vault.Update> results = vaultService.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); Vault.Page snapshot = results.getSnapshot(); // DOCEND VaultJavaQueryExample5 @@ -364,7 +342,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies)); QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample21 assertThat(results.getOtherResults()).hasSize(5); @@ -413,7 +391,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies, Collections.singletonList(currency))); QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample22 assertThat(results.getOtherResults()).hasSize(27); @@ -480,7 +458,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(issuerParty, currency), Sort.Direction.DESC)); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, sumCriteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, sumCriteria); // DOCEND VaultJavaQueryExample23 assertThat(results.getOtherResults()).hasSize(12); diff --git a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt index 94b09c11c5..94d798c4e2 100644 --- a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt +++ b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt @@ -23,7 +23,8 @@ class ArgsParserTest { isRegistration = false, isVersion = false, noLocalShell = false, - sshdServer = false)) + sshdServer = false, + justGenerateNodeInfo = false)) } @Test @@ -117,4 +118,10 @@ class ArgsParserTest { val cmdLineOptions = parser.parse("--version") assertThat(cmdLineOptions.isVersion).isTrue() } + + @Test + fun `generate node infos`() { + val cmdLineOptions = parser.parse("--just-generate-node-info") + assertThat(cmdLineOptions.justGenerateNodeInfo).isTrue() + } } diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index f713bf0f5e..0020970296 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -1,6 +1,7 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable +import net.corda.client.rpc.PermissionException import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.Issued @@ -24,13 +25,9 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.CordaRPCOpsImpl import net.corda.node.internal.StartedNode +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcContext -import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.client.rpc.PermissionException import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.node.MockNetwork @@ -65,12 +62,9 @@ class CordaRPCOpsImplTest { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - - mockNet = MockNetwork() - val networkMap = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - aliceNode = mockNet.createNode(networkMapAddress = networkMap.network.myAddress) - notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.network.myAddress) + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) + aliceNode = mockNet.createNode() + notaryNode = mockNet.createNotaryNode(validating = false) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( startFlowPermission(), @@ -78,14 +72,13 @@ class CordaRPCOpsImplTest { )))) mockNet.runNetwork() - networkMap.internals.ensureRegistered() + mockNet.networkMapNode.internals.ensureRegistered() notary = rpc.notaryIdentities().first() } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test @@ -100,11 +93,10 @@ class CordaRPCOpsImplTest { // Check the monitoring service wallet is empty aliceNode.database.transaction { - assertFalse(aliceNode.services.vaultQueryService.queryBy().totalStatesAvailable > 0) + assertFalse(aliceNode.services.vaultService.queryBy().totalStatesAvailable > 0) } // Tell the monitoring service node to issue some cash - val recipient = aliceNode.info.chooseIdentity() val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, notary) mockNet.runNetwork() @@ -276,6 +268,6 @@ class CordaRPCOpsImplTest { @StartableByRPC class VoidRPCFlow : FlowLogic() { @Suspendable - override fun call() : Void? = null + override fun call(): Void? = null } } diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index d13fda0a4c..54117732cf 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -26,7 +26,7 @@ import kotlin.test.assertEquals class InteractiveShellTest { @Before fun setup() { - InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), MockServices.makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), MockServices.makeTestDatabaseProperties(), ::makeTestIdentityService) } @After @@ -52,7 +52,7 @@ class InteractiveShellTest { private fun check(input: String, expected: String) { var output: DummyFSM? = null InteractiveShell.runFlowFromString({ DummyFSM(it as FlowA).apply { output = this } }, input, FlowA::class.java, om) - assertEquals(expected, output!!.logic.a, input) + assertEquals(expected, output!!.flowA.a, input) } @Test @@ -83,5 +83,5 @@ class InteractiveShellTest { @Test fun party() = check("party: \"${MEGA_CORP.name}\"", MEGA_CORP.name.toString()) - class DummyFSM(val logic: FlowA) : FlowStateMachine by mock() - } + class DummyFSM(val flowA: FlowA) : FlowStateMachine by mock() +} diff --git a/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt deleted file mode 100644 index aadf3ee066..0000000000 --- a/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package net.corda.node.cordapp - -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.node.internal.cordapp.CordappLoader -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import java.nio.file.Paths - -@InitiatingFlow -class DummyFlow : FlowLogic() { - override fun call() { } -} - -@InitiatedBy(DummyFlow::class) -class LoaderTestFlow : FlowLogic() { - override fun call() { } -} - -class CordappLoaderTest { - @Test - fun `test that classes that aren't in cordapps aren't loaded`() { - // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp - val loader = CordappLoader.createDefault(Paths.get(".")) - assertThat(loader.cordapps).isEmpty() - } - - @Test - fun `test that classes that are in a cordapp are loaded`() { - val loader = CordappLoader.createWithTestPackages(listOf("net.corda.node.cordapp")) - val initiatedFlows = loader.cordapps.first().initiatedFlows - val expectedClass = loader.appClassLoader.loadClass("net.corda.node.cordapp.LoaderTestFlow").asSubclass(FlowLogic::class.java) - assertThat(initiatedFlows).contains(expectedClass) - } - - @Test - fun `isolated JAR contains a CorDapp with a contract and plugin`() { - val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!! - val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - - val actual = loader.cordapps.toTypedArray() - assertThat(actual).hasSize(1) - - val actualCordapp = actual.first() - assertThat(actualCordapp.contractClassNames).isEqualTo(listOf("net.corda.finance.contracts.isolated.AnotherDummyContract")) - assertThat(actualCordapp.initiatedFlows).isEmpty() - assertThat(actualCordapp.rpcFlows).contains(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiate").asSubclass(FlowLogic::class.java)) - assertThat(actualCordapp.services).isEmpty() - assertThat(actualCordapp.plugins).hasSize(1) - assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin") - assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) - } -} diff --git a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt new file mode 100644 index 0000000000..6135863388 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt @@ -0,0 +1,125 @@ +package net.corda.node.internal + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByService +import net.corda.core.node.AppServiceHub +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueFlow +import net.corda.node.internal.cordapp.DummyRPCFlow +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.node.MockNetwork +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +@StartableByService +class DummyServiceFlow : FlowLogic() { + companion object { + object TEST_STEP : ProgressTracker.Step("Custom progress step") + } + override val progressTracker: ProgressTracker = ProgressTracker(TEST_STEP) + + @Suspendable + override fun call(): FlowInitiator { + // We call a subFlow, otehrwise there is no chance to subscribe to the ProgressTracker + subFlow(CashIssueFlow(100.DOLLARS, OpaqueBytes.of(1), serviceHub.networkMapCache.notaryIdentities.first())) + progressTracker.currentStep = TEST_STEP + return stateMachine.flowInitiator + } +} + +@CordaService +class TestCordaService(val appServiceHub: AppServiceHub): SingletonSerializeAsToken() { + fun startServiceFlow() { + val handle = appServiceHub.startFlow(DummyServiceFlow()) + val initiator = handle.returnValue.get() + initiator as FlowInitiator.Service + assertEquals(this.javaClass.name, initiator.serviceClassName) + } + + fun startServiceFlowAndTrack() { + val handle = appServiceHub.startTrackedFlow(DummyServiceFlow()) + val count = AtomicInteger(0) + val subscriber = handle.progress.subscribe { count.incrementAndGet() } + handle.returnValue.get() + // Simply prove some progress was made. + // The actual number is currently 11, but don't want to hard code an implementation detail. + assertTrue(count.get() > 1) + subscriber.unsubscribe() + } + +} + +@CordaService +class TestCordaService2(val appServiceHub: AppServiceHub): SingletonSerializeAsToken() { + fun startInvalidRPCFlow() { + val handle = appServiceHub.startFlow(DummyRPCFlow()) + handle.returnValue.get() + } + +} + +@CordaService +class LegacyCordaService(@Suppress("UNUSED_PARAMETER") simpleServiceHub: ServiceHub) : SingletonSerializeAsToken() + +class CordaServiceTest { + lateinit var mockNet: MockNetwork + lateinit var notaryNode: StartedNode + lateinit var nodeA: StartedNode + + @Before + fun start() { + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.node.internal","net.corda.finance")) + notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = true) + nodeA = mockNet.createNode() + mockNet.startNodes() + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `Can find distinct services on node`() { + val service = nodeA.services.cordaService(TestCordaService::class.java) + val service2 = nodeA.services.cordaService(TestCordaService2::class.java) + val legacyService = nodeA.services.cordaService(LegacyCordaService::class.java) + assertEquals(TestCordaService::class.java, service.javaClass) + assertEquals(TestCordaService2::class.java, service2.javaClass) + assertNotEquals(service.appServiceHub, service2.appServiceHub) // Each gets a customised AppServiceHub + assertEquals(LegacyCordaService::class.java, legacyService.javaClass) + } + + @Test + fun `Can start StartableByService flows`() { + val service = nodeA.services.cordaService(TestCordaService::class.java) + service.startServiceFlow() + } + + @Test + fun `Can't start StartableByRPC flows`() { + val service = nodeA.services.cordaService(TestCordaService2::class.java) + assertFailsWith { service.startInvalidRPCFlow() } + } + + + @Test + fun `Test flow with progress tracking`() { + val service = nodeA.services.cordaService(TestCordaService::class.java) + service.startServiceFlowAndTrack() + } + +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt new file mode 100644 index 0000000000..6e8fab1620 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt @@ -0,0 +1,98 @@ +package net.corda.node.internal.cordapp + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.nio.file.Paths + +@InitiatingFlow +class DummyFlow : FlowLogic() { + @Suspendable + override fun call() { + } +} + +@InitiatedBy(DummyFlow::class) +class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + } +} + +@SchedulableFlow +class DummySchedulableFlow : FlowLogic() { + @Suspendable + override fun call() { + } +} + +@StartableByRPC +class DummyRPCFlow : FlowLogic() { + @Suspendable + override fun call() { + } +} + +class CordappLoaderTest { + private companion object { + val testScanPackages = listOf("net.corda.node.internal.cordapp") + val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract" + val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator" + } + + @Test + fun `test that classes that aren't in cordapps aren't loaded`() { + // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp + val loader = CordappLoader.createDefault(Paths.get(".")) + assertThat(loader.cordapps) + .hasSize(1) + .contains(CordappLoader.coreCordapp) + } + + @Test + fun `isolated JAR contains a CorDapp with a contract and plugin`() { + val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!! + val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) + + val actual = loader.cordapps.toTypedArray() + assertThat(actual).hasSize(2) + + val actualCordapp = actual.single { it != CordappLoader.coreCordapp } + assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) + assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") + assertThat(actualCordapp.rpcFlows).isEmpty() + assertThat(actualCordapp.schedulableFlows).isEmpty() + assertThat(actualCordapp.services).isEmpty() + assertThat(actualCordapp.serializationWhitelists).hasSize(1) + assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.nodeapi.internal.serialization.DefaultWhitelist") + assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) + } + + @Test + fun `flows are loaded by loader`() { + val loader = CordappLoader.createWithTestPackages(testScanPackages) + + val actual = loader.cordapps.toTypedArray() + // One core cordapp, one cordapp from this source tree, and two others due to identically named locations + // in resources and the non-test part of node. This is okay due to this being test code. In production this + // cannot happen. In gradle it will also pick up the node jar. + assertThat(actual.size == 4 || actual.size == 5).isTrue() + + val actualCordapp = actual.single { !it.initiatedFlows.isEmpty() } + assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java) + assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java) + assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java) + } + + // This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader + // being used internally. Later iterations will use a classloader per cordapp and this test can be retired. + @Test + fun `cordapp classloader can load cordapp classes`() { + val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!! + val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) + + loader.appClassLoader.loadClass(isolatedContractId) + loader.appClassLoader.loadClass(isolatedFlowName) + } +} diff --git a/node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt similarity index 81% rename from node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt rename to node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 762761d99e..9f7833f928 100644 --- a/node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -1,8 +1,7 @@ -package net.corda.node.cordapp +package net.corda.node.internal.cordapp +import com.nhaarman.mockito_kotlin.mock import net.corda.core.node.services.AttachmentStorage -import net.corda.node.internal.cordapp.CordappLoader -import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.node.MockAttachmentStorage import org.junit.Assert import org.junit.Before @@ -24,9 +23,7 @@ class CordappProviderImplTests { @Test fun `isolated jar is loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) - - provider.start(attachmentStore) + val provider = CordappProviderImpl(loader, attachmentStore) val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first()) Assert.assertNotNull(maybeAttachmentId) @@ -36,17 +33,14 @@ class CordappProviderImplTests { @Test fun `empty jar is not loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(emptyJAR)) - val provider = CordappProviderImpl(loader) - - provider.start(attachmentStore) - + val provider = CordappProviderImpl(loader, attachmentStore) Assert.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) + val provider = CordappProviderImpl(loader, mock()) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" val expected = provider.cordapps.first() @@ -59,10 +53,8 @@ class CordappProviderImplTests { @Test fun `test that we find an attachment for a cordapp contrat class`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) + val provider = CordappProviderImpl(loader, attachmentStore) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" - - provider.start(attachmentStore) val expected = provider.getAppContext(provider.cordapps.first()).attachmentId val actual = provider.getContractAttachmentID(className) diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index 95c26319c2..b3a3fb32e4 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -1,10 +1,8 @@ package net.corda.node.messaging -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.TopicStringValidator import net.corda.node.services.messaging.createMessage -import net.corda.node.services.network.NetworkMapService import net.corda.testing.node.MockNetwork import net.corda.testing.resetTestSerialization import org.junit.After @@ -49,9 +47,9 @@ class InMemoryMessagingTests { @Test fun basics() { - val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) - val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node1 = mockNet.networkMapNode + val node2 = mockNet.createNode() + val node3 = mockNet.createNode() val bits = "test-content".toByteArray() var finalDelivery: Message? = null @@ -78,9 +76,9 @@ class InMemoryMessagingTests { @Test fun broadcast() { - val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) - val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node1 = mockNet.networkMapNode + val node2 = mockNet.createNode() + val node3 = mockNet.createNode() val bits = "test-content".toByteArray() @@ -97,9 +95,9 @@ class InMemoryMessagingTests { */ @Test fun `skip unhandled messages`() { - val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) - var received: Int = 0 + val node1 = mockNet.networkMapNode + val node2 = mockNet.createNode() + var received = 0 node1.network.addMessageHandler("valid_message") { _, _ -> received++ diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 25f2f94923..93edb33b92 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -31,8 +31,6 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.`issued by` -import net.corda.finance.contracts.asset.`owned by` import net.corda.finance.flows.TwoPartyTradeFlow.Buyer import net.corda.finance.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.StartedNode @@ -46,11 +44,14 @@ import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import net.corda.testing.node.pumpReceive import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import rx.Observable import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -69,12 +70,21 @@ import kotlin.test.assertTrue * * We assume that Alice and Bob already found each other via some market, and have agreed the details already. */ -class TwoPartyTradeFlowTests { +@RunWith(Parameterized::class) +class TwoPartyTradeFlowTests(val anonymous: Boolean) { + companion object { + private val cordappPackages = listOf("net.corda.finance.contracts") + @JvmStatic + @Parameterized.Parameters + fun data(): Collection { + return listOf(true, false) + } + } + private lateinit var mockNet: MockNetwork @Before fun before() { - setCordappPackages("net.corda.finance.contracts") LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap") } @@ -82,7 +92,6 @@ class TwoPartyTradeFlowTests { fun after() { mockNet.stopNodes() LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap") - unsetCordappPackages() } @Test @@ -90,18 +99,17 @@ class TwoPartyTradeFlowTests { // We run this in parallel threads to help catch any race conditions that may exist. The other tests // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. - mockNet = MockNetwork(false, true) - - ledger(initialiseSerialization = false) { - val basketOfNodes = mockNet.createSomeNodes(3) - val notaryNode = basketOfNodes.notaryNode - val aliceNode = basketOfNodes.partyNodes[0] - val bobNode = basketOfNodes.partyNodes[1] - val bankNode = basketOfNodes.partyNodes[2] - val cashIssuer = bankNode.info.chooseIdentity().ref(1) - val cpIssuer = bankNode.info.chooseIdentity().ref(1, 2, 3) - val notary = aliceNode.services.getDefaultNotary() - + mockNet = MockNetwork(false, true, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) + val alice = aliceNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val notary = notaryNode.services.getDefaultNotary() + val cashIssuer = bank.ref(1) + val cpIssuer = bank.ref(1, 2, 3) aliceNode.internals.disableDBCloseOnStop() bobNode.internals.disableDBCloseOnStop() @@ -112,8 +120,8 @@ class TwoPartyTradeFlowTests { } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, cpIssuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second + fillUpForSeller(false, cpIssuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), null, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -141,14 +149,15 @@ class TwoPartyTradeFlowTests { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { - mockNet = MockNetwork(false, true) - - ledger(initialiseSerialization = false) { - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1) + mockNet = MockNetwork(false, true, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) + val alice = aliceNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1) val notary = aliceNode.services.getDefaultNotary() aliceNode.internals.disableDBCloseOnStop() @@ -156,12 +165,12 @@ class TwoPartyTradeFlowTests { val cashStates = bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3, - issuedBy = issuer) - } + issuedBy = issuer) + } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), null, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -196,39 +205,30 @@ class TwoPartyTradeFlowTests { @Test fun `shutdown and restore`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) - - // Let the nodes know about each other - normally the network map would handle this - mockNet.registerIdentities() - - aliceNode.database.transaction { - aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.chooseIdentityAndCert()) - } - bobNode.database.transaction { - bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.chooseIdentityAndCert()) - } + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + var bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) aliceNode.internals.disableDBCloseOnStop() bobNode.internals.disableDBCloseOnStop() val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle - val networkMapAddress = notaryNode.network.myAddress - mockNet.runNetwork() // Clear network map registration messages - val notary = aliceNode.services.getDefaultNotary() + + val notary = notaryNode.services.getDefaultNotary() + val alice = aliceNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary, issuedBy = issuer) } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), null, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) val aliceFuture = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult @@ -268,13 +268,12 @@ class TwoPartyTradeFlowTests { // ... bring the node back up ... the act of constructing the SMM will re-register the message handlers // that Bob was waiting on before the reboot occurred. - bobNode = mockNet.createNode(networkMapAddress, bobAddr.id, object : MockNetwork.Factory { + bobNode = mockNet.createNode(bobAddr.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): MockNetwork.MockNode { - return MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, bobAddr.id, overrideServices, entropyRoot) + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { + return MockNetwork.MockNode(config, network, networkMapAddr, bobAddr.id, notaryIdentity, entropyRoot) } - }, BOB.name) + }, BOB_NAME) // Find the future representing the result of this state machine again. val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second @@ -307,18 +306,15 @@ class TwoPartyTradeFlowTests { // Creates a mock node with an overridden storage service that uses a RecordingMap, that lets us test the order // of gets and puts. - private fun makeNodeWithTracking( - networkMapAddress: SingleMessageRecipient?, - name: CordaX500Name): StartedNode { + private fun makeNodeWithTracking(name: CordaX500Name): StartedNode { // Create a node in the mock network ... - return mockNet.createNode(networkMapAddress, nodeFactory = object : MockNetwork.Factory { + return mockNet.createNode(nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, - overrideServices: Map?, + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { // That constructs a recording tx storage override fun makeTransactionStorage(): WritableTransactionStorage { return RecordingTransactionStorage(database, super.makeTransactionStorage()) @@ -330,18 +326,18 @@ class TwoPartyTradeFlowTests { @Test fun `check dependencies of sale asset are resolved`() { - mockNet = MockNetwork(false) - - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) - val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + val notaryNode = mockNet.createNotaryNode() + val aliceNode = makeNodeWithTracking(ALICE_NAME) + val bobNode = makeNodeWithTracking(BOB_NAME) + val bankNode = makeNodeWithTracking(BOC_NAME) mockNet.runNetwork() notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() - - mockNet.registerIdentities() + val alice = aliceNode.info.singleIdentity() + val bob = bobNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) ledger(aliceNode.services, initialiseSerialization = false) { @@ -357,12 +353,12 @@ class TwoPartyTradeFlowTests { } val bobsFakeCash = bobNode.database.transaction { - fillUpForBuyer(false, issuer, AnonymousParty(bobNode.info.chooseIdentity().owningKey), notary) + fillUpForBuyer(false, issuer, AnonymousParty(bob.owningKey), notary) }.second val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode) val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), attachmentID, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second } val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -437,19 +433,18 @@ class TwoPartyTradeFlowTests { @Test fun `track works`() { - mockNet = MockNetwork(false) - - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) - val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + val notaryNode = mockNet.createNotaryNode() + val aliceNode = makeNodeWithTracking(ALICE_NAME) + val bobNode = makeNodeWithTracking(BOB_NAME) + val bankNode = makeNodeWithTracking(BOC_NAME) mockNet.runNetwork() notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() - - mockNet.registerIdentities() + val alice: Party = aliceNode.info.singleIdentity() + val bank: Party = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) ledger(aliceNode.services, initialiseSerialization = false) { // Insert a prospectus type attachment into the commercial paper transaction. @@ -470,8 +465,8 @@ class TwoPartyTradeFlowTests { insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode) val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), attachmentID, notary).second + fillUpForSeller(false, issuer, alice, + 1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -520,16 +515,16 @@ class TwoPartyTradeFlowTests { @Test fun `dependency with error on buyer side`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { runWithError(true, false, "at least one cash input") } } @Test fun `dependency with error on seller side`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { runWithError(false, true, "Issuances have a time-window") } } @@ -544,8 +539,7 @@ class TwoPartyTradeFlowTests { private fun runBuyerAndSeller(notary: Party, sellerNode: StartedNode, buyerNode: StartedNode, - assetToSell: StateAndRef, - anonymous: Boolean = true): RunResult { + assetToSell: StateAndRef): RunResult { val buyerFlows: Observable> = buyerNode.internals.registerInitiatedFlow(BuyerAcceptor::class.java) val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine } val seller = SellerInitiator(buyerNode.info.chooseIdentity(), notary, assetToSell, 1000.DOLLARS, anonymous) @@ -596,26 +590,24 @@ class TwoPartyTradeFlowTests { aliceError: Boolean, expectedMessageSubstring: String ) { - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) - val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bankNode = mockNet.createPartyNode(BOC_NAME) mockNet.runNetwork() notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() - - // Let the nodes know about each other - normally the network map would handle this - mockNet.registerIdentities() + val alice = aliceNode.info.singleIdentity() + val bob = bobNode.info.singleIdentity() + val bank = bankNode.info.singleIdentity() + val issuer = bank.ref(1, 2, 3) val bobsBadCash = bobNode.database.transaction { - fillUpForBuyer(bobError, issuer, bobNode.info.chooseIdentity(), - notary).second + fillUpForBuyer(bobError, issuer, bob, notary).second } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(aliceError, issuer, aliceNode.info.chooseIdentity(), - 1200.DOLLARS `issued by` issuer, null, notary).second + fillUpForSeller(aliceError, issuer, alice,1200.DOLLARS `issued by` issuer, null, notary).second } insertFakeTransactions(bobsBadCash, bobNode, notaryNode, bankNode) @@ -681,8 +673,8 @@ class TwoPartyTradeFlowTests { // wants to sell to Bob. val eb1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { // Issued money to itself. - output(Cash.PROGRAM_ID, "elbonian money 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } - output(Cash.PROGRAM_ID, "elbonian money 2", notary = notary) { 1000.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } + output(Cash.PROGRAM_ID, "elbonian money 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } + output(Cash.PROGRAM_ID, "elbonian money 2", notary = notary) { 1000.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } if (!withError) { command(issuer.party.owningKey) { Cash.Commands.Issue() } } else { @@ -700,15 +692,15 @@ class TwoPartyTradeFlowTests { // Bob gets some cash onto the ledger from BoE val bc1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { input("elbonian money 1") - output(Cash.PROGRAM_ID, "bob cash 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` owner } + output(Cash.PROGRAM_ID, "bob cash 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy owner } command(interimOwner.owningKey) { Cash.Commands.Move() } this.verifies() } val bc2 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { input("elbonian money 2") - output(Cash.PROGRAM_ID, "bob cash 2", notary = notary) { 300.DOLLARS.CASH `issued by` issuer `owned by` owner } - output(Cash.PROGRAM_ID, notary = notary) { 700.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } // Change output. + output(Cash.PROGRAM_ID, "bob cash 2", notary = notary) { 300.DOLLARS.CASH issuedBy issuer ownedBy owner } + output(Cash.PROGRAM_ID, notary = notary) { 700.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } // Change output. command(interimOwner.owningKey) { Cash.Commands.Move() } this.verifies() } diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index ea0106a076..58aea5fac8 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -3,17 +3,17 @@ package net.corda.node.services import net.corda.core.contracts.* import net.corda.core.crypto.generateKeyPair import net.corda.core.flows.NotaryChangeFlow +import net.corda.core.flows.NotaryFlow import net.corda.core.flows.StateReplacementException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.api.ServiceHubInternal import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -32,49 +32,39 @@ class NotaryChangeTests { lateinit var newNotaryNode: StartedNode lateinit var clientNodeA: StartedNode lateinit var clientNodeB: StartedNode - lateinit var notaryNewId: Party - lateinit var notaryOldId: Party + lateinit var newNotaryParty: Party + lateinit var oldNotaryParty: Party @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() - oldNotaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) - clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) - newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(SimpleNotaryService.type)) - mockNet.registerIdentities() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + oldNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + clientNodeA = mockNet.createNode() + clientNodeB = mockNet.createNode() + newNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name.copy(organisation = "Dummy Notary 2")) mockNet.runNetwork() // Clear network map registration messages oldNotaryNode.internals.ensureRegistered() - notaryNewId = newNotaryNode.info.legalIdentities[1] - notaryOldId = oldNotaryNode.info.legalIdentities[1] + oldNotaryParty = newNotaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME)!! + newNotaryParty = newNotaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME.copy(organisation = "Dummy Notary 2"))!! } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `should change notary for a state with single participant`() { - val state = issueState(clientNodeA, oldNotaryNode, notaryOldId) - val newNotary = notaryNewId - val flow = NotaryChangeFlow(state, newNotary) - val future = clientNodeA.services.startFlow(flow) - - mockNet.runNetwork() - - val newState = future.resultFuture.getOrThrow() - assertEquals(newState.state.notary, newNotary) + val state = issueState(clientNodeA, oldNotaryParty) + assertEquals(state.state.notary, oldNotaryParty) + val newState = changeNotary(state, clientNodeA, newNotaryParty) + assertEquals(newState.state.notary, newNotaryParty) } @Test fun `should change notary for a state with multiple participants`() { - val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, notaryOldId) - val newNotary = notaryNewId + val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, oldNotaryParty) + val newNotary = newNotaryParty val flow = NotaryChangeFlow(state, newNotary) val future = clientNodeA.services.startFlow(flow) @@ -89,7 +79,7 @@ class NotaryChangeTests { @Test fun `should throw when a participant refuses to change Notary`() { - val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, notaryOldId) + val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, oldNotaryParty) val newEvilNotary = getTestPartyAndCertificate(CordaX500Name(organisation = "Evil R3", locality = "London", country = "GB"), generateKeyPair().public) val flow = NotaryChangeFlow(state, newEvilNotary.party) val future = clientNodeA.services.startFlow(flow) @@ -103,10 +93,10 @@ class NotaryChangeTests { @Test fun `should not break encumbrance links`() { - val issueTx = issueEncumberedState(clientNodeA, notaryOldId) + val issueTx = issueEncumberedState(clientNodeA, oldNotaryParty) val state = StateAndRef(issueTx.outputs.first(), StateRef(issueTx.id, 0)) - val newNotary = notaryNewId + val newNotary = newNotaryParty val flow = NotaryChangeFlow(state, newNotary) val future = clientNodeA.services.startFlow(flow) mockNet.runNetwork() @@ -135,6 +125,47 @@ class NotaryChangeTests { } } + @Test + fun `notary change and regular transactions are properly handled during resolution in longer chains`() { + val issued = issueState(clientNodeA, oldNotaryParty) + val moved = moveState(issued, clientNodeA, clientNodeB) + + // We don't to tx resolution when moving state to another node, so need to add the issue transaction manually + // to node B. The resolution process is tested later during notarisation. + clientNodeB.services.recordTransactions(clientNodeA.services.validatedTransactions.getTransaction(issued.ref.txhash)!!) + + val changedNotary = changeNotary(moved, clientNodeB, newNotaryParty) + val movedBack = moveState(changedNotary, clientNodeB, clientNodeA) + val changedNotaryBack = changeNotary(movedBack, clientNodeA, oldNotaryParty) + + assertEquals(issued.state, changedNotaryBack.state) + } + + private fun changeNotary(movedState: StateAndRef, node: StartedNode<*>, newNotary: Party): StateAndRef { + val flow = NotaryChangeFlow(movedState, newNotary) + val future = node.services.startFlow(flow) + mockNet.runNetwork() + + return future.resultFuture.getOrThrow() + } + + private fun moveState(state: StateAndRef, fromNode: StartedNode<*>, toNode: StartedNode<*>): StateAndRef { + val tx = DummyContract.move(state, toNode.info.chooseIdentity()) + val stx = fromNode.services.signInitialTransaction(tx) + + val notaryFlow = NotaryFlow.Client(stx) + val future = fromNode.services.startFlow(notaryFlow) + mockNet.runNetwork() + + val notarySignature = future.resultFuture.getOrThrow() + val finalTransaction = stx + notarySignature + + fromNode.services.recordTransactions(finalTransaction) + toNode.services.recordTransactions(finalTransaction) + + return finalTransaction.tx.outRef(0) + } + private fun issueEncumberedState(node: StartedNode<*>, notaryIdentity: Party): WireTransaction { val owner = node.info.chooseIdentity().ref(0) val stateA = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) @@ -161,30 +192,31 @@ class NotaryChangeTests { // - The transaction type is not a notary change transaction at all. } -fun issueState(node: StartedNode<*>, notaryNode: StartedNode<*>, notaryIdentity: Party): StateAndRef<*> { +fun issueState(node: StartedNode<*>, notaryIdentity: Party): StateAndRef { val tx = DummyContract.generateInitial(Random().nextInt(), notaryIdentity, node.info.chooseIdentity().ref(0)) - val signedByNode = node.services.signInitialTransaction(tx) - val stx = notaryNode.services.addSignature(signedByNode, notaryIdentity.owningKey) + val stx = node.services.signInitialTransaction(tx) node.services.recordTransactions(stx) - return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) + return stx.tx.outRef(0) } fun issueMultiPartyState(nodeA: StartedNode<*>, nodeB: StartedNode<*>, notaryNode: StartedNode<*>, notaryIdentity: Party): StateAndRef { - val state = TransactionState(DummyContract.MultiOwnerState(0, - listOf(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity())), DummyContract.PROGRAM_ID, notaryIdentity) - val tx = TransactionBuilder(notary = notaryIdentity).withItems(state, dummyCommand()) + val participants = listOf(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity()) + val state = TransactionState( + DummyContract.MultiOwnerState(0, participants), + DummyContract.PROGRAM_ID, notaryIdentity) + val tx = TransactionBuilder(notary = notaryIdentity).withItems(state, dummyCommand(participants.first().owningKey)) val signedByA = nodeA.services.signInitialTransaction(tx) val signedByAB = nodeB.services.addSignature(signedByA) val stx = notaryNode.services.addSignature(signedByAB, notaryIdentity.owningKey) nodeA.services.recordTransactions(stx) nodeB.services.recordTransactions(stx) - return StateAndRef(state, StateRef(stx.id, 0)) + return stx.tx.outRef(0) } -fun issueInvalidState(node: StartedNode<*>, notary: Party): StateAndRef<*> { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) +fun issueInvalidState(services: ServiceHub, identity: Party, notary: Party): StateAndRef { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) tx.setTimeWindow(Instant.now(), 30.seconds) - val stx = node.services.signInitialTransaction(tx) - node.services.recordTransactions(stx) - return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) -} + val stx = services.signInitialTransaction(tx) + services.recordTransactions(stx) + return stx.tx.outRef(0) +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index 7a55bdc679..58f11a9d61 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -1,6 +1,7 @@ package net.corda.node.services.config import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.seconds import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -29,14 +30,12 @@ class FullNodeConfigurationTest { p2pAddress = NetworkHostAndPort("localhost", 0), rpcAddress = NetworkHostAndPort("localhost", 1), messagingServerAddress = null, - extraAdvertisedServiceIds = emptyList(), - bftSMaRt = BFTSMaRtConfiguration(-1, false), - notaryNodeAddress = null, - notaryClusterAddresses = emptyList(), + notary = null, certificateChainCheckPolicies = emptyList(), devMode = true, - relay = null - ) + activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0)), + additionalNodeInfoPollingFrequencyMsec = 5.seconds.toMillis(), + relay = null) fun configWithRPCUsername(username: String) { testConfiguration.copy(rpcUsers = listOf(User(username, "pass", emptySet()))) diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 33202b4874..d54bfa8754 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -9,12 +9,12 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.ServiceHub -import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.days import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl @@ -72,14 +72,13 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") initialiseTestSerialization() countDown = CountDownLatch(1) smmHasRemovedAllFlows = CountDownLatch(1) calls = 0 val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() - database = configureDatabase(dataSourceProps, databaseProperties, createIdentityService = ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, databaseProperties, ::makeTestIdentityService) val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) val kms = MockKeyManagementService(identityService, ALICE_KEY) @@ -96,9 +95,9 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { - override val vaultService: VaultService = NodeVaultService(this) + override val vaultService: VaultServiceInternal = NodeVaultService(testClock, kms, stateLoader, database.hibernateConfig) override val testReference = this@NodeSchedulerServiceTest - override val cordappProvider: CordappProviderImpl = CordappProviderImpl(CordappLoader.createWithTestPackages()).start(attachments) + override val cordappProvider = CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.testing.contracts")), attachments) } smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) scheduler = NodeSchedulerService(services, schedulerGatedExecutor, serverThread = smmExecutor) @@ -124,7 +123,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { smmExecutor.awaitTermination(60, TimeUnit.SECONDS) database.close() resetTestSerialization() - unsetCordappPackages() } class TestState(val flowLogicRef: FlowLogicRef, val instant: Instant, val myIdentity: Party) : LinearState, SchedulableState { diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index bfd6557377..be2fc12b96 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -5,7 +5,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.services.VaultQueryService +import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM import net.corda.core.node.services.vault.PageSpecification @@ -15,10 +15,7 @@ import net.corda.core.node.services.vault.SortAttribute import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -94,13 +91,10 @@ class ScheduledFlowTests { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork(threadPerNode = true) - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) - val a = mockNet.createUnstartedNode(notaryNode.network.myAddress) - val b = mockNet.createUnstartedNode(notaryNode.network.myAddress) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.testing.contracts")) + notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + val a = mockNet.createUnstartedNode() + val b = mockNet.createUnstartedNode() notaryNode.internals.ensureRegistered() @@ -112,7 +106,6 @@ class ScheduledFlowTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test @@ -128,10 +121,10 @@ class ScheduledFlowTests { nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.chooseIdentity())) mockNet.waitQuiescent() val stateFromA = nodeA.database.transaction { - nodeA.services.vaultQueryService.queryBy().states.single() + nodeA.services.vaultService.queryBy().states.single() } val stateFromB = nodeB.database.transaction { - nodeB.services.vaultQueryService.queryBy().states.single() + nodeB.services.vaultService.queryBy().states.single() } assertEquals(1, countScheduledFlows) assertEquals("Must be same copy on both nodes", stateFromA, stateFromB) @@ -153,12 +146,12 @@ class ScheduledFlowTests { // Convert the states into maps to make error reporting easier val statesFromA: List> = nodeA.database.transaction { - queryStatesWithPaging(nodeA.services.vaultQueryService) + queryStatesWithPaging(nodeA.services.vaultService) } val statesFromB: List> = nodeB.database.transaction { - queryStatesWithPaging(nodeB.services.vaultQueryService) + queryStatesWithPaging(nodeB.services.vaultService) } - assertEquals("Expect all states to be present",2 * N, statesFromA.count()) + assertEquals("Expect all states to be present", 2 * N, statesFromA.count()) statesFromA.forEach { ref -> if (ref !in statesFromB) { throw IllegalStateException("State $ref is only present on node A.") @@ -179,13 +172,13 @@ class ScheduledFlowTests { * * @return states ordered by the transaction ID. */ - private fun queryStatesWithPaging(vaultQueryService: VaultQueryService): List> { + private fun queryStatesWithPaging(vaultService: VaultService): List> { // DOCSTART VaultQueryExamplePaging var pageNumber = DEFAULT_PAGE_NUM val states = mutableListOf>() do { val pageSpec = PageSpecification(pageSize = PAGE_SIZE, pageNumber = pageNumber) - val results = vaultQueryService.queryBy(VaultQueryCriteria(), pageSpec, SORTING) + val results = vaultService.queryBy(VaultQueryCriteria(), pageSpec, SORTING) states.addAll(results.states) pageNumber++ } while ((pageSpec.pageSize * (pageNumber)) <= results.totalStatesAvailable) diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 3d1942c01c..418322b589 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -40,7 +40,9 @@ import kotlin.test.assertNull //TODO This needs to be merged into P2PMessagingTest as that creates a more realistic environment class ArtemisMessagingTests : TestDependencyInjectionBase() { - @Rule @JvmField val temporaryFolder = TemporaryFolder() + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() val serverPort = freePort() val rpcPort = freePort() @@ -69,7 +71,7 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { baseDirectory = baseDirectory, myLegalName = ALICE.name) LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) networkMapRegistrationFuture = doneFuture(Unit) networkMapCache = PersistentNetworkMapCache(serviceHub = object : MockServiceHubInternal(database, config) {}) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 7b133c965f..b3b9a9f769 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -7,7 +7,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.send @@ -21,16 +21,11 @@ import net.corda.node.services.network.NetworkMapService.Companion.PUSH_TOPIC import net.corda.node.services.network.NetworkMapService.Companion.QUERY_TOPIC import net.corda.node.services.network.NetworkMapService.Companion.REGISTER_TOPIC import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_TOPIC -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.REMOVE -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.CHARLIE -import net.corda.testing.DUMMY_MAP -import net.corda.testing.chooseIdentity -import net.corda.testing.chooseIdentityAndCert +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import org.assertj.core.api.Assertions.assertThat @@ -49,17 +44,14 @@ abstract class AbstractNetworkMapServiceTest lateinit var alice: StartedNode companion object { - val subscriberLegalName = CordaX500Name(organisation ="Subscriber", locality ="New York", country ="US") + val subscriberLegalName = CordaX500Name(organisation = "Subscriber", locality = "New York", country = "US") } @Before fun setup() { mockNet = MockNetwork(defaultFactory = nodeFactory) - mapServiceNode = mockNet.createNode( - nodeFactory = nodeFactory, - legalName = DUMMY_MAP.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - alice = mockNet.createNode(mapServiceNode.network.myAddress, nodeFactory = nodeFactory, legalName = ALICE.name) + mapServiceNode = mockNet.networkMapNode + alice = mockNet.createNode(nodeFactory = nodeFactory, legalName = ALICE.name) mockNet.runNetwork() lastSerial = System.currentTimeMillis() } @@ -213,7 +205,7 @@ abstract class AbstractNetworkMapServiceTest private var lastSerial = Long.MIN_VALUE private fun StartedNode<*>.registration(addOrRemove: AddOrRemove, - serial: Long? = null): CordaFuture { + serial: Long? = null): CordaFuture { val distinctSerial = if (serial == null) { ++lastSerial } else { @@ -254,7 +246,7 @@ abstract class AbstractNetworkMapServiceTest } private fun addNewNodeToNetworkMap(legalName: CordaX500Name): StartedNode { - val node = mockNet.createNode(mapServiceNode.network.myAddress, legalName = legalName) + val node = mockNet.createNode(legalName = legalName) mockNet.runNetwork() lastSerial = System.currentTimeMillis() return node @@ -278,12 +270,11 @@ abstract class AbstractNetworkMapServiceTest override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, - overrideServices: Map?, + notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { - return object : MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { - override fun makeNetworkMapService() = NullNetworkMapService + return object : MockNode(config, network, null, id, notaryIdentity, entropyRoot) { + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal) = NullNetworkMapService } } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index 1297d979cb..10aa2c62ac 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -2,7 +2,6 @@ package net.corda.node.services.network import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.chooseIdentity @@ -29,10 +28,9 @@ class NetworkMapCacheTest { @Test fun registerWithNetwork() { - val nodes = mockNet.createSomeNodes(1) - val n0 = nodes.mapNode - val n1 = nodes.partyNodes[0] - val future = n1.services.networkMapCache.addMapService(n1.network, n0.network.myAddress, false, null) + mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val future = aliceNode.services.networkMapCache.addMapService(aliceNode.network, mockNet.networkMapNode.network.myAddress, false, null) mockNet.runNetwork() future.getOrThrow() } @@ -40,50 +38,59 @@ class NetworkMapCacheTest { @Test fun `key collision`() { val entropy = BigInteger.valueOf(24012017L) - val nodeA = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = ALICE.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) - val nodeB = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) - assertEquals(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity()) - + val aliceNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = ALICE.name, entropyRoot = entropy) mockNet.runNetwork() // Node A currently knows only about itself, so this returns node A - assertEquals(nodeA.services.networkMapCache.getNodesByLegalIdentityKey(nodeA.info.chooseIdentity().owningKey).singleOrNull(), nodeA.info) + assertEquals(aliceNode.services.networkMapCache.getNodesByLegalIdentityKey(aliceNode.info.chooseIdentity().owningKey).singleOrNull(), aliceNode.info) + val bobNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy) + assertEquals(aliceNode.info.chooseIdentity(), bobNode.info.chooseIdentity()) - nodeA.services.networkMapCache.addNode(nodeB.info) + aliceNode.services.networkMapCache.addNode(bobNode.info) // The details of node B write over those for node A - assertEquals(nodeA.services.networkMapCache.getNodesByLegalIdentityKey(nodeA.info.chooseIdentity().owningKey).singleOrNull(), nodeB.info) + assertEquals(aliceNode.services.networkMapCache.getNodesByLegalIdentityKey(aliceNode.info.chooseIdentity().owningKey).singleOrNull(), bobNode.info) } @Test fun `getNodeByLegalIdentity`() { - val nodes = mockNet.createSomeNodes(1) - val n0 = nodes.mapNode - val n1 = nodes.partyNodes[0] - val node0Cache: NetworkMapCache = n0.services.networkMapCache - val expected = n1.info + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val notaryCache: NetworkMapCache = notaryNode.services.networkMapCache + val expected = aliceNode.info mockNet.runNetwork() - val actual = n0.database.transaction { node0Cache.getNodeByLegalIdentity(n1.info.chooseIdentity()) } + val actual = notaryNode.database.transaction { notaryCache.getNodeByLegalIdentity(aliceNode.info.chooseIdentity()) } assertEquals(expected, actual) // TODO: Should have a test case with anonymous lookup } @Test - fun `remove node from cache`() { - val nodes = mockNet.createSomeNodes(1) - val n0 = nodes.mapNode - val n1 = nodes.partyNodes[0] - val n0Identity = n0.info.chooseIdentity() - val n1Identity = n1.info.chooseIdentity() - val node0Cache = n0.services.networkMapCache as PersistentNetworkMapCache + fun `getPeerByLegalName`() { + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val notaryCache: NetworkMapCache = notaryNode.services.networkMapCache + val expected = aliceNode.info.legalIdentities.single() + mockNet.runNetwork() - n0.database.transaction { - assertThat(node0Cache.getNodeByLegalIdentity(n1Identity) != null) - node0Cache.removeNode(n1.info) - assertThat(node0Cache.getNodeByLegalIdentity(n1Identity) == null) - assertThat(node0Cache.getNodeByLegalIdentity(n0Identity) != null) - assertThat(node0Cache.getNodeByLegalName(n1Identity.name) == null) + val actual = notaryNode.database.transaction { notaryCache.getPeerByLegalName(ALICE.name) } + assertEquals(expected, actual) + } + + @Test + fun `remove node from cache`() { + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val notaryLegalIdentity = notaryNode.info.chooseIdentity() + val alice = aliceNode.info.chooseIdentity() + val notaryCache = notaryNode.services.networkMapCache as PersistentNetworkMapCache + mockNet.runNetwork() + notaryNode.database.transaction { + assertThat(notaryCache.getNodeByLegalIdentity(alice) != null) + notaryCache.removeNode(aliceNode.info) + assertThat(notaryCache.getNodeByLegalIdentity(alice) == null) + assertThat(notaryCache.getNodeByLegalIdentity(notaryLegalIdentity) != null) + assertThat(notaryCache.getNodeByLegalName(alice.name) == null) } } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt index 2f6966097a..612c8e943a 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt @@ -1,9 +1,10 @@ package net.corda.node.services.network import net.corda.core.messaging.SingleMessageRecipient -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.messaging.MessagingService +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import java.math.BigInteger @@ -30,12 +31,11 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest, id: Int, - overrideServices: Map?, + notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { - return object : MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { - override fun makeNetworkMapService() = SwizzleNetworkMapService(services) + return object : MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal) = SwizzleNetworkMapService(network, networkMapCache) } } } @@ -44,12 +44,13 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest PersistentNetworkMapService) : NetworkMapService { + constructor(network: MessagingService, networkMapCache: NetworkMapCacheInternal) : this({ PersistentNetworkMapService(network, networkMapCache, 1) }) + var delegate = delegateFactory() fun swizzle() { delegate.unregisterNetworkHandlers() - delegate = PersistentNetworkMapService(services, 1) + delegate = delegateFactory() } } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index aaa8e49db1..f71f266388 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -33,7 +33,7 @@ class DBCheckpointStorageTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) newCheckpointStorage() } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index ec6a134f13..2b9ff96f1c 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -6,18 +6,14 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.node.services.VaultService -import net.corda.core.schemas.MappedSchema import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.finance.schemas.CashSchemaV1 -import net.corda.finance.schemas.SampleCashSchemaV2 -import net.corda.finance.schemas.SampleCashSchemaV3 +import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.vault.NodeVaultService -import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.* @@ -42,26 +38,23 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceProps = makeTestDataSourceProperties() - - val createSchemaService = { NodeSchemaService() } - - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), createSchemaService, ::makeTestIdentityService) - + database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), ::makeTestIdentityService) database.transaction { services = object : MockServices(BOB_KEY) { - override val vaultService: VaultService get() { - val vaultService = NodeVaultService(this) - hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) - return vaultService - } + override val vaultService: VaultServiceInternal + get() { + val vaultService = NodeVaultService(clock, keyManagementService, stateLoader, database.hibernateConfig) + hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) + return vaultService + } override fun recordTransactions(txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) + vaultService.notifyAll(txs.map { it.tx }) } } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index a5ac113dc6..0e1cdc1047 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -9,7 +9,9 @@ import net.corda.core.utilities.toBase58String import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.schemas.CommonSchemaV1 +import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef +import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction import net.corda.finance.DOLLARS @@ -24,8 +26,6 @@ import net.corda.finance.schemas.SampleCashSchemaV2 import net.corda.finance.schemas.SampleCashSchemaV3 import net.corda.finance.utils.sumCash import net.corda.node.services.schema.HibernateObserver -import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -73,40 +73,38 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset") - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) + val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset") + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) val dataSourceProps = makeTestDataSourceProperties() val defaultDatabaseProperties = makeTestDatabaseProperties() - val createSchemaService = { NodeSchemaService() } - database = configureDatabase(dataSourceProps, defaultDatabaseProperties, createSchemaService, ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, defaultDatabaseProperties, ::makeTestIdentityService) database.transaction { hibernateConfig = database.hibernateConfig - services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { - override val vaultService: VaultService = makeVaultService(database.hibernateConfig) - + services = object : MockServices(cordappPackages, BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { + override val vaultService = makeVaultService(database.hibernateConfig) override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) + vaultService.notifyAll(txs.map { it.tx }) } + override fun jdbcSession() = database.createSession() } hibernatePersister = services.hibernatePersister } setUpDb() - - val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) - sessionFactory = hibernateConfig.sessionFactoryForSchemas(*customSchemas.toTypedArray()) + sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) entityManager = sessionFactory.createEntityManager() criteriaBuilder = sessionFactory.criteriaBuilder } + private fun sessionFactoryForSchemas(vararg schemas: MappedSchema) = hibernateConfig.sessionFactoryForSchemas(schemas.toSet()) + @After fun cleanUp() { database.close() - unsetCordappPackages() } private fun setUpDb() { @@ -141,17 +139,17 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList - val coins = queryResults.map { it.contractState.deserialize>().data }.sumCash() + val coins = queryResults.map { it.contractState.deserialize>(context = SerializationDefaults.STORAGE_CONTEXT).data }.sumCash() assertThat(coins.toDecimal() >= BigDecimal("50.00")) } @Test fun `select by composite primary key`() { val issuedStates = - database.transaction { - services.fillWithSomeTestLinearStates(8) - services.fillWithSomeTestLinearStates(2) - } + database.transaction { + services.fillWithSomeTestLinearStates(8) + services.fillWithSomeTestLinearStates(2) + } val persistentStateRefs = issuedStates.states.map { PersistentStateRef(it.ref) }.toList() // structure query @@ -385,7 +383,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // aggregate function criteriaQuery.multiselect(cashStates.get("currency"), - criteriaBuilder.sum(cashStates.get("pennies"))) + criteriaBuilder.sum(cashStates.get("pennies"))) // group by criteriaQuery.groupBy(cashStates.get("currency")) @@ -538,8 +536,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) services.fillWithSomeTestLinearStates(2) } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -570,8 +567,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) services.fillWithSomeTestLinearStates(2) } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -629,7 +625,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = ALICE) val cashStates = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), - issuedBy = BOB.ref(0), ownedBy = (BOB)).states + issuedBy = BOB.ref(0), ownedBy = (BOB)).states // persist additional cash states explicitly with V3 schema cashStates.forEach { val cashState = it.state.data @@ -637,8 +633,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) } } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -662,9 +657,10 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { val queryResults = entityManager.createQuery(criteriaQuery).resultList queryResults.forEach { - val contractState = it.contractState.deserialize>() + val contractState = it.contractState.deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) val cashState = contractState.data as Cash.State - println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") } + println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") + } assertThat(queryResults).hasSize(12) } @@ -699,32 +695,32 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { @Test fun `query fungible states by participants`() { val firstCashState = - database.transaction { - // persist original cash states explicitly with V3 schema - cashStates.forEach { - val cashState = it.state.data - val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + database.transaction { + // persist original cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } - val moreCash = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), - issuedBy = BOB.ref(0), ownedBy = BOB).states - // persist additional cash states explicitly with V3 schema - moreCash.forEach { - val cashState = it.state.data - val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + val moreCash = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), + issuedBy = BOB.ref(0), ownedBy = BOB).states + // persist additional cash states explicitly with V3 schema + moreCash.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } - val cashStates = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = (ALICE)).states - // persist additional cash states explicitly with V3 schema - cashStates.forEach { - val cashState = it.state.data - val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + val cashStates = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = (ALICE)).states + // persist additional cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + cashStates.first() } - cashStates.first() - } // structure query val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) @@ -746,7 +742,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList queryResults.forEach { - val contractState = it.contractState.deserialize>() + val contractState = it.contractState.deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) val cashState = contractState.data as Cash.State println("${it.stateRef} with owner ${cashState.owner.owningKey.toBase58String()} and participants ${cashState.participants.map { it.owningKey.toBase58String() }}") } @@ -765,8 +761,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(2, externalId = "222") services.fillWithSomeTestLinearStates(3, externalId = "333") } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -818,8 +813,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(2, externalId = "222") services.fillWithSomeTestLinearStates(3, externalId = "333") } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -876,7 +870,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { val jdbcSession = services.jdbcSession() val prepStatement = jdbcSession.prepareStatement(nativeQuery) val rs = prepStatement.executeQuery() - // DOCEND JdbcSession + // DOCEND JdbcSession var count = 0 while (rs.next()) { val stateRef = StateRef(SecureHash.parse(rs.getString(1)), rs.getInt(2)) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index 0481ca51cd..a8a352cbf1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -11,7 +11,6 @@ import net.corda.core.internal.write import net.corda.core.internal.writeLines import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.configureDatabase import net.corda.testing.LogHelper import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -41,7 +40,7 @@ class NodeAttachmentStorageTest { LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceProperties = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), ::makeTestIdentityService) fs = Jimfs.newFileSystem(Configuration.unix()) } @@ -98,8 +97,7 @@ class NodeAttachmentStorageTest { @Test fun `corrupt entry throws exception`() { val testJar = makeTestJar() - val id = - database.transaction { + val id = database.transaction { val storage = NodeAttachmentService(MetricRegistry()) val id = testJar.read { storage.importAttachment(it) } @@ -108,7 +106,7 @@ class NodeAttachmentStorageTest { val corruptBytes = "arggghhhh".toByteArray() System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) - DatabaseTransactionManager.current().session.merge(corruptAttachment) + session.merge(corruptAttachment) id } database.transaction { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 7472b66b02..33e8bcdad4 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -57,8 +57,6 @@ class HibernateObserverTests { val testSchema = TestSchema val rawUpdatesPublisher = PublishSubject.create>() val schemaService = object : SchemaService { - override fun registerCustomSchemas(customSchemas: Set) {} - override val schemaOptions: Map = emptyMap() override fun selectSchemas(state: ContractState): Iterable = setOf(testSchema) @@ -70,8 +68,7 @@ class HibernateObserverTests { return parent } } - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { schemaService }, createIdentityService = ::makeTestIdentityService) - + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService, schemaService) @Suppress("UNUSED_VARIABLE") val observer = HibernateObserver(rawUpdatesPublisher, database.hibernateConfig) database.transaction { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index d7b90b8880..be168e8b98 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -3,11 +3,13 @@ package net.corda.node.services.schema import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.packageName import net.corda.core.messaging.startFlow import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.ServiceHubInternal +import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.MockNetwork import net.corda.testing.schemas.DummyLinearStateSchemaV1 @@ -15,6 +17,7 @@ import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType import org.junit.Test import javax.persistence.* +import kotlin.test.assertEquals import kotlin.test.assertTrue class NodeSchemaServiceTest { @@ -23,11 +26,9 @@ class NodeSchemaServiceTest { */ @Test fun `registering custom schemas for testing with MockNode`() { - val mockNet = MockNetwork() + val mockNet = MockNetwork(cordappPackages = listOf(DummyLinearStateSchemaV1::class.packageName)) val mockNode = mockNet.createNode() mockNet.runNetwork() - - mockNode.internals.registerCustomSchemas(setOf(DummyLinearStateSchemaV1)) val schemaService = mockNode.services.schemaService assertTrue(schemaService.schemaOptions.containsKey(DummyLinearStateSchemaV1)) @@ -41,7 +42,7 @@ class NodeSchemaServiceTest { */ @Test fun `auto scanning of custom schemas for testing with Driver`() { - driver (startNodesInProcess = true) { + driver(startNodesInProcess = true) { val node = startNode() val nodeHandle = node.getOrThrow() val result = nodeHandle.rpc.startFlow(::MappedSchemasFlow) @@ -50,10 +51,20 @@ class NodeSchemaServiceTest { } } + @Test + fun `custom schemas are loaded eagerly`() { + val expected = setOf("PARENTS", "CHILDREN") + assertEquals>(expected, driver { + (startNode(startInSameProcess = true).getOrThrow() as NodeHandle.InProcess).node.database.transaction { + session.createNativeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES").list() + } + }.toMutableSet().apply { retainAll(expected) }) + } + @StartableByRPC class MappedSchemasFlow : FlowLogic>() { @Suspendable - override fun call() : List { + override fun call(): List { // returning MappedSchema's as String'ified family names to avoid whitelist serialization errors return (this.serviceHub as ServiceHubInternal).schemaService.schemaOptions.keys.map { it.name } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 43869643f7..e65592f70b 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -6,10 +6,8 @@ import co.paralleluniverse.strands.concurrent.Semaphore import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef -import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* -import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.map @@ -21,24 +19,16 @@ import net.corda.core.serialization.serialize import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Change import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.finance.DOLLARS -import net.corda.finance.flows.CashIssueFlow -import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.checkpoints -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState -import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork @@ -68,68 +58,61 @@ class FlowFrameworkTests { private lateinit var mockNet: MockNetwork private val receivedSessionMessages = ArrayList() - private lateinit var node1: StartedNode - private lateinit var node2: StartedNode - private lateinit var notary1: StartedNode - private lateinit var notary2: StartedNode - private lateinit var notary1Identity: Party - private lateinit var notary2Identity: Party + private lateinit var aliceNode: StartedNode + private lateinit var bobNode: StartedNode + private lateinit var notaryIdentity: Party + private lateinit var alice: Party + private lateinit var bob: Party @Before fun start() { - setCordappPackages("net.corda.finance.contracts", "net.corda.testing.contracts") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts", "net.corda.testing.contracts")) + aliceNode = mockNet.createNode(legalName = ALICE_NAME) + bobNode = mockNet.createNode(legalName = BOB_NAME) mockNet.runNetwork() - node1.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() // We intentionally create our own notary and ignore the one provided by the network - val notaryKeyPair = generateKeyPair() - val notaryService = ServiceInfo(ValidatingNotaryService.type, CordaX500Name(commonName = ValidatingNotaryService.type.id, organisation = "Notary service 2000", locality = "London", country = "GB")) - val overrideServices = mapOf(Pair(notaryService, notaryKeyPair)) // Note that these notaries don't operate correctly as they don't share their state. They are only used for testing // service addressing. - notary1 = mockNet.createNotaryNode(networkMapAddress = node1.network.myAddress, overrideServices = overrideServices, serviceName = notaryService.name) - notary2 = mockNet.createNotaryNode(networkMapAddress = node1.network.myAddress, overrideServices = overrideServices, serviceName = notaryService.name) + val notary = mockNet.createNotaryNode() receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } mockNet.runNetwork() - // We don't create a network map, so manually handle registrations - mockNet.registerIdentities() - notary1Identity = notary1.services.myInfo.legalIdentities[1] - notary2Identity = notary2.services.myInfo.legalIdentities[1] + // Extract identities + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + notaryIdentity = notary.services.getDefaultNotary() } @After fun cleanUp() { mockNet.stopNodes() receivedSessionMessages.clear() - unsetCordappPackages() } @Test fun `newly added flow is preserved on restart`() { - node1.services.startFlow(NoOpFlow(nonTerminating = true)) - node1.internals.acceptableLiveFiberCountOnStop = 1 - val restoredFlow = node1.restartAndGetRestoredFlow() + aliceNode.services.startFlow(NoOpFlow(nonTerminating = true)) + aliceNode.internals.acceptableLiveFiberCountOnStop = 1 + val restoredFlow = aliceNode.restartAndGetRestoredFlow() assertThat(restoredFlow.flowStarted).isTrue() } @Test fun `flow can lazily use the serviceHub in its constructor`() { val flow = LazyServiceHubAccessFlow() - node1.services.startFlow(flow) + aliceNode.services.startFlow(flow) assertThat(flow.lazyTime).isNotNull() } @Test fun `exception while fiber suspended`() { - node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } - val flow = ReceiveFlow(node2.info.chooseIdentity()) - val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl + bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + val flow = ReceiveFlow(bob) + val fiber = aliceNode.services.startFlow(flow) as FlowStateMachineImpl // Before the flow runs change the suspend action to throw an exception val exceptionDuringSuspend = Exception("Thrown during suspend") fiber.actionOnSuspend = { @@ -139,31 +122,31 @@ class FlowFrameworkTests { assertThatThrownBy { fiber.resultFuture.getOrThrow() }.isSameAs(exceptionDuringSuspend) - assertThat(node1.smm.allStateMachines).isEmpty() + assertThat(aliceNode.smm.allStateMachines).isEmpty() // Make sure the fiber does actually terminate assertThat(fiber.isTerminated).isTrue() } @Test fun `flow restarted just after receiving payload`() { - node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - node1.services.startFlow(SendFlow("Hello", node2.info.chooseIdentity())) + bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + aliceNode.services.startFlow(SendFlow("Hello", bob)) // We push through just enough messages to get only the payload sent - node2.pumpReceive() - node2.internals.disableDBCloseOnStop() - node2.internals.acceptableLiveFiberCountOnStop = 1 - node2.dispose() + bobNode.pumpReceive() + bobNode.internals.disableDBCloseOnStop() + bobNode.internals.acceptableLiveFiberCountOnStop = 1 + bobNode.dispose() mockNet.runNetwork() - val restoredFlow = node2.restartAndGetRestoredFlow(node1) + val restoredFlow = bobNode.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") } @Test fun `flow added before network map does run after init`() { - val node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node + val charlieNode = mockNet.createNode() //create vanilla node val flow = NoOpFlow() - node3.services.startFlow(flow) + charlieNode.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet mockNet.runNetwork() // Allow network map messages to flow assertEquals(true, flow.flowStarted) // Now we should have run the flow @@ -171,40 +154,40 @@ class FlowFrameworkTests { @Test fun `flow added before network map will be init checkpointed`() { - var node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node + var charlieNode = mockNet.createNode() //create vanilla node val flow = NoOpFlow() - node3.services.startFlow(flow) + charlieNode.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet - node3.internals.disableDBCloseOnStop() - node3.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. - node3.dispose() + charlieNode.internals.disableDBCloseOnStop() + charlieNode.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. + charlieNode.dispose() - node3 = mockNet.createNode(node1.network.myAddress, node3.internals.id) - val restoredFlow = node3.getSingleFlow().first + charlieNode = mockNet.createNode(charlieNode.internals.id) + val restoredFlow = charlieNode.getSingleFlow().first assertEquals(false, restoredFlow.flowStarted) // Not started yet as no network activity has been allowed yet mockNet.runNetwork() // Allow network map messages to flow - node3.smm.executor.flush() + charlieNode.smm.executor.flush() assertEquals(true, restoredFlow.flowStarted) // Now we should have run the flow and hopefully cleared the init checkpoint - node3.internals.disableDBCloseOnStop() - node3.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. - node3.dispose() + charlieNode.internals.disableDBCloseOnStop() + charlieNode.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. + charlieNode.dispose() // Now it is completed the flow should leave no Checkpoint. - node3 = mockNet.createNode(node1.network.myAddress, node3.internals.id) + charlieNode = mockNet.createNode(charlieNode.internals.id) mockNet.runNetwork() // Allow network map messages to flow - node3.smm.executor.flush() - assertTrue(node3.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) + charlieNode.smm.executor.flush() + assertTrue(charlieNode.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) } @Test fun `flow loaded from checkpoint will respond to messages from before start`() { - node1.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } - node2.services.startFlow(ReceiveFlow(node1.info.chooseIdentity()).nonTerminating()) // Prepare checkpointed receive flow + aliceNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow // Make sure the add() has finished initial processing. - node2.smm.executor.flush() - node2.internals.disableDBCloseOnStop() - node2.dispose() // kill receiver - val restoredFlow = node2.restartAndGetRestoredFlow(node1) + bobNode.smm.executor.flush() + bobNode.internals.disableDBCloseOnStop() + bobNode.dispose() // kill receiver + val restoredFlow = bobNode.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") } @@ -216,26 +199,27 @@ class FlowFrameworkTests { var sentCount = 0 mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } - val node3 = mockNet.createNode(node1.network.myAddress) - val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) + val secondFlow = charlieNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } mockNet.runNetwork() + val charlie = charlieNode.info.singleIdentity() // Kick off first send and receive - node2.services.startFlow(PingPongFlow(node3.info.chooseIdentity(), payload)) - node2.database.transaction { - assertEquals(1, node2.checkpointStorage.checkpoints().size) + bobNode.services.startFlow(PingPongFlow(charlie, payload)) + bobNode.database.transaction { + assertEquals(1, bobNode.checkpointStorage.checkpoints().size) } // Make sure the add() has finished initial processing. - node2.smm.executor.flush() - node2.internals.disableDBCloseOnStop() + bobNode.smm.executor.flush() + bobNode.internals.disableDBCloseOnStop() // Restart node and thus reload the checkpoint and resend the message with same UUID - node2.dispose() - node2.database.transaction { - assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint - node2.services.networkMapCache.clearNetworkMapCache() + bobNode.dispose() + bobNode.database.transaction { + assertEquals(1, bobNode.checkpointStorage.checkpoints().size) // confirm checkpoint + bobNode.services.networkMapCache.clearNetworkMapCache() } - val node2b = mockNet.createNode(node1.network.myAddress, node2.internals.id, advertisedServices = *node2.internals.advertisedServices.toTypedArray()) - node2.internals.manuallyCloseDB() + val node2b = mockNet.createNode(bobNode.internals.id) + bobNode.internals.manuallyCloseDB() val (firstAgain, fut1) = node2b.getSingleFlow() // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. mockNet.runNetwork() @@ -250,8 +234,8 @@ class FlowFrameworkTests { node2b.database.transaction { assertEquals(0, node2b.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") } - node3.database.transaction { - assertEquals(0, node3.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") + charlieNode.database.transaction { + assertEquals(0, charlieNode.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") } assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3") assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3") @@ -261,143 +245,89 @@ class FlowFrameworkTests { @Test fun `sending to multiple parties`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() - node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - node3.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + val charlie = charlieNode.info.singleIdentity() + bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + charlieNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } val payload = "Hello World" - node1.services.startFlow(SendFlow(payload, node2.info.chooseIdentity(), node3.info.chooseIdentity())) + aliceNode.services.startFlow(SendFlow(payload, bob, charlie)) mockNet.runNetwork() - val node2Flow = node2.getSingleFlow().first - val node3Flow = node3.getSingleFlow().first - assertThat(node2Flow.receivedPayloads[0]).isEqualTo(payload) - assertThat(node3Flow.receivedPayloads[0]).isEqualTo(payload) + val bobFlow = bobNode.getSingleFlow().first + val charlieFlow = charlieNode.getSingleFlow().first + assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload) + assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload) - assertSessionTransfers(node2, - node1 sent sessionInit(SendFlow::class, payload = payload) to node2, - node2 sent sessionConfirm() to node1, - node1 sent normalEnd to node2 + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + aliceNode sent normalEnd to bobNode //There's no session end from the other flows as they're manually suspended ) - assertSessionTransfers(node3, - node1 sent sessionInit(SendFlow::class, payload = payload) to node3, - node3 sent sessionConfirm() to node1, - node1 sent normalEnd to node3 + assertSessionTransfers(charlieNode, + aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode, + charlieNode sent sessionConfirm() to aliceNode, + aliceNode sent normalEnd to charlieNode //There's no session end from the other flows as they're manually suspended ) - node2.internals.acceptableLiveFiberCountOnStop = 1 - node3.internals.acceptableLiveFiberCountOnStop = 1 + bobNode.internals.acceptableLiveFiberCountOnStop = 1 + charlieNode.internals.acceptableLiveFiberCountOnStop = 1 } @Test fun `receiving from multiple parties`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() - val node2Payload = "Test 1" - val node3Payload = "Test 2" - node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(node2Payload, it) } - node3.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(node3Payload, it) } - val multiReceiveFlow = ReceiveFlow(node2.info.chooseIdentity(), node3.info.chooseIdentity()).nonTerminating() - node1.services.startFlow(multiReceiveFlow) - node1.internals.acceptableLiveFiberCountOnStop = 1 + val charlie = charlieNode.info.singleIdentity() + val bobPayload = "Test 1" + val charliePayload = "Test 2" + bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } + charlieNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } + val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating() + aliceNode.services.startFlow(multiReceiveFlow) + aliceNode.internals.acceptableLiveFiberCountOnStop = 1 mockNet.runNetwork() - assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(node2Payload) - assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(node3Payload) + assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload) + assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload) - assertSessionTransfers(node2, - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent sessionData(node2Payload) to node1, - node2 sent normalEnd to node1 + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData(bobPayload) to aliceNode, + bobNode sent normalEnd to aliceNode ) - assertSessionTransfers(node3, - node1 sent sessionInit(ReceiveFlow::class) to node3, - node3 sent sessionConfirm() to node1, - node3 sent sessionData(node3Payload) to node1, - node3 sent normalEnd to node1 + assertSessionTransfers(charlieNode, + aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode, + charlieNode sent sessionConfirm() to aliceNode, + charlieNode sent sessionData(charliePayload) to aliceNode, + charlieNode sent normalEnd to aliceNode ) } @Test fun `both sides do a send as their first IO request`() { - node2.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } - node1.services.startFlow(PingPongFlow(node2.info.chooseIdentity(), 10L)) + bobNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } + aliceNode.services.startFlow(PingPongFlow(bob, 10L)) mockNet.runNetwork() assertSessionTransfers( - node1 sent sessionInit(PingPongFlow::class, payload = 10L) to node2, - node2 sent sessionConfirm() to node1, - node2 sent sessionData(20L) to node1, - node1 sent sessionData(11L) to node2, - node2 sent sessionData(21L) to node1, - node1 sent normalEnd to node2, - node2 sent normalEnd to node1 + aliceNode sent sessionInit(PingPongFlow::class, payload = 10L) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData(20L) to aliceNode, + aliceNode sent sessionData(11L) to bobNode, + bobNode sent sessionData(21L) to aliceNode, + aliceNode sent normalEnd to bobNode, + bobNode sent normalEnd to aliceNode ) } - @Test - fun `different notaries are picked when addressing shared notary identity`() { - assertEquals(notary1Identity, notary2Identity) - assertThat(node1.services.networkMapCache.notaryIdentities.size == 1) - node1.services.startFlow(CashIssueFlow( - 2000.DOLLARS, - OpaqueBytes.of(0x01), - notary1Identity)).resultFuture.getOrThrow() - // We pay a couple of times, the notary picking should go round robin - for (i in 1..3) { - val flow = node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.chooseIdentity())) - mockNet.runNetwork() - flow.resultFuture.getOrThrow() - } - val endpoint = mockNet.messagingNetwork.endpoint(notary1.network.myAddress as InMemoryMessagingNetwork.PeerHandle)!! - val party1Info = notary1.services.networkMapCache.getPartyInfo(notary1Identity)!! - assertTrue(party1Info is PartyInfo.DistributedNode) - val notary1Address: MessageRecipients = endpoint.getAddressOfParty(notary1.services.networkMapCache.getPartyInfo(notary1Identity)!!) - assertThat(notary1Address).isInstanceOf(InMemoryMessagingNetwork.ServiceHandle::class.java) - assertEquals(notary1Address, endpoint.getAddressOfParty(notary2.services.networkMapCache.getPartyInfo(notary2Identity)!!)) - receivedSessionMessages.expectEvents(isStrict = false) { - sequence( - // First Pay - expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java.name }) { - it.message as SessionInit - assertEquals(node1.internals.id, it.from) - assertEquals(notary1Address, it.to) - }, - expect(match = { it.message is SessionConfirm }) { - it.message as SessionConfirm - assertEquals(notary1.internals.id, it.from) - }, - // Second pay - expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java.name }) { - it.message as SessionInit - assertEquals(node1.internals.id, it.from) - assertEquals(notary1Address, it.to) - }, - expect(match = { it.message is SessionConfirm }) { - it.message as SessionConfirm - assertEquals(notary2.internals.id, it.from) - }, - // Third pay - expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java.name }) { - it.message as SessionInit - assertEquals(node1.internals.id, it.from) - assertEquals(notary1Address, it.to) - }, - expect(match = { it.message is SessionConfirm }) { - it.message as SessionConfirm - assertEquals(it.from, notary1.internals.id) - } - ) - } - } - @Test fun `other side ends before doing expected send`() { - node2.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } - val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } + val resultFuture = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { resultFuture.getOrThrow() @@ -406,11 +336,11 @@ class FlowFrameworkTests { @Test fun `receiving unexpected session end before entering sendAndReceive`() { - node2.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } + bobNode.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } val sessionEndReceived = Semaphore(0) receivedSessionMessagesObservable().filter { it.message is SessionEnd }.subscribe { sessionEndReceived.release() } - val resultFuture = node1.services.startFlow( - WaitForOtherSideEndBeforeSendAndReceive(node2.info.chooseIdentity(), sessionEndReceived)).resultFuture + val resultFuture = aliceNode.services.startFlow( + WaitForOtherSideEndBeforeSendAndReceive(bob, sessionEndReceived)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { resultFuture.getOrThrow() @@ -433,14 +363,14 @@ class FlowFrameworkTests { @Test fun `non-FlowException thrown on other side`() { - val erroringFlowFuture = node2.registerFlowFactory(ReceiveFlow::class) { + val erroringFlowFuture = bobNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } } val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps } - val receiveFlow = ReceiveFlow(node2.info.chooseIdentity()) + val receiveFlow = ReceiveFlow(bob) val receiveFlowSteps = receiveFlow.progressSteps - val receiveFlowResult = node1.services.startFlow(receiveFlow).resultFuture + val receiveFlowResult = aliceNode.services.startFlow(receiveFlow).resultFuture mockNet.runNetwork() @@ -459,20 +389,20 @@ class FlowFrameworkTests { ) assertSessionTransfers( - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent erroredEnd() to node1 + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent erroredEnd() to aliceNode ) } @Test fun `FlowException thrown on other side`() { - val erroringFlow = node2.registerFlowFactory(ReceiveFlow::class) { + val erroringFlow = bobNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps } - val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())) as FlowStateMachineImpl + val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) as FlowStateMachineImpl mockNet.runNetwork() @@ -480,8 +410,8 @@ class FlowFrameworkTests { .isThrownBy { receivingFiber.resultFuture.getOrThrow() } .withMessage("Nothing useful") .withStackTraceContaining(ReceiveFlow::class.java.name) // Make sure the stack trace is that of the receiving flow - node2.database.transaction { - assertThat(node2.checkpointStorage.checkpoints()).isEmpty() + bobNode.database.transaction { + assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty() } assertThat(receivingFiber.isTerminated).isTrue() @@ -492,9 +422,9 @@ class FlowFrameworkTests { ) assertSessionTransfers( - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent erroredEnd(erroringFlow.get().exceptionThrown) to node1 + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent erroredEnd(erroringFlow.get().exceptionThrown) to aliceNode ) // Make sure the original stack trace isn't sent down the wire assertThat((receivedSessionMessages.last().message as ErrorSessionEnd).errorResponse!!.stackTrace).isEmpty() @@ -502,12 +432,13 @@ class FlowFrameworkTests { @Test fun `FlowException propagated in invocation chain`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() + val charlie = charlieNode.info.singleIdentity() - node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } - node2.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(node3.info.chooseIdentity()) } - val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())) + charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } + bobNode.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } + val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) mockNet.runNetwork() assertThatExceptionOfType(MyFlowException::class.java) .isThrownBy { receivingFiber.resultFuture.getOrThrow() } @@ -516,34 +447,35 @@ class FlowFrameworkTests { @Test fun `FlowException thrown and there is a 3rd unrelated party flow`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val charlieNode = mockNet.createNode(legalName = CHARLIE_NAME) mockNet.runNetwork() + val charlie = charlieNode.info.singleIdentity() - // Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move - // onto node 3 which will throw the exception - val node2Fiber = node2 + // Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move + // onto Charlie which will throw the exception + val node2Fiber = bobNode .registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } .map { it.stateMachine } - node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } + charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } - val node1Fiber = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity(), node3.info.chooseIdentity())) as FlowStateMachineImpl + val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl mockNet.runNetwork() - // Node 1 will terminate with the error it received from node 3 but it won't propagate that to node 2 (as it's + // Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's // not relevant to it) but it will end its session with it assertThatExceptionOfType(MyFlowException::class.java).isThrownBy { - node1Fiber.resultFuture.getOrThrow() + aliceFiber.resultFuture.getOrThrow() } - val node2ResultFuture = node2Fiber.getOrThrow().resultFuture + val bobResultFuture = node2Fiber.getOrThrow().resultFuture assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { - node2ResultFuture.getOrThrow() + bobResultFuture.getOrThrow() } - assertSessionTransfers(node2, - node1 sent sessionInit(ReceiveFlow::class) to node2, - node2 sent sessionConfirm() to node1, - node2 sent sessionData("Hello") to node1, - node1 sent erroredEnd() to node2 + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData("Hello") to aliceNode, + aliceNode sent erroredEnd() to bobNode ) } @@ -577,16 +509,16 @@ class FlowFrameworkTests { } } - node2.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } - val resultFuture = node1.services.startFlow(RetryOnExceptionFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } + val resultFuture = aliceNode.services.startFlow(RetryOnExceptionFlow(bob)).resultFuture mockNet.runNetwork() assertThat(resultFuture.getOrThrow()).isEqualTo("Hello") } @Test fun `serialisation issue in counterparty`() { - node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } - val result = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } + val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { result.getOrThrow() @@ -595,10 +527,10 @@ class FlowFrameworkTests { @Test fun `FlowException has non-serialisable object`() { - node2.registerFlowFactory(ReceiveFlow::class) { + bobNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) } } - val result = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())).resultFuture + val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(FlowException::class.java).isThrownBy { result.getOrThrow() @@ -607,30 +539,30 @@ class FlowFrameworkTests { @Test fun `wait for transaction`() { - val ptx = TransactionBuilder(notary = notary1Identity) + val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) - .addCommand(dummyCommand(node1.info.chooseIdentity().owningKey)) - val stx = node1.services.signInitialTransaction(ptx) + .addCommand(dummyCommand(alice.owningKey)) + val stx = aliceNode.services.signInitialTransaction(ptx) - val committerFiber = node1.registerFlowFactory(WaitingFlows.Waiter::class) { + val committerFiber = aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) }.map { it.stateMachine } - val waiterStx = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.chooseIdentity())).resultFuture + val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture mockNet.runNetwork() assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow()) } @Test fun `committer throws exception before calling the finality flow`() { - val ptx = TransactionBuilder(notary = notary1Identity) + val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) .addCommand(dummyCommand()) - val stx = node1.services.signInitialTransaction(ptx) + val stx = aliceNode.services.signInitialTransaction(ptx) - node1.registerFlowFactory(WaitingFlows.Waiter::class) { + aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) { throw Exception("Error") } } - val waiter = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.chooseIdentity())).resultFuture + val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { waiter.getOrThrow() @@ -639,15 +571,15 @@ class FlowFrameworkTests { @Test fun `verify vault query service is tokenizable by force checkpointing within a flow`() { - val ptx = TransactionBuilder(notary = notary1Identity) + val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) - .addCommand(dummyCommand(node1.info.chooseIdentity().owningKey)) - val stx = node1.services.signInitialTransaction(ptx) + .addCommand(dummyCommand(alice.owningKey)) + val stx = aliceNode.services.signInitialTransaction(ptx) - node1.registerFlowFactory(VaultQueryFlow::class) { + aliceNode.registerFlowFactory(VaultQueryFlow::class) { WaitingFlows.Committer(it) } - val result = node2.services.startFlow(VaultQueryFlow(stx, node1.info.chooseIdentity())).resultFuture + val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEmpty() @@ -655,15 +587,15 @@ class FlowFrameworkTests { @Test fun `customised client flow`() { - val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } - node1.services.startFlow(CustomSendFlow("Hello", node2.info.chooseIdentity())).resultFuture + val receiveFlowFuture = bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } + aliceNode.services.startFlow(CustomSendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello") } @Test fun `customised client flow which has annotated @InitiatingFlow again`() { - val result = node1.services.startFlow(IncorrectCustomSendFlow("Hello", node2.info.chooseIdentity())).resultFuture + val result = aliceNode.services.startFlow(IncorrectCustomSendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { result.getOrThrow() @@ -672,12 +604,12 @@ class FlowFrameworkTests { @Test fun `upgraded initiating flow`() { - node2.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } - val result = node1.services.startFlow(UpgradedFlow(node2.info.chooseIdentity())).resultFuture + bobNode.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } + val result = aliceNode.services.startFlow(UpgradedFlow(bob)).resultFuture mockNet.runNetwork() assertThat(receivedSessionMessages).startsWith( - node1 sent sessionInit(UpgradedFlow::class, flowVersion = 2) to node2, - node2 sent sessionConfirm(flowVersion = 1) to node1 + aliceNode sent sessionInit(UpgradedFlow::class, flowVersion = 2) to bobNode, + bobNode sent sessionConfirm(flowVersion = 1) to aliceNode ) val (receivedPayload, node2FlowVersion) = result.getOrThrow() assertThat(receivedPayload).isEqualTo("Old initiated") @@ -686,20 +618,20 @@ class FlowFrameworkTests { @Test fun `upgraded initiated flow`() { - node2.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } - val initiatingFlow = SendFlow("Old initiating", node2.info.chooseIdentity()) - val flowInfo = node1.services.startFlow(initiatingFlow).resultFuture + bobNode.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } + val initiatingFlow = SendFlow("Old initiating", bob) + val flowInfo = aliceNode.services.startFlow(initiatingFlow).resultFuture mockNet.runNetwork() assertThat(receivedSessionMessages).startsWith( - node1 sent sessionInit(SendFlow::class, flowVersion = 1, payload = "Old initiating") to node2, - node2 sent sessionConfirm(flowVersion = 2) to node1 + aliceNode sent sessionInit(SendFlow::class, flowVersion = 1, payload = "Old initiating") to bobNode, + bobNode sent sessionConfirm(flowVersion = 2) to aliceNode ) assertThat(flowInfo.get().flowVersion).isEqualTo(2) } @Test fun `unregistered flow`() { - val future = node1.services.startFlow(SendFlow("Hello", node2.info.chooseIdentity())).resultFuture + val future = aliceNode.services.startFlow(SendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java) .isThrownBy { future.getOrThrow() } @@ -708,7 +640,7 @@ class FlowFrameworkTests { @Test fun `unknown class in session init`() { - node1.sendSessionMessage(SessionInit(random63BitValue(), "not.a.real.Class", 1, "version", null), node2) + aliceNode.sendSessionMessage(SessionInit(random63BitValue(), "not.a.real.Class", 1, "version", null), bob) mockNet.runNetwork() assertThat(receivedSessionMessages).hasSize(2) // Only the session-init and session-reject are expected val reject = receivedSessionMessages.last().message as SessionReject @@ -717,7 +649,7 @@ class FlowFrameworkTests { @Test fun `non-flow class in session init`() { - node1.sendSessionMessage(SessionInit(random63BitValue(), String::class.java.name, 1, "version", null), node2) + aliceNode.sendSessionMessage(SessionInit(random63BitValue(), String::class.java.name, 1, "version", null), bob) mockNet.runNetwork() assertThat(receivedSessionMessages).hasSize(2) // Only the session-init and session-reject are expected val reject = receivedSessionMessages.last().message as SessionReject @@ -726,23 +658,23 @@ class FlowFrameworkTests { @Test fun `single inlined sub-flow`() { - node2.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } - val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.chooseIdentity(), "Hello")).resultFuture + bobNode.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } + val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") } @Test fun `double inlined sub-flow`() { - node2.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } - val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.chooseIdentity(), "Hello")).resultFuture + bobNode.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } + val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") } @Test fun `double initiateFlow throws`() { - val future = node1.services.startFlow(DoubleInitiatingFlow()).resultFuture + val future = aliceNode.services.startFlow(DoubleInitiatingFlow()).resultFuture mockNet.runNetwork() assertThatExceptionOfType(IllegalStateException::class.java) .isThrownBy { future.getOrThrow() } @@ -753,18 +685,18 @@ class FlowFrameworkTests { private class DoubleInitiatingFlow : FlowLogic() { @Suspendable override fun call() { - initiateFlow(serviceHub.myInfo.chooseIdentity()) - initiateFlow(serviceHub.myInfo.chooseIdentity()) + initiateFlow(ourIdentity) + initiateFlow(ourIdentity) } } //////////////////////////////////////////////////////////////////////////////////////////////////////////// //region Helpers - private inline fun > StartedNode.restartAndGetRestoredFlow(networkMapNode: StartedNode<*>? = null) = internals.run { + private inline fun > StartedNode.restartAndGetRestoredFlow() = internals.run { disableDBCloseOnStop() // Handover DB to new node copy stop() - val newNode = mockNet.createNode(networkMapNode?.network?.myAddress, id, advertisedServices = *advertisedServices.toTypedArray()) + val newNode = mockNet.createNode(id) newNode.internals.acceptableLiveFiberCountOnStop = 1 manuallyCloseDB() mockNet.runNetwork() // allow NetworkMapService messages to stabilise and thus start the state machine @@ -778,8 +710,7 @@ class FlowFrameworkTests { private inline fun > StartedNode<*>.registerFlowFactory( initiatingFlowClass: KClass>, initiatedFlowVersion: Int = 1, - noinline flowFactory: (FlowSession) -> P): CordaFuture

- { + noinline flowFactory: (FlowSession) -> P): CordaFuture

{ val observable = internals.internalRegisterFlowFactory( initiatingFlowClass.java, InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory), @@ -789,16 +720,17 @@ class FlowFrameworkTests { } private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): SessionInit { - return SessionInit(0, clientFlowClass.java.name, flowVersion, "", payload) + return SessionInit(0, clientFlowClass.java.name, flowVersion, "", payload?.serialize()) } + private fun sessionConfirm(flowVersion: Int = 1) = SessionConfirm(0, 0, flowVersion, "") - private fun sessionData(payload: Any) = SessionData(0, payload) + private fun sessionData(payload: Any) = SessionData(0, payload.serialize()) private val normalEnd = NormalSessionEnd(0) private fun erroredEnd(errorResponse: FlowException? = null) = ErrorSessionEnd(0, errorResponse) - private fun StartedNode<*>.sendSessionMessage(message: SessionMessage, destination: StartedNode<*>) { + private fun StartedNode<*>.sendSessionMessage(message: SessionMessage, destination: Party) { services.networkService.apply { - val address = getAddressOfParty(PartyInfo.SingleNode(destination.info.chooseIdentity(), emptyList())) + val address = getAddressOfParty(PartyInfo.SingleNode(destination, emptyList())) send(createMessage(StateMachineManager.sessionTopic, message.serialize().bytes), address) } } @@ -842,14 +774,15 @@ class FlowFrameworkTests { private infix fun StartedNode.sent(message: SessionMessage): Pair = Pair(internals.id, message) private infix fun Pair.to(node: StartedNode<*>): SessionTransfer = SessionTransfer(first, second, node.network.myAddress) - private val FlowLogic<*>.progressSteps: CordaFuture>> get() { - return progressTracker!!.changes - .ofType(Change.Position::class.java) - .map { it.newStep } - .materialize() - .toList() - .toFuture() - } + private val FlowLogic<*>.progressSteps: CordaFuture>> + get() { + return progressTracker!!.changes + .ofType(Change.Position::class.java) + .map { it.newStep } + .materialize() + .toList() + .toFuture() + } private class LazyServiceHubAccessFlow : FlowLogic() { val lazyTime: Instant by lazy { serviceHub.clock.instant() } @@ -858,7 +791,8 @@ class FlowFrameworkTests { } private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() { - @Transient var flowStarted = false + @Transient + var flowStarted = false @Suspendable override fun call() { @@ -909,7 +843,8 @@ class FlowFrameworkTests { override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP) private var nonTerminating: Boolean = false - @Transient var receivedPayloads: List = emptyList() + @Transient + var receivedPayloads: List = emptyList() @Suspendable override fun call() { @@ -955,6 +890,7 @@ class FlowFrameworkTests { @InitiatingFlow private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val otherPartySession: FlowSession? = null) : FlowLogic() { constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession) + @Suspendable override fun call(): Any = (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } } @@ -967,8 +903,11 @@ class FlowFrameworkTests { @InitiatingFlow private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPartySession: FlowSession? = null) : FlowLogic() { constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession) - @Transient var receivedPayload: Long? = null - @Transient var receivedPayload2: Long? = null + + @Transient + var receivedPayload: Long? = null + @Transient + var receivedPayload2: Long? = null @Suspendable override fun call() { @@ -1024,9 +963,9 @@ class FlowFrameworkTests { override fun call(): List> { val otherPartySession = initiateFlow(otherParty) otherPartySession.send(stx) - // hold onto reference here to force checkpoint of vaultQueryService and thus + // hold onto reference here to force checkpoint of vaultService and thus // prove it is registered as a tokenizableService in the node - val vaultQuerySvc = serviceHub.vaultQueryService + val vaultQuerySvc = serviceHub.vaultService waitForLedgerCommit(stx.id) return vaultQuerySvc.queryBy().states } @@ -1035,6 +974,7 @@ class FlowFrameworkTests { @InitiatingFlow(version = 2) private class UpgradedFlow(val otherParty: Party, val otherPartySession: FlowSession? = null) : FlowLogic>() { constructor(otherPartySession: FlowSession) : this(otherPartySession.counterparty, otherPartySession) + @Suspendable override fun call(): Pair { val otherPartySession = this.otherPartySession ?: initiateFlow(otherParty) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index bd4c3358f3..ce79064a79 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -86,7 +86,7 @@ class DistributedImmutableMapTests : TestDependencyInjectionBase() { private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties("serverNameTablePrefix", "PORT_${myAddress.port}_"), createIdentityService = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties("serverNameTablePrefix", "PORT_${myAddress.port}_"), ::makeTestIdentityService) databases.add(database) val stateMachineFactory = { DistributedImmutableMap(database, RaftUniquenessProvider.Companion::createMap) } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 7e4781ba30..ec8155a18e 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -8,16 +8,14 @@ import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds -import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.network.NetworkMapService +import net.corda.node.services.api.ServiceHubInternal import net.corda.testing.* import net.corda.testing.contracts.DummyContract -import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -30,38 +28,37 @@ import kotlin.test.assertFailsWith class NotaryServiceTests { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode - lateinit var clientNode: StartedNode + lateinit var notaryServices: ServiceHubInternal + lateinit var aliceServices: ServiceHubInternal lateinit var notary: Party + lateinit var alice: Party @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNode = mockNet.createNode(notaryNode.network.myAddress) + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + val notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = false) + aliceServices = mockNet.createNode(legalName = ALICE_NAME).services mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() - notary = clientNode.services.getDefaultNotary() + notaryServices = notaryNode.services + notary = notaryServices.getDefaultNotary() + alice = aliceServices.myInfo.singleIdentity() } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `should sign a unique transaction with a valid time-window`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) + .addCommand(dummyCommand(alice.owningKey)) .setTimeWindow(Instant.now(), 30.seconds) - clientNode.services.signInitialTransaction(tx) + aliceServices.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -72,11 +69,11 @@ class NotaryServiceTests { @Test fun `should sign a unique transaction without a time-window`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -87,12 +84,12 @@ class NotaryServiceTests { @Test fun `should report error for transaction with an invalid time-window`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) + .addCommand(dummyCommand(alice.owningKey)) .setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) - clientNode.services.signInitialTransaction(tx) + aliceServices.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -104,17 +101,17 @@ class NotaryServiceTests { @Test fun `should sign identical transaction multiple times (signing is idempotent)`() { val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val firstAttempt = NotaryFlow.Client(stx) val secondAttempt = NotaryFlow.Client(stx) - val f1 = clientNode.services.startFlow(firstAttempt) - val f2 = clientNode.services.startFlow(secondAttempt) + val f1 = aliceServices.startFlow(firstAttempt) + val f2 = aliceServices.startFlow(secondAttempt) mockNet.runNetwork() @@ -123,25 +120,25 @@ class NotaryServiceTests { @Test fun `should report conflict when inputs are reused across transactions`() { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val stx = run { val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val stx2 = run { val tx = TransactionBuilder(notary) .addInputState(inputState) - .addInputState(issueState(clientNode)) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addInputState(issueState(aliceServices, alice)) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val firstSpend = NotaryFlow.Client(stx) val secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction. - clientNode.services.startFlow(firstSpend) - val future = clientNode.services.startFlow(secondSpend) + aliceServices.startFlow(firstSpend) + val future = aliceServices.startFlow(secondSpend) mockNet.runNetwork() @@ -153,16 +150,16 @@ class NotaryServiceTests { private fun runNotaryClient(stx: SignedTransaction): CordaFuture> { val flow = NotaryFlow.Client(stx) - val future = clientNode.services.startFlow(flow).resultFuture + val future = aliceServices.startFlow(flow).resultFuture mockNet.runNetwork() return future } - fun issueState(node: StartedNode<*>): StateAndRef<*> { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) - val signedByNode = node.services.signInitialTransaction(tx) - val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey) - node.services.recordTransactions(stx) + fun issueState(services: ServiceHub, identity: Party): StateAndRef<*> { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) + val signedByNode = services.signInitialTransaction(tx) + val stx = notaryServices.addSignature(signedByNode, notary.owningKey) + services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 1c1124cb1f..088a14e18f 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -23,7 +23,7 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) } @After @@ -32,7 +32,8 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { LogHelper.reset(PersistentUniquenessProvider::class) } - @Test fun `should commit a transaction with unused inputs without exception`() { + @Test + fun `should commit a transaction with unused inputs without exception`() { database.transaction { val provider = PersistentUniquenessProvider() val inputState = generateStateRef() @@ -41,7 +42,8 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { } } - @Test fun `should report a conflict for a transaction with previously used inputs`() { + @Test + fun `should report a conflict for a transaction with previously used inputs`() { database.transaction { val provider = PersistentUniquenessProvider() val inputState = generateStateRef() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 52a2165960..3700848e47 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -9,17 +9,14 @@ import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.getOrThrow import net.corda.core.transactions.TransactionBuilder -import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.core.utilities.getOrThrow +import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.issueInvalidState -import net.corda.node.services.network.NetworkMapService import net.corda.testing.* import net.corda.testing.contracts.DummyContract -import net.corda.testing.dummyCommand -import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -31,38 +28,37 @@ import kotlin.test.assertFailsWith class ValidatingNotaryServiceTests { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode - lateinit var clientNode: StartedNode + lateinit var notaryServices: ServiceHubInternal + lateinit var aliceServices: ServiceHubInternal lateinit var notary: Party + lateinit var alice: Party @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)) - ) - clientNode = mockNet.createNode(notaryNode.network.myAddress) + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + val notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + val aliceNode = mockNet.createNode(legalName = ALICE_NAME) mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() - notary = clientNode.services.getDefaultNotary() + notaryServices = notaryNode.services + aliceServices = aliceNode.services + notary = notaryServices.getDefaultNotary() + alice = aliceNode.info.singleIdentity() } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test fun `should report error for invalid transaction dependency`() { val stx = run { - val inputState = issueInvalidState(clientNode, notary) + val inputState = issueInvalidState(aliceServices, alice, notary) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val future = runClient(stx) @@ -76,11 +72,11 @@ class ValidatingNotaryServiceTests { fun `should report error for missing signatures`() { val expectedMissingKey = MEGA_CORP_KEY.public val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val command = Command(DummyContract.Commands.Move(), expectedMissingKey) val tx = TransactionBuilder(notary).withItems(inputState, command) - clientNode.services.signInitialTransaction(tx) + aliceServices.signInitialTransaction(tx) } val ex = assertFailsWith(NotaryException::class) { @@ -96,16 +92,16 @@ class ValidatingNotaryServiceTests { private fun runClient(stx: SignedTransaction): CordaFuture> { val flow = NotaryFlow.Client(stx) - val future = clientNode.services.startFlow(flow).resultFuture + val future = aliceServices.startFlow(flow).resultFuture mockNet.runNetwork() return future } - fun issueState(node: StartedNode<*>): StateAndRef<*> { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) - val signedByNode = node.services.signInitialTransaction(tx) - val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey) - node.services.recordTransactions(stx) + fun issueState(serviceHub: ServiceHub, identity: Party): StateAndRef<*> { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) + val signedByNode = serviceHub.signInitialTransaction(tx) + val stx = notaryServices.addSignature(signedByNode, notary.owningKey) + serviceHub.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index b5c9dc19d7..46802e8a7f 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.node.services.* import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.* @@ -45,28 +46,29 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeVaultServiceTest : TestDependencyInjectionBase() { + companion object { + private val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + } + lateinit var services: MockServices - lateinit var issuerServices: MockServices - val vaultSvc: VaultService get() = services.vaultService - val vaultQuery: VaultQueryService get() = services.vaultQueryService + private lateinit var issuerServices: MockServices + val vaultService get() = services.vaultService as NodeVaultService lateinit var database: CordaPersistence @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts.asset") LogHelper.setLevel(NodeVaultService::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BOC_KEY, DUMMY_CASH_ISSUER_KEY), - customSchemas = setOf(CashSchemaV1)) + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOC_KEY) + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, BOC_KEY) } @After fun tearDown() { database.close() LogHelper.reset(NodeVaultService::class) - unsetCordappPackages() } @Suspendable @@ -94,24 +96,21 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L)) } database.transaction { - val w1 = vaultQuery.queryBy().states + val w1 = vaultService.queryBy().states assertThat(w1).hasSize(3) - val originalVault = vaultSvc - val originalVaultQuery = vaultQuery + val originalVault = vaultService val services2 = object : MockServices() { - override val vaultService: NodeVaultService get() = originalVault as NodeVaultService + override val vaultService: NodeVaultService get() = originalVault override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) vaultService.notify(stx.tx) } } - - override val vaultQueryService: VaultQueryService get() = originalVaultQuery } - val w2 = services2.vaultQueryService.queryBy().states + val w2 = services2.vaultService.queryBy().states assertThat(w2).hasSize(3) } } @@ -122,10 +121,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L)) } database.transaction { - val w1 = vaultQuery.queryBy().states + val w1 = vaultService.queryBy().states assertThat(w1).hasSize(3) - val states = vaultQuery.queryBy(VaultQueryCriteria(stateRefs = listOf(w1[1].ref, w1[2].ref))).states + val states = vaultService.queryBy(VaultQueryCriteria(stateRefs = listOf(w1[1].ref, w1[2].ref))).states assertThat(states).hasSize(2) } } @@ -137,34 +136,34 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(3) val stateRefsToSoftLock = NonEmptySet.of(unconsumedStates[1].ref, unconsumedStates[2].ref) // soft lock two of the three states val softLockId = UUID.randomUUID() - vaultSvc.softLockReserve(softLockId, stateRefsToSoftLock) + vaultService.softLockReserve(softLockId, stateRefsToSoftLock) // all softlocked states val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(2) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(2) // my softlocked states val criteriaByLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(softLockId))) - assertThat(vaultQuery.queryBy(criteriaByLockId).states).hasSize(2) + assertThat(vaultService.queryBy(criteriaByLockId).states).hasSize(2) // excluding softlocked states - val unlockedStates1 = vaultQuery.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states + val unlockedStates1 = vaultService.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states assertThat(unlockedStates1).hasSize(1) // soft lock release one of the states explicitly - vaultSvc.softLockRelease(softLockId, NonEmptySet.of(unconsumedStates[1].ref)) - val unlockedStates2 = vaultQuery.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states + vaultService.softLockRelease(softLockId, NonEmptySet.of(unconsumedStates[1].ref)) + val unlockedStates2 = vaultService.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states assertThat(unlockedStates2).hasSize(2) // soft lock release the rest by id - vaultSvc.softLockRelease(softLockId) - val unlockedStates = vaultQuery.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states + vaultService.softLockRelease(softLockId) + val unlockedStates = vaultService.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states assertThat(unlockedStates).hasSize(3) // should be back to original states @@ -195,11 +194,11 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { backgroundExecutor.submit { try { database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } println("SOFT LOCK STATES #1 succeeded") - } catch(e: Throwable) { + } catch (e: Throwable) { println("SOFT LOCK STATES #1 failed") } finally { countDown.countDown() @@ -211,11 +210,11 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { try { Thread.sleep(100) // let 1st thread soft lock them 1st database.transaction { - vaultSvc.softLockReserve(softLockId2, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId2).states).hasSize(3) + vaultService.softLockReserve(softLockId2, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId2).states).hasSize(3) } println("SOFT LOCK STATES #2 succeeded") - } catch(e: Throwable) { + } catch (e: Throwable) { println("SOFT LOCK STATES #2 failed") } finally { countDown.countDown() @@ -224,10 +223,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { countDown.await() database.transaction { - val lockStatesId1 = vaultQuery.queryBy(criteriaByLockId1).states + val lockStatesId1 = vaultService.queryBy(criteriaByLockId1).states println("SOFT LOCK #1 final states: $lockStatesId1") assertThat(lockStatesId1.size).isIn(0, 3) - val lockStatesId2 = vaultQuery.queryBy(criteriaByLockId2).states + val lockStatesId2 = vaultService.queryBy(criteriaByLockId2).states println("SOFT LOCK #2 final states: $lockStatesId2") assertThat(lockStatesId2.size).isIn(0, 3) } @@ -248,15 +247,15 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // lock 1st state with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) + vaultService.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) val criteriaByLockId1 = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(softLockId1))) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(1) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(1) } // attempt to lock all 3 states with LockId2 database.transaction { assertThatExceptionOfType(StatesNotAvailableException::class.java).isThrownBy( - { vaultSvc.softLockReserve(softLockId2, stateRefsToSoftLock.toNonEmptySet()) } + { vaultService.softLockReserve(softLockId2, stateRefsToSoftLock.toNonEmptySet()) } ).withMessageContaining("only 2 rows available").withNoCause() } } @@ -276,14 +275,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // lock states with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } // attempt to relock same states with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } } @@ -303,14 +302,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // lock states with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(1) + vaultService.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(1) } // attempt to lock all states with LockId1 (including previously already locked one) database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock.toNonEmptySet()) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock.toNonEmptySet()) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } } @@ -321,15 +320,15 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(100.DOLLARS) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(100.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(1) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(1) } } @@ -340,7 +339,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1))) } database.transaction { - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS, + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(200.DOLLARS, onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(2) @@ -359,10 +358,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), ref = OpaqueBytes.of(3)) } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(4) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS, + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(200.DOLLARS, onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))) assertThat(spendableStatesUSD).hasSize(2) assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC) @@ -378,14 +377,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L)) } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(110.DOLLARS) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(110.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(0) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(0) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(0) } } @@ -395,15 +394,15 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L)) } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(2) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(1.DOLLARS) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(1.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(1) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(1) } } @@ -416,18 +415,18 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } database.transaction { var unlockedStates = 30 - val allStates = vaultQuery.queryBy().states + val allStates = vaultService.queryBy().states assertThat(allStates).hasSize(unlockedStates) var lockedCount = 0 for (i in 1..5) { val lockId = UUID.randomUUID() - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD.size <= unlockedStates) unlockedStates -= spendableStatesUSD.size val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId))) - val lockedStates = vaultQuery.queryBy(criteriaLocked).states + val lockedStates = vaultService.queryBy(criteriaLocked).states if (spendableStatesUSD.isNotEmpty()) { assertEquals(spendableStatesUSD.size, lockedStates.size) val lockedTotal = lockedStates.map { it.state.data }.sumCash() @@ -438,14 +437,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } } val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(lockedCount) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(lockedCount) } } @Test fun addNoteToTransaction() { - val megaCorpServices = MockServices(MEGA_CORP_KEY) - + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP_KEY) database.transaction { val freshKey = services.myInfo.chooseIdentity().owningKey @@ -457,10 +455,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.recordTransactions(usefulTX) - vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") - vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") - vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 3") - assertEquals(3, vaultSvc.getTransactionNotes(usefulTX.id).count()) + vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") + vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") + vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 3") + assertEquals(3, vaultService.getTransactionNotes(usefulTX.id).count()) // Issue more Money (GBP) val anotherBuilder = TransactionBuilder(null).apply { @@ -470,14 +468,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.recordTransactions(anotherTX) - vaultSvc.addNoteToTransaction(anotherTX.id, "GBP Sample Note 1") - assertEquals(1, vaultSvc.getTransactionNotes(anotherTX.id).count()) + vaultService.addNoteToTransaction(anotherTX.id, "GBP Sample Note 1") + assertEquals(1, vaultService.getTransactionNotes(anotherTX.id).count()) } } @Test fun `is ownable state relevant`() { - val service = (services.vaultService as NodeVaultService) + val service = vaultService val amount = Amount(1000, Issued(BOC.ref(1), GBP)) val wellKnownCash = Cash.State(amount, services.myInfo.chooseIdentity()) val myKeys = services.keyManagementService.filterMyKeys(listOf(wellKnownCash.owner.owningKey)) @@ -498,7 +496,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `correct updates are generated for general transactions`() { - val service = (services.vaultService as NodeVaultService) + val service = vaultService val vaultSubscriber = TestSubscriber>().apply { service.updates.subscribe(this) } @@ -531,7 +529,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `correct updates are generated when changing notaries`() { - val service = (services.vaultService as NodeVaultService) + val service = vaultService val notary = services.myInfo.chooseIdentity() val vaultSubscriber = TestSubscriber>().apply { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index b6aabe44c6..8d34babb97 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.packageName import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* @@ -45,11 +46,12 @@ import java.time.temporal.ChronoUnit import java.util.* class VaultQueryTests : TestDependencyInjectionBase() { - + private val cordappPackages = setOf( + "net.corda.testing.contracts", "net.corda.finance.contracts", + CashSchemaV1::class.packageName, CommercialPaperSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName).toMutableList() private lateinit var services: MockServices private lateinit var notaryServices: MockServices - private val vaultSvc: VaultService get() = services.vaultService - private val vaultQuerySvc: VaultQueryService get() = services.vaultQueryService + private val vaultService: VaultService get() = services.vaultService private val identitySvc: IdentityService = makeTestIdentityService() private lateinit var database: CordaPersistence @@ -60,23 +62,20 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts") - // register additional identities identitySvc.verifyAndRegisterIdentity(CASH_NOTARY_IDENTITY) identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), - createIdentityService = { identitySvc }, - customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1)) + createIdentityService = { identitySvc }, + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - notaryServices = MockServices(DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) } @After fun tearDown() { database.close() - unsetCordappPackages() } /** @@ -85,8 +84,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Ignore @Test fun createPersistentTestDb() { - val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = { identitySvc }) - + val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), { identitySvc }) setUpDb(database, 5000) database.close() @@ -146,7 +144,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { // DOCSTART VaultQueryExample1 - val result = vaultQuerySvc.queryBy() + val result = vaultService.queryBy() /** * Query result returns a [Vault.Page] which contains: @@ -173,7 +171,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria() // default is UNCONSUMED - val result = vaultQuerySvc.queryBy(criteria) + val result = vaultService.queryBy(criteria) assertThat(result.states).hasSize(16) assertThat(result.statesMetadata).hasSize(16) @@ -191,7 +189,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsBeforeConsume = vaultService.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(4) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) } @@ -200,7 +198,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) - val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + val resultsAfterConsume = vaultService.queryBy(consumedCriteria, paging) assertThat(resultsAfterConsume.states).hasSize(1) assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(1) } @@ -214,7 +212,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val result = vaultQuerySvc.queryBy() + val result = vaultService.queryBy() assertThat(result.states).hasSize(3) assertThat(result.statesMetadata).hasSize(3) @@ -230,7 +228,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria() // default is UNCONSUMED - val result = vaultQuerySvc.queryBy(criteria) + val result = vaultService.queryBy(criteria) assertThat(result.states).hasSize(3) assertThat(result.statesMetadata).hasSize(3) @@ -254,7 +252,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) val criteria = VaultQueryCriteria() - val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) + val results = vaultService.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) // default StateRef sort is by index then txnId: // order by @@ -285,7 +283,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sortBy = Sort(setOf(Sort.SortColumn(sortAttributeTxnId, Sort.Direction.ASC), Sort.SortColumn(sortAttributeIndex, Sort.Direction.ASC))) val criteria = VaultQueryCriteria() - val results = vaultQuerySvc.queryBy(criteria, sortBy) + val results = vaultService.queryBy(criteria, sortBy) results.statesMetadata.forEach { println(" ${it.ref}") @@ -309,7 +307,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample2 val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) - val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) + val results = vaultService.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) // DOCEND VaultQueryExample2 assertThat(results.states).hasSize(2) @@ -331,7 +329,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // default State.Status is UNCONSUMED // DOCSTART VaultQueryExample3 val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java, DealState::class.java)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample3 assertThat(results.states).hasSize(6) } @@ -351,7 +349,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(5) } } @@ -367,7 +365,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsBeforeConsume = vaultService.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(4) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) } @@ -376,7 +374,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + val resultsAfterConsume = vaultService.queryBy(consumedCriteria, paging) assertThat(resultsAfterConsume.states).hasSize(3) assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(3) } @@ -396,7 +394,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(17) } } @@ -409,7 +407,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) database.transaction { - val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsBeforeConsume = vaultService.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(1) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(1) } @@ -417,7 +415,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) // consumed 100 (spent), produced 50 (change) } database.transaction { - val resultsAfterConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsAfterConsume = vaultService.queryBy(criteria, paging) assertThat(resultsAfterConsume.states).hasSize(2) assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(2) } @@ -433,7 +431,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample4 val criteria = VaultQueryCriteria(notary = listOf(CASH_NOTARY)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample4 assertThat(results.states).hasSize(3) } @@ -450,7 +448,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } } @@ -467,7 +465,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample5 val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP, MINI_CORP)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample5 assertThat(results.states).hasSize(3) @@ -479,44 +477,44 @@ class VaultQueryTests : TestDependencyInjectionBase() { val (lockId1, lockId2) = database.transaction { val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 10, 10, Random(0L)).states.toList() - vaultSvc.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) + vaultService.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) val lockId1 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) + vaultService.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) val lockId2 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) + vaultService.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) Pair(lockId1, lockId2) } database.transaction { // excluding soft locked states val criteriaExclusive = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY)) - val resultsExclusive = vaultQuerySvc.queryBy(criteriaExclusive) + val resultsExclusive = vaultService.queryBy(criteriaExclusive) assertThat(resultsExclusive.states).hasSize(4) // only soft locked states val criteriaLockedOnly = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - val resultsLockedOnly = vaultQuerySvc.queryBy(criteriaLockedOnly) + val resultsLockedOnly = vaultService.queryBy(criteriaLockedOnly) assertThat(resultsLockedOnly.states).hasSize(6) // soft locked states by single lock id val criteriaByLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId1))) - val resultsByLockId = vaultQuerySvc.queryBy(criteriaByLockId) + val resultsByLockId = vaultService.queryBy(criteriaByLockId) assertThat(resultsByLockId.states).hasSize(2) // soft locked states by multiple lock ids val criteriaByLockIds = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId1, lockId2))) - val resultsByLockIds = vaultQuerySvc.queryBy(criteriaByLockIds) + val resultsByLockIds = vaultService.queryBy(criteriaByLockIds) assertThat(resultsByLockIds.states).hasSize(3) // unlocked and locked by `lockId2` val criteriaUnlockedAndByLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId2))) - val resultsUnlockedAndByLockIds = vaultQuerySvc.queryBy(criteriaUnlockedAndByLockId) + val resultsUnlockedAndByLockIds = vaultService.queryBy(criteriaUnlockedAndByLockId) assertThat(resultsUnlockedAndByLockIds.states).hasSize(5) // missing lockId expectedEx.expect(IllegalArgumentException::class.java) expectedEx.expectMessage("Must specify one or more lockIds") val criteriaMissingLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_AND_SPECIFIED)) - vaultQuerySvc.queryBy(criteriaMissingLockId) + vaultService.queryBy(criteriaMissingLockId) } } @@ -530,7 +528,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -545,7 +543,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notEqual(GBP.currencyCode) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -560,7 +558,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThan(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -575,7 +573,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThanOrEqual(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -590,7 +588,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThan(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -605,7 +603,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThanOrEqual(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -620,7 +618,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.between(500L, 1500L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -636,7 +634,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val currencies = listOf(CHF.currencyCode, GBP.currencyCode) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.`in`(currencies) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -652,7 +650,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val currencies = listOf(CHF.currencyCode, GBP.currencyCode) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notIn(currencies) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -667,7 +665,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.like("%BP") } // GPB val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -682,7 +680,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notLike("%BP") } // GPB val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -697,7 +695,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.isNull() } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(0) } } @@ -712,7 +710,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.notNull() } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } } @@ -743,7 +741,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg() } val avgCriteria = VaultCustomQueryCriteria(avg) - val results = vaultQuerySvc.queryBy>(sumCriteria + val results = vaultService.queryBy>(sumCriteria .and(countCriteria) .and(maxCriteria) .and(minCriteria) @@ -782,7 +780,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } val avgCriteria = VaultCustomQueryCriteria(avg) - val results = vaultQuerySvc.queryBy>(sumCriteria + val results = vaultService.queryBy>(sumCriteria .and(maxCriteria) .and(minCriteria) .and(avgCriteria)) @@ -837,7 +835,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { orderBy = Sort.Direction.DESC) } - val results = vaultQuerySvc.queryBy>(VaultCustomQueryCriteria(sum)) + val results = vaultService.queryBy>(VaultCustomQueryCriteria(sum)) // DOCEND VaultQueryExample23 assertThat(results.otherResults).hasSize(12) @@ -871,15 +869,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { // count fungible assets val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) - val fungibleStateCount = vaultQuerySvc.queryBy>(countCriteria).otherResults.single() as Long + val fungibleStateCount = vaultService.queryBy>(countCriteria).otherResults.single() as Long assertThat(fungibleStateCount).isEqualTo(10L) // count linear states - val linearStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val linearStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(linearStateCount).isEqualTo(9L) // count deal states - val dealStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val dealStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(dealStateCount).isEqualTo(3L) } } @@ -900,15 +898,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // count fungible assets val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) - val fungibleStateCount = vaultQuerySvc.queryBy>(countCriteria).otherResults.single() as Long + val fungibleStateCount = vaultService.queryBy>(countCriteria).otherResults.single() as Long assertThat(fungibleStateCount).isEqualTo(10L) // count linear states - val linearStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val linearStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(linearStateCount).isEqualTo(9L) // count deal states - val dealStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val dealStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(dealStateCount).isEqualTo(3L) } val cashUpdates = @@ -924,30 +922,30 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // count fungible assets val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED) - val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy>(countCriteriaUnconsumed).otherResults.single() as Long + val fungibleStateCountUnconsumed = vaultService.queryBy>(countCriteriaUnconsumed).otherResults.single() as Long assertThat(fungibleStateCountUnconsumed.toInt()).isEqualTo(10 - cashUpdates.consumed.size + cashUpdates.produced.size) // count linear states - val linearStateCountUnconsumed = vaultQuerySvc.queryBy(countCriteriaUnconsumed).otherResults.single() as Long + val linearStateCountUnconsumed = vaultService.queryBy(countCriteriaUnconsumed).otherResults.single() as Long assertThat(linearStateCountUnconsumed).isEqualTo(5L) // count deal states - val dealStateCountUnconsumed = vaultQuerySvc.queryBy(countCriteriaUnconsumed).otherResults.single() as Long + val dealStateCountUnconsumed = vaultService.queryBy(countCriteriaUnconsumed).otherResults.single() as Long assertThat(dealStateCountUnconsumed).isEqualTo(2L) // CONSUMED states // count fungible assets val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED) - val fungibleStateCountConsumed = vaultQuerySvc.queryBy>(countCriteriaConsumed).otherResults.single() as Long + val fungibleStateCountConsumed = vaultService.queryBy>(countCriteriaConsumed).otherResults.single() as Long assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size) // count linear states - val linearStateCountConsumed = vaultQuerySvc.queryBy(countCriteriaConsumed).otherResults.single() as Long + val linearStateCountConsumed = vaultService.queryBy(countCriteriaConsumed).otherResults.single() as Long assertThat(linearStateCountConsumed).isEqualTo(4L) // count deal states - val dealStateCountConsumed = vaultQuerySvc.queryBy(countCriteriaConsumed).otherResults.single() as Long + val dealStateCountConsumed = vaultService.queryBy(countCriteriaConsumed).otherResults.single() as Long assertThat(dealStateCountConsumed).isEqualTo(1L) } } @@ -967,7 +965,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { QueryCriteria.TimeInstantType.RECORDED, ColumnPredicate.Between(start, end)) val criteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample6 assertThat(results.states).hasSize(3) @@ -976,7 +974,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val recordedBetweenExpressionFuture = TimeCondition( QueryCriteria.TimeInstantType.RECORDED, ColumnPredicate.Between(startFuture, end)) val criteriaFuture = VaultQueryCriteria(timeCondition = recordedBetweenExpressionFuture) - assertThat(vaultQuerySvc.queryBy(criteriaFuture).states).isEmpty() + assertThat(vaultService.queryBy(criteriaFuture).states).isEmpty() } } @@ -996,7 +994,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { QueryCriteria.TimeInstantType.CONSUMED, ColumnPredicate.BinaryComparison(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, asOfDateTime)) val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, timeCondition = consumedAfterExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } @@ -1012,7 +1010,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample7 val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + val results = vaultService.queryBy(criteria, paging = pagingSpec) // DOCEND VaultQueryExample7 assertThat(results.states).hasSize(10) assertThat(results.totalStatesAvailable).isEqualTo(100) @@ -1031,7 +1029,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val pagingSpec = PageSpecification(10, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + val results = vaultService.queryBy(criteria, paging = pagingSpec) assertThat(results.states).hasSize(5) // should retrieve states 90..94 assertThat(results.totalStatesAvailable).isEqualTo(95) } @@ -1050,7 +1048,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val pagingSpec = PageSpecification(0, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + vaultService.queryBy(criteria, paging = pagingSpec) } } @@ -1065,9 +1063,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { @Suppress("EXPECTED_CONDITION") - val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, @Suppress("INTEGER_OVERFLOW")MAX_PAGE_SIZE + 1) // overflow = -2147483648 + val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, @Suppress("INTEGER_OVERFLOW") MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + vaultService.queryBy(criteria, paging = pagingSpec) } } @@ -1082,7 +1080,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } } @@ -1098,7 +1096,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sortCol2 = Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.STATE_STATUS), Sort.Direction.ASC) val sortCol3 = Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.CONSUMED_TIME), Sort.Direction.DESC) val sorting = Sort(setOf(sortCol1, sortCol2, sortCol3)) - val result = vaultQuerySvc.queryBy(VaultQueryCriteria(status = Vault.StateStatus.ALL), sorting = sorting) + val result = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.ALL), sorting = sorting) val states = result.states val metadata = result.statesMetadata @@ -1123,7 +1121,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(10) } database.transaction { - val results = vaultQuerySvc.queryBy>() + val results = vaultService.queryBy>() assertThat(results.states).hasSize(4) } } @@ -1142,7 +1140,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(2) } } @@ -1154,7 +1152,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(10) } database.transaction { - val results = vaultQuerySvc.queryBy() + val results = vaultService.queryBy() assertThat(results.states).hasSize(3) } } @@ -1169,7 +1167,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } // should now have x2 CONSUMED + x2 UNCONSUMED (one spent + one change) database.transaction { - val results = vaultQuerySvc.queryBy(FungibleAssetQueryCriteria()) + val results = vaultService.queryBy(FungibleAssetQueryCriteria()) assertThat(results.statesMetadata).hasSize(2) assertThat(results.states).hasSize(2) } @@ -1192,7 +1190,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -1205,7 +1203,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val results = vaultQuerySvc.queryBy() + val results = vaultService.queryBy() assertThat(results.states).hasSize(13) } } @@ -1224,7 +1222,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } } @@ -1241,7 +1239,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample8 val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() val criteria = LinearStateQueryCriteria(linearId = listOf(linearIds.first(), linearIds.last())) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample8 assertThat(results.states).hasSize(2) } @@ -1259,7 +1257,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val linearIds = listOf(linearState1.states.first().state.data.linearId, linearState3.states.first().state.data.linearId) val criteria = LinearStateQueryCriteria(linearId = linearIds) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -1276,7 +1274,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val externalIds = listOf(linearState1.states.first().state.data.linearId.externalId!!, linearState3.states.first().state.data.linearId.externalId!!) val criteria = LinearStateQueryCriteria(externalId = externalIds) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -1297,7 +1295,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample9 val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(linearStateCriteria and vaultCriteria) + val results = vaultService.queryBy(linearStateCriteria and vaultCriteria) // DOCEND VaultQueryExample9 assertThat(results.states).hasSize(4) } @@ -1320,7 +1318,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) + val results = vaultService.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) results.states.forEach { println("${it.state.data.linearId.id}") } assertThat(results.states).hasSize(8) } @@ -1337,7 +1335,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val vaultCriteria = VaultQueryCriteria() val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy((vaultCriteria), sorting = sorting) + val results = vaultService.queryBy((vaultCriteria), sorting = sorting) results.states.forEach { println(it.state.data.linearString) } assertThat(results.states).hasSize(6) } @@ -1358,7 +1356,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy(compositeCriteria, sorting = sorting) + val results = vaultService.queryBy(compositeCriteria, sorting = sorting) assertThat(results.statesMetadata).hasSize(4) assertThat(results.states).hasSize(4) } @@ -1375,7 +1373,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val vaultCriteria = VaultQueryCriteria() val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Custom(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java, "linearString"), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy((vaultCriteria), sorting = sorting) + val results = vaultService.queryBy((vaultCriteria), sorting = sorting) results.states.forEach { println(it.state.data.linearString) } assertThat(results.states).hasSize(6) } @@ -1397,7 +1395,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }, status = Vault.StateStatus.CONSUMED) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) + val results = vaultService.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) assertThat(results.states).hasSize(3) } } @@ -1411,7 +1409,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val results = vaultQuerySvc.queryBy() + val results = vaultService.queryBy() assertThat(results.states).hasSize(3) } } @@ -1424,7 +1422,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample10 val criteria = LinearStateQueryCriteria(externalId = listOf("456", "789")) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample10 assertThat(results.states).hasSize(2) @@ -1439,11 +1437,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "789")) } database.transaction { - val all = vaultQuerySvc.queryBy() + val all = vaultService.queryBy() all.states.forEach { println(it.state) } val criteria = LinearStateQueryCriteria(externalId = listOf("456")) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -1459,7 +1457,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample11 val criteria = LinearStateQueryCriteria(participants = parties) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample11 assertThat(results.states).hasSize(1) @@ -1481,7 +1479,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC), issuerRef = listOf(BOC.ref(1).reference, BOC.ref(2).reference)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(2) } } @@ -1491,25 +1489,23 @@ class VaultQueryTests : TestDependencyInjectionBase() { // GBP issuer val gbpCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1001)) val gbpCashIssuer = Party(CordaX500Name(organisation = "British Pounds Cash Issuer", locality = "London", country = "GB"), gbpCashIssuerKey.public).ref(1) - val gbpCashIssuerServices = MockServices(gbpCashIssuerKey) + val gbpCashIssuerServices = MockServices(cordappPackages, gbpCashIssuerKey) // USD issuer val usdCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1002)) val usdCashIssuer = Party(CordaX500Name(organisation = "US Dollars Cash Issuer", locality = "New York", country = "US"), usdCashIssuerKey.public).ref(1) - val usdCashIssuerServices = MockServices(usdCashIssuerKey) + val usdCashIssuerServices = MockServices(cordappPackages, usdCashIssuerKey) // CHF issuer val chfCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1003)) val chfCashIssuer = Party(CordaX500Name(organisation = "Swiss Francs Cash Issuer", locality = "Zurich", country = "CH"), chfCashIssuerKey.public).ref(1) - val chfCashIssuerServices = MockServices(chfCashIssuerKey) - + val chfCashIssuerServices = MockServices(cordappPackages, chfCashIssuerKey) database.transaction { - services.fillWithSomeTestCash(100.POUNDS, gbpCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (gbpCashIssuer)) services.fillWithSomeTestCash(100.DOLLARS, usdCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (usdCashIssuer)) services.fillWithSomeTestCash(100.SWISS_FRANCS, chfCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (chfCashIssuer)) } database.transaction { val criteria = FungibleAssetQueryCriteria(issuer = listOf(gbpCashIssuer.party, usdCashIssuer.party)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(2) } } @@ -1523,7 +1519,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(1) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) } } @@ -1540,7 +1536,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample5.2 val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP, BOC)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample5.2 assertThat(results.states).hasSize(2) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) @@ -1560,7 +1556,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample12 val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } val criteria = VaultCustomQueryCriteria(ccyIndex) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) // DOCEND VaultQueryExample12 assertThat(results.states).hasSize(3) @@ -1580,7 +1576,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } val ccyCriteria = VaultCustomQueryCriteria(ccyIndex) - val results = vaultQuerySvc.queryBy>(sumCriteria.and(ccyCriteria)) + val results = vaultService.queryBy>(sumCriteria.and(ccyCriteria)) assertThat(results.otherResults).hasSize(2) assertThat(results.otherResults[0]).isEqualTo(30000L) @@ -1601,7 +1597,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val ccyIndex = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } val criteria = VaultCustomQueryCriteria(ccyIndex) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.otherResults).hasSize(6) assertThat(results.otherResults[0]).isEqualTo(110000L) @@ -1624,7 +1620,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample13 val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(2500L) }) - val results = vaultQuerySvc.queryBy(fungibleAssetCriteria) + val results = vaultService.queryBy(fungibleAssetCriteria) // DOCEND VaultQueryExample13 assertThat(results.states).hasSize(4) // POUNDS, SWISS_FRANCS @@ -1642,7 +1638,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample14 val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) // DOCEND VaultQueryExample14 assertThat(results.states).hasSize(1) @@ -1661,7 +1657,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } val customCriteria = VaultCustomQueryCriteria(ccyIndex) val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(5000L) }) - val results = vaultQuerySvc.queryBy(fungibleAssetCriteria.and(customCriteria)) + val results = vaultService.queryBy(fungibleAssetCriteria.and(customCriteria)) assertThat(results.states).hasSize(1) // POUNDS > 50 } @@ -1700,7 +1696,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val ccyIndex = builder { CommercialPaperSchemaV1.PersistentCommercialPaperState::currency.equal(USD.currencyCode) } val criteria1 = QueryCriteria.VaultCustomQueryCriteria(ccyIndex) - val result = vaultQuerySvc.queryBy(criteria1) + val result = vaultService.queryBy(criteria1) Assertions.assertThat(result.states).hasSize(1) Assertions.assertThat(result.statesMetadata).hasSize(1) @@ -1746,7 +1742,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val criteria2 = QueryCriteria.VaultCustomQueryCriteria(maturityIndex) val criteria3 = QueryCriteria.VaultCustomQueryCriteria(faceValueIndex) - vaultQuerySvc.queryBy(criteria1.and(criteria3).and(criteria2)) + vaultService.queryBy(criteria1.and(criteria3).and(criteria2)) } Assertions.assertThat(result.states).hasSize(1) Assertions.assertThat(result.statesMetadata).hasSize(1) @@ -1755,6 +1751,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `query attempting to use unregistered schema`() { + tearDown() + cordappPackages -= SampleCashSchemaV3::class.packageName + setUp() database.transaction { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) @@ -1766,7 +1765,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val criteria = VaultCustomQueryCriteria(logicalExpression) assertThatThrownBy { - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) }.isInstanceOf(VaultQueryException::class.java).hasMessageContaining("Please register the entity") } } @@ -1794,7 +1793,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val customCriteria2 = VaultCustomQueryCriteria(quantityIndex) val criteria = generalCriteria.and(customCriteria1.and(customCriteria2)) - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } // DOCEND VaultQueryExample20 @@ -1819,7 +1818,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val recordedBetweenExpression = TimeCondition(TimeInstantType.RECORDED, builder { between(start, end) }) val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) - val results = vaultQuerySvc.queryBy(basicCriteria) + val results = vaultService.queryBy(basicCriteria) assertThat(results.states).hasSize(1) } @@ -1837,7 +1836,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val externalIdCondition = builder { VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") } val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) - val results = vaultQuerySvc.queryBy(externalIdCustomCriteria) + val results = vaultService.queryBy(externalIdCustomCriteria) assertThat(results.states).hasSize(1) } @@ -1866,7 +1865,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) val criteria = basicCriteria.and(customCriteria) - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } assertThat(results.states).hasSize(1) @@ -1894,7 +1893,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val uuidCustomCriteria = VaultCustomQueryCriteria(uuidCondition) val criteria = externalIdCustomCriteria or uuidCustomCriteria - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } assertThat(results.statesMetadata).hasSize(2) assertThat(results.states).hasSize(2) @@ -1911,7 +1910,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE)) - val results = vaultQuerySvc.queryBy(linearStateCriteria) + val results = vaultService.queryBy(linearStateCriteria) assertThat(results.states).hasSize(1) assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") @@ -1931,7 +1930,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE, BOB, CHARLIE)) - val results = vaultQuerySvc.queryBy(linearStateCriteria) + val results = vaultService.queryBy(linearStateCriteria) assertThat(results.states).hasSize(1) assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") @@ -1952,7 +1951,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.isNull() val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) - vaultQuerySvc.queryBy(externalIdCustomCriteria) + vaultService.queryBy(externalIdCustomCriteria) } assertThat(results.states).hasSize(1) } @@ -1972,7 +1971,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.notNull() val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) - vaultQuerySvc.queryBy(externalIdCustomCriteria) + vaultService.queryBy(externalIdCustomCriteria) } assertThat(results.states).hasSize(2) } @@ -2001,7 +2000,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) // Execute query - val results = services.vaultQueryService.queryBy>(baseCriteria and enrichedCriteria, sorter).states + val results = services.vaultService.queryBy>(baseCriteria and enrichedCriteria, sorter).states assertThat(results).hasSize(4) } } @@ -2015,7 +2014,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { // DOCSTART VaultQueryExample15 - vaultQuerySvc.trackBy().updates // UNCONSUMED default + vaultService.trackBy().updates // UNCONSUMED default // DOCEND VaultQueryExample15 } val (linearStates, dealStates) = @@ -2057,7 +2056,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - vaultQuerySvc.trackBy(criteria).updates + vaultService.trackBy(criteria).updates } val (linearStates, dealStates) = database.transaction { @@ -2103,7 +2102,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.trackBy(criteria).updates + vaultService.trackBy(criteria).updates } val (linearStates, dealStates) = database.transaction { @@ -2158,7 +2157,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { // DOCSTART VaultQueryExample16 - val (snapshot, updates) = vaultQuerySvc.trackBy() + val (snapshot, updates) = vaultService.trackBy() // DOCEND VaultQueryExample16 assertThat(snapshot.states).hasSize(0) updates @@ -2207,7 +2206,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { // DOCSTART VaultQueryExample17 - val (snapshot, updates) = vaultQuerySvc.trackBy() + val (snapshot, updates) = vaultService.trackBy() // DOCEND VaultQueryExample17 assertThat(snapshot.states).hasSize(0) updates diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 9c969f0950..feb6530062 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -4,8 +4,8 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.LinearState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AnonymousParty +import net.corda.core.internal.packageName import net.corda.core.node.services.Vault -import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria @@ -35,31 +35,31 @@ import kotlin.test.assertEquals // TODO: Move this to the cash contract tests once mock services are further split up. class VaultWithCashTest : TestDependencyInjectionBase() { + companion object { + private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + } + lateinit var services: MockServices lateinit var issuerServices: MockServices - val vault: VaultService get() = services.vaultService - val vaultQuery: VaultQueryService get() = services.vaultQueryService + val vaultService: VaultService get() = services.vaultService lateinit var database: CordaPersistence lateinit var notaryServices: MockServices @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset") - LogHelper.setLevel(VaultWithCashTest::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(DUMMY_CASH_ISSUER_KEY, DUMMY_NOTARY_KEY), - customSchemas = setOf(CashSchemaV1)) + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) - notaryServices = MockServices(DUMMY_NOTARY_KEY) + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, DUMMY_NOTARY_KEY) } @After fun tearDown() { LogHelper.reset(VaultWithCashTest::class) database.close() - unsetCordappPackages() } @Test @@ -69,7 +69,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) } database.transaction { - val w = vaultQuery.queryBy().states + val w = vaultService.queryBy().states assertEquals(3, w.size) val state = w[0].state.data @@ -83,28 +83,28 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Test fun `issue and spend total correctly and irrelevant ignored`() { - val megaCorpServices = MockServices(MEGA_CORP_KEY) + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP_KEY) val freshKey = services.keyManagementService.freshKey() val usefulTX = - database.transaction { - // A tx that sends us money. - val usefulBuilder = TransactionBuilder(null) - Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - megaCorpServices.signInitialTransaction(usefulBuilder) - } + database.transaction { + // A tx that sends us money. + val usefulBuilder = TransactionBuilder(null) + Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) + megaCorpServices.signInitialTransaction(usefulBuilder) + } database.transaction { assertEquals(0.DOLLARS, services.getCashBalance(USD)) services.recordTransactions(usefulTX) } val spendTX = - database.transaction { - // A tx that spends our money. - val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) - val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) - notaryServices.addSignature(spendPTX) - } + database.transaction { + // A tx that spends our money. + val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) + Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) + val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) + notaryServices.addSignature(spendPTX) + } database.transaction { assertEquals(100.DOLLARS, services.getCashBalance(USD)) } @@ -141,8 +141,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() { println("Cash balance: ${services.getCashBalance(USD)}") } database.transaction { - assertThat(vaultQuery.queryBy().states).hasSize(10) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(0) + assertThat(vaultService.queryBy().states).hasSize(10) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(0) } val backgroundExecutor = Executors.newFixedThreadPool(2) @@ -157,9 +157,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") - val unconsumedStates1 = vaultQuery.queryBy() - val consumedStates1 = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) - val lockedStates1 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates1 = vaultService.queryBy() + val consumedStates1 = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) + val lockedStates1 = vaultService.queryBy(criteriaLocked).states println("""txn1 states: UNCONSUMED: ${unconsumedStates1.totalStatesAvailable} : $unconsumedStates1, CONSUMED: ${consumedStates1.totalStatesAvailable} : $consumedStates1, @@ -167,16 +167,16 @@ class VaultWithCashTest : TestDependencyInjectionBase() { """) services.recordTransactions(txn1) println("txn1: Cash balance: ${services.getCashBalance(USD)}") - val unconsumedStates2 = vaultQuery.queryBy() - val consumedStates2 = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) - val lockedStates2 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates2 = vaultService.queryBy() + val consumedStates2 = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) + val lockedStates2 = vaultService.queryBy(criteriaLocked).states println("""txn1 states: UNCONSUMED: ${unconsumedStates2.totalStatesAvailable} : $unconsumedStates2, CONSUMED: ${consumedStates2.totalStatesAvailable} : $consumedStates2, LOCKED: ${lockedStates2.count()} : $lockedStates2 """) txn1 - } catch(e: Exception) { + } catch (e: Exception) { println(e) } } @@ -193,9 +193,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") - val unconsumedStates1 = vaultQuery.queryBy() - val consumedStates1 = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) - val lockedStates1 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates1 = vaultService.queryBy() + val consumedStates1 = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) + val lockedStates1 = vaultService.queryBy(criteriaLocked).states println("""txn2 states: UNCONSUMED: ${unconsumedStates1.totalStatesAvailable} : $unconsumedStates1, CONSUMED: ${consumedStates1.totalStatesAvailable} : $consumedStates1, @@ -203,16 +203,16 @@ class VaultWithCashTest : TestDependencyInjectionBase() { """) services.recordTransactions(txn2) println("txn2: Cash balance: ${services.getCashBalance(USD)}") - val unconsumedStates2 = vaultQuery.queryBy() - val consumedStates2 = vaultQuery.queryBy() - val lockedStates2 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates2 = vaultService.queryBy() + val consumedStates2 = vaultService.queryBy() + val lockedStates2 = vaultService.queryBy(criteriaLocked).states println("""txn2 states: UNCONSUMED: ${unconsumedStates2.totalStatesAvailable} : $unconsumedStates2, CONSUMED: ${consumedStates2.totalStatesAvailable} : $consumedStates2, LOCKED: ${lockedStates2.count()} : $lockedStates2 """) txn2 - } catch(e: Exception) { + } catch (e: Exception) { println(e) } } @@ -256,20 +256,21 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val linearId = UniqueIdentifier() val dummyIssue = - database.transaction { // Issue a linear state - val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY) - .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity)), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) - .addCommand(dummyCommand(notaryServices.myInfo.chooseIdentity().owningKey)) - val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) - val dummyIssue = services.addSignature(dummyIssuePtx) + database.transaction { + // Issue a linear state + val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY) + .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity)), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + .addCommand(dummyCommand(notaryServices.myInfo.chooseIdentity().owningKey)) + val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) + val dummyIssue = services.addSignature(dummyIssuePtx) - dummyIssue.toLedgerTransaction(services).verify() + dummyIssue.toLedgerTransaction(services).verify() - services.recordTransactions(dummyIssue) - dummyIssue - } + services.recordTransactions(dummyIssue) + dummyIssue + } database.transaction { - assertThat(vaultQuery.queryBy().states).hasSize(1) + assertThat(vaultService.queryBy().states).hasSize(1) // Move the same state val dummyMoveBuilder = TransactionBuilder(notary = DUMMY_NOTARY) @@ -284,7 +285,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.recordTransactions(dummyMove) } database.transaction { - assertThat(vaultQuery.queryBy().states).hasSize(1) + assertThat(vaultService.queryBy().states).hasSize(1) } } @@ -298,14 +299,14 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L)) } database.transaction { - val cash = vaultQuery.queryBy().states + val cash = vaultService.queryBy().states cash.forEach { println(it.state.data.amount) } } database.transaction { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val deals = vaultQuery.queryBy().states + val deals = vaultService.queryBy().states deals.forEach { println(it.state.data.linearId.externalId!!) } } @@ -318,10 +319,10 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.recordTransactions(spendTX) } database.transaction { - val consumedStates = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states + val consumedStates = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states assertEquals(3, consumedStates.count()) - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertEquals(7, unconsumedStates.count()) } } @@ -335,14 +336,14 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } val deals = - database.transaction { - vaultQuery.queryBy().states - } + database.transaction { + vaultService.queryBy().states + } database.transaction { services.fillWithSomeTestLinearStates(3) } database.transaction { - val linearStates = vaultQuery.queryBy().states + val linearStates = vaultService.queryBy().states linearStates.forEach { println(it.state.data.linearId) } // Create a txn consuming different contract types @@ -359,10 +360,10 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.recordTransactions(dummyMove) } database.transaction { - val consumedStates = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states + val consumedStates = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states assertEquals(2, consumedStates.count()) - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertEquals(6, unconsumedStates.count()) } } diff --git a/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt b/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt index 4187645e33..b1994a3d0c 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt @@ -11,12 +11,14 @@ class AffinityExecutorTests { var _executor: AffinityExecutor.ServiceAffinityExecutor? = null val executor: AffinityExecutor.ServiceAffinityExecutor get() = _executor!! - @After fun shutdown() { + @After + fun shutdown() { _executor?.shutdown() _executor = null } - @Test fun `flush handles nested executes`() { + @Test + fun `flush handles nested executes`() { _executor = AffinityExecutor.ServiceAffinityExecutor("test4", 1) var nestedRan = false val latch = CountDownLatch(1) @@ -29,7 +31,8 @@ class AffinityExecutorTests { assertTrue(nestedRan) } - @Test fun `single threaded affinity executor runs on correct thread`() { + @Test + fun `single threaded affinity executor runs on correct thread`() { val thisThread = Thread.currentThread() _executor = AffinityExecutor.ServiceAffinityExecutor("test thread", 1) assertTrue(!executor.isOnThread) @@ -50,7 +53,8 @@ class AffinityExecutorTests { assertEquals(thread2.get(), thread.get()) } - @Test fun `pooled executor`() { + @Test + fun `pooled executor`() { _executor = AffinityExecutor.ServiceAffinityExecutor("test2", 3) assertFalse(executor.isOnThread) diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index ffac359f49..e7b9ca1623 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -21,7 +21,7 @@ class ObservablesTests { val toBeClosed = mutableListOf() fun createDatabase(): CordaPersistence { - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) toBeClosed += database return database } diff --git a/node/src/test/resources/net/corda/node/cordapp/empty.jar b/node/src/test/resources/net/corda/node/internal/cordapp/empty.jar similarity index 100% rename from node/src/test/resources/net/corda/node/cordapp/empty.jar rename to node/src/test/resources/net/corda/node/internal/cordapp/empty.jar diff --git a/node/src/test/resources/net/corda/node/cordapp/isolated.jar b/node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar similarity index 100% rename from node/src/test/resources/net/corda/node/cordapp/isolated.jar rename to node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 170f9e265c..b977d45ffb 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' @@ -37,10 +38,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow"]]] directory "./build/nodes" - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices["corda.notary.validating"] + notary = [validating : true] p2pPort 10002 rpcPort 10003 cordapps = [] @@ -48,7 +48,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank A,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 cordapps = [] @@ -56,7 +55,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 @@ -102,3 +100,11 @@ task runRecipient(type: JavaExec) { args '--role' args 'RECIPIENT' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.attachment' + ) + } +} diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index eaccdee089..b1c0ace017 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -2,26 +2,26 @@ package net.corda.attachmentdemo import net.corda.core.utilities.getOrThrow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import org.junit.Test import java.util.concurrent.CompletableFuture.supplyAsync class AttachmentDemoTest { // run with a 10,000,000 bytes in-memory zip file. In practice, a slightly bigger file will be used (~10,002,000 bytes). - @Test fun `attachment demo using a 10MB zip file`() { + @Test + fun `attachment demo using a 10MB zip file`() { val numOfExpectedBytes = 10_000_000 - driver(dsl = { + driver(isDebug = true, portAllocation = PortAllocation.Incremental(20000)) { val demoUser = listOf(User("demo", "demo", setOf(startFlowPermission()))) val (nodeA, nodeB) = listOf( - startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser), - startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser), - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))) + startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser, maximumHeapSize = "1g"), + startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser, maximumHeapSize = "1g"), + startNotaryNode(DUMMY_NOTARY.name, validating = false)) .map { it.getOrThrow() } startWebserver(nodeB).getOrThrow() @@ -39,6 +39,6 @@ class AttachmentDemoTest { senderThread.getOrThrow() recipientThread.getOrThrow() - }, isDebug = true) + } } } diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 8eba73075f..17313ec688 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -74,6 +74,7 @@ fun main(args: Array) { } /** An in memory test zip attachment of at least numOfClearBytes size, will be used. */ +// DOCSTART 2 fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K. val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(numOfClearBytes, 0) val executor = Executors.newScheduledThreadPool(2) @@ -93,6 +94,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash. // Make sure we have the file in storage if (!rpc.attachmentExists(hash)) { inputStream.use { + val avail = inputStream.available() val id = rpc.uploadAttachment(it) require(hash == id) { "Id was '$id' instead of '$hash'" } } @@ -104,6 +106,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash. val stx = flowHandle.returnValue.getOrThrow() println("Sent ${stx.id}") } +// DOCEND 2 @StartableByRPC class AttachmentDemoFlow(private val otherSide: Party, @@ -131,6 +134,7 @@ class AttachmentDemoFlow(private val otherSide: Party, } } +// DOCSTART 1 fun recipient(rpc: CordaRPCOps, webPort: Int) { println("Waiting to receive transaction ...") val stx = rpc.internalVerifiedTransactionsFeed().updates.toBlocking().first() @@ -169,6 +173,7 @@ fun recipient(rpc: CordaRPCOps, webPort: Int) { println("Error: no attachments found in ${wtx.id}") } } +// DOCEND 1 private fun printHelp(parser: OptionParser) { println(""" diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt index b331dbacc7..f63d1c68a7 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt @@ -1,12 +1,10 @@ package net.corda.attachmentdemo import net.corda.core.internal.div -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.nodeapi.User import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.User import net.corda.testing.driver.driver /** @@ -16,7 +14,7 @@ import net.corda.testing.driver.driver fun main(args: Array) { val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) driver(isDebug = true, driverDirectory = "build" / "attachment-demo-nodes") { - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name, validating = false) startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser) startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser) waitForAllNodesToFinish() diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 4da684ed47..be0c49807d 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' @@ -49,18 +50,16 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too. - // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:finance:$corda_release_version"] } node { name "O=BankOfCorda,L=London,C=GB" - advertisedServices = ["corda.issuer.USD"] + extraConfig = [issuableCurrencies : ["USD"]] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -77,7 +76,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=BigCorporation,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 @@ -143,3 +141,11 @@ task runWebCashIssue(type: JavaExec) { args '--currency' args 'GBP' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.bankofcorda' + ) + } +} diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt index 22e33c41b9..8077105e41 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt @@ -3,8 +3,6 @@ package net.corda.bank import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.BOC import net.corda.testing.driver.driver import net.corda.testing.notary @@ -16,7 +14,7 @@ class BankOfCordaHttpAPITest { fun `issuer flow via Http`() { driver(extraCordappPackagesToScan = listOf("net.corda.finance"), dsl = { val bigCorpNodeFuture = startNode(providedName = BIGCORP_LEGAL_NAME) - val nodeBankOfCordaFuture = startNode(providedName = BOC.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val nodeBankOfCordaFuture = startNotaryNode(BOC.name, validating = false) val (nodeBankOfCorda) = listOf(nodeBankOfCordaFuture, bigCorpNodeFuture).map { it.getOrThrow() } val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress val notaryName = notary().node.nodeInfo.legalIdentities[1].name diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 33a36ec75f..147d6a65e0 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -8,8 +8,6 @@ import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.driver @@ -22,7 +20,7 @@ class BankOfCordaRPCClientTest { val bocManager = User("bocManager", "password1", permissions = setOf( startFlowPermission())) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet()) - val nodeBankOfCordaFuture = startNode(providedName = BOC.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), rpcUsers = listOf(bocManager)) + val nodeBankOfCordaFuture = startNotaryNode(BOC.name, rpcUsers = listOf(bocManager), validating = false) val nodeBigCorporationFuture = startNode(providedName = BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpCFO)) val (nodeBankOfCorda, nodeBigCorporation) = listOf(nodeBankOfCordaFuture, nodeBigCorporationFuture).map { it.getOrThrow() } @@ -33,8 +31,8 @@ class BankOfCordaRPCClientTest { // Big Corporation RPC Client val bigCorpClient = nodeBigCorporation.rpcClientToNode() val bigCorpProxy = bigCorpClient.start("bigCorpCFO", "password2").proxy - bocProxy.waitUntilNetworkReady() - bigCorpProxy.waitUntilNetworkReady() + bocProxy.waitUntilNetworkReady().getOrThrow() + bigCorpProxy.waitUntilNetworkReady().getOrThrow() // Register for Bank of Corda Vault updates val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -43,12 +41,14 @@ class BankOfCordaRPCClientTest { // Register for Big Corporation Vault updates val vaultUpdatesBigCorp = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates + val bigCorporation = bigCorpProxy.wellKnownPartyFromX500Name(BIGCORP_LEGAL_NAME)!! + // Kick-off actual Issuer Flow val anonymous = true val notary = bocProxy.notaryIdentities().first() bocProxy.startFlow(::CashIssueAndPaymentFlow, 1000.DOLLARS, BIG_CORP_PARTY_REF, - nodeBigCorporation.nodeInfo.chooseIdentity(), + bigCorporation, anonymous, notary).returnValue.getOrThrow() diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index e1b75e3f1b..81dbf12166 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -9,8 +9,7 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC import net.corda.testing.DUMMY_NOTARY @@ -55,7 +54,7 @@ private class BankOfCordaDriver { val role = options.valueOf(roleArg)!! val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, - "1", BOC.name, DUMMY_NOTARY.name.copy(commonName = "corda.notary.validating")) + "1", BOC.name, DUMMY_NOTARY.name.copy(commonName = ValidatingNotaryService.id)) try { when (role) { @@ -70,8 +69,7 @@ private class BankOfCordaDriver { val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf( startFlowPermission())) - startNode(providedName = DUMMY_NOTARY.name, - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name, validating = true) val bankOfCorda = startNode( providedName = BOC.name, rpcUsers = listOf(bankUser)) diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index 455c090973..a50e99b727 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -36,7 +36,7 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { // TODO: privileged security controls required client.start("bankUser", "test").use { connection -> val rpc = connection.proxy - rpc.waitUntilNetworkReady() + rpc.waitUntilNetworkReady().getOrThrow() // Resolve parties via RPC val issueToParty = rpc.wellKnownPartyFromX500Name(params.issueToPartyName) diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index d1bd45a246..abbfaf3772 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' apply plugin: 'application' @@ -50,10 +51,9 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices = ["corda.notary.validating", "corda.interest_rates"] + notary = [validating : true] p2pPort 10002 rpcPort 10003 webPort 10004 @@ -62,7 +62,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank A,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -71,7 +70,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 @@ -117,4 +115,9 @@ publishing { jar { from sourceSets.test.output + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.irs' + ) + } } diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index bc72cc65d7..a598595f0b 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -21,9 +21,7 @@ import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.irs.contract.InterestRateSwap import net.corda.irs.utilities.uploadFile import net.corda.node.services.config.FullNodeConfiguration -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.driver.driver import net.corda.testing.http.HttpApi @@ -50,9 +48,7 @@ class IRSDemoTest : IntegrationTestCategory { fun `runs IRS demo`() { driver(useTestClock = true, isDebug = true) { val (controller, nodeA, nodeB) = listOf( - startNode( - providedName = DUMMY_NOTARY.name, - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))), + startNotaryNode(DUMMY_NOTARY.name, validating = false), startNode(providedName = DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), startNode(providedName = DUMMY_BANK_B.name)) .map { it.getOrThrow() } @@ -98,7 +94,7 @@ class IRSDemoTest : IntegrationTestCategory { } private fun getFixingDateObservable(config: FullNodeConfiguration): Observable { - val client = CordaRPCClient(config.rpcAddress!!, initialiseSerialization = false) + val client = CordaRPCClient(config.rpcAddress!!) val proxy = client.start("user", "password").proxy val vaultUpdates = proxy.vaultTrackBy().updates diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index a28d299d24..579de8cde6 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -2,11 +2,10 @@ package net.corda.irs.api import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command -import net.corda.core.crypto.MerkleTreeException import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.* import net.corda.core.internal.ThreadBox -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.FilteredTransaction @@ -78,7 +77,7 @@ object NodeInterestRates { @ThreadSafe // DOCSTART 3 @CordaService - class Oracle(private val services: ServiceHub) : SingletonSerializeAsToken() { + class Oracle(private val services: AppServiceHub) : SingletonSerializeAsToken() { private val mutex = ThreadBox(InnerState()) init { diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index 8070d86f29..c81d80592c 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -141,11 +141,12 @@ class FloatingRatePaymentEvent(date: LocalDate, val CSVHeader = RatePaymentEvent.CSVHeader + ",FixingDate" } - override val flow: Amount get() { - // TODO: Should an uncalculated amount return a zero ? null ? etc. - val v = rate.ratioUnit?.value ?: return Amount(0, notional.token) - return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token) - } + override val flow: Amount + get() { + // TODO: Should an uncalculated amount return a zero ? null ? etc. + val v = rate.ratioUnit?.value ?: return Amount(0, notional.token) + return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token) + } override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow" @@ -609,6 +610,7 @@ class InterestRateSwap : Contract { override val participants: List get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer) + // DOCSTART 1 override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { val nextFixingOf = nextFixingOf() ?: return null @@ -616,6 +618,7 @@ class InterestRateSwap : Contract { val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).fromTime!! return ScheduledActivity(flowLogicRefFactory.create(FixingFlow.FixingRoleDecider::class.java, thisStateRef), instant) } + // DOCEND 1 override fun generateAgreement(notary: Party): TransactionBuilder { return InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, oracle, notary) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index 12ef6e2cd4..c362e602a5 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -22,9 +22,9 @@ open class RatioUnit(val value: BigDecimal) { // TODO: Discuss this type } /** - * A class to reprecent a percentage in an unambiguous way. + * A class to represent a percentage in an unambiguous way. */ -open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) { +open class PercentageRatioUnit(val percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) { override fun toString() = value.times(BigDecimal(100)).toString() + "%" } @@ -39,6 +39,7 @@ val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this) * Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc. */ @JsonIgnoreProperties(ignoreUnknown = true) +@CordaSerializable open class Rate(val ratioUnit: RatioUnit? = null) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -52,7 +53,7 @@ open class Rate(val ratioUnit: RatioUnit? = null) { } /** - * @returns the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings + * @return the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings * that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested * for equality. */ @@ -70,7 +71,6 @@ class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0") override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other) - override fun hashCode() = super.hashCode() } /** diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index e7d1074362..f2cb795f36 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.NodeInfo +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -46,7 +46,6 @@ object FixingFlow { @Suspendable override fun assembleSharedTX(handshake: TwoPartyDealFlow.Handshake): Triple, List> { - @Suppress("UNCHECKED_CAST") val fixOf = deal.nextFixingOf()!! // TODO Do we need/want to substitute in new public keys for the Parties? @@ -91,9 +90,8 @@ object FixingFlow { class Floater(override val otherSideSession: FlowSession, override val payload: FixingSession, override val progressTracker: ProgressTracker = TwoPartyDealFlow.Primary.tracker()) : TwoPartyDealFlow.Primary() { - @Suppress("UNCHECKED_CAST") private val dealToFix: StateAndRef by transient { - val state = serviceHub.loadState(payload.ref) as TransactionState + val state: TransactionState = uncheckedCast(serviceHub.loadState(payload.ref)) StateAndRef(state, payload.ref) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index 285d477a69..4f148f01f6 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -3,8 +3,8 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy +import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable @@ -16,7 +16,6 @@ import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf import net.corda.irs.flows.RatesFixFlow.FixOutOfRange import java.math.BigDecimal -import java.util.* import java.util.function.Predicate // This code is unit tested in NodeInterestRates.kt @@ -44,8 +43,7 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, fun tracker(fixName: String) = ProgressTracker(QUERYING(fixName), WORKING, SIGNING) } - @CordaSerializable - class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : Exception("Fix out of range by $byAmount") + class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : FlowException("Fix out of range by $byAmount") @CordaSerializable data class QueryRequest(val queries: List) @@ -100,7 +98,7 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, override fun call(): Fix { val oracleSession = initiateFlow(oracle) // TODO: add deadline to receive - val resp = oracleSession.sendAndReceive>(QueryRequest(listOf(fixOf))) + val resp = oracleSession.sendAndReceive>(QueryRequest(listOf(fixOf))) return resp.unwrap { val fix = it.first() diff --git a/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html b/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html index 7ea4e8d3b9..53ea8d4c8e 100644 --- a/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html +++ b/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html @@ -64,10 +64,9 @@

@@ -120,20 +119,18 @@
diff --git a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json index 81bd5a4162..dc4a5be379 100644 --- a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json +++ b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json @@ -1,6 +1,6 @@ { "fixedLeg": { - "fixedRatePayer": "8Kqd4oWdx4KQGHGR7xcgpFf9JmP6HiXqTf85NpSgdSu431EGEhujA6ePaFD", + "fixedRatePayer": "MCowBQYDK2VwAyEAzswVB9wd3XKVlRwpCIjwla25BE0bc9aW5t8GXWg71Pw=", "notional": "$25000000", "paymentFrequency": "SemiAnnual", "effectiveDate": "2016-03-11", @@ -22,7 +22,7 @@ "interestPeriodAdjustment": "Adjusted" }, "floatingLeg": { - "floatingRatePayer": "8Kqd4oWdx4KQGHGJSFTX4kdZukmHohBRN3gvPekticL4eHTdmbJTVZNZJUj", + "floatingRatePayer": "MCowBQYDK2VwAyEAa3nFfmoJUjkoLASBjpYRLz8DpAAbqXpWTCOFKj8epfw=", "notional": { "quantity": 2500000000, "token": "USD" @@ -71,7 +71,7 @@ "notificationTime": "2:00pm London", "resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ", "interestRate": { - "oracle": "Rates Service Provider", + "oracle": "C=ES,L=Madrid,O=Rates Service Provider", "tenor": { "name": "6M" }, diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt index 6cd3425e58..d1e770b257 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt @@ -1,8 +1,6 @@ package net.corda.irs import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY @@ -15,9 +13,7 @@ import net.corda.testing.driver.driver fun main(args: Array) { driver(dsl = { val (controller, nodeA, nodeB) = listOf( - startNode( - providedName = DUMMY_NOTARY.name, - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))), + startNotaryNode(DUMMY_NOTARY.name, validating = false), startNode(providedName = DUMMY_BANK_A.name), startNode(providedName = DUMMY_BANK_B.name)) .map { it.getOrThrow() } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 905ee4dbdc..d43192cdf8 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,8 +14,6 @@ import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.`issued by` -import net.corda.finance.contracts.asset.`owned by` import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -25,6 +23,7 @@ import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties import net.corda.testing.node.MockServices.Companion.makeTestIdentityService +import net.corda.testing.node.createMockCordaService import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -63,17 +62,16 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts") - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) database.transaction { - oracle = NodeInterestRates.Oracle(services).apply { knownFixes = TEST_DATA } + oracle = createMockCordaService(services, NodeInterestRates::Oracle) + oracle.knownFixes = TEST_DATA } } @After fun tearDown() { database.close() - unsetCordappPackages() } @Test @@ -202,9 +200,9 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `network tearoff`() { - val mockNet = MockNetwork(initialiseSerialization = false) + val mockNet = MockNetwork(initialiseSerialization = false, cordappPackages = listOf("net.corda.finance.contracts")) val n1 = mockNet.createNotaryNode() - val oracleNode = mockNet.createNode(n1.network.myAddress).apply { + val oracleNode = mockNet.createNode().apply { internals.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) internals.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) database.transaction { @@ -242,7 +240,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { } private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems( - TransactionState(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY)) + TransactionState(1000.DOLLARS.CASH issuedBy DUMMY_CASH_ISSUER ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY)) private fun makeFullTx() = makePartialTX().withItems(dummyCommand()) } diff --git a/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry b/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry index 3f23b14e6a..d19d0b4b92 100644 --- a/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry +++ b/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry @@ -1,3 +1,2 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry net.corda.irs.plugin.IRSPlugin net.corda.irs.plugin.IRSDemoPlugin \ No newline at end of file diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index d472b1e5b3..d0ee4ea3e3 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -5,10 +5,10 @@ apply plugin: 'application' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'us.kirchmeier.capsule' -dependencies { - compile project(':samples:irs-demo') - compile project(':node-driver') +// Warning: The network visualiser is not a Cordapp so please do not use it as an example of how +// to build a cordapp +dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" testCompile "junit:junit:$junit_version" @@ -17,12 +17,12 @@ dependencies { compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') + compile project(':node-driver') + compile project(':finance') + compile project(':samples:irs-demo') - // Cordapp dependencies // GraphStream: For visualisation compileOnly "co.paralleluniverse:capsule:$capsule_version" - compile "org.graphstream:gs-core:1.3" - compile "org.graphstream:gs-ui:1.3" } idea { @@ -42,3 +42,11 @@ task deployVisualiser(type: FatCapsule) { javaAgents = [configurations.quasar.singleFile.name] } } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.network.visualiser' + ) + } +} diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt index f9751e8d6c..f8f9c1f625 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt @@ -86,7 +86,7 @@ class VisualiserViewModel { // top right: 33.0469,64.3209 try { return node.place.coordinate.project(view.mapImage.fitWidth, view.mapImage.fitHeight, 64.3209, 29.8406, -23.2031, 33.0469) - } catch(e: Exception) { + } catch (e: Exception) { throw Exception("Cannot project ${node.started!!.info.chooseIdentity()}", e) } } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 4642412d9b..0df1f7cdc2 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -11,6 +11,7 @@ import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.queryBy import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction @@ -96,7 +97,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val swaps = node1.database.transaction { - node1.services.vaultQueryService.queryBy().states + node1.services.vaultService.queryBy().states } val theDealRef: StateAndRef = swaps.single() @@ -140,6 +141,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten node2.internals.registerInitiatedFlow(FixingFlow.Fixer::class.java) val notaryId = node1.rpcOps.notaryIdentities().first() + @InitiatingFlow class StartDealFlow(val otherParty: Party, val payload: AutoOffer) : FlowLogic() { @@ -155,9 +157,8 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val acceptDealFlows: Observable = node2.internals.registerInitiatedFlow(AcceptDealFlow::class.java) - @Suppress("UNCHECKED_CAST") val acceptorTxFuture = acceptDealFlows.toFuture().toCompletableFuture().thenCompose { - (it.stateMachine as FlowStateMachine).resultFuture.toCompletableFuture() + uncheckedCast, FlowStateMachine>(it.stateMachine).resultFuture.toCompletableFuture() } showProgressFor(listOf(node1, node2)) diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 6ba3f19e18..b9abcd6538 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -2,27 +2,21 @@ package net.corda.netmap.simulation import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name -import net.corda.finance.utils.CityDatabase -import net.corda.finance.utils.WorldMapLocation import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.utilities.ProgressTracker +import net.corda.finance.utils.CityDatabase +import net.corda.finance.utils.WorldMapLocation import net.corda.irs.api.NodeInterestRates import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.testing.DUMMY_MAP -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.DUMMY_REGULATOR +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import net.corda.testing.node.TestClock import net.corda.testing.node.setTo -import net.corda.testing.testNodeConfiguration import rx.Observable import rx.subjects.PublishSubject import java.math.BigInteger @@ -54,9 +48,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, // This puts together a mock network of SimulatedNodes. open class SimulatedNode(config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger) - : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, overrideServices, entropyRoot) { + : MockNetwork.MockNode(config, mockNet, networkMapAddress, id, notaryIdentity, entropyRoot) { override val started: StartedNode? get() = uncheckedCast(super.started) override fun findMyLocation(): WorldMapLocation? { return configuration.myLegalName.locality.let { CityDatabase[it] } @@ -67,21 +61,20 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, var counter = 0 override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val letter = 'A' + counter val (city, country) = bankLocations[counter++ % bankLocations.size] val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = CordaX500Name(organisation = "Bank $letter", locality = city, country = country)) - return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) + return SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) } fun createAll(): List { return bankLocations.mapIndexed { i, _ -> // Use deterministic seeds so the simulation is stable. Needed so that party owning keys are stable. - mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) + mockNet.createUnstartedNode(nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) } } } @@ -90,25 +83,23 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, object NetworkMapNodeFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): SimulatedNode { - require(advertisedServices.containsType(NetworkMapService.type)) + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_MAP.name) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) {} + return object : SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) {} } } object NotaryNodeFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): SimulatedNode { - require(advertisedServices.containsType(SimpleNotaryService.type)) + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { + requireNotNull(config.notary) val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = DUMMY_NOTARY.name) - return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) + myLegalName = DUMMY_NOTARY.name, + notaryConfig = config.notary) + return SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) } } @@ -117,12 +108,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val RATES_SERVICE_NAME = CordaX500Name(organisation = "Rates Service Provider", locality = "Madrid", country = "ES") override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = RATES_SERVICE_NAME) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) @@ -138,12 +128,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, object RegulatorFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_REGULATOR.name) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) { // TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request. // So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it. // But that's fine for visualisation purposes. @@ -151,12 +140,12 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } } - val mockNet = MockNetwork(networkSendManuallyPumped, runAsync) + val mockNet = MockNetwork(networkSendManuallyPumped, runAsync, cordappPackages = listOf("net.corda.irs.contract", "net.corda.finance.contract")) // This one must come first. - val networkMap = mockNet.createNode(nodeFactory = NetworkMapNodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) - val notary = mockNet.createNode(networkMap.network.myAddress, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) - val regulators = listOf(mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = RegulatorFactory)) - val ratesOracle = mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = RatesOracleFactory) + val networkMap = mockNet.startNetworkMapNode(nodeFactory = NetworkMapNodeFactory) + val notary = mockNet.createNotaryNode(validating = false, nodeFactory = NotaryNodeFactory) + val regulators = listOf(mockNet.createUnstartedNode(nodeFactory = RegulatorFactory)) + val ratesOracle = mockNet.createUnstartedNode(nodeFactory = RatesOracleFactory) // All nodes must be in one of these two lists for the purposes of the visualiser tool. val serviceProviders: List = listOf(notary.internals, ratesOracle, networkMap.internals) @@ -167,8 +156,10 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, // These are used from the network visualiser tool. private val _allFlowSteps = PublishSubject.create>() private val _doneSteps = PublishSubject.create>() - @Suppress("unused") val allFlowSteps: Observable> = _allFlowSteps - @Suppress("unused") val doneSteps: Observable> = _doneSteps + @Suppress("unused") + val allFlowSteps: Observable> = _allFlowSteps + @Suppress("unused") + val doneSteps: Observable> = _doneSteps private var pumpCursor = 0 @@ -264,7 +255,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, fun start(): Future { mockNet.startNodes() - mockNet.registerIdentities() // Wait for all the nodes to have finished registering with the network map service. return networkInitialisationFinished.thenCompose { startMainSimulation() } } @@ -279,9 +269,3 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, mockNet.stopNodes() } } - -/** - * Helper function for verifying that a service info contains the given type of advertised service. For non-simulation cases - * this is a configuration matter rather than implementation. - */ -fun Iterable.containsType(type: ServiceType) = any { it.type == type } \ No newline at end of file diff --git a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt index 7a329a9ad4..6da9d13091 100644 --- a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt +++ b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt @@ -2,26 +2,12 @@ package net.corda.netmap.simulation import net.corda.core.utilities.getOrThrow import net.corda.testing.LogHelper -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test class IRSSimulationTest { // TODO: These tests should be a lot more complete. - - @Before - fun setup() { - setCordappPackages("net.corda.irs.contract") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - - @Test fun `runs to completion`() { + @Test + fun `runs to completion`() { LogHelper.setLevel("+messages") // FIXME: Don't manipulate static state in tests. val sim = IRSSimulation(false, false, null) val future = sim.start() diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index b2d3f3e341..4d3f97f065 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' @@ -45,13 +46,18 @@ publishing { } } -task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT']) +task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom']) task deployNodesSingle(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesSingle" definitionClass = 'net.corda.notarydemo.SingleNotaryCordform' } +task deployNodesCustom(type: Cordform, dependsOn: 'jar') { + directory "./build/nodes/nodesCustom" + definitionClass = 'net.corda.notarydemo.CustomNotaryCordform' +} + task deployNodesRaft(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesRaft" definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' @@ -66,3 +72,11 @@ task notarise(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'net.corda.notarydemo.NotariseKt' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.notary' + ) + } +} diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 87c12cd663..92669b5714 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -5,16 +5,19 @@ import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div -import net.corda.core.internal.stream -import net.corda.core.internal.toTypedArray import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.config.BFTSMaRtConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.ALICE import net.corda.testing.BOB -import net.corda.testing.internal.demorun.* +import net.corda.testing.internal.demorun.name +import net.corda.testing.internal.demorun.node +import net.corda.testing.internal.demorun.notary +import net.corda.testing.internal.demorun.rpcUsers +import net.corda.testing.internal.demorun.runNodes fun main(args: Array) = BFTNotaryCordform.runNodes() @@ -23,10 +26,8 @@ private val notaryNames = createNotaryNames(clusterSize) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { - private val serviceType = BFTNonValidatingNotaryService.type - private val clusterName = CordaX500Name(serviceType.id, "BFT", "Zurich", "CH") - private val advertisedService = ServiceInfo(serviceType, clusterName) +object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { + private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") init { node { @@ -40,12 +41,10 @@ object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", not p2pPort(10005) rpcPort(10006) } - val clusterAddresses = (0 until clusterSize).stream().mapToObj { NetworkHostAndPort("localhost", 11000 + it * 10) }.toTypedArray() + val clusterAddresses = (0 until clusterSize).map { NetworkHostAndPort("localhost", 11000 + it * 10) } fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { name(notaryNames[replicaId]) - advertisedServices(advertisedService) - notaryClusterAddresses(*clusterAddresses) - bftReplicaId(replicaId) + notary(NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses))) configure() } notaryNode(0) { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt new file mode 100644 index 0000000000..900150da69 --- /dev/null +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -0,0 +1,36 @@ +package net.corda.notarydemo + +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition +import net.corda.core.internal.div +import net.corda.node.services.config.NotaryConfig +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.internal.demorun.* + +fun main(args: Array) = CustomNotaryCordform.runNodes() + +object CustomNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { + init { + node { + name(ALICE.name) + p2pPort(10002) + rpcPort(10003) + rpcUsers(notaryDemoUser) + } + node { + name(BOB.name) + p2pPort(10005) + rpcPort(10006) + } + node { + name(DUMMY_NOTARY.name) + p2pPort(10009) + rpcPort(10010) + notary(NotaryConfig(validating = true, custom = true)) + } + } + + override fun setup(context: CordformContext) {} +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt similarity index 54% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index 3f56cb0a56..55adae9ee2 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -1,20 +1,27 @@ -package net.corda.docs +package net.corda.notarydemo import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.TransactionWithSignatures import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException +/** + * A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey]. + * + * Note that at present only a single-node notary service can be customised. + */ // START 1 @CordaService -class MyCustomValidatingNotaryService(override val services: ServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { +class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = PersistentUniquenessProvider() @@ -25,6 +32,7 @@ class MyCustomValidatingNotaryService(override val services: ServiceHub, overrid } // END 1 +@Suppress("UNUSED_PARAMETER") // START 2 class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) { /** @@ -35,10 +43,19 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating override fun receiveAndVerifyTx(): TransactionParts { try { val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) - checkNotary(stx.notary) - checkSignatures(stx) - val wtx = stx.tx - return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow, wtx.notary) + val notary = stx.notary + checkNotary(notary) + var timeWindow: TimeWindow? = null + val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { + stx.resolveNotaryChangeTransaction(serviceHub) + } else { + val wtx = stx.tx + customVerify(wtx.toLedgerTransaction(serviceHub)) + timeWindow = wtx.timeWindow + stx + } + checkSignatures(transactionWithSignatures) + return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { is TransactionVerificationException, @@ -48,9 +65,13 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } } - private fun checkSignatures(stx: SignedTransaction) { + private fun customVerify(transaction: LedgerTransaction) { + // Add custom verification logic + } + + private fun checkSignatures(tx: TransactionWithSignatures) { try { - stx.verifySignaturesExcept(service.notaryIdentityKey) + tx.verifySignaturesExcept(service.notaryIdentityKey) } catch (e: SignatureException) { throw NotaryException(NotaryError.TransactionInvalid(e)) } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index c107c44578..cabacf9476 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -31,7 +31,7 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { private val counterparty by lazy { val parties = rpc.networkMapSnapshot() parties.fold(ArrayList()) { acc, elem -> - acc.addAll(elem.legalIdentitiesAndCerts.filter { it.name == BOB.name}) + acc.addAll(elem.legalIdentitiesAndCerts.filter { it.name == BOB.name }) acc }.single().party } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 2aa043364a..198867dcf1 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -6,7 +6,8 @@ import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.config.RaftConfig import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.ALICE @@ -21,10 +22,8 @@ private val notaryNames = createNotaryNames(3) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { - private val serviceType = RaftValidatingNotaryService.type - private val clusterName = CordaX500Name(serviceType.id, "Raft", "Zurich", "CH") - private val advertisedService = ServiceInfo(serviceType, clusterName) +object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { + private val clusterName = CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH") init { node { @@ -38,28 +37,23 @@ object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", no p2pPort(10005) rpcPort(10006) } - fun notaryNode(index: Int, configure: CordformNode.() -> Unit) = node { + fun notaryNode(index: Int, nodePort: Int, clusterPort: Int? = null, configure: CordformNode.() -> Unit) = node { name(notaryNames[index]) - advertisedServices(advertisedService) + val clusterAddresses = if (clusterPort != null ) listOf(NetworkHostAndPort("localhost", clusterPort)) else emptyList() + notary(NotaryConfig(validating = true, raft = RaftConfig(NetworkHostAndPort("localhost", nodePort), clusterAddresses))) configure() } - notaryNode(0) { - notaryNodePort(10008) + notaryNode(0, 10008) { p2pPort(10009) rpcPort(10010) } - val clusterAddress = NetworkHostAndPort("localhost", 10008) // Otherwise each notary forms its own cluster. - notaryNode(1) { - notaryNodePort(10012) + notaryNode(1, 10012, 10008) { p2pPort(10013) rpcPort(10014) - notaryClusterAddresses(clusterAddress) } - notaryNode(2) { - notaryNodePort(10016) + notaryNode(2, 10016, 10008) { p2pPort(10017) rpcPort(10018) - notaryClusterAddresses(clusterAddress) } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index e9c45fd7e4..22cc63540d 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -1,18 +1,21 @@ package net.corda.notarydemo +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition import net.corda.core.internal.div -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.User import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient -import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformContext -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.testing.internal.demorun.* +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.internal.demorun.name +import net.corda.testing.internal.demorun.node +import net.corda.testing.internal.demorun.notary +import net.corda.testing.internal.demorun.rpcUsers +import net.corda.testing.internal.demorun.runNodes fun main(args: Array) = SingleNotaryCordform.runNodes() @@ -20,7 +23,7 @@ val notaryDemoUser = User("demou", "demop", setOf(startFlowPermission * Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -19,46 +19,46 @@ import java.util.stream.Collectors; */ public class DirectoryMarketDataBuilder extends ExampleMarketDataBuilder { - /** - * The path to the root of the directory structure. - */ - private final Path rootPath; + /** + * The path to the root of the directory structure. + */ + private final Path rootPath; - /** - * Constructs an instance. - * - * @param rootPath the path to the root of the directory structure - */ - public DirectoryMarketDataBuilder(Path rootPath) { - this.rootPath = rootPath; - } - - //------------------------------------------------------------------------- - @Override - protected Collection getAllResources(String subdirectoryName) { - File dir = rootPath.resolve(subdirectoryName).toFile(); - if (!dir.exists()) { - throw new IllegalArgumentException(Messages.format("Directory does not exist: {}", dir)); + /** + * Constructs an instance. + * + * @param rootPath the path to the root of the directory structure + */ + public DirectoryMarketDataBuilder(Path rootPath) { + this.rootPath = rootPath; } - return Arrays.stream(dir.listFiles()) - .filter(f -> !f.isHidden()) - .map(ResourceLocator::ofFile) - .collect(Collectors.toList()); - } - @Override - protected ResourceLocator getResource(String subdirectoryName, String resourceName) { - File file = rootPath.resolve(subdirectoryName).resolve(resourceName).toFile(); - if (!file.exists()) { - return null; + //------------------------------------------------------------------------- + @Override + protected Collection getAllResources(String subdirectoryName) { + File dir = rootPath.resolve(subdirectoryName).toFile(); + if (!dir.exists()) { + throw new IllegalArgumentException(Messages.format("Directory does not exist: {}", dir)); + } + return Arrays.stream(dir.listFiles()) + .filter(f -> !f.isHidden()) + .map(ResourceLocator::ofFile) + .collect(Collectors.toList()); } - return ResourceLocator.ofFile(file); - } - @Override - protected boolean subdirectoryExists(String subdirectoryName) { - File file = rootPath.resolve(subdirectoryName).toFile(); - return file.exists(); - } + @Override + protected ResourceLocator getResource(String subdirectoryName, String resourceName) { + File file = rootPath.resolve(subdirectoryName).resolve(resourceName).toFile(); + if (!file.exists()) { + return null; + } + return ResourceLocator.ofFile(file); + } + + @Override + protected boolean subdirectoryExists(String subdirectoryName) { + File file = rootPath.resolve(subdirectoryName).toFile(); + return file.exists(); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java index 5780b6c0d9..7073b361b2 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -18,40 +18,41 @@ import java.util.Locale; */ public final class ExampleData { - /** - * Restricted constructor. - */ - private ExampleData() { - } - - //------------------------------------------------------------------------- - /** - * Loads a golden copy of expected results from a text file. - * - * @param name the name of the results - * @return the loaded results - */ - public static String loadExpectedResults(String name) { - String classpathResourceName = String.format(Locale.ENGLISH, "classpath:goldencopy/%s.txt", name); - ResourceLocator resourceLocator = ResourceLocator.of(classpathResourceName); - try { - return resourceLocator.getCharSource().read().trim(); - } catch (IOException ex) { - throw new UncheckedIOException(name, ex); + /** + * Restricted constructor. + */ + private ExampleData() { } - } - /** - * Loads a trade report template from the standard INI format. - * - * @param templateName the name of the template - * @return the loaded report template - */ - public static TradeReportTemplate loadTradeReportTemplate(String templateName) { - String resourceName = String.format(Locale.ENGLISH, "classpath:example-reports/%s.ini", templateName); - ResourceLocator resourceLocator = ResourceLocator.of(resourceName); - IniFile ini = IniFile.of(resourceLocator.getCharSource()); - return TradeReportTemplate.load(ini); - } + //------------------------------------------------------------------------- + + /** + * Loads a golden copy of expected results from a text file. + * + * @param name the name of the results + * @return the loaded results + */ + public static String loadExpectedResults(String name) { + String classpathResourceName = String.format(Locale.ENGLISH, "classpath:goldencopy/%s.txt", name); + ResourceLocator resourceLocator = ResourceLocator.of(classpathResourceName); + try { + return resourceLocator.getCharSource().read().trim(); + } catch (IOException ex) { + throw new UncheckedIOException(name, ex); + } + } + + /** + * Loads a trade report template from the standard INI format. + * + * @param templateName the name of the template + * @return the loaded report template + */ + public static TradeReportTemplate loadTradeReportTemplate(String templateName) { + String resourceName = String.format(Locale.ENGLISH, "classpath:example-reports/%s.ini", templateName); + ResourceLocator resourceLocator = ResourceLocator.of(resourceName); + IniFile ini = IniFile.of(resourceLocator.getCharSource()); + return TradeReportTemplate.load(ini); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java index a0fb78bed6..9ed74a4b23 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -10,25 +10,26 @@ package com.opengamma.strata.examples.marketdata; */ public final class ExampleMarketData { - /** - * Root resource directory of the built-in example market data - */ - private static final String EXAMPLE_MARKET_DATA_ROOT = "example-marketdata"; + /** + * Root resource directory of the built-in example market data + */ + private static final String EXAMPLE_MARKET_DATA_ROOT = "example-marketdata"; - /** - * Restricted constructor. - */ - private ExampleMarketData() { - } + /** + * Restricted constructor. + */ + private ExampleMarketData() { + } - //------------------------------------------------------------------------- - /** - * Gets a market data builder for the built-in example market data. - * - * @return the market data builder - */ - public static ExampleMarketDataBuilder builder() { - return ExampleMarketDataBuilder.ofResource(EXAMPLE_MARKET_DATA_ROOT); - } + //------------------------------------------------------------------------- + + /** + * Gets a market data builder for the built-in example market data. + * + * @return the market data builder + */ + public static ExampleMarketDataBuilder builder() { + return ExampleMarketDataBuilder.ofResource(EXAMPLE_MARKET_DATA_ROOT); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java index a59374c7f6..1f81eb3015 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -67,355 +67,358 @@ import static com.opengamma.strata.collect.Guavate.toImmutableList; */ public abstract class ExampleMarketDataBuilder { - private static final Logger log = LoggerFactory.getLogger(ExampleMarketDataBuilder.class); + private static final Logger log = LoggerFactory.getLogger(ExampleMarketDataBuilder.class); - /** The name of the subdirectory containing historical fixings. */ - private static final String HISTORICAL_FIXINGS_DIR = "historical-fixings"; + /** The name of the subdirectory containing historical fixings. */ + private static final String HISTORICAL_FIXINGS_DIR = "historical-fixings"; - /** The name of the subdirectory containing calibrated rates curves. */ - private static final String CURVES_DIR = "curves"; - /** The name of the curve groups file. */ - private static final String CURVES_GROUPS_FILE = "groups.csv"; - /** The name of the curve settings file. */ - private static final String CURVES_SETTINGS_FILE = "settings.csv"; + /** The name of the subdirectory containing calibrated rates curves. */ + private static final String CURVES_DIR = "curves"; + /** The name of the curve groups file. */ + private static final String CURVES_GROUPS_FILE = "groups.csv"; + /** The name of the curve settings file. */ + private static final String CURVES_SETTINGS_FILE = "settings.csv"; - /** The name of the directory containing CDS ISDA yield curve, credit curve and static data. */ - private static final String CREDIT_DIR = "credit"; - private static final String CDS_YIELD_CURVES_FILE = "cds.yieldCurves.csv"; - private static final String SINGLE_NAME_CREDIT_CURVES_FILE = "singleName.creditCurves.csv"; - private static final String SINGLE_NAME_STATIC_DATA_FILE = "singleName.staticData.csv"; - private static final String INDEX_CREDIT_CURVES_FILE = "index.creditCurves.csv"; - private static final String INDEX_STATIC_DATA_FILE = "index.staticData.csv"; + /** The name of the directory containing CDS ISDA yield curve, credit curve and static data. */ + private static final String CREDIT_DIR = "credit"; + private static final String CDS_YIELD_CURVES_FILE = "cds.yieldCurves.csv"; + private static final String SINGLE_NAME_CREDIT_CURVES_FILE = "singleName.creditCurves.csv"; + private static final String SINGLE_NAME_STATIC_DATA_FILE = "singleName.staticData.csv"; + private static final String INDEX_CREDIT_CURVES_FILE = "index.creditCurves.csv"; + private static final String INDEX_STATIC_DATA_FILE = "index.staticData.csv"; - /** The name of the subdirectory containing simple market quotes. */ - private static final String QUOTES_DIR = "quotes"; - /** The name of the quotes file. */ - private static final String QUOTES_FILE = "quotes.csv"; + /** The name of the subdirectory containing simple market quotes. */ + private static final String QUOTES_DIR = "quotes"; + /** The name of the quotes file. */ + private static final String QUOTES_FILE = "quotes.csv"; - //------------------------------------------------------------------------- - /** - * Creates an instance from a given classpath resource root location using the class loader - * which created this class. - *

- * This is designed to handle resource roots which may physically correspond to a directory on - * disk, or be located within a jar file. - * - * @param resourceRoot the resource root path - * @return the market data builder - */ - public static ExampleMarketDataBuilder ofResource(String resourceRoot) { - return ofResource(resourceRoot, ExampleMarketDataBuilder.class.getClassLoader()); - } + //------------------------------------------------------------------------- - /** - * Creates an instance from a given classpath resource root location, using the given class loader - * to find the resource. - *

- * This is designed to handle resource roots which may physically correspond to a directory on - * disk, or be located within a jar file. - * - * @param resourceRoot the resource root path - * @param classLoader the class loader with which to find the resource - * @return the market data builder - */ - public static ExampleMarketDataBuilder ofResource(String resourceRoot, ClassLoader classLoader) { - // classpath resources are forward-slash separated - String qualifiedRoot = resourceRoot; - qualifiedRoot = qualifiedRoot.startsWith("/") ? qualifiedRoot.substring(1) : qualifiedRoot; - qualifiedRoot = qualifiedRoot.startsWith("\\") ? qualifiedRoot.substring(1) : qualifiedRoot; - qualifiedRoot = qualifiedRoot.endsWith("/") ? qualifiedRoot : qualifiedRoot + "/"; - URL url = classLoader.getResource(qualifiedRoot); - if (url == null) { - throw new IllegalArgumentException(Messages.format("Classpath resource not found: {}", qualifiedRoot)); - } - if (url.getProtocol() != null && "jar".equals(url.getProtocol().toLowerCase(Locale.ENGLISH))) { - // Inside a JAR - int classSeparatorIdx = url.getFile().indexOf("!"); - if (classSeparatorIdx == -1) { - throw new IllegalArgumentException(Messages.format("Unexpected JAR file URL: {}", url)); - } - String jarPath = url.getFile().substring("file:".length(), classSeparatorIdx); - File jarFile; - try { - jarFile = new File(jarPath); - } catch (Exception e) { - throw new IllegalArgumentException(Messages.format("Unable to create file for JAR: {}", jarPath), e); - } - return new JarMarketDataBuilder(jarFile, resourceRoot); - } else { - // Resource is on disk - File file; - try { - file = new File(url.toURI()); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(Messages.format("Unexpected file location: {}", url), e); - } - return new DirectoryMarketDataBuilder(file.toPath()); - } - } - - /** - * Creates an instance from a given directory root. - * - * @param rootPath the root directory - * @return the market data builder - */ - public static ExampleMarketDataBuilder ofPath(Path rootPath) { - return new DirectoryMarketDataBuilder(rootPath); - } - - //------------------------------------------------------------------------- - /** - * Builds a market data snapshot from this environment. - * - * @param marketDataDate the date of the market data - * @return the snapshot - */ - public ImmutableMarketData buildSnapshot(LocalDate marketDataDate) { - ImmutableMarketDataBuilder builder = ImmutableMarketData.builder(marketDataDate); - loadFixingSeries(builder); - loadRatesCurves(builder, marketDataDate); - loadQuotes(builder, marketDataDate); - loadFxRates(builder); - loadCreditMarketData(builder, marketDataDate); - return builder.build(); - } - - /** - * Gets the rates market lookup to use with this environment. - * - * @param marketDataDate the date of the market data - * @return the rates lookup - */ - public RatesMarketDataLookup ratesLookup(LocalDate marketDataDate) { - SortedMap curves = loadAllRatesCurves(); - return RatesMarketDataLookup.of(curves.get(marketDataDate)); - } - - /** - * Gets all rates curves. - * - * @return the map of all rates curves - */ - public SortedMap loadAllRatesCurves() { - if (!subdirectoryExists(CURVES_DIR)) { - throw new IllegalArgumentException("No rates curves directory found"); - } - ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); - if (curveGroupsResource == null) { - throw new IllegalArgumentException(Messages.format( - "Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE)); - } - ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); - if (curveSettingsResource == null) { - throw new IllegalArgumentException(Messages.format( - "Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE)); - } - ListMultimap curveGroups = - RatesCurvesCsvLoader.loadAllDates(curveGroupsResource, curveSettingsResource, getRatesCurvesResources()); - - // There is only one curve group in the market data file so this will always succeed - Map curveGroupMap = Maps.transformValues(curveGroups.asMap(), groups -> groups.iterator().next()); - return new TreeMap<>(curveGroupMap); - } - - //------------------------------------------------------------------------- - private void loadFixingSeries(ImmutableMarketDataBuilder builder) { - if (!subdirectoryExists(HISTORICAL_FIXINGS_DIR)) { - log.debug("No historical fixings directory found"); - return; - } - try { - Collection fixingSeriesResources = getAllResources(HISTORICAL_FIXINGS_DIR); - Map fixingSeries = FixingSeriesCsvLoader.load(fixingSeriesResources); - builder.addTimeSeriesMap(fixingSeries); - } catch (Exception e) { - log.error("Error loading fixing series", e); - } - } - - private void loadRatesCurves(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { - if (!subdirectoryExists(CURVES_DIR)) { - log.debug("No rates curves directory found"); - return; + /** + * Creates an instance from a given classpath resource root location using the class loader + * which created this class. + *

+ * This is designed to handle resource roots which may physically correspond to a directory on + * disk, or be located within a jar file. + * + * @param resourceRoot the resource root path + * @return the market data builder + */ + public static ExampleMarketDataBuilder ofResource(String resourceRoot) { + return ofResource(resourceRoot, ExampleMarketDataBuilder.class.getClassLoader()); } - ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); - if (curveGroupsResource == null) { - log.error("Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE); - return; + /** + * Creates an instance from a given classpath resource root location, using the given class loader + * to find the resource. + *

+ * This is designed to handle resource roots which may physically correspond to a directory on + * disk, or be located within a jar file. + * + * @param resourceRoot the resource root path + * @param classLoader the class loader with which to find the resource + * @return the market data builder + */ + public static ExampleMarketDataBuilder ofResource(String resourceRoot, ClassLoader classLoader) { + // classpath resources are forward-slash separated + String qualifiedRoot = resourceRoot; + qualifiedRoot = qualifiedRoot.startsWith("/") ? qualifiedRoot.substring(1) : qualifiedRoot; + qualifiedRoot = qualifiedRoot.startsWith("\\") ? qualifiedRoot.substring(1) : qualifiedRoot; + qualifiedRoot = qualifiedRoot.endsWith("/") ? qualifiedRoot : qualifiedRoot + "/"; + URL url = classLoader.getResource(qualifiedRoot); + if (url == null) { + throw new IllegalArgumentException(Messages.format("Classpath resource not found: {}", qualifiedRoot)); + } + if (url.getProtocol() != null && "jar".equals(url.getProtocol().toLowerCase(Locale.ENGLISH))) { + // Inside a JAR + int classSeparatorIdx = url.getFile().indexOf("!"); + if (classSeparatorIdx == -1) { + throw new IllegalArgumentException(Messages.format("Unexpected JAR file URL: {}", url)); + } + String jarPath = url.getFile().substring("file:".length(), classSeparatorIdx); + File jarFile; + try { + jarFile = new File(jarPath); + } catch (Exception e) { + throw new IllegalArgumentException(Messages.format("Unable to create file for JAR: {}", jarPath), e); + } + return new JarMarketDataBuilder(jarFile, resourceRoot); + } else { + // Resource is on disk + File file; + try { + file = new File(url.toURI()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(Messages.format("Unexpected file location: {}", url), e); + } + return new DirectoryMarketDataBuilder(file.toPath()); + } } - ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); - if (curveSettingsResource == null) { - log.error("Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE); - return; - } - try { - Collection curvesResources = getRatesCurvesResources(); - List ratesCurves = - RatesCurvesCsvLoader.load(marketDataDate, curveGroupsResource, curveSettingsResource, curvesResources); - - for (CurveGroup group : ratesCurves) { - // add entry for higher level discount curve name - group.getDiscountCurves().forEach( - (ccy, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); - // add entry for higher level forward curve name - group.getForwardCurves().forEach( - (idx, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); - } - - } catch (Exception e) { - log.error("Error loading rates curves", e); - } - } - - // load quotes - private void loadQuotes(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { - if (!subdirectoryExists(QUOTES_DIR)) { - log.debug("No quotes directory found"); - return; + /** + * Creates an instance from a given directory root. + * + * @param rootPath the root directory + * @return the market data builder + */ + public static ExampleMarketDataBuilder ofPath(Path rootPath) { + return new DirectoryMarketDataBuilder(rootPath); } - ResourceLocator quotesResource = getResource(QUOTES_DIR, QUOTES_FILE); - if (quotesResource == null) { - log.error("Unable to load quotes: quotes file not found at {}/{}", QUOTES_DIR, QUOTES_FILE); - return; + //------------------------------------------------------------------------- + + /** + * Builds a market data snapshot from this environment. + * + * @param marketDataDate the date of the market data + * @return the snapshot + */ + public ImmutableMarketData buildSnapshot(LocalDate marketDataDate) { + ImmutableMarketDataBuilder builder = ImmutableMarketData.builder(marketDataDate); + loadFixingSeries(builder); + loadRatesCurves(builder, marketDataDate); + loadQuotes(builder, marketDataDate); + loadFxRates(builder); + loadCreditMarketData(builder, marketDataDate); + return builder.build(); } - try { - Map quotes = QuotesCsvLoader.load(marketDataDate, quotesResource); - builder.addValueMap(quotes); - - } catch (Exception ex) { - log.error("Error loading quotes", ex); - } - } - - private void loadFxRates(ImmutableMarketDataBuilder builder) { - // TODO - load from CSV file - format to be defined - builder.addValue(FxRateId.of(Currency.GBP, Currency.USD), FxRate.of(Currency.GBP, Currency.USD, 1.61)); - } - - //------------------------------------------------------------------------- - private Collection getRatesCurvesResources() { - return getAllResources(CURVES_DIR).stream() - .filter(res -> !res.getLocator().endsWith(CURVES_GROUPS_FILE)) - .filter(res -> !res.getLocator().endsWith(CURVES_SETTINGS_FILE)) - .collect(toImmutableList()); - } - - private void loadCreditMarketData(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { - if (!subdirectoryExists(CREDIT_DIR)) { - log.debug("No credit curves directory found"); - return; + /** + * Gets the rates market lookup to use with this environment. + * + * @param marketDataDate the date of the market data + * @return the rates lookup + */ + public RatesMarketDataLookup ratesLookup(LocalDate marketDataDate) { + SortedMap curves = loadAllRatesCurves(); + return RatesMarketDataLookup.of(curves.get(marketDataDate)); } - String creditMarketDataDateDirectory = String.format( - Locale.ENGLISH, - "%s/%s", - CREDIT_DIR, - marketDataDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); + /** + * Gets all rates curves. + * + * @return the map of all rates curves + */ + public SortedMap loadAllRatesCurves() { + if (!subdirectoryExists(CURVES_DIR)) { + throw new IllegalArgumentException("No rates curves directory found"); + } + ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); + if (curveGroupsResource == null) { + throw new IllegalArgumentException(Messages.format( + "Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE)); + } + ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); + if (curveSettingsResource == null) { + throw new IllegalArgumentException(Messages.format( + "Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE)); + } + ListMultimap curveGroups = + RatesCurvesCsvLoader.loadAllDates(curveGroupsResource, curveSettingsResource, getRatesCurvesResources()); - if (!subdirectoryExists(creditMarketDataDateDirectory)) { - log.debug("Unable to load market data: directory not found at {}", creditMarketDataDateDirectory); - return; + // There is only one curve group in the market data file so this will always succeed + Map curveGroupMap = Maps.transformValues(curveGroups.asMap(), groups -> groups.iterator().next()); + return new TreeMap<>(curveGroupMap); } - loadCdsYieldCurves(builder, creditMarketDataDateDirectory); - loadCdsSingleNameSpreadCurves(builder, creditMarketDataDateDirectory); - loadCdsIndexSpreadCurves(builder, creditMarketDataDateDirectory); - } - - private void loadCdsYieldCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { - ResourceLocator cdsYieldCurvesResource = getResource(creditMarketDataDateDirectory, CDS_YIELD_CURVES_FILE); - if (cdsYieldCurvesResource == null) { - log.debug("Unable to load cds yield curves: file not found at {}/{}", creditMarketDataDateDirectory, - CDS_YIELD_CURVES_FILE); - return; + //------------------------------------------------------------------------- + private void loadFixingSeries(ImmutableMarketDataBuilder builder) { + if (!subdirectoryExists(HISTORICAL_FIXINGS_DIR)) { + log.debug("No historical fixings directory found"); + return; + } + try { + Collection fixingSeriesResources = getAllResources(HISTORICAL_FIXINGS_DIR); + Map fixingSeries = FixingSeriesCsvLoader.load(fixingSeriesResources); + builder.addTimeSeriesMap(fixingSeries); + } catch (Exception e) { + log.error("Error loading fixing series", e); + } } - CharSource inputSource = cdsYieldCurvesResource.getCharSource(); - Map yieldCuves = MarkitYieldCurveDataParser.parse(inputSource); + private void loadRatesCurves(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { + if (!subdirectoryExists(CURVES_DIR)) { + log.debug("No rates curves directory found"); + return; + } - for (IsdaYieldCurveInputsId id : yieldCuves.keySet()) { - IsdaYieldCurveInputs curveInputs = yieldCuves.get(id); - builder.addValue(id, curveInputs); - } - } + ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); + if (curveGroupsResource == null) { + log.error("Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE); + return; + } - private void loadCdsSingleNameSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { - ResourceLocator singleNameCurvesResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE); - if (singleNameCurvesResource == null) { - log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, - SINGLE_NAME_CREDIT_CURVES_FILE); - return; + ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); + if (curveSettingsResource == null) { + log.error("Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE); + return; + } + try { + Collection curvesResources = getRatesCurvesResources(); + List ratesCurves = + RatesCurvesCsvLoader.load(marketDataDate, curveGroupsResource, curveSettingsResource, curvesResources); + + for (CurveGroup group : ratesCurves) { + // add entry for higher level discount curve name + group.getDiscountCurves().forEach( + (ccy, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); + // add entry for higher level forward curve name + group.getForwardCurves().forEach( + (idx, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); + } + + } catch (Exception e) { + log.error("Error loading rates curves", e); + } } - ResourceLocator singleNameStaticDataResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_STATIC_DATA_FILE); - if (singleNameStaticDataResource == null) { - log.debug("Unable to load single name static data: file not found at {}/{}", creditMarketDataDateDirectory, - SINGLE_NAME_STATIC_DATA_FILE); - return; + // load quotes + private void loadQuotes(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { + if (!subdirectoryExists(QUOTES_DIR)) { + log.debug("No quotes directory found"); + return; + } + + ResourceLocator quotesResource = getResource(QUOTES_DIR, QUOTES_FILE); + if (quotesResource == null) { + log.error("Unable to load quotes: quotes file not found at {}/{}", QUOTES_DIR, QUOTES_FILE); + return; + } + + try { + Map quotes = QuotesCsvLoader.load(marketDataDate, quotesResource); + builder.addValueMap(quotes); + + } catch (Exception ex) { + log.error("Error loading quotes", ex); + } } - try { - CharSource inputCreditCurvesSource = singleNameCurvesResource.getCharSource(); - CharSource inputStaticDataSource = singleNameStaticDataResource.getCharSource(); - MarkitSingleNameCreditCurveDataParser.parse(builder, inputCreditCurvesSource, inputStaticDataSource); - } catch (Exception ex) { - throw new RuntimeException(String.format( - Locale.ENGLISH, - "Unable to read single name spread curves: exception at %s/%s", - creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE), ex); - } - } - - private void loadCdsIndexSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { - ResourceLocator inputCurvesResource = getResource(creditMarketDataDateDirectory, INDEX_CREDIT_CURVES_FILE); - if (inputCurvesResource == null) { - log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, - INDEX_CREDIT_CURVES_FILE); - return; + private void loadFxRates(ImmutableMarketDataBuilder builder) { + // TODO - load from CSV file - format to be defined + builder.addValue(FxRateId.of(Currency.GBP, Currency.USD), FxRate.of(Currency.GBP, Currency.USD, 1.61)); } - ResourceLocator inputStaticDataResource = getResource(creditMarketDataDateDirectory, INDEX_STATIC_DATA_FILE); - if (inputStaticDataResource == null) { - log.debug("Unable to load index static data: file not found at {}/{}", creditMarketDataDateDirectory, - INDEX_STATIC_DATA_FILE); - return; + //------------------------------------------------------------------------- + private Collection getRatesCurvesResources() { + return getAllResources(CURVES_DIR).stream() + .filter(res -> !res.getLocator().endsWith(CURVES_GROUPS_FILE)) + .filter(res -> !res.getLocator().endsWith(CURVES_SETTINGS_FILE)) + .collect(toImmutableList()); } - CharSource indexCreditCurvesSource = inputCurvesResource.getCharSource(); - CharSource indexStaticDataSource = inputStaticDataResource.getCharSource(); - MarkitIndexCreditCurveDataParser.parse(builder, indexCreditCurvesSource, indexStaticDataSource); + private void loadCreditMarketData(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { + if (!subdirectoryExists(CREDIT_DIR)) { + log.debug("No credit curves directory found"); + return; + } - } + String creditMarketDataDateDirectory = String.format( + Locale.ENGLISH, + "%s/%s", + CREDIT_DIR, + marketDataDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); - //------------------------------------------------------------------------- - /** - * Gets all available resources from a given subdirectory. - * - * @param subdirectoryName the name of the subdirectory - * @return a collection of locators for the resources in the subdirectory - */ - protected abstract Collection getAllResources(String subdirectoryName); + if (!subdirectoryExists(creditMarketDataDateDirectory)) { + log.debug("Unable to load market data: directory not found at {}", creditMarketDataDateDirectory); + return; + } - /** - * Gets a specific resource from a given subdirectory. - * - * @param subdirectoryName the name of the subdirectory - * @param resourceName the name of the resource - * @return a locator for the requested resource - */ - protected abstract ResourceLocator getResource(String subdirectoryName, String resourceName); + loadCdsYieldCurves(builder, creditMarketDataDateDirectory); + loadCdsSingleNameSpreadCurves(builder, creditMarketDataDateDirectory); + loadCdsIndexSpreadCurves(builder, creditMarketDataDateDirectory); + } - /** - * Checks whether a specific subdirectory exists. - * - * @param subdirectoryName the name of the subdirectory - * @return whether the subdirectory exists - */ - protected abstract boolean subdirectoryExists(String subdirectoryName); + private void loadCdsYieldCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { + ResourceLocator cdsYieldCurvesResource = getResource(creditMarketDataDateDirectory, CDS_YIELD_CURVES_FILE); + if (cdsYieldCurvesResource == null) { + log.debug("Unable to load cds yield curves: file not found at {}/{}", creditMarketDataDateDirectory, + CDS_YIELD_CURVES_FILE); + return; + } + + CharSource inputSource = cdsYieldCurvesResource.getCharSource(); + Map yieldCuves = MarkitYieldCurveDataParser.parse(inputSource); + + for (IsdaYieldCurveInputsId id : yieldCuves.keySet()) { + IsdaYieldCurveInputs curveInputs = yieldCuves.get(id); + builder.addValue(id, curveInputs); + } + } + + private void loadCdsSingleNameSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { + ResourceLocator singleNameCurvesResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE); + if (singleNameCurvesResource == null) { + log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, + SINGLE_NAME_CREDIT_CURVES_FILE); + return; + } + + ResourceLocator singleNameStaticDataResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_STATIC_DATA_FILE); + if (singleNameStaticDataResource == null) { + log.debug("Unable to load single name static data: file not found at {}/{}", creditMarketDataDateDirectory, + SINGLE_NAME_STATIC_DATA_FILE); + return; + } + + try { + CharSource inputCreditCurvesSource = singleNameCurvesResource.getCharSource(); + CharSource inputStaticDataSource = singleNameStaticDataResource.getCharSource(); + MarkitSingleNameCreditCurveDataParser.parse(builder, inputCreditCurvesSource, inputStaticDataSource); + } catch (Exception ex) { + throw new RuntimeException(String.format( + Locale.ENGLISH, + "Unable to read single name spread curves: exception at %s/%s", + creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE), ex); + } + } + + private void loadCdsIndexSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { + ResourceLocator inputCurvesResource = getResource(creditMarketDataDateDirectory, INDEX_CREDIT_CURVES_FILE); + if (inputCurvesResource == null) { + log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, + INDEX_CREDIT_CURVES_FILE); + return; + } + + ResourceLocator inputStaticDataResource = getResource(creditMarketDataDateDirectory, INDEX_STATIC_DATA_FILE); + if (inputStaticDataResource == null) { + log.debug("Unable to load index static data: file not found at {}/{}", creditMarketDataDateDirectory, + INDEX_STATIC_DATA_FILE); + return; + } + + CharSource indexCreditCurvesSource = inputCurvesResource.getCharSource(); + CharSource indexStaticDataSource = inputStaticDataResource.getCharSource(); + MarkitIndexCreditCurveDataParser.parse(builder, indexCreditCurvesSource, indexStaticDataSource); + + } + + //------------------------------------------------------------------------- + + /** + * Gets all available resources from a given subdirectory. + * + * @param subdirectoryName the name of the subdirectory + * @return a collection of locators for the resources in the subdirectory + */ + protected abstract Collection getAllResources(String subdirectoryName); + + /** + * Gets a specific resource from a given subdirectory. + * + * @param subdirectoryName the name of the subdirectory + * @param resourceName the name of the resource + * @return a locator for the requested resource + */ + protected abstract ResourceLocator getResource(String subdirectoryName, String resourceName); + + /** + * Checks whether a specific subdirectory exists. + * + * @param subdirectoryName the name of the subdirectory + * @return whether the subdirectory exists + */ + protected abstract boolean subdirectoryExists(String subdirectoryName); } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java index cc599609da..34ed9064e4 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -22,96 +22,96 @@ import java.util.stream.Collectors; */ public class JarMarketDataBuilder extends ExampleMarketDataBuilder { - /** - * The JAR file containing the expected structure of resources. - */ - private final File jarFile; - /** - * The root path to the resources within the JAR file. - */ - private final String rootPath; - /** - * A cache of JAR entries under the root path. - */ - private final ImmutableSet entries; + /** + * The JAR file containing the expected structure of resources. + */ + private final File jarFile; + /** + * The root path to the resources within the JAR file. + */ + private final String rootPath; + /** + * A cache of JAR entries under the root path. + */ + private final ImmutableSet entries; - /** - * Constructs an instance. - * - * @param jarFile the JAR file containing the expected structure of resources - * @param rootPath the root path to the resources within the JAR file - */ - public JarMarketDataBuilder(File jarFile, String rootPath) { - // classpath resources are forward-slash separated - String jarRoot = rootPath.startsWith("/") ? rootPath.substring(1) : rootPath; - if (!jarRoot.endsWith("/")) { - jarRoot += "/"; - } - this.jarFile = jarFile; - this.rootPath = jarRoot; - this.entries = getEntries(jarFile, rootPath); - } - - //------------------------------------------------------------------------- - @Override - protected Collection getAllResources(String subdirectoryName) { - String resolvedSubdirectory = subdirectoryName + "/"; - return entries.stream() - .filter(e -> e.startsWith(resolvedSubdirectory) && !e.equals(resolvedSubdirectory)) - .map(e -> getEntryLocator(rootPath + e)) - .collect(Collectors.toSet()); - } - - @Override - protected ResourceLocator getResource(String subdirectoryName, String resourceName) { - String fullLocation = String.format(Locale.ENGLISH, "%s%s/%s", rootPath, subdirectoryName, resourceName); - try (JarFile jar = new JarFile(jarFile)) { - JarEntry entry = jar.getJarEntry(fullLocation); - if (entry == null) { - return null; - } - return getEntryLocator(entry.getName()); - } catch (Exception e) { - throw new IllegalArgumentException( - Messages.format("Error loading resource from JAR file: {}", jarFile), e); - } - } - - @Override - protected boolean subdirectoryExists(String subdirectoryName) { - // classpath resources are forward-slash separated - String resolvedName = subdirectoryName.startsWith("/") ? subdirectoryName.substring(1) : subdirectoryName; - if (!resolvedName.endsWith("/")) { - resolvedName += "/"; - } - return entries.contains(resolvedName); - } - - //------------------------------------------------------------------------- - // Gets the resource locator corresponding to a given entry - private ResourceLocator getEntryLocator(String entryName) { - return ResourceLocator.of(ResourceLocator.CLASSPATH_URL_PREFIX + entryName); - } - - private static ImmutableSet getEntries(File jarFile, String rootPath) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - try (JarFile jar = new JarFile(jarFile)) { - Enumeration jarEntries = jar.entries(); - while (jarEntries.hasMoreElements()) { - JarEntry entry = jarEntries.nextElement(); - String entryName = entry.getName(); - if (entryName.startsWith(rootPath) && !entryName.equals(rootPath)) { - String relativeEntryPath = entryName.substring(rootPath.length() + 1); - if (!relativeEntryPath.trim().isEmpty()) { - builder.add(relativeEntryPath); - } + /** + * Constructs an instance. + * + * @param jarFile the JAR file containing the expected structure of resources + * @param rootPath the root path to the resources within the JAR file + */ + public JarMarketDataBuilder(File jarFile, String rootPath) { + // classpath resources are forward-slash separated + String jarRoot = rootPath.startsWith("/") ? rootPath.substring(1) : rootPath; + if (!jarRoot.endsWith("/")) { + jarRoot += "/"; } - } - } catch (Exception e) { - throw new IllegalArgumentException( - Messages.format("Error scanning entries in JAR file: {}", jarFile), e); + this.jarFile = jarFile; + this.rootPath = jarRoot; + this.entries = getEntries(jarFile, rootPath); + } + + //------------------------------------------------------------------------- + @Override + protected Collection getAllResources(String subdirectoryName) { + String resolvedSubdirectory = subdirectoryName + "/"; + return entries.stream() + .filter(e -> e.startsWith(resolvedSubdirectory) && !e.equals(resolvedSubdirectory)) + .map(e -> getEntryLocator(rootPath + e)) + .collect(Collectors.toSet()); + } + + @Override + protected ResourceLocator getResource(String subdirectoryName, String resourceName) { + String fullLocation = String.format(Locale.ENGLISH, "%s%s/%s", rootPath, subdirectoryName, resourceName); + try (JarFile jar = new JarFile(jarFile)) { + JarEntry entry = jar.getJarEntry(fullLocation); + if (entry == null) { + return null; + } + return getEntryLocator(entry.getName()); + } catch (Exception e) { + throw new IllegalArgumentException( + Messages.format("Error loading resource from JAR file: {}", jarFile), e); + } + } + + @Override + protected boolean subdirectoryExists(String subdirectoryName) { + // classpath resources are forward-slash separated + String resolvedName = subdirectoryName.startsWith("/") ? subdirectoryName.substring(1) : subdirectoryName; + if (!resolvedName.endsWith("/")) { + resolvedName += "/"; + } + return entries.contains(resolvedName); + } + + //------------------------------------------------------------------------- + // Gets the resource locator corresponding to a given entry + private ResourceLocator getEntryLocator(String entryName) { + return ResourceLocator.of(ResourceLocator.CLASSPATH_URL_PREFIX + entryName); + } + + private static ImmutableSet getEntries(File jarFile, String rootPath) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + try (JarFile jar = new JarFile(jarFile)) { + Enumeration jarEntries = jar.entries(); + while (jarEntries.hasMoreElements()) { + JarEntry entry = jarEntries.nextElement(); + String entryName = entry.getName(); + if (entryName.startsWith(rootPath) && !entryName.equals(rootPath)) { + String relativeEntryPath = entryName.substring(rootPath.length() + 1); + if (!relativeEntryPath.trim().isEmpty()) { + builder.add(relativeEntryPath); + } + } + } + } catch (Exception e) { + throw new IllegalArgumentException( + Messages.format("Error scanning entries in JAR file: {}", jarFile), e); + } + return builder.build(); } - return builder.build(); - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java index 7b3bdd8c86..9f220e2e58 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -44,217 +44,219 @@ import java.util.Map; */ public class MarkitIndexCreditCurveDataParser { - // Markit date format with the month in full caps. e.g. 11-JUL-14 - private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() - .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); + // Markit date format with the month in full caps. e.g. 11-JUL-14 + private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() + .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); - enum Columns { + enum Columns { - Series("Series"), - Version("Version"), - Term("Term"), - RedCode("RED Code"), - Maturity("Maturity"), - CompositeSpread("Composite Spread"), - ModelSpread("Model Spread"); + Series("Series"), + Version("Version"), + Term("Term"), + RedCode("RED Code"), + Maturity("Maturity"), + CompositeSpread("Composite Spread"), + ModelSpread("Model Spread"); - private final String columnName; + private final String columnName; - Columns(String columnName) { - this.columnName = columnName; - } - - public String getColumnName() { - return columnName; - } - } - - /** - * Parses the specified sources. - * - * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into - * @param curveSource the source of curve data to parse - * @param staticDataSource the source of static data to parse - */ - public static void parse( - ImmutableMarketDataBuilder builder, - CharSource curveSource, - CharSource staticDataSource) { - - Map> curveData = Maps.newHashMap(); - Map staticDataMap = parseStaticData(staticDataSource); - - CsvFile csv = CsvFile.of(curveSource, true); - for (CsvRow row : csv.rows()) { - String seriesText = row.getField(Columns.Series.getColumnName()); - String versionText = row.getField(Columns.Version.getColumnName()); - String termText = row.getField(Columns.Term.getColumnName()); - String redCodeText = row.getField(Columns.RedCode.getColumnName()); - String maturityText = row.getField(Columns.Maturity.getColumnName()); - String compositeSpreadText = row.getField(Columns.CompositeSpread.getColumnName()); - String modelSpreadText = row.getField(Columns.ModelSpread.getColumnName()); - - StandardId indexId = MarkitRedCode.id(redCodeText); - int indexSeries = Integer.parseInt(seriesText); - int indexAnnexVersion = Integer.parseInt(versionText); - - IsdaIndexCreditCurveInputsId id = IsdaIndexCreditCurveInputsId.of( - IndexReferenceInformation.of( - indexId, - indexSeries, - indexAnnexVersion)); - - Tenor term = Tenor.parse(termText); - LocalDate maturity = LocalDate.parse(maturityText, DATE_FORMAT); - - double spread; - if (compositeSpreadText.isEmpty()) { - if (modelSpreadText.isEmpty()) { - // there is no rate for this row, continue - continue; + Columns(String columnName) { + this.columnName = columnName; } - // fall back to the model rate is the composite is missing - spread = parseRate(modelSpreadText); - } else { - // prefer the composite rate if it is present - spread = parseRate(compositeSpreadText); - } - List points = curveData.get(id); - if (points == null) { - points = Lists.newArrayList(); - curveData.put(id, points); - } - points.add(new Point(term, maturity, spread)); + public String getColumnName() { + return columnName; + } } - for (IsdaIndexCreditCurveInputsId curveId : curveData.keySet()) { - MarkitRedCode redCode = MarkitRedCode.from(curveId.getReferenceInformation().getIndexId()); - StaticData staticData = staticDataMap.get(redCode); - ArgChecker.notNull(staticData, "Did not find a static data record for " + redCode); - CdsConvention convention = staticData.getConvention(); - double recoveryRate = staticData.getRecoveryRate(); - double indexFactor = staticData.getIndexFactor(); - // TODO add fromDate handling + /** + * Parses the specified sources. + * + * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into + * @param curveSource the source of curve data to parse + * @param staticDataSource the source of static data to parse + */ + public static void parse( + ImmutableMarketDataBuilder builder, + CharSource curveSource, + CharSource staticDataSource) { - String creditCurveName = curveId.toString(); + Map> curveData = Maps.newHashMap(); + Map staticDataMap = parseStaticData(staticDataSource); - List points = curveData.get(curveId); + CsvFile csv = CsvFile.of(curveSource, true); + for (CsvRow row : csv.rows()) { + String seriesText = row.getField(Columns.Series.getColumnName()); + String versionText = row.getField(Columns.Version.getColumnName()); + String termText = row.getField(Columns.Term.getColumnName()); + String redCodeText = row.getField(Columns.RedCode.getColumnName()); + String maturityText = row.getField(Columns.Maturity.getColumnName()); + String compositeSpreadText = row.getField(Columns.CompositeSpread.getColumnName()); + String modelSpreadText = row.getField(Columns.ModelSpread.getColumnName()); - Period[] periods = points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new); - LocalDate[] endDates = points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new); - double[] rates = points.stream().mapToDouble(s -> s.getRate()).toArray(); + StandardId indexId = MarkitRedCode.id(redCodeText); + int indexSeries = Integer.parseInt(seriesText); + int indexAnnexVersion = Integer.parseInt(versionText); - IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( - CurveName.of(creditCurveName), - periods, - endDates, - rates, - convention, - indexFactor); + IsdaIndexCreditCurveInputsId id = IsdaIndexCreditCurveInputsId.of( + IndexReferenceInformation.of( + indexId, + indexSeries, + indexAnnexVersion)); - builder.addValue(curveId, curveInputs); + Tenor term = Tenor.parse(termText); + LocalDate maturity = LocalDate.parse(maturityText, DATE_FORMAT); - IsdaIndexRecoveryRateId recoveryRateId = IsdaIndexRecoveryRateId.of(curveId.getReferenceInformation()); - CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); + double spread; + if (compositeSpreadText.isEmpty()) { + if (modelSpreadText.isEmpty()) { + // there is no rate for this row, continue + continue; + } + // fall back to the model rate is the composite is missing + spread = parseRate(modelSpreadText); + } else { + // prefer the composite rate if it is present + spread = parseRate(compositeSpreadText); + } - builder.addValue(recoveryRateId, cdsRecoveryRate); - } - } + List points = curveData.get(id); + if (points == null) { + points = Lists.newArrayList(); + curveData.put(id, points); + } + points.add(new Point(term, maturity, spread)); + } - // parses the static data file - private static Map parseStaticData(CharSource source) { - CsvFile csv = CsvFile.of(source, true); + for (IsdaIndexCreditCurveInputsId curveId : curveData.keySet()) { + MarkitRedCode redCode = MarkitRedCode.from(curveId.getReferenceInformation().getIndexId()); + StaticData staticData = staticDataMap.get(redCode); + ArgChecker.notNull(staticData, "Did not find a static data record for " + redCode); + CdsConvention convention = staticData.getConvention(); + double recoveryRate = staticData.getRecoveryRate(); + double indexFactor = staticData.getIndexFactor(); + // TODO add fromDate handling - Map result = Maps.newHashMap(); - for (CsvRow row : csv.rows()) { - String redCodeText = row.getField("RedCode"); - String fromDateText = row.getField("From Date"); - String conventionText = row.getField("Convention"); - String recoveryRateText = row.getField("Recovery Rate"); - String indexFactorText = row.getField("Index Factor"); + String creditCurveName = curveId.toString(); - MarkitRedCode redCode = MarkitRedCode.of(redCodeText); - LocalDate fromDate = LocalDate.parse(fromDateText, DATE_FORMAT); - CdsConvention convention = CdsConvention.of(conventionText); - double recoveryRate = parseRate(recoveryRateText); - double indexFactor = Double.parseDouble(indexFactorText); + List points = curveData.get(curveId); - result.put(redCode, new StaticData(fromDate, convention, recoveryRate, indexFactor)); - } - return result; - } + Period[] periods = points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new); + LocalDate[] endDates = points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new); + double[] rates = points.stream().mapToDouble(s -> s.getRate()).toArray(); - //------------------------------------------------------------------------- - /** - * Stores the parsed static data. - */ - private static class StaticData { + IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( + CurveName.of(creditCurveName), + periods, + endDates, + rates, + convention, + indexFactor); - private LocalDate fromDate; - private CdsConvention convention; - private double recoveryRate; - private double indexFactor; + builder.addValue(curveId, curveInputs); - private StaticData(LocalDate fromDate, CdsConvention convention, double recoveryRate, double indexFactor) { - this.fromDate = fromDate; - this.convention = convention; - this.recoveryRate = recoveryRate; - this.indexFactor = indexFactor; + IsdaIndexRecoveryRateId recoveryRateId = IsdaIndexRecoveryRateId.of(curveId.getReferenceInformation()); + CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); + + builder.addValue(recoveryRateId, cdsRecoveryRate); + } } - @SuppressWarnings("unused") - public LocalDate getFromDate() { - return fromDate; + // parses the static data file + private static Map parseStaticData(CharSource source) { + CsvFile csv = CsvFile.of(source, true); + + Map result = Maps.newHashMap(); + for (CsvRow row : csv.rows()) { + String redCodeText = row.getField("RedCode"); + String fromDateText = row.getField("From Date"); + String conventionText = row.getField("Convention"); + String recoveryRateText = row.getField("Recovery Rate"); + String indexFactorText = row.getField("Index Factor"); + + MarkitRedCode redCode = MarkitRedCode.of(redCodeText); + LocalDate fromDate = LocalDate.parse(fromDateText, DATE_FORMAT); + CdsConvention convention = CdsConvention.of(conventionText); + double recoveryRate = parseRate(recoveryRateText); + double indexFactor = Double.parseDouble(indexFactorText); + + result.put(redCode, new StaticData(fromDate, convention, recoveryRate, indexFactor)); + } + return result; } - public CdsConvention getConvention() { - return convention; + //------------------------------------------------------------------------- + + /** + * Stores the parsed static data. + */ + private static class StaticData { + + private LocalDate fromDate; + private CdsConvention convention; + private double recoveryRate; + private double indexFactor; + + private StaticData(LocalDate fromDate, CdsConvention convention, double recoveryRate, double indexFactor) { + this.fromDate = fromDate; + this.convention = convention; + this.recoveryRate = recoveryRate; + this.indexFactor = indexFactor; + } + + @SuppressWarnings("unused") + public LocalDate getFromDate() { + return fromDate; + } + + public CdsConvention getConvention() { + return convention; + } + + public double getRecoveryRate() { + return recoveryRate; + } + + public double getIndexFactor() { + return indexFactor; + } } - public double getRecoveryRate() { - return recoveryRate; + //------------------------------------------------------------------------- + + /** + * Stores the parsed data points. + */ + private static class Point { + private final Tenor tenor; + + private final LocalDate date; + + private final double rate; + + private Point(Tenor tenor, LocalDate date, double rate) { + this.tenor = tenor; + this.date = date; + this.rate = rate; + } + + public Tenor getTenor() { + return tenor; + } + + public LocalDate getDate() { + return date; + } + + public double getRate() { + return rate; + } } - public double getIndexFactor() { - return indexFactor; + // Converts from a string percentage rate with a percent sign to a double rate + // e.g. 0.12% => 0.0012d + private static double parseRate(String input) { + return Double.parseDouble(input.replace("%", "")) / 100d; } - } - - //------------------------------------------------------------------------- - /** - * Stores the parsed data points. - */ - private static class Point { - private final Tenor tenor; - - private final LocalDate date; - - private final double rate; - - private Point(Tenor tenor, LocalDate date, double rate) { - this.tenor = tenor; - this.date = date; - this.rate = rate; - } - - public Tenor getTenor() { - return tenor; - } - - public LocalDate getDate() { - return date; - } - - public double getRate() { - return rate; - } - } - - // Converts from a string percentage rate with a percent sign to a double rate - // e.g. 0.12% => 0.0012d - private static double parseRate(String input) { - return Double.parseDouble(input.replace("%", "")) / 100d; - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java index bae17190a3..0581a6c772 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -19,72 +19,74 @@ import org.joda.convert.FromString; * http://www.markit.com/product/reference-data-cds */ public final class MarkitRedCode - extends TypedString { + extends TypedString { - /** - * Serialization version. - */ - private static final long serialVersionUID = 1L; + /** + * Serialization version. + */ + private static final long serialVersionUID = 1L; - /** - * Scheme used in an OpenGamma {@link StandardId} where the value is a Markit RED code. - */ - public static final String MARKIT_REDCODE_SCHEME = "MarkitRedCode"; + /** + * Scheme used in an OpenGamma {@link StandardId} where the value is a Markit RED code. + */ + public static final String MARKIT_REDCODE_SCHEME = "MarkitRedCode"; - //------------------------------------------------------------------------- - /** - * Obtains an instance from the specified name. - *

- * RED codes must be 6 or 9 characters long. - * - * @param name the name of the field - * @return a RED code - */ - @FromString - public static MarkitRedCode of(String name) { - ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); - return new MarkitRedCode(name); - } + //------------------------------------------------------------------------- - /** - * Converts from a standard identifier ensuring the scheme is correct. - * - * @param id standard id identifying a RED code - * @return the equivalent RED code - */ - public static MarkitRedCode from(StandardId id) { - Preconditions.checkArgument(id.getScheme().equals(MARKIT_REDCODE_SCHEME)); - return MarkitRedCode.of(id.getValue()); - } + /** + * Obtains an instance from the specified name. + *

+ * RED codes must be 6 or 9 characters long. + * + * @param name the name of the field + * @return a RED code + */ + @FromString + public static MarkitRedCode of(String name) { + ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); + return new MarkitRedCode(name); + } - /** - * Creates a standard identifier using the correct Markit RED code scheme. - * - * @param name the Markit RED code, 6 or 9 characters long - * @return the equivalent standard identifier - */ - public static StandardId id(String name) { - ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); - return StandardId.of(MARKIT_REDCODE_SCHEME, name); - } + /** + * Converts from a standard identifier ensuring the scheme is correct. + * + * @param id standard id identifying a RED code + * @return the equivalent RED code + */ + public static MarkitRedCode from(StandardId id) { + Preconditions.checkArgument(id.getScheme().equals(MARKIT_REDCODE_SCHEME)); + return MarkitRedCode.of(id.getValue()); + } - /** - * Creates an instance. - * - * @param name the RED code - */ - private MarkitRedCode(String name) { - super(name); - } + /** + * Creates a standard identifier using the correct Markit RED code scheme. + * + * @param name the Markit RED code, 6 or 9 characters long + * @return the equivalent standard identifier + */ + public static StandardId id(String name) { + ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); + return StandardId.of(MARKIT_REDCODE_SCHEME, name); + } - //------------------------------------------------------------------------- - /** - * Converts this RED code to a standard identifier. - * - * @return the standard identifier - */ - public StandardId toStandardId() { - return StandardId.of(MARKIT_REDCODE_SCHEME, getName()); - } + /** + * Creates an instance. + * + * @param name the RED code + */ + private MarkitRedCode(String name) { + super(name); + } + + //------------------------------------------------------------------------- + + /** + * Converts this RED code to a standard identifier. + * + * @return the standard identifier + */ + public StandardId toStandardId() { + return StandardId.of(MARKIT_REDCODE_SCHEME, getName()); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java index 35e87365a6..be4cdd1b16 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -14,96 +14,97 @@ import com.opengamma.strata.product.credit.RestructuringClause; */ public enum MarkitRestructuringClause { - /** - * Modified-Modified Restructuring 2003. - */ - MM, - /** - * Modified-Modified Restructuring 2014. - */ - MM14, - /** - * Modified Restructuring 2003. - */ - MR, - /** - * Modified Restructuring 2014. - */ - MR14, - /** - * Cum/Old/Full Restructuring 2003. - */ - CR, - /** - * Cum/Old/Full Restructuring 2014. - */ - CR14, - /** - * Ex/No restructuring 2003. - */ - XR, - /** - * Ex/No restructuring 2014. - */ - XR14; + /** + * Modified-Modified Restructuring 2003. + */ + MM, + /** + * Modified-Modified Restructuring 2014. + */ + MM14, + /** + * Modified Restructuring 2003. + */ + MR, + /** + * Modified Restructuring 2014. + */ + MR14, + /** + * Cum/Old/Full Restructuring 2003. + */ + CR, + /** + * Cum/Old/Full Restructuring 2014. + */ + CR14, + /** + * Ex/No restructuring 2003. + */ + XR, + /** + * Ex/No restructuring 2014. + */ + XR14; - //------------------------------------------------------------------------- - /** - * Converts Markit code to standard restructuring clause. - * - * @return the converted clause - */ - public RestructuringClause translate() { - switch (this) { - case MM: - return RestructuringClause.MOD_MOD_RESTRUCTURING_2003; - case MM14: - return RestructuringClause.MOD_MOD_RESTRUCTURING_2014; - case MR: - return RestructuringClause.MODIFIED_RESTRUCTURING_2003; - case MR14: - return RestructuringClause.MODIFIED_RESTRUCTURING_2014; - case CR: - return RestructuringClause.CUM_RESTRUCTURING_2003; - case CR14: - return RestructuringClause.CUM_RESTRUCTURING_2014; - case XR: - return RestructuringClause.NO_RESTRUCTURING_2003; - case XR14: - return RestructuringClause.NO_RESTRUCTURING_2014; - default: - throw new IllegalStateException("Unmapped restructuring clause. Do not have mapping for " + this); + //------------------------------------------------------------------------- + + /** + * Converts Markit code to standard restructuring clause. + * + * @return the converted clause + */ + public RestructuringClause translate() { + switch (this) { + case MM: + return RestructuringClause.MOD_MOD_RESTRUCTURING_2003; + case MM14: + return RestructuringClause.MOD_MOD_RESTRUCTURING_2014; + case MR: + return RestructuringClause.MODIFIED_RESTRUCTURING_2003; + case MR14: + return RestructuringClause.MODIFIED_RESTRUCTURING_2014; + case CR: + return RestructuringClause.CUM_RESTRUCTURING_2003; + case CR14: + return RestructuringClause.CUM_RESTRUCTURING_2014; + case XR: + return RestructuringClause.NO_RESTRUCTURING_2003; + case XR14: + return RestructuringClause.NO_RESTRUCTURING_2014; + default: + throw new IllegalStateException("Unmapped restructuring clause. Do not have mapping for " + this); + } } - } - /** - * Converts restructuring clause to Markit equivalent. - * - * @param restructuringClause the clause to convert - * @return the converted clause - */ - public static MarkitRestructuringClause from(RestructuringClause restructuringClause) { - switch (restructuringClause) { - case MOD_MOD_RESTRUCTURING_2003: - return MM; - case MOD_MOD_RESTRUCTURING_2014: - return MM14; - case MODIFIED_RESTRUCTURING_2003: - return MR; - case MODIFIED_RESTRUCTURING_2014: - return MR14; - case CUM_RESTRUCTURING_2003: - return CR; - case CUM_RESTRUCTURING_2014: - return CR14; - case NO_RESTRUCTURING_2003: - return XR; - case NO_RESTRUCTURING_2014: - return XR14; - default: - throw new UnsupportedOperationException("Unknown restructuring clause. Do not have mapping for " + restructuringClause); + /** + * Converts restructuring clause to Markit equivalent. + * + * @param restructuringClause the clause to convert + * @return the converted clause + */ + public static MarkitRestructuringClause from(RestructuringClause restructuringClause) { + switch (restructuringClause) { + case MOD_MOD_RESTRUCTURING_2003: + return MM; + case MOD_MOD_RESTRUCTURING_2014: + return MM14; + case MODIFIED_RESTRUCTURING_2003: + return MR; + case MODIFIED_RESTRUCTURING_2014: + return MR14; + case CUM_RESTRUCTURING_2003: + return CR; + case CUM_RESTRUCTURING_2014: + return CR14; + case NO_RESTRUCTURING_2003: + return XR; + case NO_RESTRUCTURING_2014: + return XR14; + default: + throw new UnsupportedOperationException("Unknown restructuring clause. Do not have mapping for " + restructuringClause); + } } - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java index 53829372d4..8aca3087d0 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -14,75 +14,76 @@ import com.opengamma.strata.product.credit.SeniorityLevel; */ public enum MarkitSeniorityLevel { - /** - * Senior domestic. - */ - SECDOM, + /** + * Senior domestic. + */ + SECDOM, - /** - * Senior foreign. - */ - SNRFOR, + /** + * Senior foreign. + */ + SNRFOR, - /** - * Subordinate, Lower Tier 2. - */ - SUBLT2, + /** + * Subordinate, Lower Tier 2. + */ + SUBLT2, - /** - * Subordinate Tier 1. - */ - PREFT1, + /** + * Subordinate Tier 1. + */ + PREFT1, - /** - * Subordinate, Upper Tier 2. - */ - JRSUBUT2; + /** + * Subordinate, Upper Tier 2. + */ + JRSUBUT2; - //------------------------------------------------------------------------- - /** - * Converts Markit code to standard seniority level. - * - * @return the converted level - */ - public SeniorityLevel translate() { - switch (this) { - case SECDOM: - return SeniorityLevel.SENIOR_SECURED_DOMESTIC; - case SNRFOR: - return SeniorityLevel.SENIOR_UNSECURED_FOREIGN; - case SUBLT2: - return SeniorityLevel.SUBORDINATE_LOWER_TIER_2; - case PREFT1: - return SeniorityLevel.SUBORDINATE_TIER_1; - case JRSUBUT2: - return SeniorityLevel.SUBORDINATE_UPPER_TIER_2; - default: - throw new IllegalStateException("Unmapped seniority level. Do not have mapping for " + this); + //------------------------------------------------------------------------- + + /** + * Converts Markit code to standard seniority level. + * + * @return the converted level + */ + public SeniorityLevel translate() { + switch (this) { + case SECDOM: + return SeniorityLevel.SENIOR_SECURED_DOMESTIC; + case SNRFOR: + return SeniorityLevel.SENIOR_UNSECURED_FOREIGN; + case SUBLT2: + return SeniorityLevel.SUBORDINATE_LOWER_TIER_2; + case PREFT1: + return SeniorityLevel.SUBORDINATE_TIER_1; + case JRSUBUT2: + return SeniorityLevel.SUBORDINATE_UPPER_TIER_2; + default: + throw new IllegalStateException("Unmapped seniority level. Do not have mapping for " + this); + } } - } - /** - * Converts seniority level to Markit equivalent. - * - * @param seniorityLevel the level to convert - * @return the converted level - */ - public static MarkitSeniorityLevel from(SeniorityLevel seniorityLevel) { - switch (seniorityLevel) { - case SENIOR_SECURED_DOMESTIC: - return SECDOM; - case SENIOR_UNSECURED_FOREIGN: - return SNRFOR; - case SUBORDINATE_LOWER_TIER_2: - return SUBLT2; - case SUBORDINATE_TIER_1: - return PREFT1; - case SUBORDINATE_UPPER_TIER_2: - return JRSUBUT2; - default: - throw new IllegalArgumentException("Unknown seniority level. Do not have mapping for " + seniorityLevel); + /** + * Converts seniority level to Markit equivalent. + * + * @param seniorityLevel the level to convert + * @return the converted level + */ + public static MarkitSeniorityLevel from(SeniorityLevel seniorityLevel) { + switch (seniorityLevel) { + case SENIOR_SECURED_DOMESTIC: + return SECDOM; + case SENIOR_UNSECURED_FOREIGN: + return SNRFOR; + case SUBORDINATE_LOWER_TIER_2: + return SUBLT2; + case SUBORDINATE_TIER_1: + return PREFT1; + case SUBORDINATE_UPPER_TIER_2: + return JRSUBUT2; + default: + throw new IllegalArgumentException("Unknown seniority level. Do not have mapping for " + seniorityLevel); + } } - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java index 3ad85682a9..1da9114513 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -56,141 +56,141 @@ import java.util.Scanner; */ public class MarkitSingleNameCreditCurveDataParser { - // Markit date format with the month in full caps. e.g. 11-JUL-14 - private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() - .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); + // Markit date format with the month in full caps. e.g. 11-JUL-14 + private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() + .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); - // Index used to access the specified columns of string data in the file - private static final int DATE = 0; - private static final int RED_CODE = 3; - private static final int TIER = 4; - private static final int CURRENCY = 5; - private static final int DOCS_CLAUSE = 6; - private static final int FIRST_SPREAD_COLUMN = 8; - private static final int RECOVERY = 19; + // Index used to access the specified columns of string data in the file + private static final int DATE = 0; + private static final int RED_CODE = 3; + private static final int TIER = 4; + private static final int CURRENCY = 5; + private static final int DOCS_CLAUSE = 6; + private static final int FIRST_SPREAD_COLUMN = 8; + private static final int RECOVERY = 19; - private static final List TENORS = ImmutableList.of( - Tenor.TENOR_6M, - Tenor.TENOR_1Y, - Tenor.TENOR_2Y, - Tenor.TENOR_3Y, - Tenor.TENOR_4Y, - Tenor.TENOR_5Y, - Tenor.TENOR_7Y, - Tenor.TENOR_10Y, - Tenor.TENOR_15Y, - Tenor.TENOR_20Y, - Tenor.TENOR_30Y); + private static final List TENORS = ImmutableList.of( + Tenor.TENOR_6M, + Tenor.TENOR_1Y, + Tenor.TENOR_2Y, + Tenor.TENOR_3Y, + Tenor.TENOR_4Y, + Tenor.TENOR_5Y, + Tenor.TENOR_7Y, + Tenor.TENOR_10Y, + Tenor.TENOR_15Y, + Tenor.TENOR_20Y, + Tenor.TENOR_30Y); - /** - * Parses the specified sources. - * - * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into - * @param curveSource the source of curve data to parse - * @param staticDataSource the source of static data to parse - */ - public static void parse( - ImmutableMarketDataBuilder builder, - CharSource curveSource, - CharSource staticDataSource) { + /** + * Parses the specified sources. + * + * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into + * @param curveSource the source of curve data to parse + * @param staticDataSource the source of static data to parse + */ + public static void parse( + ImmutableMarketDataBuilder builder, + CharSource curveSource, + CharSource staticDataSource) { - Map conventions = parseStaticData(staticDataSource); - try (Scanner scanner = new Scanner(curveSource.openStream())) { - while (scanner.hasNextLine()) { + Map conventions = parseStaticData(staticDataSource); + try (Scanner scanner = new Scanner(curveSource.openStream())) { + while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - // skip over header rows - if (line.startsWith("V5 CDS Composites by Convention") || - line.trim().isEmpty() || - line.startsWith("\"Date\",")) { - continue; + String line = scanner.nextLine(); + // skip over header rows + if (line.startsWith("V5 CDS Composites by Convention") || + line.trim().isEmpty() || + line.startsWith("\"Date\",")) { + continue; + } + String[] columns = line.split(","); + for (int i = 0; i < columns.length; i++) { + // get rid of quotes and trim the string + columns[i] = columns[i].replaceFirst("^\"", "").replaceFirst("\"$", "").trim(); + } + + LocalDate valuationDate = LocalDate.parse(columns[DATE], DATE_FORMAT); + + MarkitRedCode redCode = MarkitRedCode.of(columns[RED_CODE]); + SeniorityLevel seniorityLevel = MarkitSeniorityLevel.valueOf(columns[TIER]).translate(); + Currency currency = Currency.parse(columns[CURRENCY]); + RestructuringClause restructuringClause = MarkitRestructuringClause.valueOf(columns[DOCS_CLAUSE]).translate(); + + double recoveryRate = parseRate(columns[RECOVERY]); + + SingleNameReferenceInformation referenceInformation = SingleNameReferenceInformation.of( + redCode.toStandardId(), + seniorityLevel, + currency, + restructuringClause); + + IsdaSingleNameCreditCurveInputsId curveId = IsdaSingleNameCreditCurveInputsId.of(referenceInformation); + + List periodsList = Lists.newArrayList(); + List ratesList = Lists.newArrayList(); + for (int i = 0; i < TENORS.size(); i++) { + String rateString = columns[FIRST_SPREAD_COLUMN + i]; + if (rateString.isEmpty()) { + // no data at this point + continue; + } + periodsList.add(TENORS.get(i).getPeriod()); + ratesList.add(parseRate(rateString)); + } + + String creditCurveName = curveId.toString(); + + CdsConvention cdsConvention = conventions.get(redCode); + + Period[] periods = periodsList.stream().toArray(Period[]::new); + LocalDate[] endDates = Lists + .newArrayList(periods) + .stream() + .map(p -> cdsConvention.calculateUnadjustedMaturityDateFromValuationDate(valuationDate, p)) + .toArray(LocalDate[]::new); + + double[] rates = ratesList.stream().mapToDouble(s -> s).toArray(); + double unitScalingFactor = 1d; // for single name, we don't do any scaling (no index factor) + + IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( + CurveName.of(creditCurveName), + periods, + endDates, + rates, + cdsConvention, + unitScalingFactor); + + builder.addValue(curveId, curveInputs); + + IsdaSingleNameRecoveryRateId recoveryRateId = IsdaSingleNameRecoveryRateId.of(referenceInformation); + CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); + + builder.addValue(recoveryRateId, cdsRecoveryRate); + + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); } - String[] columns = line.split(","); - for (int i = 0; i < columns.length; i++) { - // get rid of quotes and trim the string - columns[i] = columns[i].replaceFirst("^\"", "").replaceFirst("\"$", "").trim(); - } - - LocalDate valuationDate = LocalDate.parse(columns[DATE], DATE_FORMAT); - - MarkitRedCode redCode = MarkitRedCode.of(columns[RED_CODE]); - SeniorityLevel seniorityLevel = MarkitSeniorityLevel.valueOf(columns[TIER]).translate(); - Currency currency = Currency.parse(columns[CURRENCY]); - RestructuringClause restructuringClause = MarkitRestructuringClause.valueOf(columns[DOCS_CLAUSE]).translate(); - - double recoveryRate = parseRate(columns[RECOVERY]); - - SingleNameReferenceInformation referenceInformation = SingleNameReferenceInformation.of( - redCode.toStandardId(), - seniorityLevel, - currency, - restructuringClause); - - IsdaSingleNameCreditCurveInputsId curveId = IsdaSingleNameCreditCurveInputsId.of(referenceInformation); - - List periodsList = Lists.newArrayList(); - List ratesList = Lists.newArrayList(); - for (int i = 0; i < TENORS.size(); i++) { - String rateString = columns[FIRST_SPREAD_COLUMN + i]; - if (rateString.isEmpty()) { - // no data at this point - continue; - } - periodsList.add(TENORS.get(i).getPeriod()); - ratesList.add(parseRate(rateString)); - } - - String creditCurveName = curveId.toString(); - - CdsConvention cdsConvention = conventions.get(redCode); - - Period[] periods = periodsList.stream().toArray(Period[]::new); - LocalDate[] endDates = Lists - .newArrayList(periods) - .stream() - .map(p -> cdsConvention.calculateUnadjustedMaturityDateFromValuationDate(valuationDate, p)) - .toArray(LocalDate[]::new); - - double[] rates = ratesList.stream().mapToDouble(s -> s).toArray(); - double unitScalingFactor = 1d; // for single name, we don't do any scaling (no index factor) - - IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( - CurveName.of(creditCurveName), - periods, - endDates, - rates, - cdsConvention, - unitScalingFactor); - - builder.addValue(curveId, curveInputs); - - IsdaSingleNameRecoveryRateId recoveryRateId = IsdaSingleNameRecoveryRateId.of(referenceInformation); - CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); - - builder.addValue(recoveryRateId, cdsRecoveryRate); - - } - } catch (IOException ex) { - throw new UncheckedIOException(ex); } - } - // parses the static data file of RED code to convention - private static Map parseStaticData(CharSource source) { - CsvFile csv = CsvFile.of(source, true); - Map result = Maps.newHashMap(); - for (CsvRow row : csv.rows()) { - String redCodeText = row.getField("RedCode"); - String conventionText = row.getField("Convention"); - result.put(MarkitRedCode.of(redCodeText), CdsConvention.of(conventionText)); + // parses the static data file of RED code to convention + private static Map parseStaticData(CharSource source) { + CsvFile csv = CsvFile.of(source, true); + Map result = Maps.newHashMap(); + for (CsvRow row : csv.rows()) { + String redCodeText = row.getField("RedCode"); + String conventionText = row.getField("Convention"); + result.put(MarkitRedCode.of(redCodeText), CdsConvention.of(conventionText)); + } + return result; } - return result; - } - // Converts from a string percentage rate with a percent sign to a double rate - // e.g. 0.12% => 0.0012d - private static double parseRate(String input) { - return Double.parseDouble(input.replace("%", "")) / 100d; - } + // Converts from a string percentage rate with a percent sign to a double rate + // e.g. 0.12% => 0.0012d + private static double parseRate(String input) { + return Double.parseDouble(input.replace("%", "")) / 100d; + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java index a928a36a07..c47caacf54 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -31,104 +31,105 @@ import java.util.Map; */ public class MarkitYieldCurveDataParser { - private static final String DATE = "Valuation Date"; - private static final String TENOR = "Tenor"; - private static final String INSTRUMENT = "Instrument Type"; - private static final String RATE = "Rate"; - private static final String CONVENTION = "Curve Convention"; + private static final String DATE = "Valuation Date"; + private static final String TENOR = "Tenor"; + private static final String INSTRUMENT = "Instrument Type"; + private static final String RATE = "Rate"; + private static final String CONVENTION = "Curve Convention"; - /** - * Parses the specified source. - * - * @param source the source to parse - * @return the map of parsed yield curve par rates - */ - public static Map parse(CharSource source) { - // parse the curve data - Map> curveData = Maps.newHashMap(); - CsvFile csv = CsvFile.of(source, true); - for (CsvRow row : csv.rows()) { - String dateText = row.getField(DATE); - String tenorText = row.getField(TENOR); - String instrumentText = row.getField(INSTRUMENT); - String rateText = row.getField(RATE); - String conventionText = row.getField(CONVENTION); + /** + * Parses the specified source. + * + * @param source the source to parse + * @return the map of parsed yield curve par rates + */ + public static Map parse(CharSource source) { + // parse the curve data + Map> curveData = Maps.newHashMap(); + CsvFile csv = CsvFile.of(source, true); + for (CsvRow row : csv.rows()) { + String dateText = row.getField(DATE); + String tenorText = row.getField(TENOR); + String instrumentText = row.getField(INSTRUMENT); + String rateText = row.getField(RATE); + String conventionText = row.getField(CONVENTION); - Point point = new Point( - Tenor.parse(tenorText), - LocalDate.parse(dateText, DateTimeFormatter.ISO_LOCAL_DATE), - mapUnderlyingType(instrumentText), - Double.parseDouble(rateText)); - IsdaYieldCurveConvention convention = IsdaYieldCurveConvention.of(conventionText); + Point point = new Point( + Tenor.parse(tenorText), + LocalDate.parse(dateText, DateTimeFormatter.ISO_LOCAL_DATE), + mapUnderlyingType(instrumentText), + Double.parseDouble(rateText)); + IsdaYieldCurveConvention convention = IsdaYieldCurveConvention.of(conventionText); - List points = curveData.get(convention); - if (points == null) { - points = Lists.newArrayList(); - curveData.put(convention, points); - } - points.add(point); + List points = curveData.get(convention); + if (points == null) { + points = Lists.newArrayList(); + curveData.put(convention, points); + } + points.add(point); + } + + // convert the curve data into the result map + Map result = Maps.newHashMap(); + for (IsdaYieldCurveConvention convention : curveData.keySet()) { + List points = curveData.get(convention); + result.put(IsdaYieldCurveInputsId.of(convention.getCurrency()), + IsdaYieldCurveInputs.of( + CurveName.of(convention.getName()), + points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new), + points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new), + points.stream().map(s -> s.getInstrumentType()).toArray(IsdaYieldCurveUnderlyingType[]::new), + points.stream().mapToDouble(s -> s.getRate()).toArray(), + convention)); + } + return result; } - // convert the curve data into the result map - Map result = Maps.newHashMap(); - for (IsdaYieldCurveConvention convention : curveData.keySet()) { - List points = curveData.get(convention); - result.put(IsdaYieldCurveInputsId.of(convention.getCurrency()), - IsdaYieldCurveInputs.of( - CurveName.of(convention.getName()), - points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new), - points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new), - points.stream().map(s -> s.getInstrumentType()).toArray(IsdaYieldCurveUnderlyingType[]::new), - points.stream().mapToDouble(s -> s.getRate()).toArray(), - convention)); - } - return result; - } - - // parse the M/S instrument type flag - private static IsdaYieldCurveUnderlyingType mapUnderlyingType(String type) { - switch (type) { - case "M": - return IsdaYieldCurveUnderlyingType.ISDA_MONEY_MARKET; - case "S": - return IsdaYieldCurveUnderlyingType.ISDA_SWAP; - default: - throw new IllegalStateException("Unknown underlying type, only M or S allowed: " + type); - } - } - - //------------------------------------------------------------------------- - /** - * Stores the parsed data points. - */ - private static class Point { - private final Tenor tenor; - private final LocalDate date; - private final IsdaYieldCurveUnderlyingType instrumentType; - private final double rate; - - private Point(Tenor tenor, LocalDate baseDate, IsdaYieldCurveUnderlyingType instrumentType, double rate) { - this.tenor = tenor; - this.date = baseDate.plus(tenor.getPeriod()); - this.instrumentType = instrumentType; - this.rate = rate; + // parse the M/S instrument type flag + private static IsdaYieldCurveUnderlyingType mapUnderlyingType(String type) { + switch (type) { + case "M": + return IsdaYieldCurveUnderlyingType.ISDA_MONEY_MARKET; + case "S": + return IsdaYieldCurveUnderlyingType.ISDA_SWAP; + default: + throw new IllegalStateException("Unknown underlying type, only M or S allowed: " + type); + } } - public Tenor getTenor() { - return tenor; - } + //------------------------------------------------------------------------- - public LocalDate getDate() { - return date; - } + /** + * Stores the parsed data points. + */ + private static class Point { + private final Tenor tenor; + private final LocalDate date; + private final IsdaYieldCurveUnderlyingType instrumentType; + private final double rate; - public IsdaYieldCurveUnderlyingType getInstrumentType() { - return instrumentType; - } + private Point(Tenor tenor, LocalDate baseDate, IsdaYieldCurveUnderlyingType instrumentType, double rate) { + this.tenor = tenor; + this.date = baseDate.plus(tenor.getPeriod()); + this.instrumentType = instrumentType; + this.rate = rate; + } - public double getRate() { - return rate; + public Tenor getTenor() { + return tenor; + } + + public LocalDate getDate() { + return date; + } + + public IsdaYieldCurveUnderlyingType getInstrumentType() { + return instrumentType; + } + + public double getRate() { + return rate; + } } - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java index e05d86d71e..9e92d72356 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java @@ -1,7 +1,9 @@ /** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. + *

+ * Credit market data for examples. */ /** diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java index a0688333c4..c286a8db4b 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java @@ -1,7 +1,9 @@ /** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. + *

+ * Market data for examples. */ /** diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt index a10b756319..3fb9118fd5 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt @@ -254,13 +254,14 @@ class PortfolioApi(val rpc: CordaRPCOps) { val parties = rpc.networkMapSnapshot() val notaries = rpc.notaryIdentities() // TODO We are not able to filter by network map node now - val counterParties = parties.filterNot { it.legalIdentities.any { it in notaries } - || ownParty in it.legalIdentities + val counterParties = parties.filterNot { + it.legalIdentities.any { it in notaries } + || ownParty in it.legalIdentities } return AvailableParties( self = ApiParty(ownParty.owningKey.toBase58String(), ownParty.name), // TODO It will show all identities including service identities. - counterparties = counterParties.flatMap { it.legalIdentitiesAndCerts.map { ApiParty(it.owningKey.toBase58String(), it.name) }} + counterparties = counterParties.flatMap { it.legalIdentitiesAndCerts.map { ApiParty(it.owningKey.toBase58String(), it.name) } } ) } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index 56c7001682..cfccafbc4d 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -70,6 +70,7 @@ object SimmFlow { private val existing: StateAndRef?) : FlowLogic>() { constructor(otherParty: Party, valuationDate: LocalDate) : this(otherParty, valuationDate, null) + lateinit var notary: Party lateinit var otherPartySession: FlowSession @@ -80,7 +81,7 @@ object SimmFlow { notary = serviceHub.networkMapCache.notaryIdentities.first() // TODO We should pass the notary as a parameter to the flow, not leave it to random choice. val criteria = LinearStateQueryCriteria(participants = listOf(otherParty)) - val trades = serviceHub.vaultQueryService.queryBy(criteria).states + val trades = serviceHub.vaultService.queryBy(criteria).states val portfolio = Portfolio(trades, valuationDate) otherPartySession = initiateFlow(otherParty) @@ -89,7 +90,7 @@ object SimmFlow { } else { updatePortfolio(portfolio, existing) } - val portfolioStateRef = serviceHub.vaultQueryService.queryBy(criteria).states.first() + val portfolioStateRef = serviceHub.vaultService.queryBy(criteria).states.first() val state = updateValuation(portfolioStateRef) logger.info("SimmFlow done") @@ -126,7 +127,7 @@ object SimmFlow { private fun updateValuation(stateRef: StateAndRef): RevisionedState { logger.info("Agreeing valuations") val state = stateRef.state.data - val portfolio = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = state.portfolio)).states.toPortfolio() + val portfolio = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = state.portfolio)).states.toPortfolio() val valuer = serviceHub.identityService.wellKnownPartyFromAnonymous(state.valuer) require(valuer != null) { "Valuer party must be known to this node" } @@ -211,7 +212,7 @@ object SimmFlow { @Suspendable override fun call() { val criteria = LinearStateQueryCriteria(participants = listOf(replyToSession.counterparty)) - val trades = serviceHub.vaultQueryService.queryBy(criteria).states + val trades = serviceHub.vaultService.queryBy(criteria).states val portfolio = Portfolio(trades) logger.info("SimmFlow receiver started") offer = replyToSession.receive().unwrap { it } @@ -220,7 +221,7 @@ object SimmFlow { } else { updatePortfolio(portfolio) } - val portfolioStateRef = serviceHub.vaultQueryService.queryBy(criteria).states.first() + val portfolioStateRef = serviceHub.vaultService.queryBy(criteria).states.first() updateValuation(portfolioStateRef) } @@ -318,7 +319,7 @@ object SimmFlow { logger.info("Handshake finished, awaiting Simm update") replyToSession.send(Ack) // Hack to state that this party is ready. subFlow(object : StateRevisionFlow.Receiver(replyToSession) { - override fun verifyProposal(stx:SignedTransaction, proposal: Proposal) { + override fun verifyProposal(stx: SignedTransaction, proposal: Proposal) { super.verifyProposal(stx, proposal) if (proposal.modification.portfolio != portfolio.refs) throw StateReplacementException() } @@ -327,7 +328,7 @@ object SimmFlow { @Suspendable private fun updateValuation(stateRef: StateAndRef) { - val portfolio = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = stateRef.state.data.portfolio)).states.toPortfolio() + val portfolio = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = stateRef.state.data.portfolio)).states.toPortfolio() val valuer = serviceHub.identityService.wellKnownPartyFromAnonymous(stateRef.state.data.valuer) ?: throw IllegalStateException("Unknown valuer party ${stateRef.state.data.valuer}") val valuation = agreeValuation(portfolio, offer.valuationDate, valuer) subFlow(object : StateRevisionFlow.Receiver(replyToSession) { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt index 8f607163ef..599e9d4345 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt @@ -20,7 +20,7 @@ object SimmRevaluation { class Initiator(private val curStateRef: StateRef, private val valuationDate: LocalDate) : FlowLogic() { @Suspendable override fun call() { - val stateAndRef = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = listOf(curStateRef))).states.single() + val stateAndRef = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = listOf(curStateRef))).states.single() val curState = stateAndRef.state.data if (ourIdentity == curState.participants[0]) { val otherParty = serviceHub.identityService.wellKnownPartyFromAnonymous(curState.participants[1]) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt index 49918f85c3..7616b9e932 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt @@ -15,7 +15,7 @@ import net.corda.vega.contracts.RevisionedState */ object StateRevisionFlow { open class Requester(curStateRef: StateAndRef>, - updatedData: T) : AbstractStateReplacementFlow.Instigator, RevisionedState, T>(curStateRef, updatedData) { + updatedData: T) : AbstractStateReplacementFlow.Instigator, RevisionedState, T>(curStateRef, updatedData) { override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val state = originalState.state.data val tx = state.generateRevision(originalState.state.notary, originalState, modification) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt index 270f15e57e..daf4cdcb07 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt @@ -1,7 +1,7 @@ package net.corda.vega.plugin import com.fasterxml.jackson.databind.ObjectMapper -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.SerializationWhitelist import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.vega.api.PortfolioApi import net.corda.webserver.services.WebServerPluginRegistry @@ -9,7 +9,7 @@ import java.util.function.Function /** * [SimmService] is the object that makes available the flows and services for the Simm agreement / evaluation flow. - * It is loaded via discovery - see [CordaPluginRegistry]. + * It is loaded via discovery - see [SerializationWhitelist]. * It is also the object that enables a human usable web service for demo purpose * It is loaded via discovery see [WebServerPluginRegistry]. */ diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt index 2cbd235203..626de57761 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt @@ -10,35 +10,30 @@ import com.opengamma.strata.market.curve.CurveName import com.opengamma.strata.market.param.CurrencyParameterSensitivities import com.opengamma.strata.market.param.CurrencyParameterSensitivity import com.opengamma.strata.market.param.TenorDateParameterMetadata -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.SerializationCustomization +import net.corda.core.serialization.SerializationWhitelist import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.InitialMarginTriple import net.corda.webserver.services.WebServerPluginRegistry /** * [SimmService] is the object that makes available the flows and services for the Simm agreement / evaluation flow. - * It is loaded via discovery - see [CordaPluginRegistry]. + * It is loaded via discovery - see [SerializationWhitelist]. * It is also the object that enables a human usable web service for demo purpose * It is loaded via discovery see [WebServerPluginRegistry]. */ -class SimmPluginRegistry : CordaPluginRegistry() { - override fun customizeSerialization(custom: SerializationCustomization): Boolean { - custom.apply { - // OpenGamma classes. - addToWhitelist(MultiCurrencyAmount::class.java, - Ordering.natural>().javaClass, - CurrencyAmount::class.java, - Currency::class.java, - InitialMarginTriple::class.java, - CordaMarketData::class.java, - CurrencyParameterSensitivities::class.java, - CurrencyParameterSensitivity::class.java, - DoubleArray::class.java, - CurveName::class.java, - TenorDateParameterMetadata::class.java, - Tenor::class.java) - } - return true - } +class SimmPluginRegistry : SerializationWhitelist { + override val whitelist = listOf( + MultiCurrencyAmount::class.java, + Ordering.natural>().javaClass, + CurrencyAmount::class.java, + Currency::class.java, + InitialMarginTriple::class.java, + CordaMarketData::class.java, + CurrencyParameterSensitivities::class.java, + CurrencyParameterSensitivity::class.java, + DoubleArray::class.java, + CurveName::class.java, + TenorDateParameterMetadata::class.java, + Tenor::class.java + ) } diff --git a/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index e2faa7858d..0000000000 --- a/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.vega.plugin.SimmPluginRegistry diff --git a/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist new file mode 100644 index 0000000000..78b86947ec --- /dev/null +++ b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist @@ -0,0 +1 @@ +net.corda.vega.plugin.SimmPluginRegistry diff --git a/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java b/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java index 56a2dd2e6e..dfd18bbbd8 100644 --- a/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java +++ b/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java @@ -48,97 +48,97 @@ import static java.util.stream.Collectors.toList; */ public class SwapExampleX { - public static final LocalDate VALUATION_DATE = LocalDate.of(2016, 6, 6); + public static final LocalDate VALUATION_DATE = LocalDate.of(2016, 6, 6); - public static void main(String[] args) { - CurveGroupDefinition curveGroupDefinition = loadCurveGroup(); - MarketData marketData = loadMarketData(); - List trades = ImmutableList.of(createVanillaFixedVsLibor3mSwap(), createVanillaFixedVsLibor6mSwap()); - CurveCalibrator calibrator = CurveCalibrator.of(1e-9, 1e-9, 100, CalibrationMeasures.PAR_SPREAD); - ImmutableRatesProvider ratesProvider = calibrator.calibrate(curveGroupDefinition, marketData, ReferenceData.standard()); - MarketDataFxRateProvider fxRateProvider = MarketDataFxRateProvider.of(marketData); - ImmutableRatesProvider combinedRatesProvider = ImmutableRatesProvider.combined(fxRateProvider, ratesProvider); + public static void main(String[] args) { + CurveGroupDefinition curveGroupDefinition = loadCurveGroup(); + MarketData marketData = loadMarketData(); + List trades = ImmutableList.of(createVanillaFixedVsLibor3mSwap(), createVanillaFixedVsLibor6mSwap()); + CurveCalibrator calibrator = CurveCalibrator.of(1e-9, 1e-9, 100, CalibrationMeasures.PAR_SPREAD); + ImmutableRatesProvider ratesProvider = calibrator.calibrate(curveGroupDefinition, marketData, ReferenceData.standard()); + MarketDataFxRateProvider fxRateProvider = MarketDataFxRateProvider.of(marketData); + ImmutableRatesProvider combinedRatesProvider = ImmutableRatesProvider.combined(fxRateProvider, ratesProvider); - List resolvedTrades = trades.stream().map(trade -> trade.resolve(ReferenceData.standard())).collect(toList()); - DiscountingSwapProductPricer pricer = DiscountingSwapProductPricer.DEFAULT; + List resolvedTrades = trades.stream().map(trade -> trade.resolve(ReferenceData.standard())).collect(toList()); + DiscountingSwapProductPricer pricer = DiscountingSwapProductPricer.DEFAULT; - CurrencyParameterSensitivities totalSensitivities = CurrencyParameterSensitivities.empty(); - MultiCurrencyAmount totalCurrencyExposure = MultiCurrencyAmount.empty(); + CurrencyParameterSensitivities totalSensitivities = CurrencyParameterSensitivities.empty(); + MultiCurrencyAmount totalCurrencyExposure = MultiCurrencyAmount.empty(); - for (ResolvedSwapTrade resolvedTrade : resolvedTrades) { - ResolvedSwap swap = resolvedTrade.getProduct(); + for (ResolvedSwapTrade resolvedTrade : resolvedTrades) { + ResolvedSwap swap = resolvedTrade.getProduct(); - PointSensitivities pointSensitivities = pricer.presentValueSensitivity(swap, combinedRatesProvider).build(); - CurrencyParameterSensitivities sensitivities = combinedRatesProvider.parameterSensitivity(pointSensitivities); - MultiCurrencyAmount currencyExposure = pricer.currencyExposure(swap, combinedRatesProvider); + PointSensitivities pointSensitivities = pricer.presentValueSensitivity(swap, combinedRatesProvider).build(); + CurrencyParameterSensitivities sensitivities = combinedRatesProvider.parameterSensitivity(pointSensitivities); + MultiCurrencyAmount currencyExposure = pricer.currencyExposure(swap, combinedRatesProvider); - totalSensitivities = totalSensitivities.combinedWith(sensitivities); - totalCurrencyExposure = totalCurrencyExposure.plus(currencyExposure); + totalSensitivities = totalSensitivities.combinedWith(sensitivities); + totalCurrencyExposure = totalCurrencyExposure.plus(currencyExposure); + } + //PortfolioNormalizer normalizer = new PortfolioNormalizer(Currency.EUR, combinedRatesProvider); + //RwamBimmNotProductClassesCalculator calculatorTotal = new RwamBimmNotProductClassesCalculator( + // fxRateProvider, + // Currency.EUR, + // IsdaConfiguration.INSTANCE); +// + //Triple margin = BimmAnalysisUtils.computeMargin( + // combinedRatesProvider, + // normalizer, + // calculatorTotal, + // totalSensitivities, + // totalCurrencyExposure); +// + //System.out.println(margin); } - //PortfolioNormalizer normalizer = new PortfolioNormalizer(Currency.EUR, combinedRatesProvider); - //RwamBimmNotProductClassesCalculator calculatorTotal = new RwamBimmNotProductClassesCalculator( - // fxRateProvider, - // Currency.EUR, - // IsdaConfiguration.INSTANCE); -// - //Triple margin = BimmAnalysisUtils.computeMargin( - // combinedRatesProvider, - // normalizer, - // calculatorTotal, - // totalSensitivities, - // totalCurrencyExposure); -// - //System.out.println(margin); - } - //-------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------- - /** - * Load the market quotes and FX rates from data files. - */ - private static MarketData loadMarketData() { - Path dataDir = Paths.get("src/test/resources/data"); - Path quotesFile = dataDir.resolve("BIMM-MARKET-QUOTES-20160606.csv"); - Path fxFile = dataDir.resolve("BIMM-FX-RATES-20160606.csv"); + /** + * Load the market quotes and FX rates from data files. + */ + private static MarketData loadMarketData() { + Path dataDir = Paths.get("src/test/resources/data"); + Path quotesFile = dataDir.resolve("BIMM-MARKET-QUOTES-20160606.csv"); + Path fxFile = dataDir.resolve("BIMM-FX-RATES-20160606.csv"); - Map quotes = QuotesCsvLoader.load(VALUATION_DATE, ImmutableList.of(ResourceLocator.ofPath(quotesFile))); - Map fxRates = FxRatesCsvLoader.load(VALUATION_DATE, ResourceLocator.ofPath(fxFile)); - return ImmutableMarketData.builder(VALUATION_DATE).addValueMap(quotes).addValueMap(fxRates).build(); - } + Map quotes = QuotesCsvLoader.load(VALUATION_DATE, ImmutableList.of(ResourceLocator.ofPath(quotesFile))); + Map fxRates = FxRatesCsvLoader.load(VALUATION_DATE, ResourceLocator.ofPath(fxFile)); + return ImmutableMarketData.builder(VALUATION_DATE).addValueMap(quotes).addValueMap(fxRates).build(); + } - /** - * Loads the curve group definition from data files. - * - * A curve group maps from curve name to index for forward curves and curve name to currency for discount curves. - */ - private static CurveGroupDefinition loadCurveGroup() { - Path settingsDir = Paths.get("src/test/resources/settings"); - Map curveGroups = RatesCalibrationCsvLoader.load( - ResourceLocator.ofPath(settingsDir.resolve("BIMM-groups-EUR.csv")), - ResourceLocator.ofPath(settingsDir.resolve("BIMM-settings-EUR.csv")), - ResourceLocator.ofPath(settingsDir.resolve("BIMM-nodes-EUR.csv"))); - return curveGroups.get(CurveGroupName.of("BIMM")); - } + /** + * Loads the curve group definition from data files. + *

+ * A curve group maps from curve name to index for forward curves and curve name to currency for discount curves. + */ + private static CurveGroupDefinition loadCurveGroup() { + Path settingsDir = Paths.get("src/test/resources/settings"); + Map curveGroups = RatesCalibrationCsvLoader.load( + ResourceLocator.ofPath(settingsDir.resolve("BIMM-groups-EUR.csv")), + ResourceLocator.ofPath(settingsDir.resolve("BIMM-settings-EUR.csv")), + ResourceLocator.ofPath(settingsDir.resolve("BIMM-nodes-EUR.csv"))); + return curveGroups.get(CurveGroupName.of("BIMM")); + } - //-------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------- - private static SwapTrade createVanillaFixedVsLibor3mSwap() { - return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_3M.createTrade( - VALUATION_DATE, - Tenor.TENOR_4Y, - BuySell.BUY, - 200_000_000, - 0.015, - ReferenceData.standard()); - } + private static SwapTrade createVanillaFixedVsLibor3mSwap() { + return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_3M.createTrade( + VALUATION_DATE, + Tenor.TENOR_4Y, + BuySell.BUY, + 200_000_000, + 0.015, + ReferenceData.standard()); + } - private static SwapTrade createVanillaFixedVsLibor6mSwap() { - return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_6M.createTrade( - VALUATION_DATE, - Tenor.TENOR_10Y, - BuySell.SELL, - 100_000_000, - 0.013, - ReferenceData.standard()); - } + private static SwapTrade createVanillaFixedVsLibor6mSwap() { + return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_6M.createTrade( + VALUATION_DATE, + Tenor.TENOR_10Y, + BuySell.SELL, + 100_000_000, + 0.013, + ReferenceData.standard()); + } } diff --git a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt index ee855f6ffc..9cc72b5af0 100644 --- a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt +++ b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt @@ -1,12 +1,10 @@ package net.corda.vega import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_BANK_C import net.corda.testing.DUMMY_NOTARY -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.driver.driver /** @@ -16,7 +14,7 @@ import net.corda.testing.driver.driver */ fun main(args: Array) { driver(dsl = { - val notaryFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, validating = false) val nodeAFuture = startNode(providedName = DUMMY_BANK_A.name) val nodeBFuture = startNode(providedName = DUMMY_BANK_B.name) val nodeCFuture = startNode(providedName = DUMMY_BANK_C.name) diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 216405f66f..e3d363e516 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' @@ -50,17 +51,14 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into TraderDemoClientApi so if you change it here, change it there too. - // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] p2pPort 10002 cordapps = ["net.corda:finance:$corda_release_version"] } node { name "O=Bank A,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 cordapps = ["net.corda:finance:$corda_release_version"] @@ -68,7 +66,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 cordapps = ["net.corda:finance:$corda_release_version"] @@ -76,7 +73,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=BankOfCorda,L=New York,C=US" - advertisedServices = [] p2pPort 10011 rpcPort 10012 cordapps = ["net.corda:finance:$corda_release_version"] @@ -121,3 +117,11 @@ task runSeller(type: JavaExec) { args '--role' args 'SELLER' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.trader' + ) + } +} \ No newline at end of file diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 2625e43280..61580c8632 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -1,6 +1,7 @@ package net.corda.traderdemo import net.corda.client.rpc.CordaRPCClient +import net.corda.core.internal.packageName import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis import net.corda.finance.DOLLARS @@ -9,35 +10,20 @@ import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.CommercialPaperSchemaV1 import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.poll import net.corda.testing.node.NodeBasedTest -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before import org.junit.Test import java.util.concurrent.Executors -class TraderDemoTest : NodeBasedTest() { - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.asset", "net.corda.finance.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - +class TraderDemoTest : NodeBasedTest(listOf( + "net.corda.finance.contracts.asset", "net.corda.finance.contracts", + CashSchemaV1::class.packageName, CommercialPaperSchemaV1::class.packageName)) { @Test fun `runs trader demo`() { val demoUser = User("demo", "demo", setOf(startFlowPermission())) @@ -45,22 +31,19 @@ class TraderDemoTest : NodeBasedTest() { startFlowPermission(), startFlowPermission(), startFlowPermission())) - val notaryFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, validating = false) val nodeAFuture = startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)) val nodeBFuture = startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)) val bankNodeFuture = startNode(BOC.name, rpcUsers = listOf(bankUser)) val (nodeA, nodeB, bankNode) = listOf(nodeAFuture, nodeBFuture, bankNodeFuture, notaryFuture).map { it.getOrThrow() } nodeA.internals.registerInitiatedFlow(BuyerFlow::class.java) - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1, CommercialPaperSchemaV1)) - val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { - val client = CordaRPCClient(it.internals.configuration.rpcAddress!!, initialiseSerialization = false) + val client = CordaRPCClient(it.internals.configuration.rpcAddress!!) client.start(demoUser.username, demoUser.password).proxy } val nodeBankRpc = let { - val client = CordaRPCClient(bankNode.internals.configuration.rpcAddress!!, initialiseSerialization = false) + val client = CordaRPCClient(bankNode.internals.configuration.rpcAddress!!) client.start(bankUser.username, bankUser.password).proxy } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt index 689d3b4e52..43c82f5565 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt @@ -44,17 +44,17 @@ private class TraderDemo { } // What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role - // will contact the buyer and actually make something happen. + // will contact the buyer and actually make something happen. We intentionally use large amounts here. val role = options.valueOf(roleArg)!! if (role == Role.BANK) { val bankHost = NetworkHostAndPort("localhost", bankRpcPort) CordaRPCClient(bankHost).use("demo", "demo") { - TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName) + TraderDemoClientApi(it.proxy).runIssuer(1_100_000_000_000.DOLLARS, buyerName, sellerName) } } else { val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort) CordaRPCClient(sellerHost).use("demo", "demo") { - TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, buyerName) + TraderDemoClientApi(it.proxy).runSeller(1_000_000_000_000.DOLLARS, buyerName) } } } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index e373de677d..101171880c 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -27,19 +27,21 @@ import java.util.* * Interface for communicating with nodes running the trader demo. */ class TraderDemoClientApi(val rpc: CordaRPCOps) { - val cashCount: Long get() { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) - return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long - } + val cashCount: Long + get() { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) + return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long + } val dollarCashBalance: Amount get() = rpc.getCashBalance(USD) - val commercialPaperCount: Long get() { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) - return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long - } + val commercialPaperCount: Long + get() { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) + return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long + } fun runIssuer(amount: Amount, buyerName: CordaX500Name, sellerName: CordaX500Name) { val ref = OpaqueBytes.of(1) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt index 297f033ca1..cd1e7fed4b 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt @@ -31,7 +31,9 @@ class CommercialPaperIssueFlow(private val amount: Amount, companion object { val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9") + object ISSUING : ProgressTracker.Step("Issuing and timestamping some commercial paper") + fun tracker() = ProgressTracker(ISSUING) } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index 890885227f..2a08170192 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -40,7 +40,7 @@ class SellerFlow(private val otherParty: Party, progressTracker.currentStep = SELF_ISSUING val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false) - val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first() + val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java).states.first() progressTracker.currentStep = TRADING diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index 5b41ae9ec3..4a032576f5 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -3,8 +3,6 @@ package net.corda.traderdemo import net.corda.core.internal.div import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC import net.corda.testing.DUMMY_BANK_A @@ -27,7 +25,7 @@ fun main(args: Array) { val user = User("user1", "test", permissions = setOf(startFlowPermission(), startFlowPermission(), startFlowPermission())) - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name, validating = false) startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser) startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser) startNode(providedName = BOC.name, rpcUsers = listOf(user)) diff --git a/settings.gradle b/settings.gradle index 9bdb3b1ad1..f4058ed729 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,7 @@ include 'webserver:webcapsule' include 'experimental' include 'experimental:sandbox' include 'experimental:quasar-hook' +include 'experimental:kryo-hook' include 'experimental:intellij-plugin' include 'verifier' include 'test-common' diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt index 0aa4bec5e4..ae31e3bbb5 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt @@ -2,10 +2,7 @@ package net.corda.testing import co.paralleluniverse.fibers.Suspendable import net.corda.client.jackson.JacksonSupport -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStackSnapshot -import net.corda.core.flows.StartableByRPC -import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.* import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.read @@ -14,11 +11,13 @@ import net.corda.core.serialization.CordaSerializable import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.driver.driver +import net.corda.testing.node.MockNetwork import org.junit.Ignore import org.junit.Test import java.nio.file.Path import java.time.LocalDate import kotlin.test.assertEquals +import kotlin.test.assertNull import kotlin.test.assertTrue @CordaSerializable @@ -178,6 +177,31 @@ class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic() { + + @Suspendable + override fun call() { + val flowStackSnapshot = flowStackSnapshot() + val mySession = initiateFlow(ourIdentity) + mySession.sendAndReceive("Ping") + } +} + +@InitiatedBy(FlowStackSnapshotSerializationTestingFlow::class) +class DummyFlow(private val otherSideSession: FlowSession) : FlowLogic() { + + @Suspendable + override fun call() { + val message = otherSideSession.receive() + otherSideSession.send("$message Pong") + } +} + fun readFlowStackSnapshotFromDir(baseDir: Path, flowId: StateMachineRunId): FlowStackSnapshot { val snapshotFile = flowSnapshotDir(baseDir, flowId) / "flowStackSnapshot.json" return snapshotFile.read { @@ -263,6 +287,22 @@ class FlowStackSnapshotTest { } } + @Test + fun `flowStackSnapshot object is serializable`() { + val mockNet = MockNetwork(threadPerNode = true) + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + val node = mockNet.createPartyNode() + node.internals.registerInitiatedFlow(DummyFlow::class.java) + node.services.startFlow(FlowStackSnapshotSerializationTestingFlow()).resultFuture.get() + val thrown = try { + mockNet.stopNodes() + null + } catch (exception: Exception) { + exception + } + assertNull(thrown) + } + @Test fun `persistFlowStackSnapshot stack traces are aligned with stack objects`() { driver(startNodesInProcess = true) { diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 43260d5500..c9cf4ae4a0 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -5,12 +5,10 @@ import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines import net.corda.core.utilities.getOrThrow +import net.corda.node.internal.NodeStartup import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_REGULATOR -import net.corda.node.internal.NodeStartup -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -40,7 +38,7 @@ class DriverTests { @Test fun `simple node startup and shutdown`() { val handles = driver { - val notary = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val notary = startNotaryNode(DUMMY_NOTARY.name, validating = false) val regulator = startNode(providedName = DUMMY_REGULATOR.name) listOf(nodeMustBeUp(notary), nodeMustBeUp(regulator)) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt index b29cd52654..1188e1571e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt @@ -1,21 +1,22 @@ package net.corda.node.testing import com.codahale.metrics.MetricRegistry -import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.node.StateLoader import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.StateLoaderImpl import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService -import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService @@ -34,8 +35,7 @@ import java.time.Clock open class MockServiceHubInternal( override val database: CordaPersistence, override val configuration: NodeConfiguration, - val customVault: VaultService? = null, - val customVaultQuery: VaultQueryService? = null, + val customVault: VaultServiceInternal? = null, val keyManagement: KeyManagementService? = null, val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, @@ -45,16 +45,14 @@ open class MockServiceHubInternal( val mapCache: NetworkMapCacheInternal? = null, val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), - val schemas: SchemaService? = NodeSchemaService(), val customContractUpgradeService: ContractUpgradeService? = null, val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2), - override val cordappProvider: CordappProvider = CordappProviderImpl(CordappLoader.createDefault(Paths.get("."))).start(attachments) -) : ServiceHubInternal { - override val vaultQueryService: VaultQueryService - get() = customVaultQuery ?: throw UnsupportedOperationException() + override val cordappProvider: CordappProviderInternal = CordappProviderImpl(CordappLoader.createDefault(Paths.get(".")), attachments), + protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions) +) : ServiceHubInternal, StateLoader by stateLoader { override val transactionVerifierService: TransactionVerifierService get() = customTransactionVerifierService ?: throw UnsupportedOperationException() - override val vaultService: VaultService + override val vaultService: VaultServiceInternal get() = customVault ?: throw UnsupportedOperationException() override val contractUpgradeService: ContractUpgradeService get() = customContractUpgradeService ?: throw UnsupportedOperationException() @@ -75,8 +73,7 @@ open class MockServiceHubInternal( override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val rpcFlows: List>> get() = throw UnsupportedOperationException() - override val schemaService: SchemaService - get() = schemas ?: throw UnsupportedOperationException() + override val schemaService get() = throw UnsupportedOperationException() override val auditService: AuditService = DummyAuditService() lateinit var smm: StateMachineManager diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt index 398cc9262c..73e36aba5e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt @@ -5,8 +5,6 @@ package net.corda.testing import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.CordaRPCOps -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.driver.DriverDSLExposedInterface @@ -19,9 +17,15 @@ import net.corda.testing.driver.DriverDSLExposedInterface * node construction won't start until you access the members. You can get one of these from the * [alice], [bob] and [aliceBobAndNotary] functions. */ -class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExposedInterface, services: Set) { +class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExposedInterface, ifNotaryIsValidating: Boolean?) { val rpcUsers = listOf(User("admin", "admin", setOf("ALL"))) // TODO: Randomize? - val nodeFuture by lazy { driver.startNode(providedName = party.name, rpcUsers = rpcUsers, advertisedServices = services) } + val nodeFuture by lazy { + if (ifNotaryIsValidating != null) { + driver.startNotaryNode(providedName = party.name, rpcUsers = rpcUsers, validating = ifNotaryIsValidating) + } else { + driver.startNode(providedName = party.name, rpcUsers = rpcUsers) + } + } val node by lazy { nodeFuture.get()!! } val rpc by lazy { node.rpcClientToNode() } @@ -34,17 +38,19 @@ class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExp * Returns a plain, entirely stock node pre-configured with the [ALICE] identity. Note that a random key will be generated * for it: you won't have [ALICE_KEY]. */ -fun DriverDSLExposedInterface.alice(): PredefinedTestNode = PredefinedTestNode(ALICE, this, emptySet()) +fun DriverDSLExposedInterface.alice(): PredefinedTestNode = PredefinedTestNode(ALICE, this, null) + /** * Returns a plain, entirely stock node pre-configured with the [BOB] identity. Note that a random key will be generated * for it: you won't have [BOB_KEY]. */ -fun DriverDSLExposedInterface.bob(): PredefinedTestNode = PredefinedTestNode(BOB, this, emptySet()) +fun DriverDSLExposedInterface.bob(): PredefinedTestNode = PredefinedTestNode(BOB, this, null) + /** * Returns a plain single node notary pre-configured with the [DUMMY_NOTARY] identity. Note that a random key will be generated * for it: you won't have [DUMMY_NOTARY_KEY]. */ -fun DriverDSLExposedInterface.notary(): PredefinedTestNode = PredefinedTestNode(DUMMY_NOTARY, this, setOf(ServiceInfo(ValidatingNotaryService.type))) +fun DriverDSLExposedInterface.notary(): PredefinedTestNode = PredefinedTestNode(DUMMY_NOTARY, this, true) /** * Returns plain, entirely stock nodes pre-configured with the [ALICE], [BOB] and [DUMMY_NOTARY] X.500 names in that diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index 088eaaf6f0..551a8075b8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -8,8 +8,8 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.node.ServiceHub import net.corda.core.transactions.TransactionBuilder import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.VerifierType -import net.corda.testing.node.MockCordappProvider import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties @@ -20,7 +20,8 @@ import java.nio.file.Path * Creates and tests a ledger built by the passed in dsl. The provided services can be customised, otherwise a default * of a freshly built [MockServices] is used. */ -@JvmOverloads fun ledger( +@JvmOverloads +fun ledger( services: ServiceHub = MockServices(), initialiseSerialization: Boolean = true, dsl: LedgerDSL.() -> Unit @@ -40,19 +41,23 @@ import java.nio.file.Path * * @see LedgerDSLInterpreter._transaction */ -@JvmOverloads fun transaction( +@JvmOverloads +fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), initialiseSerialization: Boolean = true, + cordappPackages: List = emptyList(), dsl: TransactionDSL.() -> EnforceVerifyOrFail -) = ledger(initialiseSerialization = initialiseSerialization) { +) = ledger(services = MockServices(cordappPackages), initialiseSerialization = initialiseSerialization) { dsl(TransactionDSL(TestTransactionDSLInterpreter(this.interpreter, transactionBuilder))) } fun testNodeConfiguration( baseDirectory: Path, - myLegalName: CordaX500Name): NodeConfiguration { + myLegalName: CordaX500Name, + notaryConfig: NotaryConfig? = null): NodeConfiguration { abstract class MockableNodeConfiguration : NodeConfiguration // Otherwise Mockito is defeated by val getters. + val nc = spy() whenever(nc.baseDirectory).thenReturn(baseDirectory) whenever(nc.myLegalName).thenReturn(myLegalName) @@ -60,6 +65,7 @@ fun testNodeConfiguration( whenever(nc.keyStorePassword).thenReturn("cordacadevpass") whenever(nc.trustStorePassword).thenReturn("trustpass") whenever(nc.rpcUsers).thenReturn(emptyList()) + whenever(nc.notary).thenReturn(notaryConfig) whenever(nc.dataSourceProperties).thenReturn(makeTestDataSourceProperties(myLegalName.organisation)) whenever(nc.database).thenReturn(makeTestDatabaseProperties()) whenever(nc.emailAddress).thenReturn("") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt index 8bb2bd9a0c..c5156c16e2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.map import net.corda.core.internal.div +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.RPCUserService @@ -63,7 +64,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, - ops : I + ops: I ): CordaFuture /** @@ -109,8 +110,8 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, customPort: NetworkHostAndPort? = null, - ops : I - ) : CordaFuture + ops: I + ): CordaFuture /** * Starts a Netty RPC client. @@ -179,16 +180,19 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { brokerHandle: RpcBrokerHandle ): RpcServerHandle } + inline fun RPCDriverExposedDSLInterface.startInVmRpcClient( username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default ) = startInVmRpcClient(I::class.java, username, password, configuration) + inline fun RPCDriverExposedDSLInterface.startRandomRpcClient( hostAndPort: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ) = startRandomRpcClient(I::class.java, hostAndPort, username, password) + inline fun RPCDriverExposedDSLInterface.startRpcClient( rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, @@ -199,7 +203,8 @@ inline fun RPCDriverExposedDSLInterface.startRpcClient( interface RPCDriverInternalDSLInterface : DriverDSLInternalInterface, RPCDriverExposedDSLInterface data class RpcBrokerHandle( - val hostAndPort: NetworkHostAndPort?,/** null if this is an InVM broker */ + val hostAndPort: NetworkHostAndPort?, + /** null if this is an InVM broker */ val clientTransportConfiguration: TransportConfiguration, val serverControl: ActiveMQServerControl ) @@ -252,6 +257,7 @@ private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityMan override fun validateUser(user: String?, password: String?, certificates: Array?): String? { return validate(user, password) } + override fun validateUserAndRole(user: String?, password: String?, roles: MutableSet?, checkType: CheckType?, address: String?, connection: RemotingConnection?): String? { return validate(user, password) } @@ -259,6 +265,7 @@ private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityMan private fun isValid(user: String?, password: String?): Boolean { return rpcUser.username == user && rpcUser.password == password } + private fun validate(user: String?, password: String?): String? { return if (isValid(user, password)) user else null } @@ -302,6 +309,7 @@ data class RPCDriverDSL( } ) } + fun createInVmRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long): Configuration { return ConfigurationImpl().apply { acceptorConfigurations = setOf(TransportConfiguration(InVMAcceptorFactory::class.java.name)) @@ -309,6 +317,7 @@ data class RPCDriverDSL( configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } + fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: NetworkHostAndPort): Configuration { val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name) return ConfigurationImpl().apply { @@ -320,6 +329,7 @@ data class RPCDriverDSL( configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } + val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name) fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration { return ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), hostAndPort, null) @@ -502,13 +512,13 @@ class RandomRpcUser { add(Generator.string()) add(Generator.int()) } + data class Call(val method: Method, val call: () -> Any?) @JvmStatic fun main(args: Array) { require(args.size == 4) - @Suppress("UNCHECKED_CAST") - val rpcClass = Class.forName(args[0]) as Class + val rpcClass: Class = uncheckedCast(Class.forName(args[0])) val hostAndPort = NetworkHostAndPort.parse(args[1]) val username = args[2] val password = args[3] diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index a191d2b2f3..ed9ca09e6d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -9,6 +9,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.cordform.NodeDefinition +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name @@ -23,14 +24,13 @@ import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.* import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType +import net.corda.nodeapi.config.toConfig import net.corda.nodeapi.internal.addShutdownHook import net.corda.testing.* import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO @@ -67,6 +67,10 @@ import kotlin.concurrent.thread private val log: Logger = loggerFor() +private val DEFAULT_POLL_INTERVAL = 500.millis + +private const val DEFAULT_WARN_COUNT = 120 + /** * This is the interface that's exposed to DSL users. */ @@ -78,7 +82,6 @@ interface DriverDSLExposedInterface : CordformContext { * when called from Java code. * @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something * random. Note that this must be unique as the driver uses it as a primary key! - * @param advertisedServices The set of services to be advertised by the node. Defaults to empty set. * @param verifierType The type of transaction verifier to use. See: [VerifierType] * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running @@ -88,26 +91,34 @@ interface DriverDSLExposedInterface : CordformContext { fun startNode( defaultParameters: NodeParameters = NodeParameters(), providedName: CordaX500Name? = defaultParameters.providedName, - advertisedServices: Set = defaultParameters.advertisedServices, rpcUsers: List = defaultParameters.rpcUsers, verifierType: VerifierType = defaultParameters.verifierType, customOverrides: Map = defaultParameters.customOverrides, - startInSameProcess: Boolean? = defaultParameters.startInSameProcess): CordaFuture + startInSameProcess: Boolean? = defaultParameters.startInSameProcess, + maximumHeapSize: String = defaultParameters.maximumHeapSize): CordaFuture + + // TODO This method has been added temporarily, to be deleted once the set of notaries is defined at the network level. + fun startNotaryNode(providedName: CordaX500Name, + rpcUsers: List = emptyList(), + verifierType: VerifierType = VerifierType.InMemory, + customOverrides: Map = emptyMap(), + //TODO Switch the default value + validating: Boolean = true): CordaFuture /** - * Helper function for starting a [node] with custom parameters from Java. + * Helper function for starting a [Node] with custom parameters from Java. * - * @param defaultParameters The default parameters for the driver. - * @param dsl The dsl itself. + * @param parameters The default parameters for the driver. * @return The value returned in the [dsl] closure. */ - fun startNode(parameters: NodeParameters): CordaFuture { + fun startNode(parameters: NodeParameters): CordaFuture { return startNode(defaultParameters = parameters) } fun startNodes( nodes: List, - startInSameProcess: Boolean? = null + startInSameProcess: Boolean? = null, + maximumHeapSize: String = "200m" ): List> /** @@ -115,7 +126,6 @@ interface DriverDSLExposedInterface : CordformContext { * * @param notaryName The legal name of the advertised distributed notary service. * @param clusterSize Number of nodes to create for the cluster. - * @param type The advertised notary service type. Currently the only supported type is [RaftValidatingNotaryService.type]. * @param verifierType The type of transaction verifier to use. See: [VerifierType] * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running @@ -125,17 +135,19 @@ interface DriverDSLExposedInterface : CordformContext { fun startNotaryCluster( notaryName: CordaX500Name, clusterSize: Int = 3, - type: ServiceType = RaftValidatingNotaryService.type, verifierType: VerifierType = VerifierType.InMemory, rpcUsers: List = emptyList(), startInSameProcess: Boolean? = null): CordaFuture>> + /** Call [startWebserver] with a default maximumHeapSize. */ + fun startWebserver(handle: NodeHandle): CordaFuture = startWebserver(handle, "200m") + /** * Starts a web server for a node - * * @param handle The handle for the node that this webserver connects to via RPC. + * @param maximumHeapSize Argument for JVM -Xmx option e.g. "200m". */ - fun startWebserver(handle: NodeHandle): CordaFuture + fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture /** * Starts a network map service node. Note that only a single one should ever be running, so you will probably want @@ -143,7 +155,7 @@ interface DriverDSLExposedInterface : CordformContext { * @param startInProcess Determines if the node should be started inside this process. If null the Driver-level * value will be used. */ - fun startDedicatedNetworkMapService(startInProcess: Boolean? = null): CordaFuture + fun startDedicatedNetworkMapService(startInProcess: Boolean? = null, maximumHeapSize: String = "200m"): CordaFuture fun waitForAllNodesToFinish() @@ -156,16 +168,25 @@ interface DriverDSLExposedInterface : CordformContext { * @param check The function being polled. * @return A future that completes with the non-null value [check] has returned. */ - fun pollUntilNonNull(pollName: String, pollInterval: Duration = 500.millis, warnCount: Int = 120, check: () -> A?): CordaFuture + fun pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture /** * Polls the given function until it returns true. * @see pollUntilNonNull */ - fun pollUntilTrue(pollName: String, pollInterval: Duration = 500.millis, warnCount: Int = 120, check: () -> Boolean): CordaFuture { + fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture { return pollUntilNonNull(pollName, pollInterval, warnCount) { if (check()) Unit else null } } + /** + * Polls until a given node knows about presence of another node via its own NetworkMap + */ + fun NodeHandle.pollUntilKnowsAbout(another: NodeHandle, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT): CordaFuture { + return pollUntilTrue("${nodeInfo.legalIdentities} knows about ${another.nodeInfo.legalIdentities}", pollInterval, warnCount) { + another.nodeInfo in rpc.networkMapSnapshot() + } + } + val shutdownManager: ShutdownManager } @@ -219,7 +240,7 @@ sealed class NodeHandle { } } - fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!, initialiseSerialization = false) + fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!) /** * Stops the referenced node. @@ -252,22 +273,22 @@ sealed class PortAllocation { } /** - * Helper builder for configuring a [node] from Java. + * Helper builder for configuring a [Node] from Java. */ data class NodeParameters( val providedName: CordaX500Name? = null, - val advertisedServices: Set = emptySet(), val rpcUsers: List = emptyList(), val verifierType: VerifierType = VerifierType.InMemory, val customOverrides: Map = emptyMap(), - val startInSameProcess: Boolean? = null + val startInSameProcess: Boolean? = null, + val maximumHeapSize: String = "200m" ) { fun setProvidedName(providedName: CordaX500Name?) = copy(providedName = providedName) - fun setAdvertisedServices(advertisedServices: Set) = copy(advertisedServices = advertisedServices) fun setRpcUsers(rpcUsers: List) = copy(rpcUsers = rpcUsers) fun setVerifierType(verifierType: VerifierType) = copy(verifierType = verifierType) fun setCustomerOverrides(customOverrides: Map) = copy(customOverrides = customOverrides) fun setStartInSameProcess(startInSameProcess: Boolean?) = copy(startInSameProcess = startInSameProcess) + fun setMaximumHeapSize(maximumHeapSize: String) = copy(maximumHeapSize = maximumHeapSize) } /** @@ -336,7 +357,7 @@ fun driver( /** * Helper function for starting a [driver] with custom parameters from Java. * - * @param defaultParameters The default parameters for the driver. + * @param parameters The default parameters for the driver. * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ @@ -407,7 +428,7 @@ fun getTimestampAsDirectoryName(): String { return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now()) } -class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : Exception("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") +class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : CordaException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") /** * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. @@ -503,7 +524,6 @@ class ShutdownManager(private val executorService: ExecutorService) { } fun shutdown() { - unsetCordappPackages() val shutdownActionFutures = state.locked { if (isShutdown) { emptyList Unit>>() @@ -587,14 +607,14 @@ class DriverDSL( val isDebug: Boolean, val networkMapStartStrategy: NetworkMapStartStrategy, val startNodesInProcess: Boolean, - val extraCordappPackagesToScan: List + extraCordappPackagesToScan: List ) : DriverDSLInternalInterface { private val dedicatedNetworkMapAddress = portAllocation.nextHostAndPort() private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! private var _shutdownManager: ShutdownManager? = null override val shutdownManager get() = _shutdownManager!! - private val packagesToScanString = extraCordappPackagesToScan + getCallerPackage() + private val cordappPackages = extraCordappPackagesToScan + getCallerPackage() class State { val processes = ArrayList>() @@ -631,7 +651,7 @@ class DriverDSL( private fun establishRpc(config: FullNodeConfiguration, processDeathFuture: CordaFuture): CordaFuture { val rpcAddress = config.rpcAddress!! - val client = CordaRPCClient(rpcAddress, initialiseSerialization = false) + val client = CordaRPCClient(rpcAddress) val connectionFuture = poll(executorService, "RPC connection") { try { client.start(config.rpcUsers[0].username, config.rpcUsers[0].password) @@ -660,9 +680,9 @@ class DriverDSL( } } is NetworkMapStartStrategy.Nominated -> { - serviceConfig(networkMapCandidates.filter { + serviceConfig(networkMapCandidates.single { it.name == legalName.toString() - }.single().config.getString("p2pAddress").let(NetworkHostAndPort.Companion::parse)).let { + }.config.getString("p2pAddress").let(NetworkHostAndPort.Companion::parse)).let { { nodeName: CordaX500Name -> if (nodeName == legalName) null else it } } } @@ -673,11 +693,11 @@ class DriverDSL( override fun startNode( defaultParameters: NodeParameters, providedName: CordaX500Name?, - advertisedServices: Set, rpcUsers: List, verifierType: VerifierType, customOverrides: Map, - startInSameProcess: Boolean? + startInSameProcess: Boolean?, + maximumHeapSize: String ): CordaFuture { val p2pAddress = portAllocation.nextHostAndPort() val rpcAddress = portAllocation.nextHostAndPort() @@ -696,67 +716,86 @@ class DriverDSL( "p2pAddress" to p2pAddress.toString(), "rpcAddress" to rpcAddress.toString(), "webAddress" to webAddress.toString(), - "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "networkMapService" to networkMapServiceConfigLookup(name), "useTestClock" to useTestClock, - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers.map { it.toMap() }, + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers.map { it.toConfig().root().unwrapped() }, "verifierType" to verifierType.name ) + customOverrides ) - return startNodeInternal(config, webAddress, startInSameProcess) + return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) } - override fun startNodes(nodes: List, startInSameProcess: Boolean?): List> { + override fun startNotaryNode(providedName: CordaX500Name, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + validating: Boolean): CordaFuture { + val config = customOverrides + NotaryConfig(validating).toConfigMap() + return startNode(providedName = providedName, rpcUsers = rpcUsers, verifierType = verifierType, customOverrides = config) + } + + override fun startNodes(nodes: List, startInSameProcess: Boolean?, maximumHeapSize: String): List> { val networkMapServiceConfigLookup = networkMapServiceConfigLookup(nodes) return nodes.map { node -> portAllocation.nextHostAndPort() // rpcAddress val webAddress = portAllocation.nextHostAndPort() val name = CordaX500Name.parse(node.name) val rpcUsers = node.rpcUsers + val notary = if (node.notary != null) mapOf("notary" to node.notary) else emptyMap() val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), allowMissingConfig = true, - configOverrides = node.config + mapOf( - "extraAdvertisedServiceIds" to node.advertisedServices, + configOverrides = node.config + notary + mapOf( "networkMapService" to networkMapServiceConfigLookup(name), - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers, - "notaryClusterAddresses" to node.notaryClusterAddresses + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers ) ) - startNodeInternal(config, webAddress, startInSameProcess) + startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) } } + // TODO This mapping is done is several plaecs including the gradle plugin. In general we need a better way of + // generating the configs for the nodes, probably making use of Any.toConfig() + private fun NotaryConfig.toConfigMap(): Map = mapOf("notary" to toConfig().root().unwrapped()) + override fun startNotaryCluster( notaryName: CordaX500Name, clusterSize: Int, - type: ServiceType, verifierType: VerifierType, rpcUsers: List, startInSameProcess: Boolean? ): CordaFuture>> { + fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { + val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() + val config = NotaryConfig(validating = true, raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) + return config.toConfigMap() + } + val nodeNames = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") } val paths = nodeNames.map { baseDirectory(it) } ServiceIdentityGenerator.generateToDisk(paths, notaryName) - val advertisedServices = setOf(ServiceInfo(type, notaryName)) - val notaryClusterAddress = portAllocation.nextHostAndPort() + val clusterAddress = portAllocation.nextHostAndPort() // Start the first node that will bootstrap the cluster val firstNotaryFuture = startNode( providedName = nodeNames.first(), - advertisedServices = advertisedServices, rpcUsers = rpcUsers, verifierType = verifierType, - customOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString(), - "database.serverNameTablePrefix" to if (nodeNames.isNotEmpty()) nodeNames.first().toString().replace(Regex("[^0-9A-Za-z]+"), "") else ""), + customOverrides = notaryConfig(clusterAddress) + mapOf( + "database.serverNameTablePrefix" to if (nodeNames.isNotEmpty()) nodeNames.first().toString().replace(Regex("[^0-9A-Za-z]+"), "") else "" + ), startInSameProcess = startInSameProcess ) // All other nodes will join the cluster val restNotaryFutures = nodeNames.drop(1).map { val nodeAddress = portAllocation.nextHostAndPort() - val configOverride = mapOf("notaryNodeAddress" to nodeAddress.toString(), "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()), - "database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "")) - startNode(providedName = it, advertisedServices = advertisedServices, rpcUsers = rpcUsers, verifierType = verifierType, customOverrides = configOverride) + startNode( + providedName = it, + rpcUsers = rpcUsers, + verifierType = verifierType, + customOverrides = notaryConfig(nodeAddress, clusterAddress) + mapOf( + "database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "") + )) } return firstNotaryFuture.flatMap { firstNotary -> @@ -784,9 +823,9 @@ class DriverDSL( throw IllegalStateException("Webserver at ${handle.webAddress} has died") } - override fun startWebserver(handle: NodeHandle): CordaFuture { + override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val processFuture = DriverDSL.startWebserver(executorService, handle, debugPort) + val processFuture = DriverDSL.startWebserver(executorService, handle, debugPort, maximumHeapSize) registerProcess(processFuture) return processFuture.map { queryWebserver(handle, it) } } @@ -794,8 +833,6 @@ class DriverDSL( override fun start() { _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) _shutdownManager = ShutdownManager(executorService) - // We set this property so that in-process nodes find cordapps. Out-of-process nodes need this passed in when started. - setCordappPackages(*packagesToScanString.toTypedArray()) if (networkMapStartStrategy.startDedicated) { startDedicatedNetworkMapService().andForget(log) // Allow it to start concurrently with other nodes. } @@ -809,7 +846,7 @@ class DriverDSL( override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName)) - override fun startDedicatedNetworkMapService(startInProcess: Boolean?): CordaFuture { + override fun startDedicatedNetworkMapService(startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { val webAddress = portAllocation.nextHostAndPort() val rpcAddress = portAllocation.nextHostAndPort() val networkMapLegalName = networkMapStartStrategy.legalName @@ -824,17 +861,15 @@ class DriverDSL( "rpcAddress" to rpcAddress.toString(), "rpcUsers" to defaultRpcUserList, "p2pAddress" to dedicatedNetworkMapAddress.toString(), - "useTestClock" to useTestClock, - "extraAdvertisedServiceIds" to listOf(ServiceInfo(NetworkMapService.type).toString()) - ) + "useTestClock" to useTestClock) ) - return startNodeInternal(config, webAddress, startInProcess) + return startNodeInternal(config, webAddress, startInProcess, maximumHeapSize) } - private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?): CordaFuture { + private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { val nodeConfiguration = config.parseAs() if (startInProcess ?: startNodesInProcess) { - val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) + val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config, cordappPackages) shutdownManager.registerShutdown( nodeAndThreadFuture.map { (node, thread) -> { @@ -852,7 +887,7 @@ class DriverDSL( } } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, packagesToScanString.joinToString(",")) + val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, cordappPackages, maximumHeapSize) registerProcess(processFuture) return processFuture.flatMap { process -> val processDeathFuture = poll(executorService, "process death") { @@ -860,14 +895,16 @@ class DriverDSL( } establishRpc(nodeConfiguration, processDeathFuture).flatMap { rpc -> // Call waitUntilNetworkReady in background in case RPC is failing over: - val networkMapFuture = executorService.fork { + val forked = executorService.fork { rpc.waitUntilNetworkReady() - }.flatMap { it } + } + val networkMapFuture = forked.flatMap { it } firstOf(processDeathFuture, networkMapFuture) { if (it == processDeathFuture) { throw ListenProcessDeathException(nodeConfiguration.p2pAddress, process) } processDeathFuture.cancel(false) + log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: ${webAddress}") NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, nodeConfiguration, webAddress, debugPort, process) } } @@ -882,7 +919,7 @@ class DriverDSL( } companion object { - private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toMap()) + private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped()) private val names = arrayOf( ALICE.name, @@ -895,14 +932,15 @@ class DriverDSL( private fun startInProcessNode( executorService: ScheduledExecutorService, nodeConf: FullNodeConfiguration, - config: Config + config: Config, + cordappPackages: List ): CordaFuture, Thread>> { return executorService.fork { log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}") // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) // TODO pass the version in? - val node = Node(nodeConf, nodeConf.calculateServices(), MOCK_VERSION_INFO, initialiseSerialization = false).start() + val node = Node(nodeConf, MOCK_VERSION_INFO, initialiseSerialization = false, cordappLoader = CordappLoader.createDefaultWithTestPackages(nodeConf, cordappPackages)).start() val nodeThread = thread(name = nodeConf.myLegalName.organisation) { node.internals.run() } @@ -917,7 +955,8 @@ class DriverDSL( quasarJarPath: String, debugPort: Int?, overriddenSystemProperties: Map, - packagesToScanString: String + cordappPackages: List, + maximumHeapSize: String ): CordaFuture { val processFuture = executorService.fork { log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}") @@ -927,7 +966,7 @@ class DriverDSL( val systemProperties = overriddenSystemProperties + mapOf( "name" to nodeConf.myLegalName, "visualvm.display.name" to "corda-${nodeConf.myLegalName}", - "net.corda.node.cordapp.scan.packages" to packagesToScanString, + Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator), "java.io.tmpdir" to System.getProperty("java.io.tmpdir") // Inherit from parent process ) // See experimental/quasar-hook/README.md for how to generate. @@ -946,7 +985,8 @@ class DriverDSL( jdwpPort = debugPort, extraJvmArguments = extraJvmArguments, errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", - workingDirectory = nodeConf.baseDirectory + workingDirectory = nodeConf.baseDirectory, + maximumHeapSize = maximumHeapSize ) } return processFuture.flatMap { process -> @@ -957,7 +997,8 @@ class DriverDSL( private fun startWebserver( executorService: ScheduledExecutorService, handle: NodeHandle, - debugPort: Int? + debugPort: Int?, + maximumHeapSize: String ): CordaFuture { return executorService.fork { val className = "net.corda.webserver.WebServer" @@ -969,7 +1010,9 @@ class DriverDSL( "-Dname=node-${handle.configuration.p2pAddress}-webserver", "-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process ), - errorLogPath = Paths.get("error.$className.log") + errorLogPath = Paths.get("error.$className.log"), + workingDirectory = null, + maximumHeapSize = maximumHeapSize ) }.flatMap { process -> addressMustBeBoundFuture(executorService, handle.webAddress, process).map { process } } } @@ -978,7 +1021,8 @@ class DriverDSL( return Exception() .stackTrace .first { it.fileName != "Driver.kt" } - .let { Class.forName(it.className).`package`.name } + .let { Class.forName(it.className).`package`?.name } + ?: throw IllegalStateException("Function instantiating driver must be defined in a package.") } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt index c020d830a1..d0a5c6492d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt @@ -10,7 +10,7 @@ object ProcessUtilities { arguments: List, jdwpPort: Int? = null ): Process { - return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, emptyList(), null, null) + return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, emptyList(), null, null, null) } fun startCordaProcess( @@ -19,11 +19,12 @@ object ProcessUtilities { jdwpPort: Int?, extraJvmArguments: List, errorLogPath: Path?, - workingDirectory: Path? = null + workingDirectory: Path?, + maximumHeapSize: String ): Process { // FIXME: Instead of hacking our classpath, use the correct classpath for className. val classpath = defaultClassPath.split(pathSeparator).filter { !(it / "log4j2-test.xml").exists() }.joinToString(pathSeparator) - return startJavaProcessImpl(className, arguments, classpath, jdwpPort, extraJvmArguments, errorLogPath, workingDirectory) + return startJavaProcessImpl(className, arguments, classpath, jdwpPort, extraJvmArguments, errorLogPath, workingDirectory, maximumHeapSize) } fun startJavaProcessImpl( @@ -33,12 +34,13 @@ object ProcessUtilities { jdwpPort: Int?, extraJvmArguments: List, errorLogPath: Path?, - workingDirectory: Path? + workingDirectory: Path?, + maximumHeapSize: String? ): Process { val command = mutableListOf().apply { add((System.getProperty("java.home") / "bin" / "java").toString()) (jdwpPort != null) && add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort") - add("-Xmx200m") + if (maximumHeapSize != null) add("-Xmx$maximumHeapSize") add("-XX:+UseG1GC") addAll(extraJvmArguments) add("-cp") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt index c7006d02f4..dcf0255a9a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt @@ -5,9 +5,9 @@ package net.corda.testing.internal.demorun import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.User +import net.corda.nodeapi.config.toConfig fun CordformDefinition.node(configure: CordformNode.() -> Unit) { addNode { cordformNode -> cordformNode.configure() } @@ -19,10 +19,6 @@ fun CordformNode.rpcUsers(vararg users: User) { rpcUsers = users.map { it.toMap() } } -fun CordformNode.advertisedServices(vararg services: ServiceInfo) { - advertisedServices = services.map { it.toString() } -} - -fun CordformNode.notaryClusterAddresses(vararg addresses: NetworkHostAndPort) { - notaryClusterAddresses = addresses.map { it.toString() } +fun CordformNode.notary(notaryConfig: NotaryConfig) { + notary = notaryConfig.toConfig().root().unwrapped() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt index 2897c3e92f..cf452a9116 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt @@ -4,8 +4,6 @@ package net.corda.testing.internal.demorun import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode -import net.corda.core.identity.CordaX500Name -import net.corda.testing.driver.NetworkMapStartStrategy import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver @@ -20,7 +18,6 @@ fun CordformDefinition.clean() { fun CordformDefinition.runNodes() = driver( isDebug = true, driverDirectory = driverDirectory, - networkMapStartStrategy = NetworkMapStartStrategy.Nominated(CordaX500Name.parse(networkMapNodeName)), portAllocation = PortAllocation.Incremental(10001) ) { setup(this) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index bf109863eb..a44cf1c4a5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -143,7 +143,8 @@ class InMemoryMessagingNetwork( } /** This can be set to an object which can inject artificial latency between sender/recipient pairs. */ - @Volatile var latencyCalculator: LatencyCalculator? = null + @Volatile + var latencyCalculator: LatencyCalculator? = null private val timer = Timer() @Synchronized @@ -278,7 +279,6 @@ class InMemoryMessagingNetwork( _sentMessages.onNext(transfer) } - @CordaSerializable private data class InMemoryMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID, @@ -286,7 +286,6 @@ class InMemoryMessagingNetwork( override fun toString() = "$topicSession#${String(data)}" } - @CordaSerializable private data class InMemoryReceivedMessage(override val topicSession: TopicSession, override val data: ByteArray, override val platformVersion: Int, @@ -327,7 +326,7 @@ class InMemoryMessagingNetwork( while (!Thread.currentThread().isInterrupted) { try { pumpReceiveInternal(true) - } catch(e: InterruptedException) { + } catch (e: InterruptedException) { break } } @@ -452,7 +451,7 @@ class InMemoryMessagingNetwork( for (handler in deliverTo) { try { handler.callback(transfer.toReceivedMessage(), handler) - } catch(e: Exception) { + } catch (e: Exception) { log.error("Caught exception in handler for $this/${handler.topicSession}", e) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 1c357a90a6..1b82ca04c1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -53,6 +53,6 @@ class MockNetworkMapCache(serviceHub: ServiceHubInternal) : PersistentNetworkMap */ @VisibleForTesting fun deleteRegistration(legalIdentity: Party): Boolean { - return partyNodes.removeIf { legalIdentity.owningKey in it.legalIdentitiesAndCerts.map { it.owningKey }} + return partyNodes.removeIf { legalIdentity.owningKey in it.legalIdentitiesAndCerts.map { it.owningKey } } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 9baeb6d470..ae810b581a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -6,6 +6,7 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert import net.corda.core.internal.concurrent.doneFuture @@ -15,33 +16,44 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.node.services.* +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.KeyManagementService +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.finance.utils.WorldMapLocation import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType +import net.corda.node.internal.cordapp.CordappLoader +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.api.SchemaService +import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.InMemoryNetworkMapService import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.* +import net.corda.node.services.transactions.BFTNonValidatingNotaryService +import net.corda.node.services.transactions.BFTSMaRt +import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CertificateAndKeyPair +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger +import java.io.Closeable import java.math.BigInteger import java.nio.file.Path import java.security.KeyPair +import java.security.PublicKey import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -68,7 +80,12 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val defaultFactory: Factory<*> = MockNetwork.DefaultFactory, - private val initialiseSerialization: Boolean = true) { + private val initialiseSerialization: Boolean = true, + private val cordappPackages: List = emptyList()) : Closeable { + companion object { + // TODO In future PR we're removing the concept of network map node so the details of this mock are not important. + val MOCK_NET_MAP = Party(CordaX500Name(organisation = "Mock Network Map", locality = "Madrid", country = "ES"), DUMMY_KEY_1.public) + } var nextNodeId = 0 private set private val filesystem = Jimfs.newFileSystem(unix()) @@ -80,6 +97,9 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, /** A read only view of the current set of executing nodes. */ val nodes: List get() = _nodes + private var _networkMapNode: StartedNode? = null + val networkMapNode: StartedNode get() = _networkMapNode ?: startNetworkMapNode() + init { if (initialiseSerialization) initialiseTestSerialization() filesystem.getPath("/nodes").createDirectory() @@ -88,21 +108,19 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, /** Allows customisation of how nodes are created. */ interface Factory { /** - * @param overrideServices a set of service entries to use in place of the node's default service entries, - * for example where a node's service is part of a cluster. + * @param notaryIdentity is an additional override to use in place of the node's default notary service, + * main usage is for when the node is part of a notary cluster. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overriden to cause nodes to have stable or colliding identity/service keys. */ fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): N + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): N } object DefaultFactory : Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, - entropyRoot: BigInteger): MockNode { - return MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { + return MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) } } @@ -130,22 +148,20 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } /** - * @param overrideServices a set of service entries to use in place of the node's default service entries, - * for example where a node's service is part of a cluster. + * @param notaryIdentity is an additional override to use in place of the node's default notary service, + * main usage is for when the node is part of a notary cluster. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overriden to cause nodes to have stable or colliding identity/service keys. */ open class MockNode(config: NodeConfiguration, val mockNet: MockNetwork, override val networkMapAddress: SingleMessageRecipient?, - advertisedServices: Set, val id: Int, - val overrideServices: Map?, + internal val notaryIdentity: Pair?, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue())) : - AbstractNode(config, advertisedServices, TestClock(), mockNet.busyLatch) { + AbstractNode(config, TestClock(), MOCK_VERSION_INFO, CordappLoader.createDefaultWithTestPackages(config, mockNet.cordappPackages), mockNet.busyLatch) { var counter = entropyRoot override val log: Logger = loggerFor() - override val platformVersion: Int get() = 1 override val serverThread: AffinityExecutor = if (mockNet.threadPerNode) ServiceAffinityExecutor("Mock node $id thread", 1) @@ -192,27 +208,15 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { - return E2ETestKeyManagementService(identityService, partyKeys + (overrideServices?.values ?: emptySet())) + return E2ETestKeyManagementService(identityService, partyKeys + (notaryIdentity?.let { setOf(it.second) } ?: emptySet())) } override fun startMessagingService(rpcOps: RPCOps) { // Nothing to do } - override fun makeNetworkMapService(): NetworkMapService { - return InMemoryNetworkMapService(services, platformVersion) - } - - override fun getNotaryIdentity(): PartyAndCertificate? { - val defaultIdentity = super.getNotaryIdentity() - val override = overrideServices?.filter { it.key.type.isNotary() }?.entries?.singleOrNull() - return if (override == null || defaultIdentity == null) - defaultIdentity - else { - // Ensure that we always have notary in name and type of it. TODO It is temporary solution until we will have proper handling of NetworkParameters - myNotaryIdentity = getTestPartyAndCertificate(defaultIdentity.name, override.value.public) - myNotaryIdentity - } + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal): NetworkMapService { + return InMemoryNetworkMapService(network, networkMapCache, 1) } // This is not thread safe, but node construction is done on a single thread, so that should always be fine @@ -231,15 +235,23 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun myAddresses() = emptyList() - // Allow unit tests to modify the plugin list before the node start, - // so they don't have to ServiceLoad test plugins into all unit tests. - val testPluginRegistries by lazy { super.pluginRegistries.toMutableList() } - override val pluginRegistries: List - get() = testPluginRegistries + // Allow unit tests to modify the serialization whitelist list before the node start, + // so they don't have to ServiceLoad test whitelists into all unit tests. + val testSerializationWhitelists by lazy { super.serializationWhitelists.toMutableList() } + override val serializationWhitelists: List + get() = testSerializationWhitelists // This does not indirect through the NodeInfo object so it can be called before the node is started. // It is used from the network visualiser tool. - @Suppress("unused") val place: WorldMapLocation get() = findMyLocation()!! + @Suppress("unused") + val place: WorldMapLocation + get() = findMyLocation()!! + + private var dbCloser: (() -> Any?)? = null + override fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T) = super.initialiseDatabasePersistence(schemaService) { + dbCloser = database::close + insideTransaction() + } fun disableDBCloseOnStop() { runOnStop.remove(dbCloser) @@ -255,12 +267,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun acceptableLiveFiberCountOnStop(): Int = acceptableLiveFiberCountOnStop - override fun makeCoreNotaryService(type: ServiceType): NotaryService? { - if (type != BFTNonValidatingNotaryService.type) return super.makeCoreNotaryService(type) - return BFTNonValidatingNotaryService(services, myNotaryIdentity!!.owningKey, object : BFTSMaRt.Cluster { + override fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster { + return object : BFTSMaRt.Cluster { override fun waitUntilAllReplicasHaveInitialized() { - val clusterNodes = mockNet.nodes.filter { myNotaryIdentity!!.owningKey in it.started!!.info.legalIdentities.map { it.owningKey } } - if (clusterNodes.size != configuration.notaryClusterAddresses.size) { + val clusterNodes = mockNet.nodes.filter { notaryKey in it.started!!.info.legalIdentities.map { it.owningKey } } + if (clusterNodes.size != bftSMaRtConfig.clusterAddresses.size) { throw IllegalStateException("Unable to enumerate all nodes in BFT cluster.") } clusterNodes.forEach { @@ -268,7 +279,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, notaryService.waitUntilReplicaHasInitialized() } } - }) + } } /** @@ -281,51 +292,63 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } } - fun createUnstartedNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, - legalName: CordaX500Name? = null, overrideServices: Map? = null, - entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, - configOverrides: (NodeConfiguration) -> Any? = {}): MockNode { - return createUnstartedNode(networkMapAddress, forcedID, defaultFactory, legalName, overrideServices, entropyRoot, *advertisedServices, configOverrides = configOverrides) + fun startNetworkMapNode(nodeFactory: Factory? = null): StartedNode { + check(_networkMapNode == null) { "Trying to start more than one network map node" } + return uncheckedCast(createNodeImpl(networkMapAddress = null, + forcedID = null, + nodeFactory = nodeFactory ?: defaultFactory, + legalName = MOCK_NET_MAP.name, + notaryIdentity = null, + entropyRoot = BigInteger.valueOf(random63BitValue()), + configOverrides = {}, + start = true + ).started!!.apply { + _networkMapNode = this + }) } - fun createUnstartedNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, nodeFactory: Factory, - legalName: CordaX500Name? = null, overrideServices: Map? = null, + fun createUnstartedNode(forcedID: Int? = null, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, + entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), + configOverrides: (NodeConfiguration) -> Any? = {}): MockNode { + return createUnstartedNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, configOverrides = configOverrides) + } + + fun createUnstartedNode(forcedID: Int? = null, nodeFactory: Factory, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): N { - return createNodeImpl(networkMapAddress, forcedID, nodeFactory, false, legalName, overrideServices, entropyRoot, advertisedServices, configOverrides) + val networkMapAddress = networkMapNode.network.myAddress + return createNodeImpl(networkMapAddress, forcedID, nodeFactory, false, legalName, notaryIdentity, entropyRoot, configOverrides) } /** * Returns a node, optionally created by the passed factory method. - * @param overrideServices a set of service entries to use in place of the node's default service entries, + * @param notaryIdentity a set of service entries to use in place of the node's default service entries, * for example where a node's service is part of a cluster. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overridden to cause nodes to have stable or colliding identity/service keys. * @param configOverrides add/override behaviour of the [NodeConfiguration] mock object. */ - fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, - legalName: CordaX500Name? = null, overrideServices: Map? = null, + fun createNode(forcedID: Int? = null, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): StartedNode { - return createNode(networkMapAddress, forcedID, defaultFactory, legalName, overrideServices, entropyRoot, *advertisedServices, configOverrides = configOverrides) + return createNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, configOverrides = configOverrides) } /** Like the other [createNode] but takes a [Factory] and propagates its [MockNode] subtype. */ - fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, nodeFactory: Factory, - legalName: CordaX500Name? = null, overrideServices: Map? = null, + fun createNode(forcedID: Int? = null, nodeFactory: Factory, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): StartedNode { - return uncheckedCast(createNodeImpl(networkMapAddress, forcedID, nodeFactory, true, legalName, overrideServices, entropyRoot, advertisedServices, configOverrides).started)!! + val networkMapAddress = networkMapNode.network.myAddress + return uncheckedCast(createNodeImpl(networkMapAddress, forcedID, nodeFactory, true, legalName, notaryIdentity, entropyRoot, configOverrides).started)!! } private fun createNodeImpl(networkMapAddress: SingleMessageRecipient?, forcedID: Int?, nodeFactory: Factory, - start: Boolean, legalName: CordaX500Name?, overrideServices: Map?, + start: Boolean, legalName: CordaX500Name?, notaryIdentity: Pair?, entropyRoot: BigInteger, - advertisedServices: Array, configOverrides: (NodeConfiguration) -> Any?): N { val id = forcedID ?: nextNodeId++ val config = testNodeConfiguration( @@ -334,7 +357,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, whenever(it.dataSourceProperties).thenReturn(makeTestDataSourceProperties("node_${id}_net_$networkId")) configOverrides(it) } - return nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, overrideServices, entropyRoot).apply { + return nodeFactory.create(config, this, networkMapAddress, id, notaryIdentity, entropyRoot).apply { if (start) { start() if (threadPerNode && networkMapAddress != null) nodeReadyFuture.getOrThrow() // XXX: What about manually-started nodes? @@ -366,57 +389,25 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } } - /** - * Register network identities in identity service, normally it's done on network map cache change, but we may run without - * network map service. - */ - fun registerIdentities(){ - nodes.forEach { itNode -> - itNode.started!!.database.transaction { - nodes.map { it.started!!.info.legalIdentitiesAndCerts.first() }.map(itNode.started!!.services.identityService::verifyAndRegisterIdentity) - } - } - } - - /** - * A bundle that separates the generic user nodes and service-providing nodes. A real network might not be so - * clearly separated, but this is convenient for testing. - */ - data class BasketOfNodes(val partyNodes: List>, val notaryNode: StartedNode, val mapNode: StartedNode) - - /** - * Sets up a network with the requested number of nodes (defaulting to two), with one or more service nodes that - * run a notary, network map, any oracles etc. - */ @JvmOverloads - fun createSomeNodes(numPartyNodes: Int = 2, nodeFactory: Factory<*> = defaultFactory, notaryKeyPair: KeyPair? = DUMMY_NOTARY_KEY): BasketOfNodes { - require(nodes.isEmpty()) - val notaryServiceInfo = ServiceInfo(SimpleNotaryService.type) - val notaryOverride = if (notaryKeyPair != null) - mapOf(Pair(notaryServiceInfo, notaryKeyPair)) - else - null - val mapNode = createNode(nodeFactory = nodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) - val mapAddress = mapNode.network.myAddress - val notaryNode = createNode(mapAddress, nodeFactory = nodeFactory, overrideServices = notaryOverride, advertisedServices = notaryServiceInfo) - val nodes = (1..numPartyNodes).map { - createPartyNode(mapAddress) - } - return BasketOfNodes(nodes, notaryNode, mapNode) + fun createNotaryNode(legalName: CordaX500Name = DUMMY_NOTARY.name, validating: Boolean = true): StartedNode { + return createNode(legalName = legalName, configOverrides = { + whenever(it.notary).thenReturn(NotaryConfig(validating)) + }) } - fun createNotaryNode(networkMapAddress: SingleMessageRecipient? = null, - legalName: CordaX500Name? = null, - overrideServices: Map? = null, - serviceName: CordaX500Name? = null): StartedNode { - return createNode(networkMapAddress, legalName = legalName, overrideServices = overrideServices, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type, serviceName))) + fun createNotaryNode(legalName: CordaX500Name = DUMMY_NOTARY.name, + validating: Boolean = true, + nodeFactory: Factory): StartedNode { + return createNode(legalName = legalName, nodeFactory = nodeFactory, configOverrides = { + whenever(it.notary).thenReturn(NotaryConfig(validating)) + }) } - fun createPartyNode(networkMapAddress: SingleMessageRecipient, - legalName: CordaX500Name? = null, - overrideServices: Map? = null): StartedNode { - return createNode(networkMapAddress, legalName = legalName, overrideServices = overrideServices) + @JvmOverloads + fun createPartyNode(legalName: CordaX500Name? = null, + notaryIdentity: Pair? = null): StartedNode { + return createNode(legalName = legalName, notaryIdentity = notaryIdentity) } @Suppress("unused") // This is used from the network visualiser tool. @@ -424,7 +415,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, return when (msgRecipient) { is SingleMessageRecipient -> nodes.single { it.started!!.network.myAddress == msgRecipient } is InMemoryMessagingNetwork.ServiceHandle -> { - nodes.firstOrNull { it.advertisedServices.any { it.name == msgRecipient.party.name } } + nodes.firstOrNull { it.started!!.info.isLegalIdentity(msgRecipient.party) } ?: throw IllegalArgumentException("Couldn't find node advertising service with owning party name: ${msgRecipient.party.name} ") } else -> throw IllegalArgumentException("Method not implemented for different type of message recipients") @@ -446,4 +437,17 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, fun waitQuiescent() { busyLatch.await() } + + override fun close() { + stopNodes() + } } + +fun network(nodesCount: Int, action: MockNetwork.(nodes: List>, notary: StartedNode) -> Unit) { + MockNetwork().use { + it.runNetwork() + val notary = it.createNotaryNode() + val nodes = (1..nodesCount).map { _ -> it.createPartyNode() } + action(it, nodes, notary) + } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 73dc1f883d..fcd5689800 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,20 +1,27 @@ package net.corda.testing.node +import com.google.common.collect.MutableClassToInstanceMap import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* +import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.FlowHandle +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.node.services.* -import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.node.VersionInfo +import net.corda.node.internal.StateLoaderImpl import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage +import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshCertificate @@ -24,7 +31,6 @@ import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransacti import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService -import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -43,7 +49,12 @@ import java.util.* * A singleton utility that only provides a mock identity, key and storage service. However, this is sufficient for * building chains of transactions and verifying them. It isn't sufficient for testing flows however. */ -open class MockServices(cordappPackages: List = emptyList(), vararg val keys: KeyPair) : ServiceHub { +open class MockServices( + cordappLoader: CordappLoader, + override val validatedTransactions: WritableTransactionStorage, + protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions), + vararg val keys: KeyPair +) : ServiceHub, StateLoader by stateLoader { companion object { @JvmStatic @@ -75,7 +86,9 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val fun makeTestDatabaseProperties(key: String? = null, value: String? = null): Properties { val props = Properties() props.setProperty("transactionIsolationLevel", "repeatableRead") //for other possible values see net.corda.node.utilities.CordaPeristence.parserTransactionIsolationLevel(String) - if (key != null) { props.setProperty(key, value) } + if (key != null) { + props.setProperty(key, value) + } return props } @@ -87,38 +100,33 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val /** * Makes database and mock services appropriate for unit tests. - * - * @param customSchemas a set of schemas being used by [NodeSchemaService] - * @param keys a lis of [KeyPair] instances to be used by [MockServices]. Defualts to [MEGA_CORP_KEY] + * @param keys a list of [KeyPair] instances to be used by [MockServices]. Defualts to [MEGA_CORP_KEY] * @param createIdentityService a lambda function returning an instance of [IdentityService]. Defauts to [InMemoryIdentityService]. * * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. */ @JvmStatic - fun makeTestDatabaseAndMockServices(customSchemas: Set = emptySet(), - keys: List = listOf(MEGA_CORP_KEY), + fun makeTestDatabaseAndMockServices(keys: List = listOf(MEGA_CORP_KEY), createIdentityService: () -> IdentityService = { makeTestIdentityService() }, cordappPackages: List = emptyList()): Pair { + val cordappLoader = CordappLoader.createWithTestPackages(cordappPackages) val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() - val createSchemaService = { NodeSchemaService(customSchemas) } val identityServiceRef: IdentityService by lazy { createIdentityService() } - val database = configureDatabase(dataSourceProps, databaseProperties, createSchemaService, { identityServiceRef }) + val database = configureDatabase(dataSourceProps, databaseProperties, { identityServiceRef }, NodeSchemaService(cordappLoader)) val mockService = database.transaction { - object : MockServices(cordappPackages, *(keys.toTypedArray())) { + object : MockServices(cordappLoader, *(keys.toTypedArray())) { override val identityService: IdentityService = database.transaction { identityServiceRef } - override val vaultService: VaultService = makeVaultService(database.hibernateConfig) + override val vaultService = makeVaultService(database.hibernateConfig) override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) + vaultService.notifyAll(txs.map { it.tx }) } - override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(database.hibernateConfig, vaultService) - override fun jdbcSession(): Connection = database.createSession() } } @@ -126,9 +134,10 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val } } + private constructor(cordappLoader: CordappLoader, vararg keys: KeyPair) : this(cordappLoader, MockTransactionStorage(), keys = *keys) + constructor(cordappPackages: List, vararg keys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), keys = *keys) constructor(vararg keys: KeyPair) : this(emptyList(), *keys) - - constructor() : this(emptyList(), generateKeyPair()) + constructor() : this(generateKeyPair()) val key: KeyPair get() = keys.first() @@ -141,34 +150,36 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val } } - final override val attachments: AttachmentStorage = MockAttachmentStorage() - override val validatedTransactions: WritableTransactionStorage = MockTransactionStorage() + final override val attachments = MockAttachmentStorage() val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage() override val identityService: IdentityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DEV_TRUST_ROOT) override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *keys) } override val vaultService: VaultService get() = throw UnsupportedOperationException() override val contractUpgradeService: ContractUpgradeService get() = throw UnsupportedOperationException() - override val vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() - override val myInfo: NodeInfo get() { - val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) - return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) - } + override val myInfo: NodeInfo + get() { + val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) + return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) + } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - val mockCordappProvider: MockCordappProvider = MockCordappProvider(CordappLoader.createWithTestPackages(cordappPackages + CordappLoader.testPackages)).start(attachments) as MockCordappProvider - override val cordappProvider: CordappProvider = mockCordappProvider - + val mockCordappProvider = MockCordappProvider(cordappLoader, attachments) + override val cordappProvider: CordappProvider get() = mockCordappProvider lateinit var hibernatePersister: HibernateObserver - fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration( { NodeSchemaService() }, makeTestDatabaseProperties(), { identityService })): VaultService { - val vaultService = NodeVaultService(this) + fun makeVaultService(hibernateConfig: HibernateConfiguration): VaultServiceInternal { + val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, stateLoader, hibernateConfig) hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService } - override fun cordaService(type: Class): T = throw IllegalArgumentException("${type.name} not found") + val cordappServices = MutableClassToInstanceMap.create() + override fun cordaService(type: Class): T { + require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } + return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + } override fun jdbcSession(): Connection = throw UnsupportedOperationException() } @@ -239,3 +250,23 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id] } + +fun createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T { + class MockAppServiceHubImpl(val serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T) : AppServiceHub, ServiceHub by serviceHub { + val serviceInstance: T + + init { + serviceInstance = serviceConstructor(this) + serviceHub.cordappServices.putInstance(serviceInstance.javaClass, serviceInstance) + } + + override fun startFlow(flow: FlowLogic): FlowHandle { + throw UnsupportedOperationException() + } + + override fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle { + throw UnsupportedOperationException() + } + } + return MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index ce703de814..36dfb43a45 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -5,20 +5,16 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.* import net.corda.core.internal.createDirectories import net.corda.core.internal.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.Node import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType -import net.corda.node.services.config.ConfigHelper -import net.corda.node.services.config.FullNodeConfiguration -import net.corda.node.services.config.configOf -import net.corda.node.services.config.plus -import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.RaftValidatingNotaryService +import net.corda.node.internal.cordapp.CordappLoader +import net.corda.node.services.config.* import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs +import net.corda.nodeapi.config.toConfig import net.corda.testing.DUMMY_MAP import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.driver.addressMustNotBeBoundFuture @@ -36,7 +32,7 @@ import kotlin.concurrent.thread * purposes. Use the driver if you need to run the nodes in separate processes otherwise this class will suffice. */ // TODO Some of the logic here duplicates what's in the driver -abstract class NodeBasedTest : TestDependencyInjectionBase() { +abstract class NodeBasedTest(private val cordappPackages: List = emptyList()) : TestDependencyInjectionBase() { companion object { private val WHITESPACE = "\\s++".toRegex() } @@ -87,11 +83,10 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { */ fun startNetworkMapNode(legalName: CordaX500Name = DUMMY_MAP.name, platformVersion: Int = 1, - advertisedServices: Set = emptySet(), rpcUsers: List = emptyList(), configOverrides: Map = emptyMap()): StartedNode { check(_networkMapNode == null || _networkMapNode!!.info.legalIdentitiesAndCerts.first().name == legalName) - return startNodeInternal(legalName, platformVersion, advertisedServices + ServiceInfo(NetworkMapService.type), rpcUsers, configOverrides).apply { + return startNodeInternal(legalName, platformVersion, rpcUsers, configOverrides).apply { _networkMapNode = this } } @@ -99,7 +94,6 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { @JvmOverloads fun startNode(legalName: CordaX500Name, platformVersion: Int = 1, - advertisedServices: Set = emptySet(), rpcUsers: List = emptyList(), configOverrides: Map = emptyMap(), noNetworkMap: Boolean = false, @@ -123,37 +117,50 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { val node = startNodeInternal( legalName, platformVersion, - advertisedServices, rpcUsers, networkMapConf + configOverrides, noNetworkMap) return if (waitForConnection) node.internals.nodeReadyFuture.map { node } else doneFuture(node) } - fun startNotaryCluster(notaryName: CordaX500Name, - clusterSize: Int, - serviceType: ServiceType = RaftValidatingNotaryService.type): CordaFuture>> { + // TODO This method has been added temporarily, to be deleted once the set of notaries is defined at the network level. + fun startNotaryNode(name: CordaX500Name, + rpcUsers: List = emptyList(), + validating: Boolean = true): CordaFuture> { + return startNode(name, rpcUsers = rpcUsers, configOverrides = mapOf("notary" to mapOf("validating" to validating))) + } + + fun startNotaryCluster(notaryName: CordaX500Name, clusterSize: Int): CordaFuture>> { + fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { + val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() + val config = NotaryConfig(validating = true, raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) + return mapOf("notary" to config.toConfig().root().unwrapped()) + } + ServiceIdentityGenerator.generateToDisk( (0 until clusterSize).map { baseDirectory(notaryName.copy(organisation = "${notaryName.organisation}-$it")) }, notaryName) - val serviceInfo = ServiceInfo(serviceType, notaryName) - val nodeAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() } + val nodeAddresses = getFreeLocalPorts("localhost", clusterSize) val masterNodeFuture = startNode( CordaX500Name(organisation = "${notaryName.organisation}-0", locality = notaryName.locality, country = notaryName.country), - advertisedServices = setOf(serviceInfo), - configOverrides = mapOf("notaryNodeAddress" to nodeAddresses[0], - "database" to mapOf("serverNameTablePrefix" to if (clusterSize > 1) "${notaryName.organisation}0".replace(Regex("[^0-9A-Za-z]+"), "") else ""))) + configOverrides = notaryConfig(nodeAddresses[0]) + mapOf( + "database" to mapOf( + "serverNameTablePrefix" to if (clusterSize > 1) "${notaryName.organisation}0".replace(Regex("[^0-9A-Za-z]+"), "") else "" + ) + ) + ) val remainingNodesFutures = (1 until clusterSize).map { startNode( CordaX500Name(organisation = "${notaryName.organisation}-$it", locality = notaryName.locality, country = notaryName.country), - advertisedServices = setOf(serviceInfo), - configOverrides = mapOf( - "notaryNodeAddress" to nodeAddresses[it], - "notaryClusterAddresses" to listOf(nodeAddresses[0]), - "database" to mapOf("serverNameTablePrefix" to "${notaryName.organisation}$it".replace(Regex("[^0-9A-Za-z]+"), "")))) + configOverrides = notaryConfig(nodeAddresses[it], nodeAddresses[0]) + mapOf( + "database" to mapOf( + "serverNameTablePrefix" to "${notaryName.organisation}$it".replace(Regex("[^0-9A-Za-z]+"), "") + ) + ) + ) } return remainingNodesFutures.transpose().flatMap { remainingNodes -> @@ -165,7 +172,6 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { private fun startNodeInternal(legalName: CordaX500Name, platformVersion: Int, - advertisedServices: Set, rpcUsers: List, configOverrides: Map, noNetworkMap: Boolean = false): StartedNode { @@ -179,15 +185,17 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { "myLegalName" to legalName.toString(), "p2pAddress" to p2pAddress, "rpcAddress" to localPort[1].toString(), - "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "rpcUsers" to rpcUsers.map { it.toMap() }, "noNetworkMap" to noNetworkMap ) + configOverrides ) val parsedConfig = config.parseAs() - val node = Node(parsedConfig, parsedConfig.calculateServices(), MOCK_VERSION_INFO.copy(platformVersion = platformVersion), - initialiseSerialization = false).start() + val node = Node( + parsedConfig, + MOCK_VERSION_INFO.copy(platformVersion = platformVersion), + initialiseSerialization = false, + cordappLoader = CordappLoader.createDefaultWithTestPackages(parsedConfig, cordappPackages)).start() nodes += node thread(name = legalName.organisation) { node.internals.run() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 726b0c5202..3b7bf9130e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -37,7 +37,7 @@ class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort val monitoringService = MonitoringService(MetricRegistry()) val identity: KeyPair = generateKeyPair() val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) - val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, { NodeSchemaService() }, { InMemoryIdentityService(trustRoot = trustRoot) }) + val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, { InMemoryIdentityService(trustRoot = trustRoot) }) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.organisation, 1) // TODO: We should have a dummy service hub rather than change behaviour in tests diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt index 690f765c3d..e1149b1fda 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt @@ -25,7 +25,8 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC /** * Advance this [Clock] by the specified [Duration] for testing purposes. */ - @Synchronized fun advanceBy(duration: Duration) { + @Synchronized + fun advanceBy(duration: Duration) { delegateClock = offset(delegateClock, duration) notifyMutationObservers() } @@ -35,7 +36,8 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC * * This will only be approximate due to the time ticking away, but will be some time shortly after the requested [Instant]. */ - @Synchronized fun setTo(newInstant: Instant) = advanceBy(instant() until newInstant) + @Synchronized + fun setTo(newInstant: Instant) = advanceBy(instant() until newInstant) @Synchronized override fun instant(): Instant { return delegateClock.instant() diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index ef0c1ff87b..cc003edc6b 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -13,7 +13,7 @@ class NodeConfig( val p2pPort: Int, val rpcPort: Int, val webPort: Int, - val extraServices: List, + val isNotary: Boolean, val users: List, var networkMap: NodeConfig? = null ) { @@ -27,18 +27,25 @@ class NodeConfig( * The configuration object depends upon the networkMap, * which is mutable. */ - fun toFileConfig(): Config = empty() - .withValue("myLegalName", valueFor(legalName.toString())) - .withValue("p2pAddress", addressValueFor(p2pPort)) - .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) - .withFallback(optional("networkMapService", networkMap, { c, n -> - c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.legalName.toString())) - })) - .withValue("webAddress", addressValueFor(webPort)) - .withValue("rpcAddress", addressValueFor(rpcPort)) - .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) - .withValue("useTestClock", valueFor(true)) + //TODO Make use of Any.toConfig + private fun toFileConfig(): Config { + val config = empty() + .withValue("myLegalName", valueFor(legalName.toString())) + .withValue("p2pAddress", addressValueFor(p2pPort)) + .withFallback(optional("networkMapService", networkMap, { c, n -> + c.withValue("address", addressValueFor(n.p2pPort)) + .withValue("legalName", valueFor(n.legalName.toString())) + })) + .withValue("webAddress", addressValueFor(webPort)) + .withValue("rpcAddress", addressValueFor(rpcPort)) + .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) + .withValue("useTestClock", valueFor(true)) + return if (isNotary) { + config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true))) + } else { + config + } + } fun toText(): String = toFileConfig().root().render(renderOptions) diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 6ab3de90d7..ae43e144db 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -46,6 +46,7 @@ class NodeProcess( class Factory(val buildDirectory: Path = Paths.get("build"), val cordaJar: Path = Paths.get(this::class.java.getResource("/corda.jar").toURI())) { val nodesDirectory = buildDirectory / formatter.format(Instant.now()) + init { nodesDirectory.createDirectories() } @@ -95,11 +96,11 @@ class NodeProcess( private fun startNode(nodeDir: Path): Process { val builder = ProcessBuilder() - .command(javaPath.toString(), "-jar", cordaJar.toString()) - .directory(nodeDir.toFile()) + .command(javaPath.toString(), "-jar", cordaJar.toString()) + .directory(nodeDir.toFile()) builder.environment().putAll(mapOf( - "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString() + "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString() )) return builder.start() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 5a55586924..71be78c0bd 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -1,4 +1,4 @@ -@file:Suppress("UNUSED_PARAMETER", "UNCHECKED_CAST") +@file:Suppress("UNUSED_PARAMETER") @file:JvmName("CoreTestUtils") package net.corda.testing @@ -17,7 +17,6 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CertificateAndKeyPair @@ -70,11 +69,11 @@ val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CordaX500Name(organisation = "MiniCorp", locality = "London", country = "GB"), MINI_CORP_PUBKEY) val MINI_CORP: Party get() = MINI_CORP_IDENTITY.party +val BOC_NAME: CordaX500Name = CordaX500Name(organisation = "BankOfCorda", locality = "London", country = "GB") val BOC_KEY: KeyPair by lazy { generateKeyPair() } val BOC_PUBKEY: PublicKey get() = BOC_KEY.public -val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CordaX500Name(organisation = "BankOfCorda", locality = "London", country = "GB"), BOC_PUBKEY) +val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOC_NAME, BOC_PUBKEY) val BOC: Party get() = BOC_IDENTITY.party -val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference val BIG_CORP_KEY: KeyPair by lazy { generateKeyPair() } val BIG_CORP_PUBKEY: PublicKey get() = BIG_CORP_KEY.public @@ -117,8 +116,8 @@ fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + ( * to the Node, some other process else could allocate the returned ports. */ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { - val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 } - return (freePort .. freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) } + val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 } + return (freePort..freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) } } @JvmOverloads @@ -146,16 +145,18 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey, trustR return getTestPartyAndCertificate(Party(name, publicKey), trustRoot) } -inline fun kryoSpecific(reason: String, function: () -> Unit) = if(!AMQP_ENABLED) { +@Suppress("unused") +inline fun T.kryoSpecific(reason: String, function: () -> Unit) = if (!AMQP_ENABLED) { function() } else { - loggerFor().info("Ignoring Kryo specific test, reason: $reason" ) + loggerFor().info("Ignoring Kryo specific test, reason: $reason") } -inline fun amqpSpecific(reason: String, function: () -> Unit) = if(AMQP_ENABLED) { +@Suppress("unused") +inline fun T.amqpSpecific(reason: String, function: () -> Unit) = if (AMQP_ENABLED) { function() } else { - loggerFor().info("Ignoring AMQP specific test, reason: $reason" ) + loggerFor().info("Ignoring AMQP specific test, reason: $reason") } /** @@ -163,21 +164,16 @@ inline fun amqpSpecific(reason: String, function: () -> Unit) * TODO: Should be removed after multiple identities are introduced. */ fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCerts.first() + fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party +/** + * Extract a single identity from the node info. Throws an error if the node has multiple identities. + */ +fun NodeInfo.singleIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCerts.single() + +/** + * Extract a single identity from the node info. Throws an error if the node has multiple identities. + */ +fun NodeInfo.singleIdentity(): Party = singleIdentityAndCert().party /** Returns the identity of the first notary found on the network */ fun ServiceHub.getDefaultNotary(): Party = networkMapCache.notaryIdentities.first() - -/** - * Set the package to scan for cordapps - this overrides the default behaviour of scanning the cordapps directory - * @param packageName A package name that you wish to scan for cordapps - */ -fun setCordappPackages(vararg packageNames: String) { - CordappLoader.testPackages = packageNames.toList() -} - -/** - * Unsets the default overriding behaviour of [setCordappPackage] - */ -fun unsetCordappPackages() { - CordappLoader.testPackages = emptyList() -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt new file mode 100644 index 0000000000..693fffeca9 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt @@ -0,0 +1,25 @@ +package net.corda.testing + +import java.time.Duration + +/** + * Ideas borrowed from "io.kotlintest" with some improvements made + * This is meant for use from Kotlin code use only mainly due to it's inline/reified nature + */ +inline fun eventually(duration: Duration, f: () -> R): R { + val end = System.nanoTime() + duration.toNanos() + var times = 0 + while (System.nanoTime() < end) { + try { + return f() + } catch (e: Throwable) { + when (e) { + is E -> { + }// ignore and continue + else -> throw e // unexpected exception type - rethrow + } + } + times++ + } + throw AssertionError("Test failed after $duration; attempted $times times") +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt index 1128186c67..0db4754f4c 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt @@ -1,6 +1,7 @@ package net.corda.testing import com.google.common.util.concurrent.SettableFuture +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.getOrThrow import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -213,9 +214,8 @@ private sealed class ExpectComposeState { class Single(val single: ExpectCompose.Single) : ExpectComposeState() { override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>? = if (single.expect.clazz.isAssignableFrom(event.javaClass)) { - @Suppress("UNCHECKED_CAST") - val coercedEvent = event as T - if (single.expect.match(event)) { + val coercedEvent: T = uncheckedCast(event) + if (single.expect.match(coercedEvent)) { Pair({ single.expect.expectClosure(coercedEvent) }, Finished()) } else { null @@ -285,8 +285,7 @@ private sealed class ExpectComposeState { is ExpectCompose.Single -> { // This coercion should not be needed but kotlin can't reason about existential type variables(T) // so here we're coercing T into E (even though T is invariant). - @Suppress("UNCHECKED_CAST") - Single(expectCompose as ExpectCompose.Single) + Single(uncheckedCast(expectCompose)) } is ExpectCompose.Sequential -> { if (expectCompose.sequence.size > 0) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt index 94eb3a7a12..74f622bc37 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt @@ -16,6 +16,7 @@ import net.corda.core.internal.div import net.corda.core.internal.write import net.corda.core.serialization.SerializeAsToken import net.corda.client.jackson.JacksonSupport +import net.corda.core.internal.uncheckedCast import net.corda.node.services.statemachine.FlowStackSnapshotFactory import java.nio.file.Path import java.time.Instant @@ -54,7 +55,8 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { val objectStack = getObjectStack(stack).toList() val frameOffsets = getFrameOffsets(stack) val frameObjects = frameOffsets.map { (frameOffset, frameSize) -> - objectStack.subList(frameOffset + 1, frameOffset + frameSize + 1) + // We need to convert the sublist to a list due to the Kryo lack of support when serializing + objectStack.subList(frameOffset + 1, frameOffset + frameSize + 1).toList() } // We drop the first element as it is corda internal call irrelevant from the perspective of a CordApp developer val relevantStackTrace = removeConstructorStackTraceElements(stackTrace).drop(1) @@ -76,14 +78,15 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { return FlowStackSnapshot(Instant.now(), flowClass.name, frames) } - private val StackTraceElement.instrumentedAnnotation: Instrumented? get() { - Class.forName(className).methods.forEach { - if (it.name == methodName && it.isAnnotationPresent(Instrumented::class.java)) { - return it.getAnnotation(Instrumented::class.java) + private val StackTraceElement.instrumentedAnnotation: Instrumented? + get() { + Class.forName(className).methods.forEach { + if (it.name == methodName && it.isAnnotationPresent(Instrumented::class.java)) { + return it.getAnnotation(Instrumented::class.java) + } } + return null } - return null - } private fun removeConstructorStackTraceElements(stackTrace: List): List { val newStackTrace = ArrayList() @@ -134,11 +137,10 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { } -private inline fun R.getField(name: String): A { +private inline fun R.getField(name: String): A { val field = R::class.java.getDeclaredField(name) field.isAccessible = true - @Suppress("UNCHECKED_CAST") - return field.get(this) as A + return uncheckedCast(field.get(this)) } private fun getFiberStack(fiber: Fiber<*>): Stack { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt index c3fb15115e..9f4f829433 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt @@ -127,7 +127,7 @@ class LedgerDSL by interpreter { /** - * @see LedgerDSLInterpreter._transaction + * Creates and adds a transaction to the ledger. */ @JvmOverloads fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), @@ -135,7 +135,7 @@ class LedgerDSL String.outputStateAndRef(): StateAndRef = retrieveOutputStateAndRef(S::class.java, this) @@ -156,7 +156,7 @@ class LedgerDSL().state.data /** - * @see OutputStateLookup.retrieveOutputStateAndRef + * Retrieves an output previously defined by [TransactionDSLInterpreter._output] with a label passed in. */ fun retrieveOutput(clazz: Class, label: String) = retrieveOutputStateAndRef(clazz, label).state.data diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt index ad2e488e91..1fab8508fc 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt @@ -1,5 +1,6 @@ package net.corda.testing +import net.corda.core.internal.packageName import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.LoggerContext @@ -25,7 +26,7 @@ object LogHelper { } } - fun setLevel(vararg classes: KClass<*>) = setLevel(*classes.map { "+" + it.java.`package`.name }.toTypedArray()) + fun setLevel(vararg classes: KClass<*>) = setLevel(*classes.map { "+" + it.packageName }.toTypedArray()) /** Removes custom configuration for the specified logger names */ fun reset(vararg names: String) { @@ -35,7 +36,7 @@ object LogHelper { loggerContext.updateLoggers(config) } - fun reset(vararg classes: KClass<*>) = reset(*classes.map { it.java.`package`.name }.toTypedArray()) + fun reset(vararg classes: KClass<*>) = reset(*classes.map { it.packageName }.toTypedArray()) /** Updates logging level for the specified Log4j logger name */ private fun setLevel(name: String, level: Level) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt index d3a6f9f65d..91eaecf0a0 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt @@ -1,5 +1,6 @@ package net.corda.testing +import net.corda.core.internal.uncheckedCast import kotlin.reflect.KCallable import kotlin.reflect.jvm.reflect @@ -9,18 +10,17 @@ import kotlin.reflect.jvm.reflect * different combinations of parameters. */ -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, f: (A) -> R) = - measure(listOf(a), f.reflect()!!) { (f as ((Any?)->R))(it[0]) } -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, b: Iterable, f: (A, B) -> R) = - measure(listOf(a, b), f.reflect()!!) { (f as ((Any?,Any?)->R))(it[0], it[1]) } -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, b: Iterable, c: Iterable, f: (A, B, C) -> R) = - measure(listOf(a, b, c), f.reflect()!!) { (f as ((Any?,Any?,Any?)->R))(it[0], it[1], it[2]) } -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, b: Iterable, c: Iterable, d: Iterable, f: (A, B, C, D) -> R) = - measure(listOf(a, b, c, d), f.reflect()!!) { (f as ((Any?,Any?,Any?,Any?)->R))(it[0], it[1], it[2], it[3]) } +fun measure(a: Iterable, f: (A) -> R) = + measure(listOf(a), f.reflect()!!) { f(uncheckedCast(it[0])) } + +fun measure(a: Iterable, b: Iterable, f: (A, B) -> R) = + measure(listOf(a, b), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1])) } + +fun measure(a: Iterable, b: Iterable, c: Iterable, f: (A, B, C) -> R) = + measure(listOf(a, b, c), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1]), uncheckedCast(it[2])) } + +fun measure(a: Iterable, b: Iterable, c: Iterable, d: Iterable, f: (A, B, C, D) -> R) = + measure(listOf(a, b, c, d), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1]), uncheckedCast(it[2]), uncheckedCast(it[3])) } private fun measure(paramIterables: List>, kCallable: KCallable, call: (Array) -> R): Iterable> { val kParameters = kCallable.parameters @@ -33,8 +33,8 @@ private fun measure(paramIterables: List>, kCallable: KCallab } data class MeasureResult( - val parameters: List>, - val result: R + val parameters: List>, + val result: R ) fun iterateLexical(iterables: List>): Iterable> { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index 5da78a32af..891808e4b5 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -17,37 +17,37 @@ inline fun withTestSerialization(block: () -> T): T { } fun initialiseTestSerialization() { + // Stop the CordaRPCClient from trying to setup the defaults as we're about to do it now + KryoClientSerializationScheme.isInitialised.set(true) // Check that everything is configured for testing with mutable delegating instances. try { - check(SerializationDefaults.SERIALIZATION_FACTORY is TestSerializationFactory) { - "Found non-test serialization configuration: ${SerializationDefaults.SERIALIZATION_FACTORY}" - } - } catch(e: IllegalStateException) { + check(SerializationDefaults.SERIALIZATION_FACTORY is TestSerializationFactory) + } catch (e: IllegalStateException) { SerializationDefaults.SERIALIZATION_FACTORY = TestSerializationFactory() } try { check(SerializationDefaults.P2P_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.P2P_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.RPC_SERVER_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.RPC_SERVER_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.RPC_CLIENT_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.RPC_CLIENT_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.STORAGE_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.STORAGE_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.CHECKPOINT_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.CHECKPOINT_CONTEXT = TestSerializationContext() } @@ -68,19 +68,18 @@ fun initialiseTestSerialization() { registerScheme(AMQPServerSerializationScheme()) } - val AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable" - // TODO: Remove these "if" conditions once we fully switched to AMQP - (SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = if (java.lang.Boolean.getBoolean(AMQP_ENABLE_PROP_NAME)) { - AMQP_P2P_CONTEXT - } else { - KRYO_P2P_CONTEXT - } + (SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = if (isAmqpEnabled()) AMQP_P2P_CONTEXT else KRYO_P2P_CONTEXT (SerializationDefaults.RPC_SERVER_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_SERVER_CONTEXT (SerializationDefaults.RPC_CLIENT_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_CLIENT_CONTEXT - (SerializationDefaults.STORAGE_CONTEXT as TestSerializationContext).delegate = KRYO_STORAGE_CONTEXT + (SerializationDefaults.STORAGE_CONTEXT as TestSerializationContext).delegate = if (isAmqpEnabled()) AMQP_STORAGE_CONTEXT else KRYO_STORAGE_CONTEXT (SerializationDefaults.CHECKPOINT_CONTEXT as TestSerializationContext).delegate = KRYO_CHECKPOINT_CONTEXT } +private const val AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable" + +// TODO: Remove usages of this function when we fully switched to AMQP +private fun isAmqpEnabled(): Boolean = java.lang.Boolean.getBoolean(AMQP_ENABLE_PROP_NAME) + fun resetTestSerialization() { (SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate = null (SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = null diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index a7f7e11157..9e73764f06 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -31,6 +31,7 @@ val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) /** Dummy notary identity for tests and simulations */ val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY) val DUMMY_NOTARY: Party get() = Party(CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"), DUMMY_NOTARY_KEY.public) +val DUMMY_NOTARY_SERVICE_NAME: CordaX500Name = DUMMY_NOTARY.name.copy(commonName = "corda.notary.validating") val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) } /** Dummy network map service identity for tests and simulations */ @@ -51,17 +52,20 @@ val DUMMY_BANK_C: Party get() = Party(CordaX500Name(organisation = "Bank C", loc val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) } /** Dummy individual identity for tests and simulations */ val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE) -val ALICE: Party get() = Party(CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES"), ALICE_KEY.public) +val ALICE_NAME = CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES") +val ALICE: Party get() = Party(ALICE_NAME, ALICE_KEY.public) val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) } /** Dummy individual identity for tests and simulations */ val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB) -val BOB: Party get() = Party(CordaX500Name(organisation = "Bob Plc", locality = "Rome", country = "IT"), BOB_KEY.public) +val BOB_NAME = CordaX500Name(organisation = "Bob Plc", locality = "Rome", country = "IT") +val BOB: Party get() = Party(BOB_NAME, BOB_KEY.public) val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) } /** Dummy individual identity for tests and simulations */ val CHARLIE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CHARLIE) -val CHARLIE: Party get() = Party(CordaX500Name(organisation = "Charlie Ltd", locality = "Athens", country = "GR"), CHARLIE_KEY.public) +val CHARLIE_NAME = CordaX500Name(organisation = "Charlie Ltd", locality = "Athens", country = "GR") +val CHARLIE: Party get() = Party(CHARLIE_NAME, CHARLIE_KEY.public) val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) } /** Dummy regulator for tests and simulations */ diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 84d0512a05..48658ea021 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -4,12 +4,16 @@ import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.crypto.NullKeys.NULL_SIGNATURE +import net.corda.core.flows.FlowException import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.node.ServiceHub +import net.corda.core.node.ServicesForResolution import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.testing.contracts.DummyContract +import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockCordappProvider import java.io.InputStream import java.security.KeyPair @@ -51,9 +55,9 @@ sealed class EnforceVerifyOrFail { internal object Token : EnforceVerifyOrFail() } -class DuplicateOutputLabel(label: String) : Exception("Output label '$label' already used") -class DoubleSpentInputs(ids: List) : Exception("Transactions spend the same input. Conflicting transactions ids: '$ids'") -class AttachmentResolutionException(attachmentId: SecureHash) : Exception("Attachment with id $attachmentId not found") +class DuplicateOutputLabel(label: String) : FlowException("Output label '$label' already used") +class DoubleSpentInputs(ids: List) : FlowException("Transactions spend the same input. Conflicting transactions ids: '$ids'") +class AttachmentResolutionException(attachmentId: SecureHash) : FlowException("Attachment with id $attachmentId not found") /** * This interpreter builds a transaction, and [TransactionDSL.verifies] that the resolved transaction is correct. Note @@ -70,7 +74,7 @@ data class TestTransactionDSLInterpreter private constructor( transactionBuilder: TransactionBuilder ) : this(ledgerInterpreter, transactionBuilder, HashMap()) - val services = object : ServiceHub by ledgerInterpreter.services { + val services = object : ServicesForResolution by ledgerInterpreter.services { override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef(stateRef) override val cordappProvider: CordappProvider = ledgerInterpreter.services.cordappProvider } @@ -134,7 +138,7 @@ data class TestTransactionDSLInterpreter private constructor( ) = dsl(TransactionDSL(copy())) override fun _attachment(contractClassName: ContractClassName) { - (services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services) + (services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage) } } @@ -188,8 +192,8 @@ data class TestLedgerDSLInterpreter private constructor( nonVerifiedTransactionWithLocations[stateRef.txhash] ?: throw TransactionResolutionException(stateRef.txhash) val output = transactionWithLocation.transaction.outputs[stateRef.index] - return if (S::class.java.isAssignableFrom(output.data.javaClass)) @Suppress("UNCHECKED_CAST") { - output as TransactionState + return if (S::class.java.isAssignableFrom(output.data.javaClass)) { + uncheckedCast(output) } else { throw TypeMismatch(requested = S::class.java, actual = output.data.javaClass) } @@ -270,7 +274,7 @@ data class TestLedgerDSLInterpreter private constructor( transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Unit - ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations, fillTransaction = true) + ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations, fillTransaction = true) override fun tweak( dsl: LedgerDSL + return uncheckedCast(stateAndRef) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index 894d2ccd0f..d1fd436222 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -20,7 +20,7 @@ import java.time.Instant /** * This interface defines the bare bone functionality that a Transaction DSL interpreter should implement. * @param The return type of [verifies]/[failsWith] and the like. It is generic so that we have control over whether - * we want to enforce users to call these methods (@see [EnforceVerifyOrFail]) or not. + * we want to enforce users to call these methods (see [EnforceVerifyOrFail]) or not. */ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { /** @@ -41,7 +41,7 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * @param encumbrance The position of the encumbrance state. * @param attachmentConstraint The attachment constraint * @param contractState The state itself. - * @params contractClassName The class name of the contract that verifies this state. + * @param contractClassName The class name of the contract that verifies this state. */ fun _output(contractClassName: ContractClassName, label: String?, @@ -109,7 +109,7 @@ class TransactionDSL(val interpreter: T) : Tr fun input(contractClassName: ContractClassName, stateClosure: () -> ContractState) = input(contractClassName, stateClosure()) /** - * @see TransactionDSLInterpreter._output + * Adds an output to the transaction. */ @JvmOverloads fun output(contractClassName: ContractClassName, @@ -121,24 +121,27 @@ class TransactionDSL(val interpreter: T) : Tr _output(contractClassName, label, notary, encumbrance, attachmentConstraint, contractStateClosure()) /** - * @see TransactionDSLInterpreter._output + * Adds a labelled output to the transaction. */ @JvmOverloads fun output(contractClassName: ContractClassName, label: String, contractState: ContractState, attachmentConstraint: AttachmentConstraint = AutomaticHashConstraint) = _output(contractClassName, label, DUMMY_NOTARY, null, attachmentConstraint, contractState) + /** + * Adds an output to the transaction. + */ @JvmOverloads fun output(contractClassName: ContractClassName, contractState: ContractState, attachmentConstraint: AttachmentConstraint = AutomaticHashConstraint) = - _output(contractClassName,null, DUMMY_NOTARY, null, attachmentConstraint, contractState) + _output(contractClassName, null, DUMMY_NOTARY, null, attachmentConstraint, contractState) /** - * @see TransactionDSLInterpreter._command + * Adds a command to the transaction. */ fun command(vararg signers: PublicKey, commandDataClosure: () -> CommandData) = _command(listOf(*signers), commandDataClosure()) /** - * @see TransactionDSLInterpreter._command + * Adds a command to the transaction. */ fun command(signer: PublicKey, commandData: CommandData) = _command(listOf(signer), commandData) @@ -156,5 +159,5 @@ class TransactionDSL(val interpreter: T) : Tr */ fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName) - fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it)} + fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt index 855994138e..28341e2e92 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt @@ -5,11 +5,13 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import kotlin.reflect.jvm.jvmName // The dummy contract doesn't do anything useful. It exists for testing purposes, but has to be serializable data class DummyContract(val blank: Any? = null) : Contract { + + val PROGRAM_ID = "net.corda.testing.contracts.DummyContract" + interface State : ContractState { val magicNumber: Int } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt index 180aaa7b85..5b7f80044d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt @@ -1,8 +1,8 @@ package net.corda.testing.contracts import net.corda.core.contracts.* -import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.identity.AbstractParty +import net.corda.core.internal.UpgradeCommand import net.corda.core.node.ServicesForResolution import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder @@ -35,7 +35,6 @@ class DummyContractV2 : UpgradedContract, - override val linearId: UniqueIdentifier) : DealState, QueryableState - { + override val linearId: UniqueIdentifier) : DealState, QueryableState { constructor(participants: List = listOf(), ref: String) : this(participants, UniqueIdentifier(ref)) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index 8abe70d329..a921cd00c4 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -32,7 +32,7 @@ import java.util.* @JvmOverloads fun ServiceHub.fillWithSomeTestDeals(dealIds: List, participants: List = emptyList(), - notary: Party = DUMMY_NOTARY) : Vault { + notary: Party = DUMMY_NOTARY): Vault { val myKey: PublicKey = myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) @@ -63,7 +63,7 @@ fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, linearString: String = "", linearNumber: Long = 0L, linearBoolean: Boolean = false, - linearTimestamp: Instant = now()) : Vault { + linearTimestamp: Instant = now()): Vault { val myKey: PublicKey = myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = DUMMY_NOTARY_KEY @@ -196,7 +196,7 @@ fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, fun ServiceHub.consume(states: List>, notary: Party) { // Create a txn consuming different contract types states.forEach { - val builder = TransactionBuilder(notary = notary).apply { + val builder = TransactionBuilder(notary = notary).apply { addInputState(it) addCommand(dummyCommand(notary.owningKey)) } @@ -238,7 +238,7 @@ fun ServiceHub.consumeAndProduce(states: List>, fun ServiceHub.consumeDeals(dealStates: List>, notary: Party) = consume(dealStates, notary) fun ServiceHub.consumeLinearStates(linearStates: List>, notary: Party) = consume(linearStates, notary) fun ServiceHub.evolveLinearStates(linearStates: List>, notary: Party) = consumeAndProduce(linearStates, notary) -fun ServiceHub.evolveLinearState(linearState: StateAndRef, notary: Party) : StateAndRef = consumeAndProduce(linearState, notary) +fun ServiceHub.evolveLinearState(linearState: StateAndRef, notary: Party): StateAndRef = consumeAndProduce(linearState, notary) /** * Consume cash, sending any change to the default identity for this node. Only suitable for use in test scenarios, @@ -254,7 +254,7 @@ fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE, notary */ @JvmOverloads fun ServiceHub.consumeCash(amount: Amount, ourIdentity: PartyAndCertificate, to: Party = CHARLIE, notary: Party): Vault.Update { - val update = vaultService.rawUpdates.toFuture() + val update = vaultService.rawUpdates.toFuture() val services = this // A tx that spends our money. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt index 743934e921..0c283fd844 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt @@ -29,6 +29,7 @@ class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { companion object { fun fromHostAndPort(hostAndPort: NetworkHostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi = HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper) + private val defaultMapper: ObjectMapper by lazy { net.corda.client.jackson.JacksonSupport.createNonRpcMapper() } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt index a0ca53fe0e..5c8736335e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt @@ -3,37 +3,33 @@ package net.corda.testing.node import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.AttachmentStorage import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import java.nio.file.Paths import java.util.* -class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(cordappLoader) { +class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : CordappProviderImpl(cordappLoader, attachmentStorage) { val cordappRegistry = mutableListOf>() - fun addMockCordapp(contractClassName: ContractClassName, services: ServiceHub) { - val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) + fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { + val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { - cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), services))) + cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), attachments))) } } override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) - private fun findOrImportAttachment(data: ByteArray, services: ServiceHub): AttachmentId { - return if (services.attachments is MockAttachmentStorage) { - val existingAttachment = (services.attachments as MockAttachmentStorage).files.filter { - Arrays.equals(it.value, data) - } - if (!existingAttachment.isEmpty()) { - existingAttachment.keys.first() - } else { - services.attachments.importAttachment(data.inputStream()) - } + private fun findOrImportAttachment(data: ByteArray, attachments: MockAttachmentStorage): AttachmentId { + val existingAttachment = attachments.files.filter { + Arrays.equals(it.value, data) + } + return if (!existingAttachment.isEmpty()) { + existingAttachment.keys.first() } else { - throw Exception("MockCordappService only requires MockAttachmentStorage") + attachments.importAttachment(data.inputStream()) } } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt index e25018bd9c..1a610c9372 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt @@ -20,8 +20,8 @@ object DummyLinearStateSchema object DummyLinearStateSchemaV1 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyLinearState::class.java)) { @Entity @Table(name = "dummy_linear_states", - indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"), - Index(name = "uuid_idx", columnList = "uuid"))) + indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"), + Index(name = "uuid_idx", columnList = "uuid"))) class PersistentDummyLinearState( /** [ContractState] attributes */ diff --git a/tools/demobench/README.md b/tools/demobench/README.md index dc42716039..ad1550b9e5 100644 --- a/tools/demobench/README.md +++ b/tools/demobench/README.md @@ -70,9 +70,6 @@ node in a new tab. ![Configure Bank Node](demobench-configure-bank.png) -This time, there will be additional services available. Select `corda.cash` and -`corda.issuer.GBP`, and then press the `Start node` button. - When you press the `Launch Web Server` this time, your browser should open to a page saying: > ### Installed CorDapps diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 6a3452b8c9..c0f21ad796 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -46,6 +46,8 @@ dependencies { // Controls FX: more java FX components http://fxexperience.com/controlsfx/ compile "org.controlsfx:controlsfx:$controlsfx_version" + compile "net.corda.plugins:cordform-common:$gradle_plugins_version" + compile project(':client:rpc') compile project(':finance') @@ -71,6 +73,7 @@ dependencies { testCompile "junit:junit:$junit_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.mockito:mockito-core:$mockito_version" } @@ -78,7 +81,8 @@ jar { manifest { attributes( 'Main-Class': mainClassName, - 'Class-Path': configurations.runtime.collect { it.getName() }.join(' ') + 'Class-Path': configurations.runtime.collect { it.getName() }.join(' '), + 'Automatic-Module-Name': 'net.corda.tools.demobench' ) } } @@ -120,12 +124,12 @@ distributions { } from(project(':finance').tasks.jar) { rename 'corda-finance-(.*)', 'corda-finance.jar' - into 'plugins' + into 'cordapps' fileMode = 0444 } from(project(':samples:bank-of-corda-demo').jar) { rename 'bank-of-corda-demo-(.*)', 'bank-of-corda.jar' - into 'plugins' + into 'cordapps' fileMode = 0444 } } @@ -199,7 +203,7 @@ task javapackage(dependsOn: distZip) { fileset(dir: dist_source, type: 'data') { include(name: 'corda/*.jar') - include(name: 'plugins/*.jar') + include(name: 'cordapps/*.jar') include(name: 'explorer/*.jar') } diff --git a/tools/demobench/demobench-configure-bank.png b/tools/demobench/demobench-configure-bank.png index 5e2c01211b..867e762506 100644 Binary files a/tools/demobench/demobench-configure-bank.png and b/tools/demobench/demobench-configure-bank.png differ diff --git a/tools/demobench/demobench-initial.png b/tools/demobench/demobench-initial.png index 27c2855e2c..d0e0b2f7db 100644 Binary files a/tools/demobench/demobench-initial.png and b/tools/demobench/demobench-initial.png differ diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index 3bf445b827..286ad1e4c6 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -6,12 +6,12 @@ import net.corda.core.internal.list import net.corda.core.utilities.loggerFor import net.corda.demobench.model.JVMConfig import net.corda.demobench.model.NodeConfig -import net.corda.demobench.model.forceDirectory +import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.readErrorLines import tornadofx.* import java.io.IOException import java.nio.file.Files -import java.nio.file.StandardCopyOption.* +import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.util.concurrent.Executors class Explorer internal constructor(private val explorerController: ExplorerController) : AutoCloseable { @@ -23,29 +23,32 @@ class Explorer internal constructor(private val explorerController: ExplorerCont private var process: Process? = null @Throws(IOException::class) - fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) { - val explorerDir = config.explorerDir.toFile() + fun open(config: NodeConfigWrapper, onExit: (NodeConfigWrapper) -> Unit) { + val explorerDir = config.explorerDir - if (!explorerDir.forceDirectory()) { - log.warn("Failed to create working directory '{}'", explorerDir.absolutePath) + try { + explorerDir.createDirectories() + } catch (e: IOException) { + log.warn("Failed to create working directory '{}'", explorerDir.toAbsolutePath()) onExit(config) return } + val legalName = config.nodeConfig.myLegalName try { installApps(config) - val user = config.users.elementAt(0) + val user = config.nodeConfig.rpcUsers[0] val p = explorerController.process( "--host=localhost", - "--port=${config.rpcPort}", + "--port=${config.nodeConfig.rpcAddress.port}", "--username=${user.username}", "--password=${user.password}") - .directory(explorerDir) + .directory(explorerDir.toFile()) .start() process = p - log.info("Launched Node Explorer for '{}'", config.legalName) + log.info("Launched Node Explorer for '{}'", legalName) // Close these streams because no-one is using them. safeClose(p.outputStream) @@ -57,21 +60,21 @@ class Explorer internal constructor(private val explorerController: ExplorerCont process = null if (errors.isEmpty()) { - log.info("Node Explorer for '{}' has exited (value={})", config.legalName, exitValue) + log.info("Node Explorer for '{}' has exited (value={})", legalName, exitValue) } else { - log.error("Node Explorer for '{}' has exited (value={}, {})", config.legalName, exitValue, errors) + log.error("Node Explorer for '{}' has exited (value={}, {})", legalName, exitValue, errors) } onExit(config) } } catch (e: IOException) { - log.error("Failed to launch Node Explorer for '{}': {}", config.legalName, e.message) + log.error("Failed to launch Node Explorer for '{}': {}", legalName, e.message) onExit(config) throw e } } - private fun installApps(config: NodeConfig) { + private fun installApps(config: NodeConfigWrapper) { // Make sure that the explorer has cordapps on its class path. This is only necessary because currently apps // require the original class files to deserialise states: Kryo serialisation doesn't let us write generic // tools that work with serialised data structures. But the AMQP serialisation revamp will fix this by @@ -80,15 +83,15 @@ class Explorer internal constructor(private val explorerController: ExplorerCont // Note: does not copy dependencies because we should soon be making all apps fat jars and dependencies implicit. // // TODO: Remove this code when serialisation has been upgraded. - val pluginsDir = config.explorerDir / "plugins" - pluginsDir.createDirectories() - config.pluginDir.list { + val cordappsDir = config.explorerDir / NodeConfig.cordappDirName + cordappsDir.createDirectories() + config.cordappsDir.list { it.forEachOrdered { path -> - val destPath = pluginsDir / path.fileName.toString() + val destPath = cordappsDir / path.fileName.toString() try { // Try making a symlink to make things faster and use less disk space. Files.createSymbolicLink(destPath, path) - } catch(e: UnsupportedOperationException) { + } catch (e: UnsupportedOperationException) { // OS doesn't support symbolic links? Files.copy(path, destPath, REPLACE_EXISTING) } catch (e: java.nio.file.FileAlreadyExistsException) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt similarity index 56% rename from tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt rename to tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt index 52a388b7a7..de0a2607b5 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt @@ -2,6 +2,6 @@ package net.corda.demobench.model import java.nio.file.Path -interface HasPlugins { - val pluginDir: Path +interface HasCordapps { + val cordappsDir: Path } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index e2eeb63203..d2dba4a687 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -1,78 +1,46 @@ package net.corda.demobench.model import com.typesafe.config.Config -import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType +import net.corda.nodeapi.config.parseAs import tornadofx.* import java.io.IOException import java.nio.file.Files import java.nio.file.Path class InstallFactory : Controller() { - private val nodeController by inject() - private val serviceController by inject() @Throws(IOException::class) fun toInstallConfig(config: Config, baseDir: Path): InstallConfig { - val p2pPort = config.parsePort("p2pAddress") - val rpcPort = config.parsePort("rpcAddress") - val webPort = config.parsePort("webAddress") - val h2Port = config.getInt("h2port") - val x500name = CordaX500Name.parse(config.getString("myLegalName")) - val extraServices = config.parseExtraServices("extraAdvertisedServiceIds") - val tempDir = Files.createTempDirectory(baseDir, ".node") - - val nodeConfig = NodeConfig( - tempDir, - x500name, - p2pPort, - rpcPort, - webPort, - h2Port, - extraServices, - config.getObjectList("rpcUsers").map { toUser(it.unwrapped()) }.toList() - ) - - if (config.hasPath("networkMapService")) { - val nmap = config.getConfig("networkMapService") - nodeConfig.networkMap = NetworkMapConfig(CordaX500Name.parse(nmap.getString("legalName")), nmap.parsePort("address")) - } else { - log.info("Node '${nodeConfig.legalName}' is the network map") + fun NetworkHostAndPort.checkPort() { + require(nodeController.isPortValid(port)) { "Invalid port $port" } } - return InstallConfig(tempDir, nodeConfig) - } + val nodeConfig = config.parseAs() + nodeConfig.p2pAddress.checkPort() + nodeConfig.rpcAddress.checkPort() + nodeConfig.webAddress.checkPort() - private fun Config.parsePort(path: String): Int { - val address = this.getString(path) - val port = NetworkHostAndPort.parse(address).port - require(nodeController.isPortValid(port), { "Invalid port $port from '$path'." }) - return port - } + val tempDir = Files.createTempDirectory(baseDir, ".node") - private fun Config.parseExtraServices(path: String): MutableList { - val services = serviceController.services.toSortedSet() + ServiceInfo(ServiceType.networkMap).toString() - return this.getStringList(path) - .filter { !it.isNullOrEmpty() } - .map { svc -> - require(svc in services, { "Unknown service '$svc'." }) - svc - }.toMutableList() - } + if (nodeConfig.isNetworkMap) { + log.info("Node '${nodeConfig.myLegalName}' is the network map") + } + return InstallConfig(tempDir, NodeConfigWrapper(tempDir, nodeConfig)) + } } /** * Wraps the configuration information for a Node * which isn't ready to be instantiated yet. */ -class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfig) : HasPlugins { +class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfigWrapper) : HasCordapps { val key = config.key - override val pluginDir: Path = baseDir.resolve("plugins") + override val cordappsDir: Path = baseDir / "cordapps" fun deleteBaseDir(): Boolean = baseDir.toFile().deleteRecursively() - fun installTo(installDir: Path) = config.moveTo(installDir) + fun installTo(installDir: Path) = config.copy(baseDir = installDir) } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt deleted file mode 100644 index 3960ee9a24..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.demobench.model - -import net.corda.core.identity.CordaX500Name - -open class NetworkMapConfig(val legalName: CordaX500Name, val p2pPort: Int) { - val key: String = legalName.organisation.toKey() -} - -fun String.stripWhitespace() = String(this.filter { !it.isWhitespace() }.toCharArray()) -fun String.toKey() = stripWhitespace().toLowerCase() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 141a9e1d68..504fdfab79 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -1,85 +1,78 @@ package net.corda.demobench.model -import com.typesafe.config.* +import com.typesafe.config.ConfigRenderOptions import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.User -import java.io.File -import java.nio.file.Files +import net.corda.nodeapi.config.toConfig import java.nio.file.Path import java.nio.file.StandardCopyOption -class NodeConfig( - baseDir: Path, - legalName: CordaX500Name, - p2pPort: Int, - val rpcPort: Int, - val webPort: Int, - val h2Port: Int, - val extraServices: MutableList = mutableListOf(), - val users: List = listOf(defaultUser), - var networkMap: NetworkMapConfig? = null -) : NetworkMapConfig(legalName, p2pPort), HasPlugins { - +/** + * This is a subset of FullNodeConfiguration, containing only those configs which we need. The node uses reference.conf + * to fill in the defaults so we're not required to specify them here. + */ +data class NodeConfig( + val myLegalName: CordaX500Name, + val p2pAddress: NetworkHostAndPort, + val rpcAddress: NetworkHostAndPort, + /** This is not used by the node but by the webserver which looks at node.conf. */ + val webAddress: NetworkHostAndPort, + val notary: NotaryService?, + val networkMapService: NetworkMapConfig?, + val h2port: Int, + val rpcUsers: List = listOf(defaultUser), + /** This is an extra config used by the Cash app. */ + val issuableCurrencies: List = emptyList() +) { companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) val defaultUser = user("guest") + val cordappDirName = "cordapps" } - val nearestCity: String = legalName.locality - val nodeDir: Path = baseDir.resolve(key) - override val pluginDir: Path = nodeDir.resolve("plugins") - val explorerDir: Path = baseDir.resolve("$key-explorer") + @Suppress("unused") + private val detectPublicIp = false + @Suppress("unused") + private val useTestClock = true + val isNetworkMap: Boolean get() = networkMapService == null + + fun toText(): String = toConfig().root().render(renderOptions) +} + +/** + * This is a mirror of NetworkMapInfo. + */ +data class NetworkMapConfig(val legalName: CordaX500Name, val address: NetworkHostAndPort) + +/** + * This is a subset of NotaryConfig. It implements [ExtraService] to avoid unnecessary copying. + */ +data class NotaryService(val validating: Boolean) : ExtraService { + override fun toString(): String = "${if (validating) "V" else "Non-v"}alidating Notary" +} + +// TODO Think of a better name +data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : HasCordapps { + val key: String = nodeConfig.myLegalName.organisation.toKey() + val nodeDir: Path = baseDir / key + val explorerDir: Path = baseDir / "$key-explorer" + override val cordappsDir: Path = nodeDir / NodeConfig.cordappDirName var state: NodeState = NodeState.STARTING - val isCashIssuer: Boolean = extraServices.any { - it.startsWith("corda.issuer.") - } - - fun isNetworkMap(): Boolean = networkMap == null - - /* - * The configuration object depends upon the networkMap, - * which is mutable. - */ - fun toFileConfig(): Config = ConfigFactory.empty() - .withValue("myLegalName", valueFor(legalName.toString())) - .withValue("p2pAddress", addressValueFor(p2pPort)) - .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) - .withFallback(optional("networkMapService", networkMap, { c, n -> - c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.legalName.toString())) - })) - .withValue("webAddress", addressValueFor(webPort)) - .withValue("rpcAddress", addressValueFor(rpcPort)) - .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) - .withValue("h2port", valueFor(h2Port)) - .withValue("useTestClock", valueFor(true)) - .withValue("detectPublicIp", valueFor(false)) - - fun toText(): String = toFileConfig().root().render(renderOptions) - - fun moveTo(baseDir: Path) = NodeConfig( - baseDir, legalName, p2pPort, rpcPort, webPort, h2Port, extraServices, users, networkMap - ) - - fun install(plugins: Collection) { - if (plugins.isNotEmpty() && pluginDir.toFile().forceDirectory()) { - plugins.forEach { - Files.copy(it, pluginDir.resolve(it.fileName.toString()), StandardCopyOption.REPLACE_EXISTING) - } + fun install(cordapps: Collection) { + if (cordapps.isEmpty()) return + cordappsDir.createDirectories() + for (cordapp in cordapps) { + cordapp.copyToDirectory(cordappsDir, StandardCopyOption.REPLACE_EXISTING) } } - } -private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) +fun user(name: String) = User(name, "letmein", setOf("ALL")) -private fun addressValueFor(port: Int) = valueFor("localhost:$port") - -private inline fun optional(path: String, obj: T?, body: (Config, T) -> Config): Config { - val config = ConfigFactory.empty() - return if (obj == null) config else body(config, obj).atPath(path) -} - -fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs() +fun String.toKey() = filter { !it.isWhitespace() }.toLowerCase() 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 798c3609e1..ddd8010bb4 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 @@ -1,10 +1,14 @@ package net.corda.demobench.model +import javafx.beans.binding.IntegerExpression import net.corda.core.identity.CordaX500Name -import net.corda.demobench.plugin.PluginController +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.noneOrSingle +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.demobench.plugin.CordappController import net.corda.demobench.pty.R3Pty -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import tornadofx.* import java.io.IOException import java.lang.management.ManagementFactory @@ -23,20 +27,22 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { } private val jvm by inject() - private val pluginController by inject() + private val cordappController by inject() + private val nodeInfoFilesCopier by inject() private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") private val command = jvm.commandFor(cordaPath).toTypedArray() - private val nodes = LinkedHashMap() + private val nodes = LinkedHashMap() private val port = AtomicInteger(firstPort) private var networkMapConfig: NetworkMapConfig? = null - val activeNodes: List get() = nodes.values.filter { - (it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING) - } + val activeNodes: List + get() = nodes.values.filter { + (it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING) + } init { log.info("Base directory: $baseDir") @@ -50,38 +56,49 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { /** * Validate a Node configuration provided by [net.corda.demobench.views.NodeTabView]. */ - fun validate(nodeData: NodeData): NodeConfig? { + fun validate(nodeData: NodeData): NodeConfigWrapper? { + fun IntegerExpression.toLocalAddress() = NetworkHostAndPort("localhost", value) + val location = nodeData.nearestCity.value - val config = NodeConfig( - baseDir, - CordaX500Name( + val nodeConfig = NodeConfig( + myLegalName = CordaX500Name( organisation = nodeData.legalName.value.trim(), locality = location.description, country = location.countryCode ), - nodeData.p2pPort.value, - nodeData.rpcPort.value, - nodeData.webPort.value, - nodeData.h2Port.value, - nodeData.extraServices.toMutableList() + p2pAddress = nodeData.p2pPort.toLocalAddress(), + rpcAddress = nodeData.rpcPort.toLocalAddress(), + webAddress = nodeData.webPort.toLocalAddress(), + notary = nodeData.extraServices.filterIsInstance().noneOrSingle(), + networkMapService = networkMapConfig, // The first node becomes the network map + h2port = nodeData.h2Port.value, + issuableCurrencies = nodeData.extraServices.filterIsInstance().map { it.currency.toString() } ) - if (nodes.putIfAbsent(config.key, config) != null) { - log.warning("Node with key '${config.key}' already exists.") + val wrapper = NodeConfigWrapper(baseDir, nodeConfig) + + if (nodes.putIfAbsent(wrapper.key, wrapper) != null) { + log.warning("Node with key '${wrapper.key}' already exists.") return null } - // The first node becomes our network map - chooseNetworkMap(config) + if (nodeConfig.isNetworkMap) { + networkMapConfig = nodeConfig.let { NetworkMapConfig(it.myLegalName, it.p2pAddress) } + log.info("Network map provided by: ${nodeConfig.myLegalName}") + } - return config + nodeInfoFilesCopier.addConfig(wrapper) + + return wrapper } - fun dispose(config: NodeConfig) { + fun dispose(config: NodeConfigWrapper) { config.state = NodeState.DEAD - if (config.networkMap == null) { - log.warning("Network map service (Node '${config.legalName}') has exited.") + nodeInfoFilesCopier.removeConfig(config) + + if (config.nodeConfig.isNetworkMap) { + log.warning("Network map service (Node '${config.nodeConfig.myLegalName}') has exited.") } } @@ -95,40 +112,26 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { fun hasNetworkMap(): Boolean = networkMapConfig != null - private fun chooseNetworkMap(config: NodeConfig) { - if (hasNetworkMap()) { - config.networkMap = networkMapConfig - } else { - config.extraServices.add(ServiceInfo(ServiceType.networkMap).toString()) - networkMapConfig = config - log.info("Network map provided by: ${config.legalName}") - } - } + fun runCorda(pty: R3Pty, config: NodeConfigWrapper): Boolean { + try { + config.nodeDir.createDirectories() - fun runCorda(pty: R3Pty, config: NodeConfig): Boolean { - val nodeDir = config.nodeDir.toFile() + // Install any built-in plugins into the working directory. + cordappController.populate(config) - if (nodeDir.forceDirectory()) { - try { - // Install any built-in plugins into the working directory. - pluginController.populate(config) + // Write this node's configuration file into its working directory. + val confFile = config.nodeDir / "node.conf" + Files.write(confFile, config.nodeConfig.toText().toByteArray()) - // Write this node's configuration file into its working directory. - val confFile = nodeDir.resolve("node.conf") - confFile.writeText(config.toText()) - - // Execute the Corda node - val cordaEnv = System.getenv().toMutableMap().apply { - jvm.setCapsuleCacheDir(this) - } - pty.run(command, cordaEnv, nodeDir.toString()) - log.info("Launched node: ${config.legalName}") - return true - } catch (e: Exception) { - log.log(Level.SEVERE, "Failed to launch Corda: ${e.message}", e) - return false + // Execute the Corda node + val cordaEnv = System.getenv().toMutableMap().apply { + jvm.setCapsuleCacheDir(this) } - } else { + pty.run(command, cordaEnv, config.nodeDir.toString()) + log.info("Launched node: ${config.nodeConfig.myLegalName}") + return true + } catch (e: Exception) { + log.log(Level.SEVERE, "Failed to launch Corda: ${e.message}", e) return false } } @@ -140,20 +143,22 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Wipe out any knowledge of previous nodes. networkMapConfig = null nodes.clear() + nodeInfoFilesCopier.reset() } /** * Add a [NodeConfig] object that has been loaded from a profile. */ - fun register(config: NodeConfig): Boolean { + fun register(config: NodeConfigWrapper): Boolean { if (nodes.putIfAbsent(config.key, config) != null) { return false } + nodeInfoFilesCopier.addConfig(config) - updatePort(config) + updatePort(config.nodeConfig) - if ((networkMapConfig == null) && config.isNetworkMap()) { - networkMapConfig = config + if (networkMapConfig == null && config.nodeConfig.isNetworkMap) { + networkMapConfig = config.nodeConfig.let { NetworkMapConfig(it.myLegalName, it.p2pAddress) } } return true @@ -163,12 +168,12 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { * Creates a node directory that can host a running instance of Corda. */ @Throws(IOException::class) - fun install(config: InstallConfig): NodeConfig { + fun install(config: InstallConfig): NodeConfigWrapper { val installed = config.installTo(baseDir) - pluginController.userPluginsFor(config).forEach { - val pluginDir = Files.createDirectories(installed.pluginDir) - val plugin = Files.copy(it, pluginDir.resolve(it.fileName.toString())) + cordappController.useCordappsFor(config).forEach { + installed.cordappsDir.createDirectories() + val plugin = it.copyToDirectory(installed.cordappsDir) log.info("Installed: $plugin") } @@ -180,7 +185,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { } private fun updatePort(config: NodeConfig) { - val nextPort = 1 + arrayOf(config.p2pPort, config.rpcPort, config.webPort, config.h2Port).max() as Int + val nextPort = 1 + arrayOf(config.p2pAddress.port, config.rpcAddress.port, config.webAddress.port, config.h2port).max() as Int port.getAndUpdate { Math.max(nextPort, it) } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt index 90147c5b43..7abdd09775 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -4,8 +4,10 @@ import javafx.beans.property.SimpleIntegerProperty import javafx.beans.property.SimpleListProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty +import javafx.collections.FXCollections.observableArrayList import net.corda.finance.utils.CityDatabase import tornadofx.* +import java.util.* object SuggestedDetails { val banks = listOf( @@ -35,7 +37,7 @@ class NodeData { val rpcPort = SimpleIntegerProperty() val webPort = SimpleIntegerProperty() val h2Port = SimpleIntegerProperty() - val extraServices = SimpleListProperty(mutableListOf().observable()) + val extraServices = SimpleListProperty(observableArrayList()) } class NodeDataModel : ItemViewModel(NodeData()) { @@ -46,3 +48,9 @@ class NodeDataModel : ItemViewModel(NodeData()) { val webPort = bind { item?.webPort } val h2Port = bind { item?.h2Port } } + +interface ExtraService + +data class CurrencyIssuer(val currency: Currency) : ExtraService { + override fun toString(): String = "Issuer $currency" +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeInfoFilesCopier.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeInfoFilesCopier.kt new file mode 100644 index 0000000000..ed69e38b93 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeInfoFilesCopier.kt @@ -0,0 +1,133 @@ +package net.corda.demobench.model + +import net.corda.cordform.CordformNode +import net.corda.core.internal.createDirectories +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.list +import rx.Observable +import rx.Scheduler +import rx.schedulers.Schedulers +import tornadofx.* +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption.COPY_ATTRIBUTES +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime +import java.util.concurrent.TimeUnit +import java.util.logging.Level + +/** + * Utility class which copies nodeInfo files across a set of running nodes. + * + * This class will create paths that it needs to poll and to where it needs to copy files in case those + * don't exist yet. + */ +class NodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()): Controller() { + + private val nodeDataMap = mutableMapOf() + + init { + Observable.interval(5, TimeUnit.SECONDS, scheduler) + .subscribe { poll() } + } + + /** + * @param nodeConfig the configuration to be added. + * Add a [NodeConfig] for a node which is about to be started. + * Its nodeInfo file will be copied to other nodes' additional-node-infos directory, and conversely, + * other nodes' nodeInfo files will be copied to this node additional-node-infos directory. + */ + @Synchronized + fun addConfig(nodeConfig: NodeConfigWrapper) { + val newNodeFile = NodeData(nodeConfig.nodeDir) + nodeDataMap[nodeConfig.nodeDir] = newNodeFile + + for (previouslySeenFile in allPreviouslySeenFiles()) { + copy(previouslySeenFile, newNodeFile.destination.resolve(previouslySeenFile.fileName)) + } + log.info("Now watching: ${nodeConfig.nodeDir}") + } + + /** + * @param nodeConfig the configuration to be removed. + * Remove the configuration of a node which is about to be stopped or already stopped. + * No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this + * one. + */ + @Synchronized + fun removeConfig(nodeConfig: NodeConfigWrapper) { + nodeDataMap.remove(nodeConfig.nodeDir) ?: return + log.info("Stopped watching: ${nodeConfig.nodeDir}") + } + + @Synchronized + fun reset() { + nodeDataMap.clear() + } + + private fun allPreviouslySeenFiles() = nodeDataMap.values.map { it.previouslySeenFiles.keys }.flatten() + + @Synchronized + private fun poll() { + for (nodeData in nodeDataMap.values) { + nodeData.nodeDir.list { paths -> + paths.filter { it.isRegularFile() } + .filter { it.fileName.toString().startsWith("nodeInfo-") } + .forEach { path -> processPath(nodeData, path) } + } + } + } + + // Takes a path under nodeData config dir and decides whether the file represented by that path needs to + // be copied. + private fun processPath(nodeData: NodeData, path: Path) { + val newTimestamp = Files.readAttributes(path, BasicFileAttributes::class.java).lastModifiedTime() + val previousTimestamp = nodeData.previouslySeenFiles.put(path, newTimestamp) ?: FileTime.fromMillis(-1) + if (newTimestamp > previousTimestamp) { + for (destination in nodeDataMap.values.filter { it.nodeDir != nodeData.nodeDir }.map { it.destination }) { + val fullDestinationPath = destination.resolve(path.fileName) + copy(path, fullDestinationPath) + } + } + } + + private fun copy(source: Path, destination: Path) { + val tempDestination = try { + Files.createTempFile(destination.parent, ".", null) + } catch (exception: IOException) { + log.log(Level.WARNING, "Couldn't create a temporary file to copy $source", exception) + throw exception + } + try { + // First copy the file to a temporary file within the appropriate directory. + Files.copy(source, tempDestination, COPY_ATTRIBUTES, REPLACE_EXISTING) + } catch (exception: IOException) { + log.log(Level.WARNING, "Couldn't copy $source to $tempDestination.", exception) + Files.delete(tempDestination) + throw exception + } + try { + // Then rename it to the desired name. This way the file 'appears' on the filesystem as an atomic operation. + Files.move(tempDestination, destination, REPLACE_EXISTING) + } catch (exception: IOException) { + log.log(Level.WARNING, "Couldn't move $tempDestination to $destination.", exception) + Files.delete(tempDestination) + throw exception + } + } + + /** + * Convenience holder for all the paths and files relative to a single node. + */ + private class NodeData(val nodeDir: Path) { + val destination: Path = nodeDir.resolve(CordformNode.NODE_INFO_DIRECTORY) + // Map from Path to its lastModifiedTime. + val previouslySeenFiles = mutableMapOf() + + init { + destination.createDirectories() + } + } +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt deleted file mode 100644 index cfd1c23038..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.demobench.model - -import tornadofx.* -import java.io.IOException -import java.io.InputStreamReader -import java.net.URL -import java.util.logging.Level - -class ServiceController(resourceName: String = "/services.conf") : Controller() { - - val services: List = loadConf(resources.url(resourceName)) - - val notaries: List = services.filter { it.startsWith("corda.notary.") }.toList() - - /* - * Load our list of known extra Corda services. - */ - private fun loadConf(url: URL?): List { - return if (url == null) { - emptyList() - } else { - try { - val set = sortedSetOf() - InputStreamReader(url.openStream()).useLines { sq -> - sq.forEach { line -> - val service = line.trim() - set.add(service) - - log.info("Supports: $service") - } - } - set.toList() - } catch (e: IOException) { - log.log(Level.SEVERE, "Failed to load $url: ${e.message}", e) - emptyList() - } - } - } -} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt deleted file mode 100644 index aadbeb7e06..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt +++ /dev/null @@ -1,15 +0,0 @@ -@file:JvmName("User") - -package net.corda.demobench.model - -import net.corda.nodeapi.User -import java.util.* - -@Suppress("UNCHECKED_CAST") -fun toUser(map: Map) = User( - map.getOrElse("username", { "none" }) as String, - map.getOrElse("password", { "none" }) as String, - LinkedHashSet(map.getOrElse("permissions", { emptyList() }) as Collection) -) - -fun user(name: String) = User(name, "letmein", setOf("ALL")) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt new file mode 100644 index 0000000000..765824f4e8 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt @@ -0,0 +1,61 @@ +package net.corda.demobench.plugin + +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.exists +import net.corda.demobench.model.HasCordapps +import net.corda.demobench.model.JVMConfig +import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper +import tornadofx.* +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.stream.Stream + +class CordappController : Controller() { + + private val jvm by inject() + private val cordappDir: Path = jvm.applicationDir.resolve(NodeConfig.cordappDirName) + private val bankOfCorda: Path = cordappDir.resolve("bank-of-corda.jar") + private val finance: Path = cordappDir.resolve("corda-finance.jar") + + /** + * Install any built-in cordapps that this node requires. + */ + @Throws(IOException::class) + fun populate(config: NodeConfigWrapper) { + if (!config.cordappsDir.exists()) { + config.cordappsDir.createDirectories() + } + if (finance.exists()) { + finance.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) + log.info("Installed 'Finance' cordapp") + } + // Nodes cannot issue cash unless they contain the "Bank of Corda" cordapp. + if (config.nodeConfig.issuableCurrencies.isNotEmpty() && bankOfCorda.exists()) { + bankOfCorda.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) + log.info("Installed 'Bank of Corda' cordapp") + } + } + + /** + * Generates a stream of a node's non-built-in cordapps. + */ + @Throws(IOException::class) + fun useCordappsFor(config: HasCordapps): Stream = walkCordapps(config.cordappsDir) + .filter { !bankOfCorda.endsWith(it.fileName) } + .filter { !finance.endsWith(it.fileName) } + + private fun walkCordapps(cordappsDir: Path): Stream { + return if (Files.isDirectory(cordappsDir)) + Files.walk(cordappsDir, 1).filter(Path::isCordapp) + else + Stream.empty() + } + +} + +fun Path.isCordapp(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar") +fun Path.inCordappsDir(): Boolean = (this.parent != null) && this.parent.endsWith("cordapps/") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt deleted file mode 100644 index 3e9d52f5ca..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.corda.demobench.plugin - -import net.corda.core.internal.copyToDirectory -import net.corda.core.internal.createDirectories -import net.corda.core.internal.exists -import net.corda.demobench.model.HasPlugins -import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeConfig -import tornadofx.* -import java.io.IOException -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption -import java.util.stream.Stream - -class PluginController : Controller() { - - private val jvm by inject() - private val pluginDir: Path = jvm.applicationDir.resolve("plugins") - private val bankOfCorda: Path = pluginDir.resolve("bank-of-corda.jar") - private val finance: Path = pluginDir.resolve("corda-finance.jar") - - /** - * Install any built-in plugins that this node requires. - */ - @Throws(IOException::class) - fun populate(config: NodeConfig) { - if (!config.pluginDir.exists()) { - config.pluginDir.createDirectories() - } - if (finance.exists()) { - finance.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) - log.info("Installed 'Finance' plugin") - } - // Nodes cannot issue cash unless they contain the "Bank of Corda" plugin. - if (config.isCashIssuer && bankOfCorda.exists()) { - bankOfCorda.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) - log.info("Installed 'Bank of Corda' plugin") - } - } - - /** - * Generates a stream of a node's non-built-in plugins. - */ - @Throws(IOException::class) - fun userPluginsFor(config: HasPlugins): Stream = walkPlugins(config.pluginDir) - .filter { !bankOfCorda.endsWith(it.fileName) } - .filter { !finance.endsWith(it.fileName) } - - private fun walkPlugins(pluginDir: Path): Stream { - return if (Files.isDirectory(pluginDir)) - Files.walk(pluginDir, 1).filter(Path::isPlugin) - else - Stream.empty() - } - -} - -fun Path.isPlugin(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar") -fun Path.inPluginsDir(): Boolean = (this.parent != null) && this.parent.endsWith("plugins/") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt index 5d9a773466..9f8a380b38 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -4,13 +4,12 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import javafx.stage.FileChooser import javafx.stage.FileChooser.ExtensionFilter -import net.corda.demobench.model.InstallConfig -import net.corda.demobench.model.InstallFactory -import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeController -import net.corda.demobench.plugin.PluginController -import net.corda.demobench.plugin.inPluginsDir -import net.corda.demobench.plugin.isPlugin +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.demobench.model.* +import net.corda.demobench.plugin.CordappController +import net.corda.demobench.plugin.inCordappsDir +import net.corda.demobench.plugin.isCordapp import tornadofx.* import java.io.File import java.io.IOException @@ -29,7 +28,7 @@ class ProfileController : Controller() { private val jvm by inject() private val baseDir: Path = jvm.dataHome private val nodeController by inject() - private val pluginController by inject() + private val cordappController by inject() private val installFactory by inject() private val chooser = FileChooser() @@ -58,15 +57,15 @@ class ProfileController : Controller() { FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs -> configs.forEach { config -> // Write the configuration file. - val nodeDir = Files.createDirectories(fs.getPath(config.key)) - val file = Files.write(nodeDir.resolve("node.conf"), config.toText().toByteArray(UTF_8)) + val nodeDir = fs.getPath(config.key).createDirectories() + val file = Files.write(nodeDir / "node.conf", config.nodeConfig.toText().toByteArray(UTF_8)) log.info("Wrote: $file") - // Write all of the non-built-in plugins. - val pluginDir = Files.createDirectory(nodeDir.resolve("plugins")) - pluginController.userPluginsFor(config).forEach { - val plugin = Files.copy(it, pluginDir.resolve(it.fileName.toString())) - log.info("Wrote: $plugin") + // Write all of the non-built-in cordapps. + val cordappDir = Files.createDirectory(nodeDir.resolve(NodeConfig.cordappDirName)) + cordappController.useCordappsFor(config).forEach { + val cordapp = Files.copy(it, cordappDir.resolve(it.fileName.toString())) + log.info("Wrote: $cordapp") } } } @@ -118,16 +117,16 @@ class ProfileController : Controller() { // Now extract all of the plugins from the ZIP file, // and copy them to a temporary location. StreamSupport.stream(fs.rootDirectories.spliterator(), false) - .flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inPluginsDir() && p.isPlugin() && attr.isRegularFile }) } - .forEach { plugin -> - val config = nodeIndex[plugin.getName(0).toString()] ?: return@forEach + .flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inCordappsDir() && p.isCordapp() && attr.isRegularFile }) } + .forEach { cordapp -> + val config = nodeIndex[cordapp.getName(0).toString()] ?: return@forEach try { - val pluginDir = Files.createDirectories(config.pluginDir) - Files.copy(plugin, pluginDir.resolve(plugin.fileName.toString())) - log.info("Loaded: $plugin") + val cordappDir = Files.createDirectories(config.cordappsDir) + Files.copy(cordapp, cordappDir.resolve(cordapp.fileName.toString())) + log.info("Loaded: $cordapp") } catch (e: Exception) { - log.log(Level.SEVERE, "Failed to extract '$plugin': ${e.message}", e) + log.log(Level.SEVERE, "Failed to extract '$cordapp': ${e.message}", e) configs.forEach { c -> c.deleteBaseDir() } throw e } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt index afc91bb6f4..cd82181367 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -21,7 +21,7 @@ class R3Pty(val name: CordaX500Name, settings: SettingsProvider, dimension: Dime val terminal = JediTermWidget(dimension, settings) - val isConnected: Boolean get() = terminal.ttyConnector?.isConnected ?: false + val isConnected: Boolean get() = terminal.ttyConnector?.isConnected == true override fun close() { log.info("Closing terminal '{}'", name) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt index 339eed00ad..a92dbd4a3d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -5,18 +5,17 @@ import net.corda.client.rpc.CordaRPCConnection import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper import java.util.* import java.util.concurrent.TimeUnit.SECONDS -class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invoke: (CordaRPCOps) -> Unit) : AutoCloseable { - +class NodeRPC(config: NodeConfigWrapper, start: (NodeConfigWrapper, CordaRPCOps) -> Unit, invoke: (CordaRPCOps) -> Unit) : AutoCloseable { private companion object { val log = loggerFor() val oneSecond = SECONDS.toMillis(1) } - private val rpcClient = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) + private val rpcClient = CordaRPCClient(NetworkHostAndPort("localhost", config.nodeConfig.rpcAddress.port)) private var rpcConnection: CordaRPCConnection? = null private val timer = Timer() @@ -24,7 +23,7 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo val setupTask = object : TimerTask() { override fun run() { try { - val user = config.users.elementAt(0) + val user = config.nodeConfig.rpcUsers[0] val connection = rpcClient.start(user.username, user.password) rpcConnection = connection val ops = connection.proxy @@ -42,7 +41,7 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo } }, 0, oneSecond) } catch (e: Exception) { - log.warn("Node '{}' not ready yet (Error: {})", config.legalName, e.message) + log.warn("Node '{}' not ready yet (Error: {})", config.nodeConfig.myLegalName, e.message) } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index 8f3ec2d19d..c9f993a5c2 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -5,6 +5,7 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory import javafx.application.Platform import javafx.beans.InvalidationListener +import javafx.collections.ListChangeListener import javafx.geometry.Pos import javafx.scene.control.ComboBox import javafx.scene.image.Image @@ -15,10 +16,14 @@ import javafx.scene.layout.Priority import javafx.stage.FileChooser import javafx.util.StringConverter import net.corda.core.internal.* -import net.corda.finance.utils.CityDatabase -import net.corda.finance.utils.WorldMapLocation import net.corda.demobench.model.* import net.corda.demobench.ui.CloseableTab +import net.corda.finance.CHF +import net.corda.finance.EUR +import net.corda.finance.GBP +import net.corda.finance.USD +import net.corda.finance.utils.CityDatabase +import net.corda.finance.utils.WorldMapLocation import org.controlsfx.control.CheckListView import tornadofx.* import java.nio.file.Path @@ -38,10 +43,10 @@ class NodeTabView : Fragment() { val cordappPathsFile: Path = jvm.dataHome / "cordapp-paths.txt" fun loadDefaultCordappPaths(): MutableList { - if (cordappPathsFile.exists()) - return cordappPathsFile.readAllLines().map { Paths.get(it) }.filter { it.exists() }.toMutableList() + return if (cordappPathsFile.exists()) + cordappPathsFile.readAllLines().map { Paths.get(it) }.filter { it.exists() }.toMutableList() else - return ArrayList() + ArrayList() } // This is shared between tabs. @@ -57,11 +62,9 @@ class NodeTabView : Fragment() { } private val nodeController by inject() - private val serviceController by inject() private val chooser = FileChooser() private val model = NodeDataModel() - private val availableServices: List = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries private val nodeTerminalView = find() private val nodeConfigView = stackpane { @@ -109,14 +112,30 @@ class NodeTabView : Fragment() { } } - fieldset("Services") { + fieldset("Additional configuration") { styleClass.addAll("services-panel") + val extraServices = if (nodeController.hasNetworkMap()) { + listOf(USD, GBP, CHF, EUR).map { CurrencyIssuer(it) } + } else { + listOf(NotaryService(true), NotaryService(false)) + } - val servicesList = CheckListView(availableServices.observable()).apply { + val servicesList = CheckListView(extraServices.observable()).apply { vboxConstraints { vGrow = Priority.ALWAYS } model.item.extraServices.set(checkModel.checkedItems) if (!nodeController.hasNetworkMap()) { checkModel.check(0) + checkModel.checkedItems.addListener(ListChangeListener { change -> + while (change.next()) { + if (change.wasAdded()) { + val item = change.addedSubList.last() + val idx = checkModel.getItemIndex(item) + checkModel.checkedIndices.forEach { + if (it != idx) checkModel.clearCheck(it) + } + } + } + }) } } add(servicesList) @@ -251,17 +270,17 @@ class NodeTabView : Fragment() { /** * Launches a preconfigured Corda node, e.g. from a saved profile. */ - fun launch(config: NodeConfig) { + fun launch(config: NodeConfigWrapper) { nodeController.register(config) launchNode(config) } - private fun launchNode(config: NodeConfig) { - val countryCode = CityDatabase.cityMap[config.nearestCity]?.countryCode + private fun launchNode(config: NodeConfigWrapper) { + val countryCode = CityDatabase.cityMap[config.nodeConfig.myLegalName.locality]?.countryCode if (countryCode != null) { nodeTab.graphic = ImageView(flags.get()[countryCode]).apply { fitWidth = 24.0; isPreserveRatio = true } } - nodeTab.text = config.legalName.organisation + nodeTab.text = config.nodeConfig.myLegalName.organisation nodeTerminalView.open(config) { exitCode -> Platform.runLater { if (exitCode == 0) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index a19fcbc484..d1e2f59492 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -20,6 +20,7 @@ import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.vault.PageSpecification import net.corda.demobench.explorer.ExplorerController import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.model.NodeController import net.corda.demobench.model.NodeState import net.corda.demobench.pty.R3Pty @@ -69,8 +70,8 @@ class NodeTerminalView : Fragment() { private lateinit var logo: ImageView private lateinit var swingTerminal: SwingNode - fun open(config: NodeConfig, onExit: (Int) -> Unit) { - nodeName.text = config.legalName.organisation + fun open(config: NodeConfigWrapper, onExit: (Int) -> Unit) { + nodeName.text = config.nodeConfig.myLegalName.organisation swingTerminal = SwingNode() swingTerminal.setOnMouseClicked { @@ -85,13 +86,13 @@ class NodeTerminalView : Fragment() { root.isVisible = true SwingUtilities.invokeLater({ - val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80), onExit) + val r3pty = R3Pty(config.nodeConfig.myLegalName, TerminalSettingsProvider(), Dimension(160, 80), onExit) pty = r3pty if (nodeController.runCorda(r3pty, config)) { swingTerminal.content = r3pty.terminal - configureDatabaseButton(config) + configureDatabaseButton(config.nodeConfig) configureExplorerButton(config) configureWebButton(config) @@ -105,7 +106,7 @@ class NodeTerminalView : Fragment() { * and close the RPC client if it has. */ if (!r3pty.isConnected) { - log.severe("Node '${config.legalName}' has failed to start.") + log.severe("Node '${config.nodeConfig.myLegalName}' has failed to start.") swingTerminal.content = null rpc?.close() } @@ -119,7 +120,7 @@ class NodeTerminalView : Fragment() { * launched the explorer and only reenable it when * the explorer has exited. */ - private fun configureExplorerButton(config: NodeConfig) { + private fun configureExplorerButton(config: NodeConfigWrapper) { launchExplorerButton.setOnAction { launchExplorerButton.isDisable = true @@ -131,7 +132,7 @@ class NodeTerminalView : Fragment() { private fun configureDatabaseButton(config: NodeConfig) { viewDatabaseButton.setOnAction { - viewer.openBrowser(config.h2Port) + viewer.openBrowser(config.h2port) } } @@ -144,7 +145,7 @@ class NodeTerminalView : Fragment() { * launched the web server and only reenable it when * the web server has exited. */ - private fun configureWebButton(config: NodeConfig) { + private fun configureWebButton(config: NodeConfigWrapper) { launchWebButton.setOnAction { if (webURL != null) { app.hostServices.showDocument(webURL.toString()) @@ -161,12 +162,12 @@ class NodeTerminalView : Fragment() { launchWebButton.text = "" launchWebButton.graphic = ProgressIndicator() - log.info("Starting web server for ${config.legalName}") + log.info("Starting web server for ${config.nodeConfig.myLegalName}") webServer.open(config).then { Platform.runLater { launchWebButton.graphic = null it.match(success = { - log.info("Web server for ${config.legalName} started on $it") + log.info("Web server for ${config.nodeConfig.myLegalName} started on $it") webURL = it launchWebButton.text = "Reopen\nweb site" app.hostServices.showDocument(it.toString()) @@ -178,13 +179,13 @@ class NodeTerminalView : Fragment() { } } - private fun launchRPC(config: NodeConfig) = NodeRPC( - config = config, - start = this::initialise, - invoke = this::pollCashBalances + private fun launchRPC(config: NodeConfigWrapper) = NodeRPC( + config = config, + start = this::initialise, + invoke = this::pollCashBalances ) - private fun initialise(config: NodeConfig, ops: CordaRPCOps) { + private fun initialise(config: NodeConfigWrapper, ops: CordaRPCOps) { try { val (txInit, txNext) = ops.internalVerifiedTransactionsFeed() val (stateInit, stateNext) = ops.vaultTrackBy(paging = pageSpecification) @@ -212,7 +213,7 @@ class NodeTerminalView : Fragment() { } config.state = NodeState.RUNNING - log.info("Node '${config.legalName}' is now ready.") + log.info("Node '${config.nodeConfig.myLegalName}' is now ready.") header.isDisable = false } @@ -225,7 +226,7 @@ class NodeTerminalView : Fragment() { ) Platform.runLater { - balance.value = if (cashBalances.isNullOrEmpty()) "0" else cashBalances + balance.value = if (cashBalances.isEmpty()) "0" else cashBalances } } catch (e: ClassNotFoundException) { // TODO: Remove this special case once Rick's serialisation work means we can deserialise states that weren't on our own classpath. @@ -238,7 +239,10 @@ class NodeTerminalView : Fragment() { header.isDisable = true subscriptions.forEach { // Don't allow any exceptions here to halt tab destruction. - try { it.unsubscribe() } catch (e: Exception) {} + try { + it.unsubscribe() + } catch (e: Exception) { + } } webServer.close() explorer.close() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt index 4ff4e23e01..5fe5f96fe1 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt @@ -2,11 +2,11 @@ package net.corda.demobench.web import com.google.common.util.concurrent.RateLimiter import net.corda.core.concurrent.CordaFuture -import net.corda.core.utilities.minutes import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.until import net.corda.core.utilities.loggerFor -import net.corda.demobench.model.NodeConfig +import net.corda.core.utilities.minutes +import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.readErrorLines import java.io.IOException import java.net.HttpURLConnection @@ -25,7 +25,7 @@ class WebServer internal constructor(private val webServerController: WebServerC private var process: Process? = null @Throws(IOException::class) - fun open(config: NodeConfig): CordaFuture { + fun open(config: NodeConfigWrapper): CordaFuture { val nodeDir = config.nodeDir.toFile() if (!nodeDir.isDirectory) { @@ -33,13 +33,14 @@ class WebServer internal constructor(private val webServerController: WebServerC return openFuture() } + val legalName = config.nodeConfig.myLegalName try { val p = webServerController.process() .directory(nodeDir) .start() process = p - log.info("Launched Web Server for '{}'", config.legalName) + log.info("Launched Web Server for '{}'", legalName) // Close these streams because no-one is using them. safeClose(p.outputStream) @@ -51,22 +52,22 @@ class WebServer internal constructor(private val webServerController: WebServerC process = null if (errors.isEmpty()) { - log.info("Web Server for '{}' has exited (value={})", config.legalName, exitValue) + log.info("Web Server for '{}' has exited (value={})", legalName, exitValue) } else { - log.error("Web Server for '{}' has exited (value={}, {})", config.legalName, exitValue, errors) + log.error("Web Server for '{}' has exited (value={}, {})", legalName, exitValue, errors) } } val future = openFuture() thread { future.capture { - log.info("Waiting for web server for ${config.legalName} to start ...") - waitForStart(config.webPort) + log.info("Waiting for web server for $legalName to start ...") + waitForStart(config.nodeConfig.webAddress.port) } } return future } catch (e: IOException) { - log.error("Failed to launch Web Server for '{}': {}", config.legalName, e.message) + log.error("Failed to launch Web Server for '{}': {}", legalName, e.message) throw e } } @@ -98,7 +99,7 @@ class WebServer internal constructor(private val webServerController: WebServerC conn.connect() conn.disconnect() return url - } catch(e: IOException) { + } catch (e: IOException) { } } throw TimeoutException("Web server did not start within ${timeout.seconds} seconds") diff --git a/tools/demobench/src/main/resources/services.conf b/tools/demobench/src/main/resources/services.conf deleted file mode 100644 index cb475c0ade..0000000000 --- a/tools/demobench/src/main/resources/services.conf +++ /dev/null @@ -1,8 +0,0 @@ -corda.notary.validating -corda.notary.simple -corda.interest_rates -corda.issuer.USD -corda.issuer.GBP -corda.issuer.CHF -corda.issuer.EUR -corda.cash diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt index cd662ace6a..33e9d4169b 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt @@ -3,7 +3,6 @@ package net.corda.demobench import net.corda.demobench.config.LoggingConfig import net.corda.demobench.model.JVMConfigTest import net.corda.demobench.model.NodeControllerTest -import net.corda.demobench.model.ServiceControllerTest import org.junit.BeforeClass import org.junit.runner.RunWith import org.junit.runners.Suite @@ -13,7 +12,6 @@ import org.junit.runners.Suite */ @RunWith(Suite::class) @Suite.SuiteClasses( - ServiceControllerTest::class, NodeControllerTest::class, JVMConfigTest::class ) @@ -25,7 +23,8 @@ class LoggingTestSuite { */ companion object { @BeforeClass - @JvmStatic fun `setup logging`() { + @JvmStatic + fun `setup logging`() { LoggingConfig() } } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt deleted file mode 100644 index 0c9892f340..0000000000 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.demobench.model - -import net.corda.core.identity.CordaX500Name -import org.bouncycastle.asn1.x500.X500Name -import org.junit.Ignore -import org.junit.Test -import kotlin.test.assertEquals - -class NetworkMapConfigTest { - @Ignore("This has been superseded by validation logic in CordaX500Name") - @Test - fun keyValue() { - val config = NetworkMapConfig(CordaX500Name.parse("O=My\tNasty Little\rLabel\n,L=London,C=GB"), 10000) - assertEquals("mynastylittlelabel", config.key) - } - - @Test - fun removeWhitespace() { - assertEquals("OneTwoThreeFour!", "One\tTwo \rThree\r\nFour!".stripWhitespace()) - } - -} diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 4970d68c11..0bf02c1ca8 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -1,190 +1,31 @@ package net.corda.demobench.model -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.NetworkMapInfo +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.FullNodeConfiguration import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs +import net.corda.nodeapi.config.toConfig import net.corda.testing.DUMMY_NOTARY import net.corda.webserver.WebServerConfig +import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.io.StringWriter import java.nio.file.Path import java.nio.file.Paths import kotlin.test.assertEquals import kotlin.test.assertFalse -import kotlin.test.assertNull import kotlin.test.assertTrue - class NodeConfigTest { - companion object { private val baseDir: Path = Paths.get(".").toAbsolutePath() private val myLegalName = CordaX500Name(organisation = "My Name", locality = "New York", country = "US") } - @Test - fun `test name`() { - val config = createConfig(legalName = myLegalName) - assertEquals(myLegalName, config.legalName) - assertEquals("myname", config.key) - } - - @Test - fun `test node directory`() { - val config = createConfig(legalName = myLegalName) - assertEquals(baseDir / "myname", config.nodeDir) - } - - @Test - fun `test explorer directory`() { - val config = createConfig(legalName = myLegalName) - assertEquals(baseDir / "myname-explorer", config.explorerDir) - } - - @Test - fun `test plugin directory`() { - val config = createConfig(legalName = myLegalName) - assertEquals(baseDir / "myname" / "plugins", config.pluginDir) - } - - @Test - fun `test P2P port`() { - val config = createConfig(p2pPort = 10001) - assertEquals(10001, config.p2pPort) - } - - @Test - fun `test rpc port`() { - val config = createConfig(rpcPort = 40002) - assertEquals(40002, config.rpcPort) - } - - @Test - fun `test web port`() { - val config = createConfig(webPort = 20001) - assertEquals(20001, config.webPort) - } - - @Test - fun `test H2 port`() { - val config = createConfig(h2Port = 30001) - assertEquals(30001, config.h2Port) - } - - @Test - fun `test services`() { - val config = createConfig(services = mutableListOf("my.service")) - assertEquals(listOf("my.service"), config.extraServices) - } - - @Test - fun `test users`() { - val config = createConfig(users = listOf(user("myuser"))) - assertEquals(listOf(user("myuser")), config.users) - } - - @Test - fun `test default state`() { - val config = createConfig() - assertEquals(NodeState.STARTING, config.state) - } - - @Test - fun `test network map`() { - val config = createConfig() - assertNull(config.networkMap) - assertTrue(config.isNetworkMap()) - } - - @Test - fun `test cash issuer`() { - val config = createConfig(services = mutableListOf("corda.issuer.GBP")) - assertTrue(config.isCashIssuer) - } - - @Test - fun `test not cash issuer`() { - val config = createConfig(services = mutableListOf("corda.issuerubbish")) - assertFalse(config.isCashIssuer) - } - - /** - * Reformat JSON via Jackson to ensure a consistent format for comparison purposes. - */ - private fun prettyPrint(content: String): String { - val mapper = ObjectMapper() - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) - mapper.enable(SerializationFeature.INDENT_OUTPUT) - val sw = StringWriter() - val parsed = mapper.readTree(content) - mapper.writeValue(sw, parsed) - return sw.toString() - } - - @Test - fun `test config text`() { - val config = createConfig( - legalName = myLegalName, - p2pPort = 10001, - rpcPort = 40002, - webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), - users = listOf(user("jenny")) - ) - assertEquals(prettyPrint("{" - + "\"detectPublicIp\":false," - + "\"extraAdvertisedServiceIds\":[\"my.service\"]," - + "\"h2port\":30001," - + "\"myLegalName\":\"C=US,L=New York,O=My Name\"," - + "\"p2pAddress\":\"localhost:10001\"," - + "\"rpcAddress\":\"localhost:40002\"," - + "\"rpcUsers\":[" - + "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"username\":\"jenny\"}" - + "]," - + "\"useTestClock\":true," - + "\"webAddress\":\"localhost:20001\"" - + "}"), prettyPrint(config.toText())) - } - - @Test - fun `test config text with network map`() { - val config = createConfig( - legalName = myLegalName, - p2pPort = 10001, - rpcPort = 40002, - webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), - users = listOf(user("jenny")) - ) - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) - - assertEquals(prettyPrint("{" - + "\"detectPublicIp\":false," - + "\"extraAdvertisedServiceIds\":[\"my.service\"]," - + "\"h2port\":30001," - + "\"myLegalName\":\"C=US,L=New York,O=My Name\"," - + "\"networkMapService\":{\"address\":\"localhost:12345\",\"legalName\":\"C=CH,L=Zurich,O=Notary Service\"}," - + "\"p2pAddress\":\"localhost:10001\"," - + "\"rpcAddress\":\"localhost:40002\"," - + "\"rpcUsers\":[" - + "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"username\":\"jenny\"}" - + "]," - + "\"useTestClock\":true," - + "\"webAddress\":\"localhost:20001\"" - + "}"), prettyPrint(config.toText())) - } - @Test fun `reading node configuration`() { val config = createConfig( @@ -192,13 +33,13 @@ class NodeConfigTest { p2pPort = 10001, rpcPort = 40002, webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), + h2port = 30001, + notary = NotaryService(validating = false), + networkMap = NetworkMapConfig(DUMMY_NOTARY.name, localPort(12345)), users = listOf(user("jenny")) ) - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) - val nodeConfig = config.toFileConfig() + val nodeConfig = config.toConfig() .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withFallback(ConfigFactory.parseResources("reference.conf")) .resolve() @@ -207,10 +48,9 @@ class NodeConfigTest { assertEquals(myLegalName, fullConfig.myLegalName) assertEquals(localPort(40002), fullConfig.rpcAddress) assertEquals(localPort(10001), fullConfig.p2pAddress) - assertEquals(listOf("my.service"), fullConfig.extraAdvertisedServiceIds) assertEquals(listOf(user("jenny")), fullConfig.rpcUsers) assertEquals(NetworkMapInfo(localPort(12345), DUMMY_NOTARY.name), fullConfig.networkMapService) - assertTrue((fullConfig.dataSourceProperties["dataSource.url"] as String).contains("AUTO_SERVER_PORT=30001")) + assertThat(fullConfig.dataSourceProperties["dataSource.url"] as String).contains("AUTO_SERVER_PORT=30001") assertTrue(fullConfig.useTestClock) assertFalse(fullConfig.detectPublicIp) } @@ -222,13 +62,13 @@ class NodeConfigTest { p2pPort = 10001, rpcPort = 40002, webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), + h2port = 30001, + notary = NotaryService(validating = false), + networkMap = NetworkMapConfig(DUMMY_NOTARY.name, localPort(12345)), users = listOf(user("jenny")) ) - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) - val nodeConfig = config.toFileConfig() + val nodeConfig = config.toConfig() .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withFallback(ConfigFactory.parseResources("web-reference.conf")) .resolve() @@ -240,35 +80,27 @@ class NodeConfigTest { assertEquals("cordacadevpass", webConfig.keyStorePassword) } - @Test - fun `test moving`() { - val config = createConfig(legalName = myLegalName) - - val elsewhere = baseDir / "elsewhere" - val moved = config.moveTo(elsewhere) - assertEquals(elsewhere / "myname", moved.nodeDir) - assertEquals(elsewhere / "myname-explorer", moved.explorerDir) - assertEquals(elsewhere / "myname" / "plugins", moved.pluginDir) - } - private fun createConfig( legalName: CordaX500Name = CordaX500Name(organisation = "Unknown", locality = "Nowhere", country = "GB"), p2pPort: Int = -1, rpcPort: Int = -1, webPort: Int = -1, - h2Port: Int = -1, - services: MutableList = mutableListOf("extra.service"), + h2port: Int = -1, + notary: NotaryService?, + networkMap: NetworkMapConfig?, users: List = listOf(user("guest")) - ) = NodeConfig( - baseDir, - legalName = legalName, - p2pPort = p2pPort, - rpcPort = rpcPort, - webPort = webPort, - h2Port = h2Port, - extraServices = services, - users = users - ) + ): NodeConfig { + return NodeConfig( + myLegalName = legalName, + p2pAddress = localPort(p2pPort), + rpcAddress = localPort(rpcPort), + webAddress = localPort(webPort), + h2port = h2port, + notary = notary, + networkMapService = networkMap, + rpcUsers = users + ) + } private fun localPort(port: Int) = NetworkHostAndPort("localhost", port) } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index f6c73aba2d..bae103a317 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -1,6 +1,7 @@ package net.corda.demobench.model import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.User import net.corda.testing.DUMMY_NOTARY import org.junit.Test @@ -51,7 +52,7 @@ class NodeControllerTest { fun `test first validated node becomes network map`() { val data = NodeData() data.legalName.value = node1Name - data.p2pPort.value = 100000 + data.p2pPort.value = 10000 assertFalse(controller.hasNetworkMap()) controller.validate(data) @@ -90,7 +91,7 @@ class NodeControllerTest { @Test fun `test register network map node`() { val config = createConfig(commonName = "Organisation is Network Map") - assertTrue(config.isNetworkMap()) + assertTrue(config.nodeConfig.isNetworkMap) assertFalse(controller.hasNetworkMap()) controller.register(config) @@ -99,9 +100,10 @@ class NodeControllerTest { @Test fun `test register non-network-map node`() { - val config = createConfig(commonName = "Organisation is not Network Map") - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 10000) - assertFalse(config.isNetworkMap()) + val config = createConfig( + commonName = "Organisation is not Network Map", + networkMap = NetworkMapConfig(DUMMY_NOTARY.name, localPort(10000))) + assertFalse(config.nodeConfig.isNetworkMap) assertFalse(controller.hasNetworkMap()) controller.register(config) @@ -146,7 +148,7 @@ class NodeControllerTest { @Test fun `test H2 port is max`() { val portNumber = NodeController.firstPort + 3478 - val config = createConfig(h2Port = portNumber) + val config = createConfig(h2port = portNumber) assertEquals(NodeController.firstPort, controller.nextPort) controller.register(config) assertEquals(portNumber + 1, controller.nextPort) @@ -166,25 +168,30 @@ class NodeControllerTest { private fun createConfig( commonName: String = "Unknown", - p2pPort: Int = -1, - rpcPort: Int = -1, - webPort: Int = -1, - h2Port: Int = -1, - services: MutableList = mutableListOf("extra.service"), + p2pPort: Int = 0, + rpcPort: Int = 0, + webPort: Int = 0, + h2port: Int = 0, + notary: NotaryService? = null, + networkMap: NetworkMapConfig? = null, users: List = listOf(user("guest")) - ) = NodeConfig( - baseDir, - legalName = CordaX500Name( - organisation = commonName, - locality = "New York", - country = "US" - ), - p2pPort = p2pPort, - rpcPort = rpcPort, - webPort = webPort, - h2Port = h2Port, - extraServices = services, - users = users - ) + ): NodeConfigWrapper { + val nodeConfig = NodeConfig( + myLegalName = CordaX500Name( + organisation = commonName, + locality = "New York", + country = "US" + ), + p2pAddress = localPort(p2pPort), + rpcAddress = localPort(rpcPort), + webAddress = localPort(webPort), + h2port = h2port, + notary = notary, + networkMapService = networkMap, + rpcUsers = users + ) + return NodeConfigWrapper(baseDir, nodeConfig) + } + private fun localPort(port: Int) = NetworkHostAndPort("localhost", port) } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeInfoFilesCopierTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeInfoFilesCopierTest.kt new file mode 100644 index 0000000000..557bd7095b --- /dev/null +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeInfoFilesCopierTest.kt @@ -0,0 +1,156 @@ +package net.corda.demobench.model + +import net.corda.cordform.CordformNode +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.eventually +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import rx.schedulers.TestScheduler +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.streams.toList +import kotlin.test.assertEquals + +/** + * tests for [NodeInfoFilesCopier] + */ +class NodeInfoFilesCopierTest { + + @Rule @JvmField var folder = TemporaryFolder() + private val rootPath get() = folder.root.toPath() + private val scheduler = TestScheduler() + companion object { + private const val ORGANIZATION = "Organization" + private const val NODE_1_PATH = "node1" + private const val NODE_2_PATH = "node2" + + private val content = "blah".toByteArray(Charsets.UTF_8) + private val GOOD_NODE_INFO_NAME = "nodeInfo-test" + private val GOOD_NODE_INFO_NAME_2 = "nodeInfo-anotherNode" + private val BAD_NODE_INFO_NAME = "something" + private val legalName = CordaX500Name(organisation = ORGANIZATION, locality = "Nowhere", country = "GB") + private val hostAndPort = NetworkHostAndPort("localhost", 1) + } + + private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase()) + + private val node1Config by lazy { createConfig(NODE_1_PATH) } + private val node2Config by lazy { createConfig(NODE_2_PATH) } + private val node1RootPath by lazy { nodeDir(NODE_1_PATH) } + private val node2RootPath by lazy { nodeDir(NODE_2_PATH) } + private val node1AdditionalNodeInfoPath by lazy { node1RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) } + private val node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) } + + lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier + + @Before + fun setUp() { + nodeInfoFilesCopier = NodeInfoFilesCopier(scheduler) + } + + @Test + fun `files created before a node is started are copied to that node`() { + // Configure the first node. + nodeInfoFilesCopier.addConfig(node1Config) + // Ensure directories are created. + advanceTime() + + // Create 2 files, a nodeInfo and another file in node1 folder. + Files.write(node1RootPath.resolve(GOOD_NODE_INFO_NAME), content) + Files.write(node1RootPath.resolve(BAD_NODE_INFO_NAME), content) + + // Configure the second node. + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied. + checkDirectoryContainsSingleFile(node2AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `polling of running nodes`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1Config) + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + // Create 2 files, one of which to be copied, in a node root path. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content) + Files.write(node2RootPath.resolve(BAD_NODE_INFO_NAME), content) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied to the other node. + checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `remove nodes`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1Config) + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + // Create a file, in node 2 root path. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content) + advanceTime() + + // Remove node 2 + nodeInfoFilesCopier.removeConfig(node2Config) + + // Create another file in node 2 directory. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied to the other node. + checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `clear`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1Config) + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + nodeInfoFilesCopier.reset() + + advanceTime() + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content) + + // Give some time to the filesystem to report the change. + Thread.sleep(100) + assertEquals(0, Files.list(node1AdditionalNodeInfoPath).toList().size) + } + + private fun advanceTime() { + scheduler.advanceTimeBy(1, TimeUnit.HOURS) + } + + private fun checkDirectoryContainsSingleFile(path: Path, filename: String) { + assertEquals(1, Files.list(path).toList().size) + val onlyFileName = Files.list(path).toList().first().fileName.toString() + assertEquals(filename, onlyFileName) + } + + private fun createConfig(relativePath: String) = + NodeConfigWrapper(rootPath.resolve(relativePath), + NodeConfig(myLegalName = legalName, + p2pAddress = hostAndPort, + rpcAddress = hostAndPort, + webAddress = hostAndPort, + h2port = -1, + notary = null, + networkMapService = null, + rpcUsers = listOf())) +} \ No newline at end of file diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt deleted file mode 100644 index 629ee90903..0000000000 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.corda.demobench.model - -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class ServiceControllerTest { - - @Test - fun `test empty`() { - val controller = ServiceController("/empty-services.conf") - assertNotNull(controller.services) - assertTrue(controller.services.isEmpty()) - - assertNotNull(controller.notaries) - assertTrue(controller.notaries.isEmpty()) - } - - @Test - fun `test duplicates`() { - val controller = ServiceController("/duplicate-services.conf") - assertNotNull(controller.services) - assertEquals(listOf("corda.example"), controller.services) - } - - @Test - fun `test notaries`() { - val controller = ServiceController("/notary-services.conf") - assertNotNull(controller.notaries) - assertEquals(listOf("corda.notary.simple"), controller.notaries) - } - - @Test - fun `test services`() { - val controller = ServiceController() - assertNotNull(controller.services) - assertTrue(controller.services.isNotEmpty()) - - assertNotNull(controller.notaries) - assertTrue(controller.notaries.isNotEmpty()) - } - -} \ No newline at end of file diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt deleted file mode 100644 index e902bcfec9..0000000000 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.corda.demobench.model - -import net.corda.nodeapi.User -import org.junit.Test -import kotlin.test.assertEquals - -class UserTest { - - @Test - fun createFromEmptyMap() { - val user = toUser(emptyMap()) - assertEquals("none", user.username) - assertEquals("none", user.password) - assertEquals(emptySet(), user.permissions) - } - - @Test - fun createFromMap() { - val map = mapOf( - "username" to "MyName", - "password" to "MyPassword", - "permissions" to listOf("Flow.MyFlow") - ) - val user = toUser(map) - assertEquals("MyName", user.username) - assertEquals("MyPassword", user.password) - assertEquals(setOf("Flow.MyFlow"), user.permissions) - } - - @Test - fun userToMap() { - val user = User("MyName", "MyPassword", setOf("Flow.MyFlow")) - val map = user.toMap() - assertEquals("MyName", map["username"]) - assertEquals("MyPassword", map["password"]) - assertEquals(setOf("Flow.MyFlow"), map["permissions"]) - } - - @Test - fun `default user`() { - val user = user("guest") - assertEquals("guest", user.username) - assertEquals("letmein", user.password) - assertEquals(setOf("ALL"), user.permissions) - } - -} diff --git a/tools/demobench/src/test/resources/duplicate-services.conf b/tools/demobench/src/test/resources/duplicate-services.conf deleted file mode 100644 index ffa2ac0e33..0000000000 --- a/tools/demobench/src/test/resources/duplicate-services.conf +++ /dev/null @@ -1,3 +0,0 @@ -corda.example -corda.example -corda.example diff --git a/tools/demobench/src/test/resources/empty-services.conf b/tools/demobench/src/test/resources/empty-services.conf deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tools/demobench/src/test/resources/notary-services.conf b/tools/demobench/src/test/resources/notary-services.conf deleted file mode 100644 index 5f833b04af..0000000000 --- a/tools/demobench/src/test/resources/notary-services.conf +++ /dev/null @@ -1,2 +0,0 @@ -corda.notary.simple -corda.example diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index 643f6463a3..3f719c7d58 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -71,3 +71,11 @@ task(runFlowTriageNodes, dependsOn: 'classes', type: JavaExec) { classpath = sourceSets.main.runtimeClasspath args '-F' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.tools.explorer' + ) + } +} \ No newline at end of file diff --git a/tools/explorer/src/main/java/ExplorerCaplet.java b/tools/explorer/src/main/java/ExplorerCaplet.java index 7ab25d09b5..fdeb2821fb 100644 --- a/tools/explorer/src/main/java/ExplorerCaplet.java +++ b/tools/explorer/src/main/java/ExplorerCaplet.java @@ -19,7 +19,7 @@ public class ExplorerCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - List classpath = augmentClasspath((List) cp, "plugins"); + List classpath = augmentClasspath((List) cp, "cordapps"); return (T) augmentClasspath(classpath, "dependencies"); } return super.attribute(attr); diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt index a77205adb8..24a95ec46b 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt @@ -7,10 +7,11 @@ enum class Positivity { Negative } -val Positivity.sign: String get() = when (this) { - Positivity.Positive -> "" - Positivity.Negative -> "-" -} +val Positivity.sign: String + get() = when (this) { + Positivity.Positive -> "" + Positivity.Negative -> "-" + } data class AmountDiff( val positivity: Positivity, diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 11026a819c..d22e628596 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -21,9 +21,6 @@ import net.corda.finance.flows.* import net.corda.finance.flows.CashExitFlow.ExitRequest import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.BOB @@ -70,22 +67,21 @@ class ExplorerSimulation(val options: OptionSet) { val portAllocation = PortAllocation.Incremental(20000) driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance")) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. - val notary = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), - customOverrides = mapOf("nearestCity" to "Zurich")) + val notary = startNotaryNode(DUMMY_NOTARY.name, customOverrides = mapOf("nearestCity" to "Zurich"), validating = false) val alice = startNode(providedName = ALICE.name, rpcUsers = arrayListOf(user), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Milan")) val bob = startNode(providedName = BOB.name, rpcUsers = arrayListOf(user), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Madrid")) val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") val issuerGBP = startNode(providedName = ukBankName, rpcUsers = arrayListOf(manager), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))), - customOverrides = mapOf("nearestCity" to "London")) + customOverrides = mapOf( + "issuableCurrencies" to listOf("GBP"), + "nearestCity" to "London")) val issuerUSD = startNode(providedName = usaBankName, rpcUsers = arrayListOf(manager), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))), - customOverrides = mapOf("nearestCity" to "New York")) + customOverrides = mapOf( + "issuableCurrencies" to listOf("USD"), + "nearestCity" to "New York")) notaryNode = notary.get() aliceNode = alice.get() @@ -132,10 +128,9 @@ class ExplorerSimulation(val options: OptionSet) { issuerNodeGBP.nodeInfo.legalIdentities.first() to issuerRPCGBP, issuerNodeUSD.nodeInfo.legalIdentities.first() to issuerRPCUSD)) - aliceRPC.waitUntilNetworkReady() - bobRPC.waitUntilNetworkReady() - issuerRPCGBP.waitUntilNetworkReady() - issuerRPCUSD.waitUntilNetworkReady() + listOf(aliceRPC, bobRPC, issuerRPCGBP, issuerRPCUSD).map { + it.waitUntilNetworkReady().getOrThrow() + } } private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) { diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt index 5df84b94fd..19bba7b639 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt @@ -7,6 +7,7 @@ import javafx.beans.property.SimpleObjectProperty import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists +import net.corda.core.internal.uncheckedCast import tornadofx.* import java.nio.file.Files import java.nio.file.Path @@ -52,14 +53,13 @@ class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable { // Save all changes in memory to properties file. fun commit() = Files.newOutputStream(path).use { config.store(it, "") } - @Suppress("UNCHECKED_CAST") private operator fun Properties.getValue(receiver: Any, metadata: KProperty<*>): T { return when (metadata.returnType.javaType) { - String::class.java -> string(metadata.name, "") as T - Int::class.java -> string(metadata.name, "0").toInt() as T - Boolean::class.java -> boolean(metadata.name) as T - Currency::class.java -> Currency.getInstance(string(metadata.name, "USD")) as T - Path::class.java -> Paths.get(string(metadata.name, "")).toAbsolutePath() as T + String::class.java -> uncheckedCast(string(metadata.name, "")) + Int::class.java -> uncheckedCast(string(metadata.name, "0").toInt()) + Boolean::class.java -> uncheckedCast(boolean(metadata.name)) + Currency::class.java -> uncheckedCast(Currency.getInstance(string(metadata.name, "USD"))) + Path::class.java -> uncheckedCast(Paths.get(string(metadata.name, "")).toAbsolutePath()) else -> throw IllegalArgumentException("Unsupported type ${metadata.returnType}") } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index cb7f69df1f..de6a41aded 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -103,7 +103,7 @@ class Network : CordaView() { hgap = 5.0 vgap = 5.0 for (identity in identities) { - val isNotary = identity.name.commonName?.let { ServiceType.parse(it).isNotary() } ?: false + val isNotary = identity.name.commonName?.let { ServiceType.parse(it).isNotary() } == true row("${if (isNotary) "Notary " else ""}Public Key :") { copyableLabel(SimpleObjectProperty(identity.owningKey.toBase58String())) } @@ -150,7 +150,7 @@ class Network : CordaView() { } override fun onDock() { - centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) ?: false }) + centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) == true }) centralLabel.value?.let { mapScrollPane.centerLabel(it) } } @@ -160,7 +160,7 @@ class Network : CordaView() { } init { - centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) ?: false }) + centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) == true }) Bindings.bindContent(notaryList.children, notaryButtons) Bindings.bindContent(peerList.children, peerButtons) // Run once when the screen is ready. diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt index b63aab5cf7..2366435bf5 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt @@ -38,7 +38,7 @@ class SearchField(private val data: ObservableList, vararg filterCriteria: (text.isNullOrBlank() && textField.isVisible) || if (category == ALL) { filterCriteria.any { it.second(data, text) } } else { - filterCriteria.toMap()[category]?.invoke(data, text) ?: false + filterCriteria.toMap()[category]?.invoke(data, text) == true } } }, arrayOf(textField.textProperty(), searchCategory.valueProperty(), textField.visibleProperty()))) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index f528e0ec66..128c2f221b 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -135,8 +135,8 @@ class TransactionViewer : CordaView("Transactions") { "Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) }, "Input" to { tx, s -> tx.inputs.resolved.any { it.state.contract.contains(s, true) } }, "Output" to { tx, s -> tx.outputs.any { it.state.contract.contains(s, true) } }, - "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) ?: false } } }, - "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) ?: false } } }, + "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) == true } } }, + "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) == true } } }, "Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } } ) root.top = searchField.root diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt index 480ae1116e..6093a235b1 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt @@ -147,7 +147,7 @@ class CashViewer : CordaView("Cash") { */ val searchField = SearchField(cashStates, "Currency" to { state, text -> state.state.data.amount.token.product.toString().contains(text, true) }, - "Issuer" to { state, text -> state.resolveIssuer().value?.name?.organisation?.contains(text, true) ?: false } + "Issuer" to { state, text -> state.resolveIssuer().value?.name?.organisation?.contains(text, true) == true } ) root.top = hbox(5.0) { button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) { diff --git a/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt b/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt deleted file mode 100644 index a363e8cd3f..0000000000 --- a/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.corda.explorer.model - -import net.corda.finance.USD -import org.junit.Test -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class IssuerModelTest { - @Test - fun `test issuer regex`() { - val regex = Regex("corda.issuer.(USD|GBP|CHF)") - assertTrue("corda.issuer.USD".matches(regex)) - assertTrue("corda.issuer.GBP".matches(regex)) - - assertFalse("corda.issuer.USD.GBP".matches(regex)) - assertFalse("corda.issuer.EUR".matches(regex)) - assertFalse("corda.issuer".matches(regex)) - - assertEquals(USD, Currency.getInstance("corda.issuer.USD".substringAfterLast("."))) - assertFailsWith(IllegalArgumentException::class) { - Currency.getInstance("corda.issuer.DOLLAR".substringBeforeLast(".")) - } - } -} \ No newline at end of file diff --git a/tools/graphs/build.gradle b/tools/graphs/build.gradle index 5e81dee1c9..01a4da9637 100644 --- a/tools/graphs/build.gradle +++ b/tools/graphs/build.gradle @@ -79,3 +79,11 @@ task graphs { } } } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.tools.graphs' + ) + } +} \ No newline at end of file diff --git a/tools/loadtest/build.gradle b/tools/loadtest/build.gradle index af7012b2e6..00480419c0 100644 --- a/tools/loadtest/build.gradle +++ b/tools/loadtest/build.gradle @@ -35,3 +35,11 @@ run { systemProperty "consoleLogLevel", System.properties.getProperty('consoleLogLevel') } } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.tools.loadtest' + ) + } +} diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt index 5b4f0f57ba..fe3e4fc621 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt @@ -47,6 +47,7 @@ fun setupJSchWithSshAgent(): JSch { override fun getSignature(data: ByteArray?) = agentProxy.sign(identity.blob, data) @Suppress("OverridingDeprecatedMember") override fun decrypt() = true + override fun getPublicKeyBlob() = identity.blob override fun setPassphrase(passphrase: ByteArray?) = true } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt index 87aa80de90..b704f50acd 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt @@ -45,6 +45,7 @@ val isNotary = { node: NodeConnection -> val notaries = node.proxy.notaryIdentities() node.info.legalIdentities.any { it in notaries } } + fun ((A) -> Boolean).or(other: (A) -> Boolean): (A) -> Boolean = { this(it) || other(it) } fun hang(hangIntervalRange: LongRange) = Disruption("Hang randomly") { node, random -> diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt index 645b31ad67..a7751ca708 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt @@ -182,7 +182,8 @@ fun runLoadTests(configuration: LoadTestConfiguration, tests: List acc + "\n" + elem.name + ": " + elem.owningKey.toBase58String() } }.joinToString("\n") 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 f9965a5ffa..05bfc06673 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -6,24 +6,24 @@ import net.corda.core.messaging.startFlow import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.config.VerifierType -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.* import net.corda.testing.driver.NetworkMapStartStrategy -import net.corda.testing.chooseIdentity import org.junit.Test import java.util.* import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertNotNull class VerifierTests { private fun generateTransactions(number: Int): List { var currentLedger = GeneratedLedger.empty - val transactions = ArrayList() + val transactions = arrayListOf() val random = SplittableRandom() for (i in 0 until number) { val (tx, ledger) = currentLedger.transactionGenerator.generateOrFail(random) @@ -113,19 +113,23 @@ class VerifierTests { @Test fun `single verifier works with a node`() { verifierDriver( - networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), - extraCordappPackagesToScan = listOf("net.corda.finance.contracts") + networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), + extraCordappPackagesToScan = listOf("net.corda.finance.contracts") ) { val aliceFuture = startNode(providedName = ALICE.name) - val notaryFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type)), verifierType = VerifierType.OutOfProcess) - val alice = aliceFuture.get() - val notary = notaryFuture.get() - val notaryIdentity = notary.nodeInfo.legalIdentities[1] - startVerifier(notary) - alice.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notaryIdentity).returnValue.get() - notary.waitUntilNumberOfVerifiers(1) + val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, verifierType = VerifierType.OutOfProcess) + val aliceNode = aliceFuture.get() + val notaryNode = notaryFuture.get() + val alice = aliceNode.rpc.wellKnownPartyFromX500Name(ALICE_NAME)!! + val notary = notaryNode.rpc.notaryPartyFromX500Name(DUMMY_NOTARY_SERVICE_NAME)!! + startVerifier(notaryNode) + notaryNode.pollUntilKnowsAbout(aliceNode).getOrThrow() + aliceNode.pollUntilKnowsAbout(notaryNode).getOrThrow() + aliceNode.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notary).returnValue.get() + notaryNode.waitUntilNumberOfVerifiers(1) for (i in 1..10) { - alice.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice.nodeInfo.chooseIdentity()).returnValue.get() + val cashFlowResult = aliceNode.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice).returnValue.get() + assertNotNull(cashFlowResult) } } } diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt index 25388e96c1..44dc37ad3a 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt @@ -36,6 +36,9 @@ fun main(args: Array) { System.setProperty("log-path", (cmdlineOptions.baseDirectory / "logs/web").toString()) val log = LoggerFactory.getLogger("Main") + println("This Corda-specific web server is deprecated and will be removed in future.") + println("Please switch to a regular web framework like Spring, J2EE or Play Framework.") + println() println("Logs can be found in ${System.getProperty("log-path")}") val conf = try { diff --git a/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt b/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt index ca47c80173..f0e20a037e 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt @@ -1,6 +1,7 @@ package net.corda.webserver.converters import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.uncheckedCast import java.lang.reflect.Type import javax.ws.rs.ext.ParamConverter import javax.ws.rs.ext.ParamConverterProvider @@ -15,8 +16,7 @@ object CordaX500NameConverter : ParamConverter { object CordaConverterProvider : ParamConverterProvider { override fun getConverter(rawType: Class, genericType: Type?, annotations: Array?): ParamConverter? { if (rawType == CordaX500Name::class.java) { - @Suppress("UNCHECKED_CAST") - return CordaX500NameConverter as ParamConverter + return uncheckedCast(CordaX500NameConverter) } return null } diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 462f792c52..8d5bf05a03 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -120,7 +120,7 @@ class NodeWebServer(val config: WebServerConfig) { } @Throws(IOException::class) - override fun writeErrorPageMessage(request: HttpServletRequest, writer: Writer, code: Int, message: String , uri: String) { + override fun writeErrorPageMessage(request: HttpServletRequest, writer: Writer, code: Int, message: String, uri: String) { writer.write("

Corda $safeLegalName

\n") super.writeErrorPageMessage(request, writer, code, message, uri) } @@ -135,10 +135,10 @@ class NodeWebServer(val config: WebServerConfig) { } val resourceConfig = ResourceConfig() - .register(ObjectMapperConfig(rpcObjectMapper)) - .register(ResponseFilter()) - .register(CordaConverterProvider) - .register(APIServerImpl(localRpc)) + .register(ObjectMapperConfig(rpcObjectMapper)) + .register(ResponseFilter()) + .register(CordaConverterProvider) + .register(APIServerImpl(localRpc)) val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis } for (webapi in webAPIsOnClasspath) { diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt index 9750f595ac..e23d420059 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt @@ -55,7 +55,7 @@ class AttachmentDownloadServlet : HttpServlet() { // Closing the output stream commits our response. We cannot change the status code after this. resp.outputStream.close() - } catch(e: FileNotFoundException) { + } catch (e: FileNotFoundException) { log.warn("404 Not Found whilst trying to handle attachment download request for ${servletContext.contextPath}/$reqPath") resp.sendError(HttpServletResponse.SC_NOT_FOUND) return diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt index eba6597428..8daa446af9 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt @@ -15,7 +15,7 @@ import javax.servlet.http.HttpServletResponse * Dumps some data about the installed CorDapps. * TODO: Add registered flow initiators. */ -class CorDappInfoServlet(val plugins: List, val rpc: CordaRPCOps): HttpServlet() { +class CorDappInfoServlet(val plugins: List, val rpc: CordaRPCOps) : HttpServlet() { @Throws(IOException::class) override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { @@ -73,7 +73,7 @@ class CorDappInfoServlet(val plugins: List, val rpc: Co for (method in resource.allMethods) { if (method.type == ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR) { - resources.add( Resource.from(resource.resourceLocator.invocable.definitionMethod.returnType)) + resources.add(Resource.from(resource.resourceLocator.invocable.definitionMethod.returnType)) } else { endpoints.add(Endpoint(method.httpMethod, "api$path", resource.path)) }