Merge Open Source to Enterprise (#79)

* Check array size before accessing

* Review fixes

* CORDA-540: Make Verifier work in AMQP mode (#1870)

* reference to finance module via not hardcoded group ID (#1515)

*  generic way to reference to group id when loading finance.jar via cordapp

* Fixed the node shell to work with the DataFeed class

* Attempt to make NodeStatePersistenceTests more stable (#1895)

By ensuring that the nodes are properly started and aware of each other before firing any flows through them.
Also minor refactoring.

* Disable unstable test on Windows (#1899)

* CORDA-530 Don't soft-lock non-fungible states (#1794)

* Don't run unlock query if nothing was locked
* Constructors should not have side-effects

* [CORDA-442] let Driver run without network map (#1890)

* [CORDA-442] let Driver run without network map

- Nodes started by driver run without a networkMapNode.

- Driver does not take a networkMapStartStrategy anymore

- a new parameter in the configuration "noNetworkMapServiceMode" allows for a node not to be a networkMapNode nor to connect to one.

- Driver now waits for each node to write its own NodeInfo file to disk and then copies it into each other node.

- When driver starts a node N, it waits for every node to be have N nodes in their network map.

Note: the code to copy around the NodeInfo files was already in DemoBench, the NodeInfoFilesCopier class was just moved from DemoBench into core (I'm very open to core not being the best place, please advise)

* Added missing cordappPackage dependencies. (#1894)

* Eliminate circular dependency of NodeSchedulerService on ServiceHub. (#1891)

* Update customSchemas documentation. (#1902)

* [CORDA-694] Commands visibility for Oracles (without sacrificing privacy) (#1835)

new checkCommandVisibility feature for Oracles

* CORDA-599 PersistentNetworkMapCache no longer circularly depends on SH (#1652)

* CORDA-725 - Change AMQP identifier to officially assigned value

This does change our header format so pre-cached test files need
regenerating

* CORDA-725 - update changelog

* CORDA-680 Update cordapp packages documentation (#1901)

* Introduce MockNetworkParameters

* Cordformation in Kotlin (#1873)

Cordformation rewritten in kotlin.

* Kotlin migration

* Review Comments

* CORDA-704: Implement `@DoNotImplement` annotation (#1903)

* Enhance the API Scanner plugin to monitor class annotations.
* Implement @DoNotImplement annotation, and apply it.
* Update API definition.
* Update API change detection to handle @DoNotImplement.
* Document the `@DoNotImplement` annotation.

* Experimental support for PostgreSQL (#1525)

* Cash selection refactoring such that 3d party DB providers are only required to implement Coin Selection SQL logic.

* Re-added debug logging statement.

* Updated to include PR review feedback from VK

* Refactoring following rebase from master.

* Fix broken JUnits following rebase.

* Use JDBC ResultSet getBlob() and added custom serializer to address concern raised by tomtau in PR.

* Fix failing JUnits.

* Experimental support for PostgreSQL: CashSelection done using window functions

* Moved postgresql version information into corda/build.gradle

* Using a PreparedStatement in CashSelectionPostgreSQLImpl

* Changed the PostgreSQL Cash Selection implementation to use the new refactored AbstractCashSelection

* * Retire MockServiceHubInternal (#1909)

* Introduce rigorousMock
* Add test-utils and node-driver to generated documentation

* Fix-up: Bank Of Corda sample (#1912)

In the previous version when running with `--role ISSUER` the application failed to start.
The reason was that in spite of `quantity` and `currency` were optional,
un-necessary `requestParams` been constructed regardless.

* move SMM

* Interface changes for multi-threading

* CORDA-351: added dependency check plugin to gradle build script (#1911)

* CORDA-351: added dependency check plugin to gradle build script

* CORDA-351: Added suppression stub file with example

* CORDA-351: added suppresionFile property

* CORDA-435 - Ensure Kryo only tests use Kryo serializatin context

Also correct lambda typos (from lamba)

* Network map service REST API wrapper (#1907)

* Network map client - WIP

* Java doc and doc for doc site

* remove javax.ws dependency

* NetworkParameter -> NetworkParameters

* move network map client to node

* Fix jetty test dependencies

* NetworkParameter -> NetworkParameters

* Address PR issues

* Address PR issues and unit test fix

* Address PR issues

* Fixing Bank-Of-Corda Demo in `master` (#1922)

* Fix-up: Bank Of Corda sample

Use correct CorDapp packages to scan

(cherry picked from commit 2caa134)

* Set adequate permissions for the nodes such that NodeExplorer can connect

(cherry picked from commit ae88242)

* Set adequate permissions for the nodes such that NodeExplorer can connect

(cherry picked from commit ae88242)

* Correct run configuration

* Fix-up port numbers

* CORDA-435 - AMQP serialisation cannot work with private vals

They won't be reported as properties by the introspector and thus we
will fail to find a constructor for them. This makes sense as we will be
unable to serialise an object whose members we cannot read

* CORDA-435 - AMQP enablement fixes

AMQP has different serialization rules than Kryo surrounding the way we
introspect objects to work out how to construct them

* [CORDA-442] make MockNetwork not start a networkmap node (#1908)

* [CORDA-442] make MockNetwork not start a networkmap node

Now MockNetwork will put the appropriate NodeInfos inside each running node networkMapCache.

Tests relating to networkmap node starting and interaction have been removed since they where relaying on MockNetwork

* Minor fix for api checker script to support macOS

* Retrofit changes from Enterprise PR #61 (#1934)

* Introduce MockNodeParameters/Args (#1923)

* CORDA-736 Add some new features to corda.jar via node.conf for testing (#1926)

* CORDA-699 Add injection or modification of memory network messages (#1920)

* Updated API stability changeset to reflect new schema attribute name.
This commit is contained in:
josecoll 2017-10-25 13:54:34 +01:00 committed by GitHub
parent 01d8ad41b4
commit ef7ccd3147
200 changed files with 4549 additions and 3192 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
#!/bin/bash
set +o posix
echo "Starting API Diff"
@ -11,7 +12,7 @@ if [ ! -f $apiCurrent ]; then
fi
# Remove the two header lines from the diff output.
diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt | tail --lines=+3`
diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt | tail -n +3`
echo "Diff contents:"
echo "$diffContents"
echo
@ -30,7 +31,15 @@ if [ $removalCount -gt 0 ]; then
fi
# Adding new abstract methods could also break the API.
newAbstracts=$(echo "$diffContents" | grep "^+" | grep "\(public\|protected\) abstract")
# However, first exclude anything with the @DoNotImplement annotation.
function forUserImpl() {
awk '/DoNotImplement/,/^##/{ next }{ print }' $1
}
userDiffContents=`diff -u <(forUserImpl $apiCurrent) <(forUserImpl $APIHOME/../build/api/api-corda-*.txt) | tail -n +3`
newAbstracts=$(echo "$userDiffContents" | grep "^+" | grep "\(public\|protected\) abstract")
abstractCount=`grep -v "^$" <<EOF | wc -l
$newAbstracts
EOF

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.1.xsd">
<!-- Example of a suppressed library -->
<!-- The suppress node can be generated from the HTML report by using the 'suppress' option for each vulnerability found
<suppress>
<notes><![CDATA[
file name: some.jar
]]></notes>
<sha1>66734244CE86857018B023A8C56AE0635C56B6A1</sha1>
<cpe>cpe:/a:apache:struts:2.0.0</cpe>
</suppress>
-->
</suppressions>

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BankOfCordaDriverKt - Issue Web" type="JetRunConfigurationType" factoryName="Kotlin">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_WEB --quantity 100 --currency USD" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BankOfCordaDriverKt - Run Stack" type="JetRunConfigurationType" factoryName="Kotlin">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="--role ISSUER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -41,9 +41,11 @@ buildscript {
ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.194' // Update docs if renamed or removed.
ext.postgresql_version = '42.1.4'
ext.rxjava_version = '1.2.4'
ext.dokka_version = '0.9.14'
ext.eddsa_version = '0.2.0'
ext.dependency_checker_version = '3.0.1'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131'
@ -66,6 +68,7 @@ buildscript {
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
classpath "org.ajoberstar:grgit:1.1.0"
classpath "net.i2p.crypto:eddsa:$eddsa_version" // Needed for ServiceIdentityGenerator in the build environment.
classpath "org.owasp:dependency-check-gradle:${dependency_checker_version}"
}
}
@ -100,7 +103,13 @@ allprojects {
apply plugin: 'kotlin'
apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'org.owasp.dependencycheck'
dependencyCheck {
suppressionFile = '.ci/dependency-checker/suppressedLibraries.xml'
cveValidForHours = 1
format = 'ALL'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

View File

@ -1,7 +1,7 @@
package net.corda.client.jackson
import com.fasterxml.jackson.databind.SerializationFeature
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.Amount
import net.corda.core.cordapp.CordappProvider
@ -9,10 +9,7 @@ import net.corda.core.crypto.*
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.finance.USD
import net.corda.testing.ALICE_PUBKEY
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.MINI_CORP
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import org.junit.Before
import org.junit.Test
@ -32,9 +29,9 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
@Before
fun setup() {
services = mock()
cordappProvider = mock()
whenever(services.cordappProvider).thenReturn(cordappProvider)
services = rigorousMock()
cordappProvider = rigorousMock()
doReturn(cordappProvider).whenever(services).cordappProvider
}
@Test
@ -91,8 +88,7 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
@Test
fun writeTransaction() {
val attachmentRef = SecureHash.randomSHA256()
whenever(cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID))
.thenReturn(attachmentRef)
doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
fun makeDummyTx(): SignedTransaction {
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
.toWireTransaction(services)

View File

@ -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.concurrent.flatMap
import net.corda.core.internal.packageName
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.messaging.StateMachineUpdate
@ -143,7 +144,7 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C
}
}
val nodeIdentity = node.info.chooseIdentity()
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).flatMap { it.resultFuture }.getOrThrow()
proxy.startFlow(::CashIssueFlow,
123.DOLLARS,
OpaqueBytes.of(0),

View File

@ -92,6 +92,7 @@ class RPCStabilityTests {
startAndStop()
}
val numberOfThreadsAfter = waitUntilNumberOfThreadsStable(executor)
assertTrue(numberOfThreadsBefore >= numberOfThreadsAfter)
executor.shutdownNow()
}

View File

@ -1,5 +1,6 @@
package net.corda.client.rpc
import net.corda.core.DoNotImplement
import net.corda.core.messaging.RPCOps
import java.io.Closeable
@ -10,6 +11,7 @@ import java.io.Closeable
* [Closeable.close] may be used to shut down the connection and release associated resources. It is an
* alias for [notifyServerAndClose].
*/
@DoNotImplement
interface RPCConnection<out I : RPCOps> : Closeable {
/**
* Holds a synthetic class that automatically forwards method calls to the server, and returns the response.

View File

@ -13,10 +13,10 @@ class SwapIdentitiesFlowTests {
@Test
fun `issue key`() {
// We run this in parallel threads to help catch any race conditions that may exist.
val mockNet = MockNetwork(false, true)
val mockNet = MockNetwork(threadPerNode = true)
// Set up values we'll need
val notaryNode = mockNet.createNotaryNode()
mockNet.createNotaryNode()
val aliceNode = mockNet.createPartyNode(ALICE.name)
val bobNode = mockNet.createPartyNode(BOB.name)
val alice = aliceNode.info.singleIdentity()
@ -53,7 +53,7 @@ class SwapIdentitiesFlowTests {
@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)
val mockNet = MockNetwork(threadPerNode = true)
// Set up values we'll need
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)
@ -78,7 +78,7 @@ class SwapIdentitiesFlowTests {
@Test
fun `verifies signature`() {
// We run this in parallel threads to help catch any race conditions that may exist.
val mockNet = MockNetwork(false, true)
val mockNet = MockNetwork(threadPerNode = true)
// Set up values we'll need
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=2.0.4
gradlePluginsVersion=2.0.6
kotlinVersion=1.1.50
guavaVersion=21.0
bouncycastleVersion=1.57

View File

@ -0,0 +1,18 @@
package net.corda.core
import java.lang.annotation.Inherited
/**
* This annotation is for interfaces and abstract classes that provide Corda functionality
* to user applications. Future versions of Corda may add new methods to such interfaces and
* classes, but will not remove or modify existing methods.
*
* Adding new methods does not break Corda's API compatibility guarantee because applications
* should not implement or extend anything annotated with [DoNotImplement]. These classes are
* only meant to be implemented by Corda itself.
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
@MustBeDocumented
@Inherited
annotation class DoNotImplement

View File

@ -2,6 +2,7 @@ package net.corda.core.contracts
import net.corda.core.identity.Party
import net.corda.core.internal.extractFile
import net.corda.core.serialization.CordaSerializable
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.OutputStream
@ -17,6 +18,7 @@ import java.util.jar.JarInputStream
* - Legal documents
* - Facts generated by oracles which might be reused a lot
*/
@CordaSerializable
interface Attachment : NamedByHash {
fun open(): InputStream
fun openAsJAR(): JarInputStream {

View File

@ -10,5 +10,6 @@ enum class ComponentGroupEnum {
COMMANDS_GROUP, // ordinal = 2.
ATTACHMENTS_GROUP, // ordinal = 3.
NOTARY_GROUP, // ordinal = 4.
TIMEWINDOW_GROUP // ordinal = 5.
TIMEWINDOW_GROUP, // ordinal = 5.
SIGNERS_GROUP // ordinal = 6.
}

View File

@ -19,7 +19,7 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
class ContractConstraintRejection(txId: SecureHash, contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
class MissingAttachmentRejection(txId: SecureHash, contractClass: String)
class MissingAttachmentRejection(txId: SecureHash, val contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable)

View File

@ -1,5 +1,6 @@
package net.corda.core.cordapp
import net.corda.core.DoNotImplement
import net.corda.core.flows.FlowLogic
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializationWhitelist
@ -17,13 +18,14 @@ import java.net.URL
* @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 serviceFlows List of [net.corda.core.node.services.CordaService] initiable flows classes
* @property schedulableFlows List of flows startable by the scheduler
* @property servies List of RPC services
* @property services List of RPC services
* @property serializationWhitelists List of Corda plugin registries
* @property customSchemas List of custom schemas
* @property jarPath The path to the JAR for this CorDapp
*/
@DoNotImplement
interface Cordapp {
val name: String
val contractClassNames: List<String>

View File

@ -1,11 +1,13 @@
package net.corda.core.cordapp
import net.corda.core.DoNotImplement
import net.corda.core.contracts.ContractClassName
import net.corda.core.node.services.AttachmentId
/**
* Provides access to what the node knows about loaded applications.
*/
@DoNotImplement
interface CordappProvider {
/**
* Exposes the current CorDapp context which will contain information and configuration of the CorDapp that

View File

@ -158,4 +158,41 @@ class PartialMerkleTree(val root: PartialTree) {
return false
return (verifyRoot == merkleRootHash)
}
/**
* Method to return the index of the input leaf in the partial Merkle tree structure.
* @param leaf the component hash to check.
* @return leaf-index of this component (starting from zero).
* @throws MerkleTreeException if the provided hash is not in the tree.
*/
@Throws(MerkleTreeException::class)
internal fun leafIndex(leaf: SecureHash): Int {
// Special handling if the tree consists of one node only.
if (root is PartialTree.IncludedLeaf && root.hash == leaf) return 0
val flagPath = mutableListOf<Boolean>()
if (!leafIndexHelper(leaf, this.root, flagPath)) throw MerkleTreeException("The provided hash $leaf is not in the tree.")
return indexFromFlagPath(flagPath)
}
// Helper function to compute the path. False means go to the left and True to the right.
// Because the path is updated recursively, the path is returned in reverse order.
private fun leafIndexHelper(leaf: SecureHash, node: PartialTree, path: MutableList<Boolean>): Boolean {
if (node is PartialTree.IncludedLeaf) {
return node.hash == leaf
} else if (node is PartialTree.Node) {
if (leafIndexHelper(leaf, node.left, path)) {
path.add(false)
return true
}
if (leafIndexHelper(leaf, node.right, path)) {
path.add(true)
return true
}
}
return false
}
// Return the leaf index from the path boolean list.
private fun indexFromFlagPath(pathList: List<Boolean>) =
pathList.mapIndexed { index, value -> if (value) (1 shl index) else 0 }.sum()
}

View File

@ -41,6 +41,16 @@ import java.time.Instant
* and request they start their counterpart flow, then make sure it's annotated with [InitiatingFlow]. This annotation
* also has a version property to allow you to version your flow and enables a node to restrict support for the flow to
* that particular version.
*
* Functions that suspend the flow (including all functions on [FlowSession]) accept a [maySkipCheckpoint] parameter
* defaulting to false, false meaning a checkpoint should always be created on suspend. This parameter may be set to
* true which allows the implementation to potentially optimise away the checkpoint, saving a roundtrip to the database.
*
* This option however comes with a big warning sign: Setting the parameter to true requires the flow's code to be
* replayable from the previous checkpoint (or start of flow) up until the next checkpoint (or end of flow) in order to
* prepare for hard failures. As suspending functions always commit the flow's database transaction regardless of this
* parameter the flow must be prepared for scenarios where a previous running of the flow *already committed its
* relevant database transactions*. Only set this option to true if you know what you're doing.
*/
abstract class FlowLogic<out T> {
/** This is where you should log things to. */
@ -123,7 +133,7 @@ abstract class FlowLogic<out T> {
*/
@Deprecated("Use FlowSession.getFlowInfo()", level = DeprecationLevel.WARNING)
@Suspendable
fun getFlowInfo(otherParty: Party): FlowInfo = stateMachine.getFlowInfo(otherParty, flowUsedForSessions)
fun getFlowInfo(otherParty: Party): FlowInfo = stateMachine.getFlowInfo(otherParty, flowUsedForSessions, maySkipCheckpoint = false)
/**
* Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
@ -157,7 +167,7 @@ abstract class FlowLogic<out T> {
@Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING)
@Suspendable
open fun <R : Any> sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any): UntrustworthyData<R> {
return stateMachine.sendAndReceive(receiveType, otherParty, payload, flowUsedForSessions)
return stateMachine.sendAndReceive(receiveType, otherParty, payload, flowUsedForSessions, retrySend = false, maySkipCheckpoint = false)
}
/**
@ -171,17 +181,17 @@ abstract class FlowLogic<out T> {
*/
@Deprecated("Use FlowSession.sendAndReceiveWithRetry()", level = DeprecationLevel.WARNING)
internal inline fun <reified R : Any> sendAndReceiveWithRetry(otherParty: Party, payload: Any): UntrustworthyData<R> {
return stateMachine.sendAndReceive(R::class.java, otherParty, payload, flowUsedForSessions, retrySend = true)
return stateMachine.sendAndReceive(R::class.java, otherParty, payload, flowUsedForSessions, retrySend = true, maySkipCheckpoint = false)
}
@Suspendable
internal fun <R : Any> FlowSession.sendAndReceiveWithRetry(receiveType: Class<R>, payload: Any): UntrustworthyData<R> {
return stateMachine.sendAndReceive(receiveType, counterparty, payload, flowUsedForSessions, retrySend = true)
return stateMachine.sendAndReceive(receiveType, counterparty, payload, flowUsedForSessions, retrySend = true, maySkipCheckpoint = false)
}
@Suspendable
internal inline fun <reified R : Any> FlowSession.sendAndReceiveWithRetry(payload: Any): UntrustworthyData<R> {
return stateMachine.sendAndReceive(R::class.java, counterparty, payload, flowUsedForSessions, retrySend = true)
return stateMachine.sendAndReceive(R::class.java, counterparty, payload, flowUsedForSessions, retrySend = true, maySkipCheckpoint = false)
}
/**
@ -206,7 +216,7 @@ abstract class FlowLogic<out T> {
@Deprecated("Use FlowSession.receive()", level = DeprecationLevel.WARNING)
@Suspendable
open fun <R : Any> receive(receiveType: Class<R>, otherParty: Party): UntrustworthyData<R> {
return stateMachine.receive(receiveType, otherParty, flowUsedForSessions)
return stateMachine.receive(receiveType, otherParty, flowUsedForSessions, maySkipCheckpoint = false)
}
/** Suspends until a message has been received for each session in the specified [sessions].
@ -250,7 +260,9 @@ abstract class FlowLogic<out T> {
*/
@Deprecated("Use FlowSession.send()", level = DeprecationLevel.WARNING)
@Suspendable
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
open fun send(otherParty: Party, payload: Any) {
stateMachine.send(otherParty, payload, flowUsedForSessions, maySkipCheckpoint = false)
}
/**
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
@ -342,7 +354,10 @@ abstract class FlowLogic<out T> {
* valid by the local node, but that doesn't imply the vault will consider it relevant.
*/
@Suspendable
fun waitForLedgerCommit(hash: SecureHash): SignedTransaction = stateMachine.waitForLedgerCommit(hash, this)
@JvmOverloads
fun waitForLedgerCommit(hash: SecureHash, maySkipCheckpoint: Boolean = false): SignedTransaction {
return stateMachine.waitForLedgerCommit(hash, this, maySkipCheckpoint = maySkipCheckpoint)
}
/**
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect

View File

@ -1,6 +1,6 @@
package net.corda.core.flows
import net.corda.core.crypto.SecureHash
import net.corda.core.DoNotImplement
import net.corda.core.serialization.CordaSerializable
/**
@ -8,6 +8,7 @@ import net.corda.core.serialization.CordaSerializable
* Typically this would be used from within the nextScheduledActivity method of a QueryableState to specify
* the flow to run at the scheduled time.
*/
@DoNotImplement
interface FlowLogicRefFactory {
fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
}
@ -24,4 +25,5 @@ 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
@DoNotImplement
interface FlowLogicRef

View File

@ -1,6 +1,7 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.DoNotImplement
import net.corda.core.identity.Party
import net.corda.core.utilities.UntrustworthyData
@ -41,6 +42,7 @@ import net.corda.core.utilities.UntrustworthyData
* will become
* otherSideSession.send(something)
*/
@DoNotImplement
abstract class FlowSession {
/**
* The [Party] on the other side of this session. In the case of a session created by [FlowLogic.initiateFlow]
@ -52,8 +54,20 @@ abstract class FlowSession {
* Returns a [FlowInfo] object describing the flow [counterparty] is using. With [FlowInfo.flowVersion] it
* provides the necessary information needed for the evolution of flows and enabling backwards compatibility.
*
* This method can be called before any send or receive has been done with [counterparty]. In such a case this will force
* them to start their flow.
* This method can be called before any send or receive has been done with [counterparty]. In such a case this will
* force them to start their flow.
*
* @param maySkipCheckpoint setting it to true indicates to the platform that it may optimise away the checkpoint.
*/
@Suspendable
abstract fun getCounterpartyFlowInfo(maySkipCheckpoint: Boolean): FlowInfo
/**
* Returns a [FlowInfo] object describing the flow [counterparty] is using. With [FlowInfo.flowVersion] it
* provides the necessary information needed for the evolution of flows and enabling backwards compatibility.
*
* This method can be called before any send or receive has been done with [counterparty]. In such a case this will
* force them to start their flow.
*/
@Suspendable
abstract fun getCounterpartyFlowInfo(): FlowInfo
@ -78,8 +92,26 @@ abstract class FlowSession {
/**
* 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
* should not be trusted until it's been thoroughly verified for consistency and that all expectations are
* is received, which must be of the given [receiveType]. 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.
*
* 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.
*
* @param maySkipCheckpoint setting it to true indicates to the platform that it may optimise away the checkpoint.
* @return an [UntrustworthyData] wrapper around the received object.
*/
@Suspendable
abstract fun <R : Any> sendAndReceive(
receiveType: Class<R>,
payload: Any, maySkipCheckpoint: Boolean
): UntrustworthyData<R>
/**
* 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 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.
*
* Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
@ -102,6 +134,19 @@ abstract class FlowSession {
return receive(R::class.java)
}
/**
* Suspends until [counterparty] sends us a message of type [receiveType].
*
* 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.
*
* @param maySkipCheckpoint setting it to true indicates to the platform that it may optimise away the checkpoint.
* @return an [UntrustworthyData] wrapper around the received object.
*/
@Suspendable
abstract fun <R : Any> receive(receiveType: Class<R>, maySkipCheckpoint: Boolean): UntrustworthyData<R>
/**
* Suspends until [counterparty] sends us a message of type [receiveType].
*
@ -114,6 +159,18 @@ abstract class FlowSession {
@Suspendable
abstract fun <R : Any> receive(receiveType: Class<R>): UntrustworthyData<R>
/**
* Queues the given [payload] for sending to the [counterparty] and continues without suspending.
*
* Note that the other party may receive the message at some arbitrary later point or not at all: if [counterparty]
* is offline then message delivery will be retried until it comes back or until the message is older than the
* network's event horizon time.
*
* @param maySkipCheckpoint setting it to true indicates to the platform that it may optimise away the checkpoint.
*/
@Suspendable
abstract fun send(payload: Any, maySkipCheckpoint: Boolean)
/**
* Queues the given [payload] for sending to the [counterparty] and continues without suspending.
*

View File

@ -1,5 +1,6 @@
package net.corda.core.identity
import net.corda.core.DoNotImplement
import net.corda.core.contracts.PartyAndReference
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes
@ -10,6 +11,7 @@ import java.security.PublicKey
* the party. In most cases [Party] or [AnonymousParty] should be used, depending on use-case.
*/
@CordaSerializable
@DoNotImplement
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

View File

@ -15,7 +15,7 @@ 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<R> {
@Suspendable
fun getFlowInfo(otherParty: Party, sessionFlow: FlowLogic<*>): FlowInfo
fun getFlowInfo(otherParty: Party, sessionFlow: FlowLogic<*>, maySkipCheckpoint: Boolean): FlowInfo
@Suspendable
fun initiateFlow(otherParty: Party, sessionFlow: FlowLogic<*>): FlowSession
@ -25,16 +25,17 @@ interface FlowStateMachine<R> {
otherParty: Party,
payload: Any,
sessionFlow: FlowLogic<*>,
retrySend: Boolean = false): UntrustworthyData<T>
retrySend: Boolean,
maySkipCheckpoint: Boolean): UntrustworthyData<T>
@Suspendable
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData<T>
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>, maySkipCheckpoint: Boolean): UntrustworthyData<T>
@Suspendable
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>, maySkipCheckpoint: Boolean)
@Suspendable
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>, maySkipCheckpoint: Boolean): SignedTransaction
@Suspendable
fun sleepUntil(until: Instant)

View File

@ -24,7 +24,6 @@ import rx.Observable
import java.io.InputStream
import java.security.PublicKey
import java.time.Instant
import java.util.*
@CordaSerializable
data class StateMachineInfo(

View File

@ -1,5 +1,6 @@
package net.corda.core.messaging
import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture
import net.corda.core.flows.StateMachineRunId
import net.corda.core.serialization.CordaSerializable
@ -11,6 +12,7 @@ import rx.Observable
* @property id The started state machine's ID.
* @property returnValue A [CordaFuture] of the flow's return value.
*/
@DoNotImplement
interface FlowHandle<A> : AutoCloseable {
val id: StateMachineRunId
val returnValue: CordaFuture<A>

View File

@ -1,9 +1,12 @@
package net.corda.core.messaging
import net.corda.core.DoNotImplement
/**
* Base interface that all RPC servers must implement. Note: in Corda there's only one RPC interface. This base
* interface is here in case we split the RPC system out into a separate library one day.
*/
@DoNotImplement
interface RPCOps {
/** Returns the RPC protocol version. Exists since version 0 so guaranteed to be present. */
val protocolVersion: Int

View File

@ -1,5 +1,6 @@
package net.corda.core.node
import net.corda.core.DoNotImplement
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.Crypto
@ -19,6 +20,7 @@ import java.time.Clock
/**
* Part of [ServiceHub].
*/
@DoNotImplement
interface StateLoader {
/**
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
@ -164,7 +166,7 @@ interface ServiceHub : ServicesForResolution {
@Throws(TransactionResolutionException::class)
fun <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return stx.resolveBaseTransaction(this).outRef<T>(stateRef.index)
return stx.resolveBaseTransaction(this).outRef(stateRef.index)
}
private val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentitiesAndCerts.first().owningKey

View File

@ -1,5 +1,6 @@
package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import java.io.IOException
@ -11,6 +12,7 @@ typealias AttachmentId = SecureHash
/**
* An attachment store records potentially large binary objects, identified by their hash.
*/
@DoNotImplement
interface AttachmentStorage {
/**
* Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open

View File

@ -1,5 +1,6 @@
package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UpgradedContract
import net.corda.core.flows.ContractUpgradeFlow
@ -9,6 +10,7 @@ import net.corda.core.flows.ContractUpgradeFlow
* a specified and mutually agreed (amongst participants) contract version.
* See also [ContractUpgradeFlow] to understand the workflow associated with contract upgrades.
*/
@DoNotImplement
interface ContractUpgradeService {
/** Get contracts we would be willing to upgrade the suggested contract to. */

View File

@ -1,6 +1,7 @@
package net.corda.core.node.services
import net.corda.core.CordaException
import net.corda.core.DoNotImplement
import net.corda.core.contracts.PartyAndReference
import net.corda.core.identity.*
import java.security.InvalidAlgorithmParameterException
@ -16,6 +17,7 @@ import java.security.cert.*
* 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.
*/
@DoNotImplement
interface IdentityService {
val trustRoot: X509Certificate
val trustAnchor: TrustAnchor

View File

@ -1,6 +1,7 @@
package net.corda.core.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.DoNotImplement
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.TransactionSignature
@ -11,6 +12,7 @@ import java.security.PublicKey
* The KMS 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.
*/
@DoNotImplement
interface KeyManagementService {
/**
* Returns a snapshot of the current signing [PublicKey]s.

View File

@ -1,5 +1,6 @@
package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
@ -18,8 +19,7 @@ import java.security.PublicKey
* 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.
*/
interface NetworkMapCache {
interface NetworkMapCache : NetworkMapCacheBase {
@CordaSerializable
sealed class MapChange {
abstract val node: NodeInfo
@ -29,6 +29,23 @@ interface NetworkMapCache {
data class Modified(override val node: NodeInfo, val previousNode: NodeInfo) : MapChange()
}
/**
* 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: [NetworkMapCache.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
* no node for the party, only that this cache is unaware of it.
*/
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
}
/** Subset of [NetworkMapCache] that doesn't depend on an [IdentityService]. */
@DoNotImplement
interface NetworkMapCacheBase {
// DOCSTART 1
/**
* A list of notary services available on the network.
@ -40,7 +57,7 @@ interface NetworkMapCache {
// DOCEND 1
/** Tracks changes to the network map cache. */
val changed: Observable<MapChange>
val changed: Observable<NetworkMapCache.MapChange>
/** Future to track completion of the NetworkMapService registration. */
val nodeReady: CordaFuture<Void?>
@ -48,20 +65,7 @@ interface NetworkMapCache {
* Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the
* first subscriber is registered so as to avoid racing with early updates.
*/
fun track(): DataFeed<List<NodeInfo>, MapChange>
/**
* 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
* no node for the party, only that this cache is unaware of it.
*/
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
fun track(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
/**
* Look up the node info for a legal name.

View File

@ -1,5 +1,6 @@
package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.DataFeed
import net.corda.core.transactions.SignedTransaction
@ -8,6 +9,7 @@ import rx.Observable
/**
* Thread-safe storage of transactions.
*/
@DoNotImplement
interface TransactionStorage {
/**
* Return the transaction with the given [id], or null if no such transaction exists.

View File

@ -1,5 +1,6 @@
package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture
import net.corda.core.transactions.LedgerTransaction
@ -7,6 +8,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
*/
@DoNotImplement
interface TransactionVerifierService {
/**
* @param transaction The transaction to be verified.

View File

@ -1,6 +1,7 @@
package net.corda.core.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
@ -14,7 +15,6 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.toFuture
import net.corda.core.utilities.NonEmptySet
import rx.Observable
import rx.subjects.PublishSubject
import java.time.Instant
import java.util.*
@ -151,6 +151,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
*
* Note that transactions we've seen are held by the storage service, not the vault.
*/
@DoNotImplement
interface VaultService {
/**
* Prefer the use of [updates] unless you know why you want to use this instead.

View File

@ -2,6 +2,7 @@
package net.corda.core.node.services.vault
import net.corda.core.DoNotImplement
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UniqueIdentifier
@ -144,6 +145,7 @@ sealed class QueryCriteria {
infix fun or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria)
}
@DoNotImplement
interface IQueryCriteriaParser {
fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate>

View File

@ -2,6 +2,7 @@
package net.corda.core.node.services.vault
import net.corda.core.DoNotImplement
import net.corda.core.internal.uncheckedCast
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
@ -10,6 +11,7 @@ import kotlin.reflect.KProperty1
import kotlin.reflect.jvm.javaGetter
@CordaSerializable
@DoNotImplement
interface Operator
enum class BinaryLogicalOperator : Operator {
@ -138,6 +140,7 @@ data class Sort(val columns: Collection<SortColumn>) {
}
@CordaSerializable
@DoNotImplement
interface Attribute
enum class CommonStateAttribute(val attributeParent: String, val attributeChild: String?) : Attribute {

View File

@ -8,6 +8,8 @@ import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.sequence
import java.sql.Blob
data class ObjectWithCompatibleContext<out T : Any>(val obj: T, val context: SerializationContext)
/**
* An abstraction for serializing and deserializing objects, with support for versioning of the wire format via
* a header / prefix in the bytes.
@ -22,6 +24,16 @@ abstract class SerializationFactory {
*/
abstract fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T
/**
* Deserialize the bytes in to an object, using the prefixed bytes to determine the format.
*
* @param byteSequence The bytes to deserialize, including a format header prefix.
* @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown.
* @param context A context that configures various parameters to deserialization.
* @return deserialized object along with [SerializationContext] to identify encoding used.
*/
abstract fun <T : Any> deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): ObjectWithCompatibleContext<T>
/**
* Serialize an object to bytes using the preferred serialization format version from the context.
*
@ -87,6 +99,8 @@ abstract class SerializationFactory {
}
}
typealias VersionHeader = ByteSequence
/**
* Parameters to serialization and deserialization.
*/
@ -94,7 +108,7 @@ interface SerializationContext {
/**
* When serializing, use the format this header sequence represents.
*/
val preferredSerializationVersion: ByteSequence
val preferredSerializationVersion: VersionHeader
/**
* The class loader to use for deserialization.
*/
@ -147,7 +161,7 @@ interface SerializationContext {
/**
* Helper method to return a new context based on this context but with serialization using the format this header sequence represents.
*/
fun withPreferredSerializationVersion(versionHeader: ByteSequence): SerializationContext
fun withPreferredSerializationVersion(versionHeader: VersionHeader): SerializationContext
/**
* The use case that we are serializing for, since it influences the implementations chosen.
@ -174,6 +188,15 @@ inline fun <reified T : Any> ByteSequence.deserialize(serializationFactory: Seri
return serializationFactory.deserialize(this, T::class.java, context)
}
/**
* Additionally returns [SerializationContext] which was used for encoding.
* It might be helpful to know [SerializationContext] to use the same encoding in the reply.
*/
inline fun <reified T : Any> ByteSequence.deserializeWithCompatibleContext(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): ObjectWithCompatibleContext<T> {
return serializationFactory.deserializeWithCompatibleContext(this, T::class.java, context)
}
/**
* Convenience extension method for deserializing SerializedBytes with type matching, utilising the defaults.
*/

View File

@ -1,5 +1,6 @@
package net.corda.core.transactions
import net.corda.core.DoNotImplement
import net.corda.core.contracts.*
import net.corda.core.identity.Party
import net.corda.core.internal.castIfPossible
@ -10,6 +11,7 @@ import java.util.function.Predicate
/**
* An abstract class defining fields shared by all transaction types in the system.
*/
@DoNotImplement
abstract class BaseTransaction : NamedByHash {
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
abstract val inputs: List<*>

View File

@ -25,7 +25,7 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes<TransactionState<ContractState>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
val commands: List<Command<*>> = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<Command<*>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
val commands: List<Command<*>> = deserialiseCommands()
override val notary: Party? = let {
val notaries: List<Party> = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes<Party>(it).deserialize() })
@ -74,6 +74,31 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
emptyList()
}
}
// Method to deserialise Commands from its two groups:
// COMMANDS_GROUP which contains the CommandData part
// and SIGNERS_GROUP which contains the Signers part.
private fun deserialiseCommands(): List<Command<*>> {
// TODO: we could avoid deserialising unrelated signers.
// However, current approach ensures the transaction is not malformed
// and it will throw if any of the signers objects is not List of public keys).
val signersList = deserialiseComponentGroup(ComponentGroupEnum.SIGNERS_GROUP, { SerializedBytes<List<PublicKey>>(it).deserialize() })
val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<CommandData>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
if (group is FilteredComponentGroup) {
check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" }
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
if (leafIndices.isNotEmpty())
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) }
} else {
// It is a WireTransaction
// or a FilteredTransaction with no Commands (in which case group is null).
check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" }
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
}
}
}
/**
@ -111,11 +136,12 @@ class FilteredTransaction private constructor(
val filteredSerialisedComponents: MutableMap<Int, MutableList<OpaqueBytes>> = hashMapOf()
val filteredComponentNonces: MutableMap<Int, MutableList<SecureHash>> = hashMapOf()
val filteredComponentHashes: MutableMap<Int, MutableList<SecureHash>> = hashMapOf() // Required for partial Merkle tree generation.
var signersIncluded = false
fun <T : Any> filter(t: T, componentGroupIndex: Int, internalIndex: Int) {
if (filtering.test(t)) {
val group = filteredSerialisedComponents[componentGroupIndex]
// Because the filter passed, we know there is a match. We also use first vs single as the init function
// Because the filter passed, we know there is a match. We also use first Vs single as the init function
// of WireTransaction ensures there are no duplicated groups.
val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex]
if (group == null) {
@ -132,6 +158,17 @@ class FilteredTransaction private constructor(
filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex])
filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex])
}
// If at least one command is visible, then all command-signers should be visible as well.
// This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details.
if (componentGroupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal && !signersIncluded) {
signersIncluded = true
val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal
// There exist commands, thus the signers group is not empty.
val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex }
filteredSerialisedComponents.put(signersGroupIndex, signersGroupComponents.components.toMutableList())
filteredComponentNonces.put(signersGroupIndex, wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList())
filteredComponentHashes.put(signersGroupIndex, wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList())
}
}
}
@ -142,6 +179,10 @@ class FilteredTransaction private constructor(
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)
// It is highlighted that because there is no a signers property in TraversableTransaction,
// one cannot specifically filter them in or out.
// The above is very important to ensure someone won't filter out the signers component group if at least one
// command is included in a FilteredTransaction.
// It's sometimes possible that when we receive a WireTransaction for which there is a new or more unknown component groups,
// we decide to filter and attach this field to a FilteredTransaction.
@ -207,7 +248,9 @@ class FilteredTransaction private constructor(
/**
* Function that checks if all of the components in a particular group are visible.
* This functionality is required on non-Validating Notaries to check that all inputs are visible.
* It might also be applied in Oracles, where an Oracle should know it can see all commands.
* It might also be applied in Oracles or any other entity requiring [Command] visibility, but because this method
* cannot distinguish between related and unrelated to the signer [Command]s, one should use the
* [checkCommandVisibility] method, which is specifically designed for [Command] visibility purposes.
* The logic behind this algorithm is that we check that the root of the provided group partialMerkleTree matches with the
* root of a fullMerkleTree if computed using all visible components.
* Note that this method is usually called after or before [verify], to also ensure that the provided partial Merkle
@ -229,18 +272,54 @@ class FilteredTransaction private constructor(
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}" }
visibilityCheck(groupPartialRoot == groupFullRoot) { "Some components for group ${group.groupIndex} are not visible" }
// Verify the top level Merkle tree from groupHashes.
visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" }
}
}
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any): Unit {
/**
* Function that checks if all of the commands that should be signed by the input public key are visible.
* This functionality is required from Oracles to check that all of the commands they should sign are visible.
* This algorithm uses the [ComponentGroupEnum.SIGNERS_GROUP] to count how many commands should be signed by the
* input [PublicKey] and it then matches it with the size of received [commands].
* Note that this method does not throw if there are no commands for this key to sign in the original [WireTransaction].
* @param publicKey signer's [PublicKey]
* @throws ComponentVisibilityException if not all of the related commands are visible.
*/
@Throws(ComponentVisibilityException::class)
fun checkCommandVisibility(publicKey: PublicKey) {
val commandSigners = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.SIGNERS_GROUP.ordinal }
val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners)
val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size
visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" }
}
// Function to return number of expected commands to sign.
private fun expectedNumOfCommands(publicKey: PublicKey, commandSigners: ComponentGroup?): Int {
checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP)
if (commandSigners == null) return 0
fun signersKeys (internalIndex: Int, opaqueBytes: OpaqueBytes): List<PublicKey> {
try {
return SerializedBytes<List<PublicKey>>(opaqueBytes.bytes).deserialize()
} catch (e: Exception) {
throw Exception("Malformed transaction, signers at index $internalIndex cannot be deserialised", e)
}
}
return commandSigners.components
.mapIndexed { internalIndex, opaqueBytes -> signersKeys(internalIndex, opaqueBytes) }
.filter { signers -> publicKey in signers }.size
}
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any) {
if (!value) {
val message = lazyMessage()
throw FilteredTransactionVerificationException(id, message.toString())
}
}
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any): Unit {
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) {
if (!value) {
val message = lazyMessage()
throw ComponentVisibilityException(id, message.toString())

View File

@ -1,5 +1,6 @@
package net.corda.core.transactions
import net.corda.core.DoNotImplement
import net.corda.core.contracts.NamedByHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.isFulfilledBy
@ -10,6 +11,7 @@ import java.security.PublicKey
import java.security.SignatureException
/** An interface for transactions containing signatures, with logic for signature verification */
@DoNotImplement
interface TransactionWithSignatures : NamedByHash {
val sigs: List<TransactionSignature>

View File

@ -6,9 +6,10 @@ import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.internal.Emoji
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.*
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes
import java.security.PublicKey
import java.security.SignatureException
import java.util.function.Predicate
@ -213,10 +214,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }))
if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }))
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() }))
// Adding commandData only to the commands group. Signers are added in their own group.
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }))
if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }))
if (notary != null) componentGroupMap.add(ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())))
if (timeWindow != null) componentGroupMap.add(ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())))
// Adding signers to their own group. This is required for command visibility purposes: a party receiving
// a FilteredTransaction can now verify it sees all the commands it should sign.
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }))
return componentGroupMap
}
}

View File

@ -3,6 +3,7 @@ package net.corda.core.concurrent
import com.nhaarman.mockito_kotlin.*
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.utilities.getOrThrow
import net.corda.testing.rigorousMock
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import org.slf4j.Logger
@ -16,7 +17,10 @@ class ConcurrencyUtilsTest {
private val f1 = openFuture<Int>()
private val f2 = openFuture<Double>()
private var invocations = 0
private val log = mock<Logger>()
private val log = rigorousMock<Logger>().also {
doNothing().whenever(it).error(any(), any<Throwable>())
}
@Test
fun `firstOf short circuit`() {
// Order not significant in this case:

View File

@ -1,13 +1,9 @@
package net.corda.core.contracts
import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.secureRandomBytes
import net.corda.core.crypto.*
import net.corda.core.serialization.serialize
import net.corda.core.transactions.ComponentGroup
import net.corda.core.transactions.ComponentVisibilityException
import net.corda.core.transactions.WireTransaction
import net.corda.core.transactions.*
import net.corda.core.utilities.OpaqueBytes
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
@ -34,13 +30,14 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) }
private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) }
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() }) }
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }) }
private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty.
private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) }
private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) }
private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) }
private val newUnknownComponentGroup = ComponentGroup(20, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8))))
private val newUnknownComponentEmptyGroup = ComponentGroup(21, emptyList())
private val newUnknownComponentGroup = ComponentGroup(100, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8))))
private val newUnknownComponentEmptyGroup = ComponentGroup(101, emptyList())
// Do not add attachments (empty list).
private val componentGroupsA by lazy {
@ -49,7 +46,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
timeWindowGroup,
signersGroup
)
}
private val wireTransactionA by lazy { WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt) }
@ -74,7 +72,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
commandGroup,
attachmentGroup,
notaryGroup,
timeWindowGroup
timeWindowGroup,
signersGroup
)
assertFails { WireTransaction(componentGroups = componentGroupsEmptyAttachment, privacySalt = privacySalt) }
@ -86,7 +85,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
timeWindowGroup,
signersGroup
)
val wireTransaction1ShuffledInputs = WireTransaction(componentGroups = componentGroupsB, privacySalt = privacySalt)
// The ID has changed due to change of the internal ordering in inputs.
@ -106,7 +106,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
inputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
timeWindowGroup,
signersGroup
)
assertEquals(wireTransactionA, WireTransaction(componentGroups = shuffledComponentGroupsA, privacySalt = privacySalt))
}
@ -123,7 +124,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
commandGroup,
ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components),
notaryGroup,
timeWindowGroup
timeWindowGroup,
signersGroup
)
assertFails { WireTransaction(componentGroupsB, privacySalt) }
@ -134,7 +136,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
commandGroup, // First commandsGroup.
commandGroup, // Second commandsGroup.
notaryGroup,
timeWindowGroup
timeWindowGroup,
signersGroup
)
assertFails { WireTransaction(componentGroupsDuplicatedCommands, privacySalt) }
@ -144,7 +147,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
timeWindowGroup,
signersGroup
)
assertFails { WireTransaction(componentGroupsC, privacySalt) }
@ -154,23 +158,24 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
commandGroup,
notaryGroup,
timeWindowGroup,
newUnknownComponentGroup // A new unknown component with ordinal 20 that we cannot process.
signersGroup,
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
)
// The old client (receiving more component types than expected) is still compatible.
val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt)
assertEquals(wireTransactionCompatibleA.availableComponentGroups, wireTransactionA.availableComponentGroups) // The known components are the same.
assertNotEquals(wireTransactionCompatibleA, wireTransactionA) // But obviously, its Merkle root has changed Vs wireTransactionA (which doesn't include this extra component).
assertEquals(6, wireTransactionCompatibleA.componentGroups.size)
// The old client will trhow if receiving an empty component (even if this unknown).
// The old client will throw if receiving an empty component (even if this is unknown).
val componentGroupsCompatibleEmptyNew = listOf(
inputGroup,
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup,
newUnknownComponentEmptyGroup // A new unknown component with ordinal 21 that we cannot process.
signersGroup,
newUnknownComponentEmptyGroup // A new unknown component with ordinal 101 that we cannot process.
)
assertFails { WireTransaction(componentGroupsCompatibleEmptyNew, privacySalt) }
}
@ -179,7 +184,9 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
fun `FilteredTransaction constructors and compatibility`() {
// Filter out all of the components.
val ftxNothing = wireTransactionA.buildFilteredTransaction(Predicate { false }) // Nothing filtered.
assertEquals(6, ftxNothing.groupHashes.size) // Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
// Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
// Note that attachments are not sent, but group hashes include the allOnesHash flag for the attachment group hash; that's why we expect +1 group hashes.
assertEquals(wireTransactionA.componentGroups.size + 1, ftxNothing.groupHashes.size)
ftxNothing.verify()
// Include all of the components.
@ -191,6 +198,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
ftxAll.checkAllComponentsVisible(ATTACHMENTS_GROUP)
ftxAll.checkAllComponentsVisible(NOTARY_GROUP)
ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP)
ftxAll.checkAllComponentsVisible(SIGNERS_GROUP)
// Filter inputs only.
fun filtering(elem: Any): Boolean {
@ -222,12 +230,14 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
assertNotNull(ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
// The old client (receiving more component types than expected) is still compatible.
val componentGroupsCompatibleA = listOf(inputGroup,
val componentGroupsCompatibleA = listOf(
inputGroup,
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup,
newUnknownComponentGroup // A new unknown component with ordinal 10,000 that we cannot process.
signersGroup,
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
)
val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt)
val ftxCompatible = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filtering))
@ -245,9 +255,288 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
ftxCompatibleAll.verify()
assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id)
// Check we received the last (6th) element that we cannot process (backwards compatibility).
assertEquals(6, ftxCompatibleAll.filteredComponentGroups.size)
// Check we received the last element that we cannot process (backwards compatibility).
assertEquals(wireTransactionCompatibleA.componentGroups.size, ftxCompatibleAll.filteredComponentGroups.size)
// Hide one component group only.
// Filter inputs only.
fun filterOutInputs(elem: Any): Boolean {
return when (elem) {
is StateRef -> false
else -> true
}
}
val ftxCompatibleNoInputs = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filterOutInputs))
ftxCompatibleNoInputs.verify()
assertFailsWith<ComponentVisibilityException> { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) }
assertEquals(wireTransactionCompatibleA.componentGroups.size - 1, ftxCompatibleNoInputs.filteredComponentGroups.size)
assertEquals(wireTransactionCompatibleA.componentGroups.map { it.groupIndex }.max()!!, ftxCompatibleNoInputs.groupHashes.size - 1)
}
@Test
fun `Command visibility tests`() {
// 1st and 3rd commands require a signature from KEY_1.
val twoCommandsforKey1 = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_1.public))
val componentGroups = listOf(
inputGroup,
outputGroup,
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
notaryGroup,
timeWindowGroup,
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }),
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
)
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt())
// Filter all commands.
fun filterCommandsOnly(elem: Any): Boolean {
return when (elem) {
is Command<*> -> true // Even if one Command is filtered, all signers are automatically filtered as well
else -> false
}
}
// Filter out commands only.
fun filterOutCommands(elem: Any): Boolean {
return when (elem) {
is Command<*> -> false
else -> true
}
}
// Filter KEY_1 commands.
fun filterKEY1Commands(elem: Any): Boolean {
return when (elem) {
is Command<*> -> DUMMY_KEY_1.public in elem.signers
else -> false
}
}
// Filter only one KEY_1 command.
fun filterTwoSignersCommands(elem: Any): Boolean {
return when (elem) {
is Command<*> -> elem.signers.size == 2 // dummyCommand(DUMMY_KEY_1.public) is filtered out.
else -> false
}
}
// Again filter only one KEY_1 command.
fun filterSingleSignersCommands(elem: Any): Boolean {
return when (elem) {
is Command<*> -> elem.signers.size == 1 // dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public) is filtered out.
else -> false
}
}
val allCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterCommandsOnly))
val noCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterOutCommands))
val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands))
val oneKey1CommandFtxA = wtx.buildFilteredTransaction(Predicate(::filterTwoSignersCommands))
val oneKey1CommandFtxB = wtx.buildFilteredTransaction(Predicate(::filterSingleSignersCommands))
allCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public)
assertFailsWith<ComponentVisibilityException> { noCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public) }
key1CommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public)
assertFailsWith<ComponentVisibilityException> { oneKey1CommandFtxA.checkCommandVisibility(DUMMY_KEY_1.public) }
assertFailsWith<ComponentVisibilityException> { oneKey1CommandFtxB.checkCommandVisibility(DUMMY_KEY_1.public) }
allCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP)
assertFailsWith<ComponentVisibilityException> { noCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) } // If we filter out all commands, signers are not sent as well.
key1CommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
oneKey1CommandFtxA.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
oneKey1CommandFtxB.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
// We don't send a list of signers.
val componentGroupsCompatible = listOf(
inputGroup,
outputGroup,
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
notaryGroup,
timeWindowGroup,
// ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }),
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
)
// Invalid Transaction. Sizes of CommandData and Signers (empty) do not match.
assertFailsWith<IllegalStateException> { WireTransaction(componentGroups = componentGroupsCompatible, privacySalt = PrivacySalt()) }
// We send smaller list of signers.
val componentGroupsLessSigners = listOf(
inputGroup,
outputGroup,
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
notaryGroup,
timeWindowGroup,
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }.subList(0, 1)), // Send first signer only.
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
)
// Invalid Transaction. Sizes of CommandData and Signers (empty) do not match.
assertFailsWith<IllegalStateException> { WireTransaction(componentGroups = componentGroupsLessSigners, privacySalt = PrivacySalt()) }
// Test if there is no command to sign.
val commandsNoKey1= listOf(dummyCommand(DUMMY_KEY_2.public))
val componentGroupsNoKey1ToSign = listOf(
inputGroup,
outputGroup,
ComponentGroup(COMMANDS_GROUP.ordinal, commandsNoKey1.map { it.value.serialize() }),
notaryGroup,
timeWindowGroup,
ComponentGroup(SIGNERS_GROUP.ordinal, commandsNoKey1.map { it.signers.serialize() }),
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
)
val wtxNoKey1 = WireTransaction(componentGroups = componentGroupsNoKey1ToSign, privacySalt = PrivacySalt())
val allCommandsNoKey1Ftx= wtxNoKey1.buildFilteredTransaction(Predicate(::filterCommandsOnly))
allCommandsNoKey1Ftx.checkCommandVisibility(DUMMY_KEY_1.public) // This will pass, because there are indeed no commands to sign in the original transaction.
}
@Test
fun `FilteredTransaction signer manipulation tests`() {
// Required to call the private constructor.
val ftxConstructor = FilteredTransaction::class.java.declaredConstructors[1]
ftxConstructor.isAccessible = true
// 1st and 3rd commands require a signature from KEY_1.
val twoCommandsforKey1 = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_1.public))
val componentGroups = listOf(
inputGroup,
outputGroup,
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
notaryGroup,
timeWindowGroup,
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() })
)
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt())
// Filter KEY_1 commands (commands 1 and 3).
fun filterKEY1Commands(elem: Any): Boolean {
return when (elem) {
is Command<*> -> DUMMY_KEY_1.public in elem.signers
else -> false
}
}
// Filter KEY_2 commands (commands 1 and 2).
fun filterKEY2Commands(elem: Any): Boolean {
return when (elem) {
is Command<*> -> DUMMY_KEY_2.public in elem.signers
else -> false
}
}
val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands))
val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands))
// val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components
val commandDataHashes = wtx.availableComponentHashes[ComponentGroupEnum.COMMANDS_GROUP.ordinal]!!
val noLastCommandDataPMT = PartialMerkleTree.build(
MerkleTree.getMerkleTree(commandDataHashes),
commandDataHashes.subList(0, 1)
)
val noLastCommandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components.subList(0, 1)
val noLastCommandDataNonces = key1CommandsFtx.filteredComponentGroups[0].nonces.subList(0, 1)
val noLastCommandDataGroup = FilteredComponentGroup(
ComponentGroupEnum.COMMANDS_GROUP.ordinal,
noLastCommandDataComponents,
noLastCommandDataNonces,
noLastCommandDataPMT
)
val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components
val signerHashes = wtx.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!
val noLastSignerPMT = PartialMerkleTree.build(
MerkleTree.getMerkleTree(signerHashes),
signerHashes.subList(0, 2)
)
val noLastSignerComponents = key1CommandsFtx.filteredComponentGroups[1].components.subList(0, 2)
val noLastSignerNonces = key1CommandsFtx.filteredComponentGroups[1].nonces.subList(0, 2)
val noLastSignerGroup = FilteredComponentGroup(
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
noLastSignerComponents,
noLastSignerNonces,
noLastSignerPMT
)
val noLastSignerGroupSamePartialTree = FilteredComponentGroup(
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
noLastSignerComponents,
noLastSignerNonces,
key1CommandsFtx.filteredComponentGroups[1].partialMerkleTree) // We don't update that, so we can catch the index mismatch.
val updatedFilteredComponentsNoSignersKey2 = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroup)
val updatedFilteredComponentsNoSignersKey2SamePMT = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree)
// There are only two components in key1CommandsFtx (commandData and signers).
assertEquals(2, key1CommandsFtx.componentGroups.size)
// Remove last signer for which there is a pointer from a visible commandData. This is the case of Key1.
// This will result to an invalid transaction.
// A command with no corresponding signer detected
// because the pointer of CommandData (3rd leaf) cannot find a corresponding (3rd) signer.
val updatedFilteredComponentsNoSignersKey1SamePMT = listOf(key1CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree)
assertFails { ftxConstructor.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoSignersKey1SamePMT, key1CommandsFtx.groupHashes) }
// Remove both last signer (KEY1) and related command.
// Update partial Merkle tree for signers.
val updatedFilteredComponentsNoLastCommandAndSigners = listOf(noLastCommandDataGroup, noLastSignerGroup)
val ftxNoLastCommandAndSigners = ftxConstructor.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoLastCommandAndSigners, key1CommandsFtx.groupHashes) as FilteredTransaction
// verify() will pass as the transaction is well-formed.
ftxNoLastCommandAndSigners.verify()
// checkCommandVisibility() will not pass, because checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) will fail.
assertFailsWith<ComponentVisibilityException> { ftxNoLastCommandAndSigners.checkCommandVisibility(DUMMY_KEY_1.public) }
// Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2.
// Do not change partial Merkle tree for signers.
// This time the object can be constructed as there is no pointer mismatch.
val ftxNoLastSigner = ftxConstructor.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2SamePMT, key2CommandsFtx.groupHashes) as FilteredTransaction
// verify() will fail as we didn't change the partial Merkle tree.
assertFailsWith<FilteredTransactionVerificationException> { ftxNoLastSigner.verify() }
// checkCommandVisibility() will not pass.
assertFailsWith<ComponentVisibilityException> { ftxNoLastSigner.checkCommandVisibility(DUMMY_KEY_2.public) }
// Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2.
// Update partial Merkle tree for signers.
val ftxNoLastSignerB = ftxConstructor.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2, key2CommandsFtx.groupHashes) as FilteredTransaction
// verify() will pass, the transaction is well-formed.
ftxNoLastSignerB.verify()
// But, checkAllComponentsVisible() will not pass.
assertFailsWith<ComponentVisibilityException> { ftxNoLastSignerB.checkCommandVisibility(DUMMY_KEY_2.public) }
// Modify last signer (we have a pointer from commandData).
// Update partial Merkle tree for signers.
val alterSignerComponents = signerComponents.subList(0, 2) + signerComponents[1] // Third one is removed and the 2nd command is added twice.
val alterSignersHashes = wtx.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2])
val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes)
val alterSignerPMTK = PartialMerkleTree.build(
alterMTree,
alterSignersHashes
)
val alterSignerGroup = FilteredComponentGroup(
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
alterSignerComponents,
key1CommandsFtx.filteredComponentGroups[1].nonces,
alterSignerPMTK
)
val alterFilteredComponents = listOf(key1CommandsFtx.filteredComponentGroups[0], alterSignerGroup)
// Do not update groupHashes.
val ftxAlterSigner = ftxConstructor.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes) as FilteredTransaction
// Visible components in signers group cannot be verified against their partial Merkle tree.
assertFailsWith<FilteredTransactionVerificationException> { ftxAlterSigner.verify() }
// Also, checkAllComponentsVisible() will not pass (groupHash matching will fail).
assertFailsWith<ComponentVisibilityException> { ftxAlterSigner.checkCommandVisibility(DUMMY_KEY_1.public) }
// Update groupHashes.
val ftxAlterSignerB = ftxConstructor.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes.subList(0, 6) + alterMTree.hash) as FilteredTransaction
// Visible components in signers group cannot be verified against their partial Merkle tree.
assertFailsWith<FilteredTransactionVerificationException> { ftxAlterSignerB.verify() }
// Also, checkAllComponentsVisible() will not pass (top level Merkle tree cannot be verified against transaction's id).
assertFailsWith<ComponentVisibilityException> { ftxAlterSignerB.checkCommandVisibility(DUMMY_KEY_1.public) }
ftxConstructor.isAccessible = false
}
}

View File

@ -1,6 +1,5 @@
package net.corda.core.crypto
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash.Companion.zeroHash
import net.corda.core.identity.Party
@ -14,10 +13,12 @@ import net.corda.testing.*
import org.junit.Test
import java.security.PublicKey
import java.util.function.Predicate
import java.util.stream.IntStream
import kotlin.streams.toList
import kotlin.test.*
class PartialMerkleTreeTest : TestDependencyInjectionBase() {
val nodes = "abcdef"
private val nodes = "abcdef"
private val hashed = nodes.map {
initialiseTestSerialization()
try {
@ -115,16 +116,18 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
val d = testTx.serialize().deserialize()
assertEquals(testTx.id, d.id)
val mt = testTx.buildFilteredTransaction(Predicate(::filtering))
val ftx = testTx.buildFilteredTransaction(Predicate(::filtering))
assertEquals(4, mt.filteredComponentGroups.size)
assertEquals(1, mt.inputs.size)
assertEquals(0, mt.attachments.size)
assertEquals(1, mt.outputs.size)
assertEquals(1, mt.commands.size)
assertNull(mt.notary)
assertNotNull(mt.timeWindow)
mt.verify()
// We expect 5 and not 4 component groups, because there is at least one command in the ftx and thus,
// the signers component is also sent (required for visibility purposes).
assertEquals(5, ftx.filteredComponentGroups.size)
assertEquals(1, ftx.inputs.size)
assertEquals(0, ftx.attachments.size)
assertEquals(1, ftx.outputs.size)
assertEquals(1, ftx.commands.size)
assertNull(ftx.notary)
assertNotNull(ftx.timeWindow)
ftx.verify()
}
@Test
@ -246,4 +249,50 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
privacySalt = privacySalt
)
}
@Test
fun `Find leaf index`() {
// A Merkle tree with 20 leaves.
val sampleLeaves = IntStream.rangeClosed(0, 19).toList().map { SecureHash.sha256(it.toString()) }
val merkleTree = MerkleTree.getMerkleTree(sampleLeaves)
// Provided hashes are not in the tree.
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("20"))) }
// One of the provided hashes is not in the tree.
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("20"), SecureHash.sha256("1"), SecureHash.sha256("5"))) }
val pmt = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("1"), SecureHash.sha256("5"), SecureHash.sha256("0"), SecureHash.sha256("19")))
// First leaf.
assertEquals(0, pmt.leafIndex(SecureHash.sha256("0")))
// Second leaf.
assertEquals(1, pmt.leafIndex(SecureHash.sha256("1")))
// A random leaf.
assertEquals(5, pmt.leafIndex(SecureHash.sha256("5")))
// The last leaf.
assertEquals(19, pmt.leafIndex(SecureHash.sha256("19")))
// The provided hash is not in the tree.
assertFailsWith<MerkleTreeException> { pmt.leafIndex(SecureHash.sha256("10")) }
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
assertFailsWith<MerkleTreeException> { pmt.leafIndex(SecureHash.sha256("30")) }
val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("0")))
assertEquals(0, pmtFirstElementOnly.leafIndex(SecureHash.sha256("0")))
// The provided hash is not in the tree.
assertFailsWith<MerkleTreeException> { pmtFirstElementOnly.leafIndex(SecureHash.sha256("10")) }
val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("19")))
assertEquals(19, pmtLastElementOnly.leafIndex(SecureHash.sha256("19")))
// The provided hash is not in the tree.
assertFailsWith<MerkleTreeException> { pmtLastElementOnly.leafIndex(SecureHash.sha256("10")) }
val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("5")))
assertEquals(5, pmtOneElement.leafIndex(SecureHash.sha256("5")))
// The provided hash is not in the tree.
assertFailsWith<MerkleTreeException> { pmtOneElement.leafIndex(SecureHash.sha256("10")) }
val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves)
for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(SecureHash.sha256(i.toString())))
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
assertFailsWith<MerkleTreeException> { pmtAllIncluded.leafIndex(SecureHash.sha256("30")) }
}
}

View File

@ -7,24 +7,21 @@ import net.corda.core.crypto.sha256
import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow
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.node.services.config.NodeConfiguration
import net.corda.node.services.persistence.NodeAttachmentService
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.node.MockNodeArgs
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.singleIdentity
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.KeyPair
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.test.assertEquals
@ -60,7 +57,6 @@ class AttachmentTests {
// Ensure that registration was successful before progressing any further
mockNet.runNetwork()
aliceNode.internals.ensureRegistered()
val alice = aliceNode.info.singleIdentity()
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
@ -98,7 +94,6 @@ class AttachmentTests {
// Ensure that registration was successful before progressing any further
mockNet.runNetwork()
aliceNode.internals.ensureRegistered()
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
@ -116,20 +111,15 @@ class AttachmentTests {
@Test
fun `malicious response`() {
// Make a node that doesn't do sanity checking at load time.
val aliceNode = mockNet.createNotaryNode(legalName = ALICE.name, nodeFactory = object : MockNetwork.Factory<MockNetwork.MockNode> {
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
id: Int, notaryIdentity: Pair<ServiceInfo, KeyPair>?,
entropyRoot: BigInteger): MockNetwork.MockNode {
return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) {
val aliceNode = mockNet.createNotaryNode(MockNodeParameters(legalName = ALICE.name), nodeFactory = object : MockNetwork.Factory<MockNetwork.MockNode> {
override fun create(args: MockNodeArgs): MockNetwork.MockNode {
return object : MockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
}
}
}, validating = false)
val bobNode = mockNet.createNode(legalName = BOB.name)
// Ensure that registration was successful before progressing any further
val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB.name))
mockNet.runNetwork()
aliceNode.internals.ensureRegistered()
val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)

View File

@ -44,7 +44,6 @@ class CollectSignaturesFlowTests {
bobNode = mockNet.createPartyNode(BOB.name)
charlieNode = mockNet.createPartyNode(CHARLIE.name)
mockNet.runNetwork()
aliceNode.internals.ensureRegistered()
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
charlie = charlieNode.info.singleIdentity()

View File

@ -47,7 +47,6 @@ class ContractUpgradeFlowTest {
// Process registration
mockNet.runNetwork()
aliceNode.internals.ensureRegistered()
notary = notaryNode.services.getDefaultNotary()
}
@ -119,7 +118,7 @@ class ContractUpgradeFlowTest {
return startRpcClient<CordaRPCOps>(
rpcAddress = startRpcServer(
rpcUser = user,
ops = CordaRPCOpsImpl(node.services, node.smm, node.database)
ops = CordaRPCOpsImpl(node.services, node.smm, node.database, node.services)
).get().broker.hostAndPort!!,
username = user.username,
password = user.password

View File

@ -6,7 +6,7 @@ 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.services.api.ServiceHubInternal
import net.corda.node.services.api.StartedNodeServices
import net.corda.testing.*
import net.corda.testing.node.MockNetwork
import org.junit.After
@ -17,8 +17,8 @@ import kotlin.test.assertFailsWith
class FinalityFlowTests {
private lateinit var mockNet: MockNetwork
private lateinit var aliceServices: ServiceHubInternal
private lateinit var bobServices: ServiceHubInternal
private lateinit var aliceServices: StartedNodeServices
private lateinit var bobServices: StartedNodeServices
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var notary: Party
@ -30,7 +30,6 @@ class FinalityFlowTests {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
mockNet.runNetwork()
aliceNode.internals.ensureRegistered()
aliceServices = aliceNode.services
bobServices = bobNode.services
alice = aliceNode.info.singleIdentity()

View File

@ -3,6 +3,7 @@ package net.corda.core.internal.concurrent
import com.nhaarman.mockito_kotlin.*
import net.corda.core.concurrent.CordaFuture
import net.corda.core.utilities.getOrThrow
import net.corda.testing.rigorousMock
import org.assertj.core.api.Assertions
import org.junit.Test
import org.slf4j.Logger
@ -31,7 +32,7 @@ class CordaFutureTest {
fun `if a listener fails its throwable is logged`() {
val f = CordaFutureImpl<Int>()
val x = Exception()
val log = mock<Logger>()
val log = rigorousMock<Logger>()
val flag = AtomicBoolean()
f.thenImpl(log) { throw x }
f.thenImpl(log) { flag.set(true) } // Must not be affected by failure of previous listener.
@ -57,7 +58,7 @@ class CordaFutureTest {
Assertions.assertThatThrownBy { g.getOrThrow() }.isSameAs(x)
}
run {
val block = mock<(Any?) -> Any?>()
val block = rigorousMock<(Any?) -> Any?>()
val f = CordaFutureImpl<Int>()
val g = f.map(block)
val x = Exception()
@ -90,7 +91,7 @@ class CordaFutureTest {
Assertions.assertThatThrownBy { g.getOrThrow() }.isSameAs(x)
}
run {
val block = mock<(Any?) -> CordaFuture<*>>()
val block = rigorousMock<(Any?) -> CordaFuture<*>>()
val f = CordaFutureImpl<Int>()
val g = f.flatMap(block)
val x = Exception()
@ -102,7 +103,8 @@ class CordaFutureTest {
@Test
fun `andForget works`() {
val log = mock<Logger>()
val log = rigorousMock<Logger>()
doNothing().whenever(log).error(any(), any<Throwable>())
val throwable = Exception("Boom")
val executor = Executors.newSingleThreadExecutor()
executor.fork { throw throwable }.andForget(log)

View File

@ -9,24 +9,21 @@ import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.TestDataVendingFlow
import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow
import net.corda.core.messaging.SingleMessageRecipient
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.node.services.config.NodeConfiguration
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.utilities.currentDBSession
import net.corda.nodeapi.internal.ServiceInfo
import net.corda.testing.chooseIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeArgs
import net.corda.testing.node.MockNodeParameters
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.nio.charset.StandardCharsets.UTF_8
import java.security.KeyPair
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.test.assertEquals
@ -74,7 +71,6 @@ class AttachmentSerializationTest {
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()
}
@After
@ -160,10 +156,9 @@ class AttachmentSerializationTest {
private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String {
client.dispose()
client = mockNet.createNode(client.internals.id, object : MockNetwork.Factory<MockNetwork.MockNode> {
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
id: Int, notaryIdentity: Pair<ServiceInfo, KeyPair>?, entropyRoot: BigInteger): MockNetwork.MockNode {
return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) {
client = mockNet.createNode(MockNodeParameters(client.internals.id), object : MockNetwork.Factory<MockNetwork.MockNode> {
override fun create(args: MockNodeArgs): MockNetwork.MockNode {
return object : MockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad }
}
}

View File

@ -6,6 +6,7 @@ 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.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
import net.corda.testing.TestDependencyInjectionBase
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
@ -26,7 +27,7 @@ class KotlinUtilsTest : TestDependencyInjectionBase() {
}
@Test
fun `checkpointing a transient property with non-capturing lamba`() {
fun `checkpointing a transient property with non-capturing lambda`() {
val original = NonCapturingTransientProperty()
val originalVal = original.transientVal
val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT)
@ -36,15 +37,15 @@ class KotlinUtilsTest : TestDependencyInjectionBase() {
}
@Test
fun `serialise transient property with non-capturing lamba`() {
fun `serialise transient property with non-capturing lambda`() {
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()
original.serialize(context = KRYO_P2P_CONTEXT)
}
@Test
fun `deserialise transient property with non-capturing lamba`() {
fun `deserialise transient property with non-capturing lambda`() {
expectedEx.expect(KryoException::class.java)
expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization")
val original = NonCapturingTransientProperty()
@ -52,7 +53,7 @@ class KotlinUtilsTest : TestDependencyInjectionBase() {
}
@Test
fun `checkpointing a transient property with capturing lamba`() {
fun `checkpointing a transient property with capturing lambda`() {
val original = CapturingTransientProperty("Hello")
val originalVal = original.transientVal
val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT)
@ -63,15 +64,15 @@ class KotlinUtilsTest : TestDependencyInjectionBase() {
}
@Test
fun `serialise transient property with capturing lamba`() {
fun `serialise transient property with capturing lambda`() {
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()
original.serialize(context = KRYO_P2P_CONTEXT)
}
@Test
fun `deserialise transient property with capturing lamba`() {
fun `deserialise transient property with capturing lambda`() {
expectedEx.expect(KryoException::class.java)
expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization")
val original = CapturingTransientProperty("Hello")

View File

@ -5,13 +5,17 @@ dependencies {
compile rootProject
}
ext {
// TODO: Add '../client/jfx/src/main/kotlin' and '../client/mock/src/main/kotlin' if we decide to make them into public API
dokkaSourceDirs = files('../core/src/main/kotlin', '../client/rpc/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin',
'../testing/test-utils/src/main/kotlin', '../testing/node-driver/src/main/kotlin')
}
dokka {
moduleName = 'corda'
outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/kotlin")
processConfigurations = ['compile']
// 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')
sourceDirs = dokkaSourceDirs
includes = ['packages.md']
jdkVersion = 8
@ -31,8 +35,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
outputFormat = "javadoc"
outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc")
processConfigurations = ['compile']
// 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')
sourceDirs = dokkaSourceDirs
includes = ['packages.md']
jdkVersion = 8

View File

@ -81,10 +81,10 @@ Custom schema registration
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan
for schemas (any class that extends the ``MappedSchema`` interface) in the `plugins` configuration directory in your CorDapp jar.
For testing purposes it is necessary to manually register custom schemas as follows:
For testing purposes it is necessary to manually register the packages containing custom schemas as follows:
- Tests using ``MockNetwork`` and ``MockNode`` must explicitly register custom schemas using the `registerCustomSchemas()` method of ``MockNode``
- Tests using ``MockServices`` must explicitly register schemas using `customSchemas` attribute of the ``MockServices`` `makeTestDatabaseAndMockServices()` helper method.
- Tests using ``MockNetwork`` and ``MockNode`` must explicitly register packages using the `cordappPackages` parameter of ``MockNetwork``
- Tests using ``MockServices`` must explicitly register packages using the `cordappPackages` parameter of the ``MockServices`` `makeTestDatabaseAndMockServices()` helper method.
.. note:: Tests using the `DriverDSL` will automatically register your custom schemas if they are in the same project structure as the driver call.

View File

@ -11,6 +11,8 @@ 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.
* Experimental support for PostgreSQL: CashSelection done using window functions
* ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions.
* Renamed "plugins" directory on nodes to "cordapps"
@ -28,7 +30,7 @@ UNRELEASED
* ``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
* Enums now respect 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.
@ -56,6 +58,17 @@ UNRELEASED
* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window, or ``null`` if the
time-window is open-ended.
* A new ``SIGNERS_GROUP`` with ordinal 6 has been added to ``ComponentGroupEnum`` that corresponds to the ``Command``
signers.
* ``PartialMerkleTree`` is equipped with a ``leafIndex`` function that returns the index of a hash (leaf) in the
partial Merkle tree structure.
* A new function ``checkCommandVisibility(publicKey: PublicKey)`` has been added to ``FilteredTransaction`` to check
if every command that a signer should receive (e.g. an Oracle) is indeed visible.
* Change the AMQP serialiser to use the oficially assigned R3 identifier rather than a placeholder.
.. _changelog_v1:
Release 1.0

View File

@ -91,3 +91,14 @@ The following modules are available but we do not commit to their stability or c
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.
The ``@DoNotImplement`` annotation
----------------------------------
Certain interfaces and abstract classes within the Corda API have been annotated
as ``@DoNotImplement``. While we undertake not to remove or modify any of these classes' existing
functionality, the annotation is a warning that we may need to extend them in future versions of Corda.
Cordapp developers should therefore just use these classes "as is", and *not* attempt to extend or implement any of them themselves.
This annotation is inherited by subclasses and subinterfaces.

View File

@ -155,6 +155,16 @@ path to the node's base directory.
:certificateSigningService: Certificate Signing Server address. It is used by the certificate signing request utility to
obtain SSL certificate. (See :doc:`permissioning` for more information.)
:jvmArgs: An optional list of JVM args, as strings, which replace those inherited from the command line when launching via ``corda.jar``
only. e.g. ``jvmArgs = [ "-Xmx220m", "-Xms220m", "-XX:+UseG1GC" ]``
:systemProperties: An optional map of additional system properties to be set when launching via ``corda.jar`` only. Keys and values
of the map should be strings. e.g. ``systemProperties = { visualvm.display.name = FooBar }``
:jarDirs: An optional list of file system directories containing JARs to include in the classpath when launching via ``corda.jar`` only.
Each should be a string. Only the JARs in the directories are added, not the directories themselves. This is useful
for including JDBC drivers and the like. e.g. ``jarDirs = [ 'lib' ]``
:relay: If provided, the node will attempt to tunnel inbound connections via an external relay. The relay's address will be
advertised to the network map service instead of the provided ``p2pAddress``.

View File

@ -1,7 +1,6 @@
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 {

View File

@ -0,0 +1,111 @@
package net.corda.docs.tutorial.mocknetwork
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.InitiatingFlow
import net.corda.core.identity.Party
import net.corda.core.messaging.MessageRecipients
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.internal.StartedNode
import net.corda.node.services.messaging.Message
import net.corda.node.services.statemachine.SessionData
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.MessagingServiceSpy
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.setMessagingServiceSpy
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
class TutorialMockNetwork {
@InitiatingFlow
class FlowA(private val otherParty: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(otherParty)
session.receive<Int>().unwrap {
requireThat { "Expected to receive 1" using (it == 1) }
}
session.receive<Int>().unwrap {
requireThat { "Expected to receive 2" using (it == 2) }
}
}
}
@InitiatedBy(FlowA::class)
class FlowB(private val session: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
session.send(1)
session.send(2)
}
}
lateinit private var mockNet: MockNetwork
lateinit private var notary: StartedNode<MockNetwork.MockNode>
lateinit private var nodeA: StartedNode<MockNetwork.MockNode>
lateinit private var nodeB: StartedNode<MockNetwork.MockNode>
@Rule
@JvmField
val expectedEx: ExpectedException = ExpectedException.none()
@Before
fun setUp() {
mockNet = MockNetwork()
notary = mockNet.createNotaryNode()
nodeA = mockNet.createPartyNode()
nodeB = mockNet.createPartyNode()
nodeB.registerInitiatedFlow(FlowB::class.java)
mockNet.runNetwork()
}
@After
fun tearDown() {
mockNet.stopNodes()
}
@Test
fun `fail if initiated doesn't send back 1 on first result`() {
// DOCSTART 1
// modify message if it's 1
nodeB.setMessagingServiceSpy(object : MessagingServiceSpy(nodeB.network) {
override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any, acknowledgementHandler: (() -> Unit)?) {
val messageData = message.data.deserialize<Any>()
if (messageData is SessionData && messageData.payload.deserialize() == 1) {
val alteredMessageData = SessionData(messageData.recipientSessionId, 99.serialize()).serialize().bytes
messagingService.send(InMemoryMessagingNetwork.InMemoryMessage(message.topicSession, alteredMessageData, message.uniqueMessageId), target, retryId)
} else {
messagingService.send(message, target, retryId)
}
}
})
// DOCEND 1
val initiatingReceiveFlow = nodeA.services.startFlow(FlowA(nodeB.info.legalIdentities.first()))
mockNet.runNetwork()
expectedEx.expect(IllegalArgumentException::class.java)
expectedEx.expectMessage("Expected to receive 1")
initiatingReceiveFlow.resultFuture.getOrThrow()
}
}

View File

@ -27,12 +27,18 @@ class CustomVaultQueryTest {
@Before
fun setup() {
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName))
mockNet = MockNetwork(
threadPerNode = true,
cordappPackages = listOf(
"net.corda.finance.contracts.asset",
CashSchemaV1::class.packageName,
"net.corda.docs"
)
)
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)
notary = nodeA.services.getDefaultNotary()
}

View File

@ -9,7 +9,7 @@ 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.services.api.ServiceHubInternal
import net.corda.node.services.api.StartedNodeServices
import net.corda.testing.*
import net.corda.testing.node.MockNetwork
import org.junit.After
@ -19,8 +19,8 @@ import kotlin.test.assertEquals
class WorkflowTransactionBuildTutorialTest {
lateinit var mockNet: MockNetwork
lateinit var aliceServices: ServiceHubInternal
lateinit var bobServices: ServiceHubInternal
lateinit var aliceServices: StartedNodeServices
lateinit var bobServices: StartedNodeServices
lateinit var alice: Party
lateinit var bob: Party

View File

@ -64,3 +64,16 @@ 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.
MockNetwork message manipulation
--------------------------------
The MockNetwork has the ability to manipulate message streams. You can use this to test your flows behaviour on corrupted,
or malicious data received.
Message modification example in ``TutorialMockNetwork.kt``:
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/mocknetwork/TutorialMockNetwork.kt
:language: kotlin
:start-after: DOCSTART 1
:end-before: DOCEND 1
:dedent: 8

View File

@ -95,9 +95,9 @@ to specify JAR URLs in the case that the CorDapp(s) involved in testing already
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
The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the
``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java)
when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files
within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the
``CordappLoader``. An example of this usage would be:
@ -108,17 +108,7 @@ within those packages will be zipped into a JAR and added to the attachment stor
@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()
network = new MockNetwork(new MockNetworkParameters().setCordappPackages(Arrays.asList("com.domain.cordapp")))
}
... // Your tests go here

View File

@ -0,0 +1,38 @@
Network Map
===========
Protocol Design
---------------
The node info publishing protocol:
* Create a ``NodeInfo`` object, and sign it to create a ``SignedData<NodeInfo>`` object. TODO: We will need list of signatures in ``SignedData`` to support multiple node identities in the future.
* Serialise the signed data and POST the data to the network map server.
* The network map server validates the signature and acknowledges the registration with a HTTP 200 response, it will return HTTP 400 "Bad Request" if the data failed validation or if the public key wasn't registered with the network.
* The network map server will sign and distribute the new network map periodically.
Node side network map update protocol:
* The Corda node will query the network map service periodically according to the ``Expires`` attribute in the HTTP header.
* The network map service returns a signed ``NetworkMap`` object, containing list of node info hashes and the network parameters hashes.
* The node updates its local copy of ``NodeInfos`` if it is different from the newly downloaded ``NetworkMap``.
Network Map service REST API:
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| Request method | Path | Description |
+================+===================================+========================================================================================================================================================+
| POST | /api/network-map/publish | Publish new ``NodeInfo`` to the network map service, the legal identity in ``NodeInfo`` must match with the identity registered with the doorman. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /api/network-map | Retrieve ``NetworkMap`` from the server, the ``NetworkMap`` object contains list of node info hashes and NetworkParameters hash. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /api/network-map/node-info/{hash} | Retrieve ``NodeInfo`` object with the same hash. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /api/network-map/parameters/{hash}| Retrieve ``NetworkParameters`` object with the same hash. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
TODO: Access control of the network map will be added in the future.

View File

@ -25,6 +25,19 @@ versions you are currently using are still in force.
We also strongly recommend cross referencing with the :doc:`changelog` to confirm changes.
UNRELEASED
----------
Testing
^^^^^^^
* The registration mechanism for CorDapps in ``MockNetwork`` unit tests has changed.
It is now done via the ``cordappPackages`` constructor parameter of MockNetwork.
This takes a list of `String` values which should be the
package names of the CorDapps containing the contract verification code you wish to load.
The ``unsetCordappPackages`` method is now redundant and has been removed.
:ref:`Milestone 14 <changelog_m14>`
------------

View File

@ -0,0 +1,83 @@
package net.corda.finance.contracts.asset.cash.selection
import net.corda.core.contracts.Amount
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toBase58String
import java.sql.Connection
import java.sql.DatabaseMetaData
import java.sql.ResultSet
import java.util.*
class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
companion object {
val JDBC_DRIVER_NAME = "PostgreSQL JDBC Driver"
val log = loggerFor<CashSelectionPostgreSQLImpl>()
}
override fun isCompatible(metadata: DatabaseMetaData): Boolean {
return metadata.driverName == JDBC_DRIVER_NAME
}
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
// This is using PostgreSQL window functions for selecting a minimum set of rows that match a request amount of coins:
// 1) This may also be possible with user-defined functions (e.g. using PL/pgSQL)
// 2) The window function accumulated column (`total`) does not include the current row (starts from 0) and cannot
// appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query.
// 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.contract_state, nested.pennies,
nested.total+nested.pennies as total_pennies, nested.lock_id
FROM
(SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies,
coalesce((SUM(ccs.pennies) OVER (PARTITION BY 1 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)), 0)
AS total, 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 = ?
AND (vs.lock_id = ? OR vs.lock_id is null)
""" +
(if (notary != null)
" AND vs.notary_name = ?" else "") +
(if (onlyFromIssuerParties.isNotEmpty())
" AND ccs.issuer_key = ANY (?)" else "") +
(if (withIssuerRefs.isNotEmpty())
" AND ccs.issuer_ref = ANY (?)" else "") +
""")
nested WHERE nested.total < ?
"""
val statement = connection.prepareStatement(selectJoin)
statement.setString(1, amount.token.toString())
statement.setString(2, lockId.toString())
var paramOffset = 0
if (notary != null) {
statement.setString(3, notary.name.toString())
paramOffset += 1
}
if (onlyFromIssuerParties.isNotEmpty()) {
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
{ it.owningKey.toBase58String() }.toTypedArray())
statement.setArray(3 + paramOffset, issuerKeys)
paramOffset += 1
}
if (withIssuerRefs.isNotEmpty()) {
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
{ it.bytes }.toTypedArray())
statement.setArray(3 + paramOffset, issuerRefs)
paramOffset += 1
}
statement.setLong(3 + paramOffset, amount.quantity)
log.debug { statement.toString() }
return statement.executeQuery()
}
}

View File

@ -1,2 +1,3 @@
net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl
net.corda.finance.contracts.asset.cash.selection.CashSelectionMySQLImpl
net.corda.finance.contracts.asset.cash.selection.CashSelectionPostgreSQLImpl

View File

@ -1,11 +1,14 @@
package net.corda.finance.contracts.asset
import net.corda.core.internal.packageName
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.finance.schemas.CashSchemaV1
import net.corda.testing.chooseIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeParameters
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
@ -14,15 +17,13 @@ class CashSelectionH2Test {
@Test
fun `check does not hold connection over retries`() {
val mockNet = MockNetwork(threadPerNode = true)
val mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName))
try {
val notaryNode = mockNet.createNotaryNode()
val bankA = mockNet.createNode(configOverrides = { existingConfig ->
val bankA = mockNet.createNode(MockNodeParameters(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.

View File

@ -16,6 +16,7 @@ import org.gradle.api.tasks.TaskAction;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@ -25,7 +26,7 @@ import java.net.URLClassLoader;
import java.util.*;
import java.util.stream.StreamSupport;
import static java.util.Collections.unmodifiableSet;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
@SuppressWarnings("unused")
@ -228,9 +229,19 @@ public class ScanApi extends DefaultTask {
private void writeClass(PrintWriter writer, ClassInfo classInfo, int modifiers) {
if (classInfo.isAnnotation()) {
/*
* Annotation declaration.
*/
writer.append(Modifier.toString(modifiers & INTERFACE_MASK));
writer.append(" @interface ").print(classInfo);
} else if (classInfo.isStandardClass()) {
/*
* Class declaration.
*/
List<String> annotationNames = toNames(readClassAnnotationsFor(classInfo));
if (!annotationNames.isEmpty()) {
writer.append(asAnnotations(annotationNames));
}
writer.append(Modifier.toString(modifiers & CLASS_MASK));
writer.append(" class ").print(classInfo);
Set<ClassInfo> superclasses = classInfo.getDirectSuperclasses();
@ -242,6 +253,13 @@ public class ScanApi extends DefaultTask {
writer.append(" implements ").print(stringOf(interfaces));
}
} else {
/*
* Interface declaration.
*/
List<String> annotationNames = toNames(readInterfaceAnnotationsFor(classInfo));
if (!annotationNames.isEmpty()) {
writer.append(asAnnotations(annotationNames));
}
writer.append(Modifier.toString(modifiers & INTERFACE_MASK));
writer.append(" interface ").print(classInfo);
Set<ClassInfo> superinterfaces = classInfo.getDirectSuperinterfaces();
@ -253,7 +271,7 @@ public class ScanApi extends DefaultTask {
}
private void writeMethods(PrintWriter writer, List<MethodInfo> methods) {
Collections.sort(methods);
sort(methods);
for (MethodInfo method : methods) {
if (isVisible(method.getAccessFlags()) // Only public and protected methods
&& isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods
@ -264,7 +282,7 @@ public class ScanApi extends DefaultTask {
}
private void writeFields(PrintWriter output, List<FieldInfo> fields) {
Collections.sort(fields);
sort(fields);
for (FieldInfo field : fields) {
if (isVisible(field.getAccessFlags()) && isValid(field.getAccessFlags(), FIELD_MASK)) {
output.append(" ").println(field);
@ -286,6 +304,36 @@ public class ScanApi extends DefaultTask {
return 0;
}
private List<String> toNames(Collection<ClassInfo> classes) {
return classes.stream()
.map(ClassInfo::toString)
.filter(ScanApi::isApplicationClass)
.collect(toList());
}
private Set<ClassInfo> readClassAnnotationsFor(ClassInfo classInfo) {
Set<ClassInfo> annotations = new HashSet<>(classInfo.getAnnotations());
annotations.addAll(selectInheritedAnnotations(classInfo.getSuperclasses()));
annotations.addAll(selectInheritedAnnotations(classInfo.getImplementedInterfaces()));
return annotations;
}
private Set<ClassInfo> readInterfaceAnnotationsFor(ClassInfo classInfo) {
Set<ClassInfo> annotations = new HashSet<>(classInfo.getAnnotations());
annotations.addAll(selectInheritedAnnotations(classInfo.getSuperinterfaces()));
return annotations;
}
/**
* Returns those annotations which have themselves been annotated as "Inherited".
*/
private List<ClassInfo> selectInheritedAnnotations(Collection<ClassInfo> classes) {
return classes.stream()
.flatMap(cls -> cls.getAnnotations().stream())
.filter(ann -> ann.hasMetaAnnotation(Inherited.class.getName()))
.collect(toList());
}
private MethodInfo filterAnnotationsFor(MethodInfo method) {
return new MethodInfo(
method.getClassName(),
@ -319,6 +367,14 @@ public class ScanApi extends DefaultTask {
return items.stream().map(ClassInfo::toString).collect(joining(", "));
}
private static String asAnnotations(Collection<String> items) {
return items.stream().collect(joining(" @", "@", " "));
}
private static boolean isApplicationClass(String typeName) {
return !typeName.startsWith("java.") && !typeName.startsWith("kotlin.");
}
private static URL toURL(File file) throws MalformedURLException {
return file.toURI().toURL();
}

View File

@ -29,7 +29,7 @@ class CordappPlugin : Plugin<Project> {
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
val jarTask = project.tasks.getByName("jar") as Jar
task.doLast {
jarTask.from(getDirectNonCordaDependencies(project).map { project.zipTree(it)}).apply {
exclude("META-INF/*.SF")
@ -71,6 +71,4 @@ class CordappPlugin : Plugin<Project> {
}
return filteredDeps.map { runtimeConfiguration.files(it) }.flatten().toSet()
}
private fun Project.configuration(name: String): Configuration = configurations.single { it.name == name }
}

View File

@ -1,7 +1,17 @@
package net.corda.plugins
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.plugins.ExtraPropertiesExtension
/**
* Mimics the "project.ext" functionality in groovy which provides a direct
* accessor to the "ext" extention (See: ExtraPropertiesExtension)
*/
@Suppress("UNCHECKED_CAST")
fun <T : Any> Project.ext(name: String): T = (extensions.findByName("ext") as ExtraPropertiesExtension).get(name) as T
fun Project.configuration(name: String): Configuration = configurations.single { it.name == name }
class Utils {
companion object {
@ -14,4 +24,5 @@ class Utils {
}
}
}
}

View File

@ -8,7 +8,6 @@ buildscript {
}
}
apply plugin: 'groovy'
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
@ -34,8 +33,8 @@ sourceSets {
dependencies {
compile gradleApi()
compile localGroovy()
compile project(":cordapp")
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"

View File

@ -1,153 +0,0 @@
package net.corda.plugins
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.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.TaskAction
import java.nio.file.Path
import java.nio.file.Paths
/**
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
*
* See documentation for examples.
*/
class Cordform extends DefaultTask {
/**
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
*/
String definitionClass
protected def directory = Paths.get("build", "nodes")
private def nodes = new ArrayList<Node>()
/**
* Set the directory to install nodes into.
*
* @param directory The directory the nodes will be installed into.
* @return
*/
void directory(String directory) {
this.directory = Paths.get(directory)
}
/**
* Add a node configuration.
*
* @param configureClosure A node configuration that will be deployed.
*/
void node(Closure configureClosure) {
nodes << (Node) project.configure(new Node(project), configureClosure)
}
/**
* Returns a node by name.
*
* @param name The name of the node as specified in the node configuration DSL.
* @return A node instance.
*/
private Node getNodeByName(String name) {
for (Node node : nodes) {
if (node.name == name) {
return node
}
}
return null
}
/**
* Installs the run script into the nodes directory.
*/
private void installRunScript() {
project.copy {
from Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar")
fileMode 0755
into "${directory}/"
}
project.copy {
from Cordformation.getPluginFile(project, "net/corda/plugins/runnodes")
// Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings.
filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.newInstance("lf"))
fileMode 0755
into "${directory}/"
}
project.copy {
from Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat")
into "${directory}/"
}
}
/**
* The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private CordformDefinition loadCordformDefinition() {
def plugin = project.convention.getPlugin(JavaPluginConvention.class)
def classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
URL[] urls = classpath.files.collect { it.toURI().toURL() }
(CordformDefinition) new URLClassLoader(urls, CordformDefinition.classLoader).loadClass(definitionClass).newInstance()
}
/**
* This task action will create and install the nodes based on the node configurations added.
*/
@TaskAction
void build() {
initializeConfiguration()
installRunScript()
nodes.each {
it.build()
}
generateNodeInfos()
}
private initializeConfiguration() {
if (null != definitionClass) {
def cd = loadCordformDefinition()
cd.nodeConfigurers.each { nc ->
node { Node it ->
nc.accept it
it.rootDir directory
}
}
cd.setup new CordformContext() {
Path baseDirectory(String nodeName) {
project.projectDir.toPath().resolve(getNodeByName(nodeName).nodeDir.toPath())
}
}
} else {
nodes.each {
it.rootDir directory
}
}
}
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()
}
}
}
}
}
}

View File

@ -1,28 +0,0 @@
package net.corda.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
/**
* The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation,
* testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner.
*/
class Cordformation implements Plugin<Project> {
/**
* Gets a resource file from this plugin's JAR file.
*
* @param project The project environment this plugin executes in.
* @param filePathInJar The file in the JAR, relative to root, you wish to access.
* @return A file handle to the file in the JAR.
*/
protected static File getPluginFile(Project project, String filePathInJar) {
return project.rootProject.resources.text.fromArchiveEntry(project.rootProject.buildscript.configurations.classpath.find {
it.name.contains('cordformation')
}, filePathInJar).asFile()
}
void apply(Project project) {
Utils.createCompileConfiguration("cordapp", project)
}
}

View File

@ -1,268 +0,0 @@
package net.corda.plugins
import com.typesafe.config.*
import net.corda.cordform.CordformNode
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import org.gradle.api.Project
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
/**
* Represents a node that will be installed.
*/
class Node extends CordformNode {
static final String NODEJAR_NAME = 'corda.jar'
static final String WEBJAR_NAME = 'corda-webserver.jar'
/**
* 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.
*/
protected List<String> cordapps = []
protected File nodeDir
private Project project
/**
* Sets whether this node will use HTTPS communication.
*
* @param isHttps True if this node uses HTTPS communication.
*/
void https(Boolean isHttps) {
config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps))
}
/**
* Sets the H2 port for this node
*/
void h2Port(Integer h2Port) {
config = config.withValue("h2port", ConfigValueFactory.fromAnyRef(h2Port))
}
void useTestClock(Boolean useTestClock) {
config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock))
}
/**
* Set the HTTP web server port for this node.
*
* @param webPort The web port number for this node.
*/
void webPort(Integer webPort) {
config = config.withValue("webAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort".toString()))
}
/**
* Set the network map address for this node.
*
* @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the
* Cordform task instead.
* @param networkMapAddress Network map node address.
* @param networkMapLegalName Network map node legal name.
*/
void networkMapAddress(String networkMapAddress, String networkMapLegalName) {
def networkMapService = new HashMap()
networkMapService.put("address", networkMapAddress)
networkMapService.put("legalName", networkMapLegalName)
config = config.withValue("networkMapService", ConfigValueFactory.fromMap(networkMapService))
}
/**
* Set the SSHD port for this node.
*
* @param sshdPort The SSHD port.
*/
void sshdPort(Integer sshdPort) {
config = config.withValue("sshdAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort".toString()))
}
Node(Project project) {
this.project = project
}
protected void rootDir(Path rootDir) {
def dirName
try {
X500Name x500Name = new X500Name(name)
dirName = x500Name.getRDNs(BCStyle.O).getAt(0).getFirst().getValue().toString()
} catch(IllegalArgumentException ignore) {
// Can't parse as an X500 name, use the full string
dirName = name
}
nodeDir = new File(rootDir.toFile(), dirName.replaceAll("\\s",""))
}
protected void build() {
configureProperties()
installCordaJar()
if (config.hasPath("webAddress")) {
installWebserverJar()
}
installBuiltCordapp()
installCordapps()
installConfig()
appendOptionalConfig()
}
/**
* Get the artemis address for this node.
*
* @return This node's P2P address.
*/
String getP2PAddress() {
return config.getString("p2pAddress")
}
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))
}
}
/**
* Installs the corda fat JAR to the node directory.
*/
private void installCordaJar() {
def cordaJar = verifyAndGetCordaJar()
project.copy {
from cordaJar
into nodeDir
rename cordaJar.name, NODEJAR_NAME
fileMode 0755
}
}
/**
* Installs the corda webserver JAR to the node directory
*/
private void installWebserverJar() {
def webJar = verifyAndGetWebserverJar()
project.copy {
from webJar
into nodeDir
rename webJar.name, WEBJAR_NAME
}
}
/**
* Installs this project's cordapp to this directory.
*/
private void installBuiltCordapp() {
def cordappsDir = new File(nodeDir, "cordapps")
project.copy {
from project.jar
into cordappsDir
}
}
/**
* Installs other cordapps to this node's cordapps directory.
*/
private void installCordapps() {
def cordappsDir = new File(nodeDir, "cordapps")
def cordapps = getCordappList()
project.copy {
from cordapps
into cordappsDir
}
}
/**
* Installs the configuration file to this node's directory and detokenises it.
*/
private void installConfig() {
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.
def tmpDir = new File(project.buildDir, "tmp")
def tmpConfFile = new File(tmpDir, 'node.conf')
Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8)
project.copy {
from tmpConfFile
into nodeDir
}
}
/**
* 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.
*
* @return A file representing the Corda JAR.
*/
private File verifyAndGetCordaJar() {
def maybeCordaJAR = project.configurations.runtime.filter {
it.toString().contains("corda-${project.corda_release_version}.jar") || it.toString().contains("corda-enterprise-${project.corda_release_version}.jar")
}
if (maybeCordaJAR.size() == 0) {
throw new RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-${project.corda_release_version}.jar\"")
} else {
def cordaJar = maybeCordaJAR.getSingleFile()
assert(cordaJar.isFile())
return cordaJar
}
}
/**
* Find the corda JAR amongst the dependencies
*
* @return A file representing the Corda webserver JAR
*/
private File verifyAndGetWebserverJar() {
def maybeJar = project.configurations.runtime.filter {
it.toString().contains("corda-webserver-${project.corda_release_version}.jar")
}
if (maybeJar.size() == 0) {
throw new RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-${project.corda_release_version}.jar\"")
} else {
def jar = maybeJar.getSingleFile()
assert(jar.isFile())
return jar
}
}
/**
* Gets a list of cordapps based on what dependent cordapps were specified.
*
* @return List of this node's cordapps.
*/
private Collection<File> getCordappList() {
// Cordapps can sometimes contain a GString instance which fails the equality test with the Java string
List<String> cordapps = this.cordapps.collect { it.toString() }
return project.configurations.cordapp.files {
cordapps.contains(it.group + ":" + it.name + ":" + it.version)
}
}
}

View File

@ -0,0 +1,198 @@
package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import net.corda.cordform.CordformNode
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
/**
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
*
* See documentation for examples.
*/
@Suppress("unused")
open class Cordform : DefaultTask() {
/**
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
*/
@Suppress("MemberVisibilityCanPrivate")
var definitionClass: String? = null
private var directory = Paths.get("build", "nodes")
private val nodes = mutableListOf<Node>()
/**
* Set the directory to install nodes into.
*
* @param directory The directory the nodes will be installed into.
*/
fun directory(directory: String) {
this.directory = Paths.get(directory)
}
/**
* Add a node configuration.
*
* @param configureClosure A node configuration that will be deployed.
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureClosure: Closure<in Node>) {
nodes += project.configure(Node(project), configureClosure) as Node
}
/**
* Add a node configuration
*
* @param configureFunc A node configuration that will be deployed
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureFunc: Node.() -> Any?): Node {
val node = Node(project).apply { configureFunc() }
nodes += node
return node
}
/**
* Returns a node by name.
*
* @param name The name of the node as specified in the node configuration DSL.
* @return A node instance.
*/
private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name }
/**
* Installs the run script into the nodes directory.
*/
private fun installRunScript() {
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar"))
fileMode = Cordformation.executableFileMode
into("$directory/")
}
}
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes"))
// Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings.
filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java)
fileMode = Cordformation.executableFileMode
into("$directory/")
}
}
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat"))
into("$directory/")
}
}
}
/**
* The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadCordformDefinition(): CordformDefinition {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, CordformDefinition::class.java.classLoader)
.loadClass(definitionClass)
.asSubclass(CordformDefinition::class.java)
.newInstance()
}
/**
* This task action will create and install the nodes based on the node configurations added.
*/
@Suppress("unused")
@TaskAction
fun build() {
project.logger.info("Running Cordform task")
initializeConfiguration()
installRunScript()
nodes.forEach(Node::build)
generateAndInstallNodeInfos()
}
private fun initializeConfiguration() {
if (definitionClass != null) {
val cd = loadCordformDefinition()
cd.nodeConfigurers.forEach {
val node = node { }
it.accept(node)
node.rootDir(directory)
}
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
} else {
nodes.forEach {
it.rootDir(directory)
}
}
}
private fun fullNodePath(node: Node): Path = project.projectDir.toPath().resolve(node.nodeDir.toPath())
private fun generateAndInstallNodeInfos() {
generateNodeInfos()
installNodeInfos()
}
private fun generateNodeInfos() {
project.logger.info("Generating node infos")
val generateTimeoutSeconds = 60L
val processes = nodes.map { node ->
project.logger.info("Generating node info for ${fullNodePath(node)}")
val logDir = File(fullNodePath(node).toFile(), "logs")
logDir.mkdirs() // Directory may not exist at this point
Pair(node, ProcessBuilder("java", "-jar", Node.nodeJarName, "--just-generate-node-info")
.directory(fullNodePath(node).toFile())
.redirectErrorStream(true)
// InheritIO causes hangs on windows due the gradle buffer also not being flushed.
// Must redirect to output or logger (node log is still written, this is just startup banner)
.redirectOutput(File(logDir, "generate-info-log.txt"))
.start())
}
try {
processes.parallelStream().forEach { (node, process) ->
if (!process.waitFor(generateTimeoutSeconds, TimeUnit.SECONDS)) {
throw GradleException("Node took longer $generateTimeoutSeconds seconds than too to generate node info - see node log at ${fullNodePath(node)}/logs")
} else if (process.exitValue() != 0) {
throw GradleException("Node exited with ${process.exitValue()} when generating node infos - see node log at ${fullNodePath(node)}/logs")
}
}
} finally {
// This will be a no-op on success - abort remaining on failure
processes.forEach {
it.second.destroyForcibly()
}
}
}
private fun installNodeInfos() {
project.logger.info("Installing node infos")
for (source in nodes) {
for (destination in nodes) {
if (source.nodeDir != destination.nodeDir) {
project.copy {
it.apply {
from(fullNodePath(source).toString())
include("nodeInfo-*")
into(fullNodePath(destination).resolve(CordformNode.NODE_INFO_DIRECTORY).toString())
}
}
}
}
}
}
}

View File

@ -0,0 +1,33 @@
package net.corda.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
/**
* The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation,
* testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner.
*/
class Cordformation : Plugin<Project> {
internal companion object {
/**
* Gets a resource file from this plugin's JAR file.
*
* @param project The project environment this plugin executes in.
* @param filePathInJar The file in the JAR, relative to root, you wish to access.
* @return A file handle to the file in the JAR.
*/
fun getPluginFile(project: Project, filePathInJar: String): File {
val archive: File? = project.rootProject.buildscript.configurations.single { it.name == "classpath" }.find {
it.name.contains("cordformation")
}
return project.rootProject.resources.text.fromArchiveEntry(archive, filePathInJar).asFile()
}
val executableFileMode = "0755".toInt(8)
}
override fun apply(project: Project) {
Utils.createCompileConfiguration("cordapp", project)
}
}

View File

@ -0,0 +1,290 @@
package net.corda.plugins
import com.typesafe.config.*
import net.corda.cordform.CordformNode
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.RDN
import org.bouncycastle.asn1.x500.style.BCStyle
import org.gradle.api.Project
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
/**
* Represents a node that will be installed.
*/
class Node(private val project: Project) : CordformNode() {
companion object {
@JvmStatic
val nodeJarName = "corda.jar"
@JvmStatic
val webJarName = "corda-webserver.jar"
private val configFileProperty = "configFile"
}
/**
* Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven
* dependency name, eg: com.example:product-name:0.1
*
* @note Your app will be installed by default and does not need to be included here.
* @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it
*/
var cordapps = mutableListOf<Any>()
private val releaseVersion = project.rootProject.ext<String>("corda_release_version")
internal lateinit var nodeDir: File
/**
* Sets whether this node will use HTTPS communication.
*
* @param isHttps True if this node uses HTTPS communication.
*/
fun https(isHttps: Boolean) {
config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps))
}
/**
* Sets the H2 port for this node
*/
fun h2Port(h2Port: Int) {
config = config.withValue("h2port", ConfigValueFactory.fromAnyRef(h2Port))
}
fun useTestClock(useTestClock: Boolean) {
config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock))
}
/**
* Set the HTTP web server port for this node.
*
* @param webPort The web port number for this node.
*/
fun webPort(webPort: Int) {
config = config.withValue("webAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort"))
}
/**
* Set the network map address for this node.
*
* @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the
* Cordform task instead.
* @param networkMapAddress Network map node address.
* @param networkMapLegalName Network map node legal name.
*/
fun networkMapAddress(networkMapAddress: String, networkMapLegalName: String) {
val networkMapService = mutableMapOf<String, String>()
networkMapService.put("address", networkMapAddress)
networkMapService.put("legalName", networkMapLegalName)
config = config.withValue("networkMapService", ConfigValueFactory.fromMap(networkMapService))
}
/**
* Set the SSHD port for this node.
*
* @param sshdPort The SSHD port.
*/
fun sshdPort(sshdPort: Int) {
config = config.withValue("sshdAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort"))
}
internal fun build() {
configureProperties()
installCordaJar()
if (config.hasPath("webAddress")) {
installWebserverJar()
}
installBuiltCordapp()
installCordapps()
installConfig()
appendOptionalConfig()
}
/**
* Get the artemis address for this node.
*
* @return This node's P2P address.
*/
fun getP2PAddress(): String {
return config.getString("p2pAddress")
}
internal fun rootDir(rootDir: Path) {
if(name == null) {
project.logger.error("Node has a null name - cannot create node")
throw IllegalStateException("Node has a null name - cannot create node")
}
val dirName = try {
val o = X500Name(name).getRDNs(BCStyle.O)
if (o.size > 0) {
o.first().first.value.toString()
} else {
name
}
} catch(_ : IllegalArgumentException) {
// Can't parse as an X500 name, use the full string
name
}
nodeDir = File(rootDir.toFile(), dirName.replace("\\s", ""))
}
private fun configureProperties() {
config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers))
if (notary != null) {
config = config.withValue("notary", ConfigValueFactory.fromMap(notary))
}
if (extraConfig != null) {
config = config.withFallback(ConfigFactory.parseMap(extraConfig))
}
}
/**
* Installs the corda fat JAR to the node directory.
*/
private fun installCordaJar() {
val cordaJar = verifyAndGetCordaJar()
project.copy {
it.apply {
from(cordaJar)
into(nodeDir)
rename(cordaJar.name, nodeJarName)
fileMode = Cordformation.executableFileMode
}
}
}
/**
* Installs the corda webserver JAR to the node directory
*/
private fun installWebserverJar() {
val webJar = verifyAndGetWebserverJar()
project.copy {
it.apply {
from(webJar)
into(nodeDir)
rename(webJar.name, webJarName)
}
}
}
/**
* Installs this project's cordapp to this directory.
*/
private fun installBuiltCordapp() {
val cordappsDir = File(nodeDir, "cordapps")
project.copy {
it.apply {
from(project.tasks.getByName("jar"))
into(cordappsDir)
}
}
}
/**
* Installs other cordapps to this node's cordapps directory.
*/
private fun installCordapps() {
val cordappsDir = File(nodeDir, "cordapps")
val cordapps = getCordappList()
project.copy {
it.apply {
from(cordapps)
into(cordappsDir)
}
}
}
/**
* Installs the configuration file to this node's directory and detokenises it.
*/
private fun installConfig() {
val options = ConfigRenderOptions.defaults().setOriginComments(false).setComments(false).setFormatted(false).setJson(false)
val configFileText = config.root().render(options).split("\n").toList()
// Need to write a temporary file first to use the project.copy, which resolves directories correctly.
val tmpDir = File(project.buildDir, "tmp")
val tmpConfFile = File(tmpDir, "node.conf")
Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8)
project.copy {
it.apply {
from(tmpConfFile)
into(nodeDir)
}
}
}
/**
* Appends installed config file with properties from an optional file.
*/
private fun appendOptionalConfig() {
val optionalConfig: File? = when {
project.findProperty(configFileProperty) != null -> //provided by -PconfigFile command line property when running Gradle task
File(project.findProperty(configFileProperty) as String)
config.hasPath(configFileProperty) -> File(config.getString(configFileProperty))
else -> null
}
if (optionalConfig != null) {
if (!optionalConfig.exists()) {
project.logger.error("$configFileProperty '$optionalConfig' not found")
} else {
val confFile = File(project.buildDir.path + "/../" + nodeDir, "node.conf")
confFile.appendBytes(optionalConfig.readBytes())
}
}
}
/**
* Find the corda JAR amongst the dependencies.
*
* @return A file representing the Corda JAR.
*/
private fun verifyAndGetCordaJar(): File {
val maybeCordaJAR = project.configuration("runtime").filter {
it.toString().contains("corda-$releaseVersion.jar") || it.toString().contains("corda-enterprise-$releaseVersion.jar")
}
if (maybeCordaJAR.isEmpty) {
throw RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-$releaseVersion.jar\"")
} else {
val cordaJar = maybeCordaJAR.singleFile
assert(cordaJar.isFile)
return cordaJar
}
}
/**
* Find the corda JAR amongst the dependencies
*
* @return A file representing the Corda webserver JAR
*/
private fun verifyAndGetWebserverJar(): File {
val maybeJar = project.configuration("runtime").filter {
it.toString().contains("corda-webserver-$releaseVersion.jar")
}
if (maybeJar.isEmpty) {
throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"")
} else {
val jar = maybeJar.singleFile
assert(jar.isFile)
return jar
}
}
/**
* Gets a list of cordapps based on what dependent cordapps were specified.
*
* @return List of this node's cordapps.
*/
private fun getCordappList(): Collection<File> {
// Cordapps can sometimes contain a GString instance which fails the equality test with the Java string
@Suppress("RemoveRedundantCallsOfConversionMethods")
val cordapps: List<String> = cordapps.map { it.toString() }
return project.configuration("cordapp").files {
cordapps.contains(it.group + ":" + it.name + ":" + it.version)
}
}
}

View File

@ -18,6 +18,8 @@ dependencies {
// TODO: Remove this dependency and the code that requires it
compile "commons-fileupload:commons-fileupload:$fileupload_version"
compile "net.corda.plugins:cordform-common:$gradle_plugins_version"
// TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:$typesafe_config_version"

View File

@ -0,0 +1,165 @@
package net.corda.nodeapi
import net.corda.cordform.CordformNode
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.createDirectories
import net.corda.core.internal.isRegularFile
import net.corda.core.internal.list
import net.corda.core.utilities.loggerFor
import rx.Observable
import rx.Scheduler
import rx.Subscription
import rx.schedulers.Schedulers
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
/**
* 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()) : AutoCloseable {
companion object {
private val log = loggerFor<NodeInfoFilesCopier>()
const val NODE_INFO_FILE_NAME_PREFIX = "nodeInfo-"
}
private val nodeDataMapBox = ThreadBox(mutableMapOf<Path, NodeData>())
/**
* Whether the NodeInfoFilesCopier is closed. When the NodeInfoFilesCopier is closed it will stop polling the
* filesystem and all the public methods except [#close] will throw.
*/
private var closed = false
private val subscription: Subscription
init {
this.subscription = Observable.interval(5, TimeUnit.SECONDS, scheduler)
.subscribe { poll() }
}
/**
* @param nodeDir a path to be watched for NodeInfos
* Add a path of 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.
*/
fun addConfig(nodeDir: Path) {
require(!closed) { "NodeInfoFilesCopier is already closed" }
nodeDataMapBox.locked {
val newNodeFile = NodeData(nodeDir)
put(nodeDir, newNodeFile)
for (previouslySeenFile in allPreviouslySeenFiles()) {
atomicCopy(previouslySeenFile, newNodeFile.additionalNodeInfoDirectory.resolve(previouslySeenFile.fileName))
}
log.info("Now watching: $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.
*/
fun removeConfig(nodeDir: Path) {
require(!closed) { "NodeInfoFilesCopier is already closed" }
nodeDataMapBox.locked {
remove(nodeDir) ?: return
log.info("Stopped watching: $nodeDir")
}
}
fun reset() {
require(!closed) { "NodeInfoFilesCopier is already closed" }
nodeDataMapBox.locked {
clear()
}
}
/**
* Stops polling the filesystem.
* This function can be called as many times as one wants.
*/
override fun close() {
if (!closed) {
closed = true
subscription.unsubscribe()
}
}
private fun allPreviouslySeenFiles() = nodeDataMapBox.alreadyLocked { values.flatMap { it.previouslySeenFiles.keys } }
private fun poll() {
nodeDataMapBox.locked {
for (nodeData in values) {
nodeData.nodeDir.list { paths ->
paths.filter { it.isRegularFile() }
.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }
.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) {
nodeDataMapBox.alreadyLocked {
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 this.values.filter { it.nodeDir != nodeData.nodeDir }.map { it.additionalNodeInfoDirectory }) {
val fullDestinationPath = destination.resolve(path.fileName)
atomicCopy(path, fullDestinationPath)
}
}
}
}
private fun atomicCopy(source: Path, destination: Path) {
val tempDestination = try {
Files.createTempFile(destination.parent, "", null)
} catch (exception: IOException) {
log.warn("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.warn("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.warn("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 additionalNodeInfoDirectory: Path = nodeDir.resolve(CordformNode.NODE_INFO_DIRECTORY)
// Map from Path to its lastModifiedTime.
val previouslySeenFiles = mutableMapOf<Path, FileTime>()
init {
additionalNodeInfoDirectory.createDirectories()
}
}
}

View File

@ -1,8 +1,8 @@
package net.corda.nodeapi
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.serialization.*
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.sequence
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.reader.MessageUtil
@ -20,12 +20,15 @@ object VerifierApi {
val responseAddress: SimpleString
) {
companion object {
fun fromClientMessage(message: ClientMessage): VerificationRequest {
return VerificationRequest(
fun fromClientMessage(message: ClientMessage): ObjectWithCompatibleContext<VerificationRequest> {
val bytes = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) }
val bytesSequence = bytes.sequence()
val (transaction, context) = bytesSequence.deserializeWithCompatibleContext<LedgerTransaction>()
val request = VerificationRequest(
message.getLongProperty(VERIFICATION_ID_FIELD_NAME),
ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) }.deserialize(),
MessageUtil.getJMSReplyTo(message)
)
transaction,
MessageUtil.getJMSReplyTo(message))
return ObjectWithCompatibleContext(request, context)
}
}
@ -49,10 +52,10 @@ object VerifierApi {
}
}
fun writeToClientMessage(message: ClientMessage) {
fun writeToClientMessage(message: ClientMessage, context: SerializationContext) {
message.putLongProperty(VERIFICATION_ID_FIELD_NAME, verificationId)
if (exception != null) {
message.putBytesProperty(RESULT_EXCEPTION_FIELD_NAME, exception.serialize().bytes)
message.putBytesProperty(RESULT_EXCEPTION_FIELD_NAME, exception.serialize(context = context).bytes)
}
}
}

View File

@ -3,6 +3,6 @@ package net.corda.nodeapi.internal.serialization
import net.corda.core.crypto.sha256
import net.corda.core.internal.AbstractAttachment
class GeneratedAttachment(bytes: ByteArray) : AbstractAttachment({ bytes }) {
class GeneratedAttachment(val bytes: ByteArray) : AbstractAttachment({ bytes }) {
override val id = bytes.sha256()
}

View File

@ -19,6 +19,7 @@ import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes
import net.corda.nodeapi.internal.AttachmentsClassLoader
import org.slf4j.LoggerFactory
import java.io.ByteArrayOutputStream
import java.io.NotSerializableException
import java.util.*
@ -37,7 +38,7 @@ object NotSupportedSerializationScheme : SerializationScheme {
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> = doThrow()
}
data class SerializationContextImpl(override val preferredSerializationVersion: ByteSequence,
data class SerializationContextImpl(override val preferredSerializationVersion: VersionHeader,
override val deserializationClassLoader: ClassLoader,
override val whitelist: ClassWhitelist,
override val properties: Map<Any, Any>,
@ -88,36 +89,54 @@ data class SerializationContextImpl(override val preferredSerializationVersion:
})
}
override fun withPreferredSerializationVersion(versionHeader: ByteSequence) = copy(preferredSerializationVersion = versionHeader)
override fun withPreferredSerializationVersion(versionHeader: VersionHeader) = copy(preferredSerializationVersion = versionHeader)
}
private const val HEADER_SIZE: Int = 8
fun ByteSequence.obtainHeaderSignature(): VersionHeader = take(HEADER_SIZE).copy()
open class SerializationFactoryImpl : SerializationFactory() {
private val creator: List<StackTraceElement> = Exception().stackTrace.asList()
private val registeredSchemes: MutableCollection<SerializationScheme> = Collections.synchronizedCollection(mutableListOf())
private val logger = LoggerFactory.getLogger(javaClass)
// TODO: This is read-mostly. Probably a faster implementation to be found.
private val schemes: ConcurrentHashMap<Pair<ByteSequence, SerializationContext.UseCase>, SerializationScheme> = ConcurrentHashMap()
private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): SerializationScheme {
private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): Pair<SerializationScheme, VersionHeader> {
// truncate sequence to 8 bytes, and make sure it's a copy to avoid holding onto large ByteArrays
return schemes.computeIfAbsent(byteSequence.take(HEADER_SIZE).copy() to target) {
val lookupKey = byteSequence.obtainHeaderSignature() to target
val scheme = schemes.computeIfAbsent(lookupKey) {
registeredSchemes
.filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) }
.forEach { return@computeIfAbsent it }
logger.warn("Cannot find serialization scheme for: $lookupKey, registeredSchemes are: $registeredSchemes")
NotSupportedSerializationScheme
}
return scheme to lookupKey.first
}
@Throws(NotSerializableException::class)
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
return asCurrent { withCurrentContext(context) { schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context) } }
return asCurrent { withCurrentContext(context) { schemeFor(byteSequence, context.useCase).first.deserialize(byteSequence, clazz, context) } }
}
@Throws(NotSerializableException::class)
override fun <T : Any> deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): ObjectWithCompatibleContext<T> {
return asCurrent {
withCurrentContext(context) {
val (scheme, versionHeader) = schemeFor(byteSequence, context.useCase)
val deserializedObject = scheme.deserialize(byteSequence, clazz, context)
ObjectWithCompatibleContext(deserializedObject, context.withPreferredSerializationVersion(versionHeader))
}
}
}
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
return asCurrent { withCurrentContext(context) { schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context) } }
return asCurrent { withCurrentContext(context) { schemeFor(context.preferredSerializationVersion, context.useCase).first.serialize(obj, context) } }
}
fun registerScheme(scheme: SerializationScheme) {

View File

@ -18,8 +18,15 @@ import java.util.*
import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField
import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema
// TODO: get an assigned number as per AMQP spec
const val DESCRIPTOR_TOP_32BITS: Long = 0xc0da0000
/**
* R3 AMQP assigned enterprise number
*
* see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers)
*
* Repeated here for brevity:
* 50530 - R3 - Mike Hearn - mike&r3.com
*/
const val DESCRIPTOR_TOP_32BITS: Long = 0xc5620000
const val DESCRIPTOR_DOMAIN: String = "net.corda"

View File

@ -1,8 +1,6 @@
package net.corda.demobench.model
package net.corda.nodeapi
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
@ -30,17 +28,13 @@ class NodeInfoFilesCopierTest {
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 GOOD_NODE_INFO_NAME = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}test"
private val GOOD_NODE_INFO_NAME_2 = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}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) }
@ -56,7 +50,7 @@ class NodeInfoFilesCopierTest {
@Test
fun `files created before a node is started are copied to that node`() {
// Configure the first node.
nodeInfoFilesCopier.addConfig(node1Config)
nodeInfoFilesCopier.addConfig(node1RootPath)
// Ensure directories are created.
advanceTime()
@ -65,7 +59,7 @@ class NodeInfoFilesCopierTest {
Files.write(node1RootPath.resolve(BAD_NODE_INFO_NAME), content)
// Configure the second node.
nodeInfoFilesCopier.addConfig(node2Config)
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
eventually<AssertionError, Unit>(Duration.ofMinutes(1)) {
@ -77,8 +71,8 @@ class NodeInfoFilesCopierTest {
@Test
fun `polling of running nodes`() {
// Configure 2 nodes.
nodeInfoFilesCopier.addConfig(node1Config)
nodeInfoFilesCopier.addConfig(node2Config)
nodeInfoFilesCopier.addConfig(node1RootPath)
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
// Create 2 files, one of which to be copied, in a node root path.
@ -95,8 +89,8 @@ class NodeInfoFilesCopierTest {
@Test
fun `remove nodes`() {
// Configure 2 nodes.
nodeInfoFilesCopier.addConfig(node1Config)
nodeInfoFilesCopier.addConfig(node2Config)
nodeInfoFilesCopier.addConfig(node1RootPath)
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
// Create a file, in node 2 root path.
@ -104,7 +98,7 @@ class NodeInfoFilesCopierTest {
advanceTime()
// Remove node 2
nodeInfoFilesCopier.removeConfig(node2Config)
nodeInfoFilesCopier.removeConfig(node2RootPath)
// Create another file in node 2 directory.
Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content)
@ -119,8 +113,8 @@ class NodeInfoFilesCopierTest {
@Test
fun `clear`() {
// Configure 2 nodes.
nodeInfoFilesCopier.addConfig(node1Config)
nodeInfoFilesCopier.addConfig(node2Config)
nodeInfoFilesCopier.addConfig(node1RootPath)
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
nodeInfoFilesCopier.reset()
@ -142,15 +136,4 @@ class NodeInfoFilesCopierTest {
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()))
}

View File

@ -1,6 +1,6 @@
package net.corda.nodeapi.internal
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
@ -17,10 +17,7 @@ import net.corda.nodeapi.DummyContractBackdoor
import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
import net.corda.nodeapi.internal.serialization.withTokenContext
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.MEGA_CORP
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.kryoSpecific
import net.corda.testing.*
import net.corda.testing.node.MockAttachmentStorage
import net.corda.testing.node.MockServices
import org.apache.commons.io.IOUtils
@ -41,8 +38,8 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
private fun SerializationContext.withAttachmentStorage(attachmentStorage: AttachmentStorage): SerializationContext {
val serviceHub = mock<ServiceHub>()
whenever(serviceHub.attachments).thenReturn(attachmentStorage)
val serviceHub = rigorousMock<ServiceHub>()
doReturn(attachmentStorage).whenever(serviceHub).attachments
return this.withServiceHub(serviceHub)
}

View File

@ -5,15 +5,13 @@ import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.util.DefaultClassResolver
import com.esotericsoftware.kryo.util.MapReferenceResolver
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever
import com.nhaarman.mockito_kotlin.*
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
import net.corda.nodeapi.internal.AttachmentsClassLoader
import net.corda.nodeapi.internal.AttachmentsClassLoaderTests
import net.corda.testing.node.MockAttachmentStorage
import net.corda.testing.rigorousMock
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
@ -108,16 +106,6 @@ class CordaClassResolverTests {
val emptyMapClass = mapOf<Any, Any>().javaClass
}
val factory: SerializationFactory = object : SerializationFactory() {
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
TODO("not implemented")
}
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
TODO("not implemented")
}
}
private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P)
private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P)
@ -252,10 +240,11 @@ class CordaClassResolverTests {
@Test
fun `Kotlin EmptyList registers as Java emptyList`() {
val javaEmptyListClass = Collections.emptyList<Any>().javaClass
val kryo = mock<Kryo>()
val kryo = rigorousMock<Kryo>()
val resolver = CordaClassResolver(allButBlacklistedContext).apply { setKryo(kryo) }
whenever(kryo.getDefaultSerializer(javaEmptyListClass)).thenReturn(DefaultSerializableSerializer())
doReturn(DefaultSerializableSerializer()).whenever(kryo).getDefaultSerializer(javaEmptyListClass)
doReturn(false).whenever(kryo).references
doReturn(false).whenever(kryo).references = any()
val registration = resolver.registerImplicit(emptyListClass)
assertNotNull(registration)
assertEquals(javaEmptyListClass, registration.type)
@ -273,10 +262,11 @@ class CordaClassResolverTests {
@Test
fun `Kotlin EmptySet registers as Java emptySet`() {
val javaEmptySetClass = Collections.emptySet<Any>().javaClass
val kryo = mock<Kryo>()
val kryo = rigorousMock<Kryo>()
val resolver = CordaClassResolver(allButBlacklistedContext).apply { setKryo(kryo) }
whenever(kryo.getDefaultSerializer(javaEmptySetClass)).thenReturn(DefaultSerializableSerializer())
doReturn(DefaultSerializableSerializer()).whenever(kryo).getDefaultSerializer(javaEmptySetClass)
doReturn(false).whenever(kryo).references
doReturn(false).whenever(kryo).references = any()
val registration = resolver.registerImplicit(emptySetClass)
assertNotNull(registration)
assertEquals(javaEmptySetClass, registration.type)
@ -294,10 +284,11 @@ class CordaClassResolverTests {
@Test
fun `Kotlin EmptyMap registers as Java emptyMap`() {
val javaEmptyMapClass = Collections.emptyMap<Any, Any>().javaClass
val kryo = mock<Kryo>()
val kryo = rigorousMock<Kryo>()
val resolver = CordaClassResolver(allButBlacklistedContext).apply { setKryo(kryo) }
whenever(kryo.getDefaultSerializer(javaEmptyMapClass)).thenReturn(DefaultSerializableSerializer())
doReturn(DefaultSerializableSerializer()).whenever(kryo).getDefaultSerializer(javaEmptyMapClass)
doReturn(false).whenever(kryo).references
doReturn(false).whenever(kryo).references = any()
val registration = resolver.registerImplicit(emptyMapClass)
assertNotNull(registration)
assertEquals(javaEmptyMapClass, registration.type)

View File

@ -3,10 +3,10 @@ package net.corda.nodeapi.internal.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.io.Output
import com.nhaarman.mockito_kotlin.mock
import net.corda.core.serialization.*
import net.corda.core.utilities.OpaqueBytes
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.rigorousMock
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
@ -35,8 +35,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() {
override fun equals(other: Any?) = other is LargeTokenizable && other.bytes.size == this.bytes.size
}
private fun serializeAsTokenContext(toBeTokenized: Any) = SerializeAsTokenContextImpl(toBeTokenized, factory, context, mock())
private fun serializeAsTokenContext(toBeTokenized: Any) = SerializeAsTokenContextImpl(toBeTokenized, factory, context, rigorousMock())
@Test
fun `write token and read tokenizable`() {
val tokenizableBefore = LargeTokenizable()

View File

@ -30,6 +30,7 @@ class EvolvabilityTests {
// data class C (val a: Int, val b: Int)
// val sc = SerializationOutput(sf).serialize(C(A, B))
// f.writeBytes(sc.bytes)
// println (path)
// 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)
@ -54,6 +55,7 @@ class EvolvabilityTests {
// data class C (val a: Int, val b: String)
// val sc = SerializationOutput(sf).serialize(C(A, B))
// f.writeBytes(sc.bytes)
// println (path)
// 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)
@ -78,7 +80,6 @@ class EvolvabilityTests {
// val sc = SerializationOutput(sf).serialize(C(A))
// f.writeBytes(sc.bytes)
// println ("Path = $path")
data class C(val a: Int, val b: Int?)
val sc2 = f.readBytes()
@ -300,9 +301,6 @@ class EvolvabilityTests {
val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.2")
val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.3")
@Suppress("UNUSED_VARIABLE")
val f = File(path1.toURI())
val a = 100
val b = 200
val c = 300
@ -312,14 +310,24 @@ class EvolvabilityTests {
//
// Version 1:
// data class C (val a: Int, val b: Int)
//
// val scc = SerializationOutput(sf).serialize(C(a, b))
// File(path1.toURI()).writeBytes(scc.bytes)
// println ("Path = $path1")
//
// Version 2 - add param c
// data class C (val c: Int, val b: Int, val a: Int)
//
// val scc = SerializationOutput(sf).serialize(C(c, b, a))
// File(path2.toURI()).writeBytes(scc.bytes)
// println ("Path = $path2")
//
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val a: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, a))
// f.writeBytes(scc.bytes)
// println ("Path = $path1")
// File(path3.toURI()).writeBytes(scc.bytes)
// println ("Path = $path3")
@Suppress("UNUSED")
data class C(val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) {
@ -409,14 +417,24 @@ class EvolvabilityTests {
//
// Version 1:
// data class C (val a: Int, val b: Int, val c: Int)
//
// val scc = SerializationOutput(sf).serialize(C(a, b, c))
// File(path1.toURI()).writeBytes(scc.bytes)
// println ("Path = $path1")
//
// Version 2 - add param c
// data class C (val b: Int, val c: Int, val d: Int, val e: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, e))
// File(path2.toURI()).writeBytes(scc.bytes)
// println ("Path = $path2")
//
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, e, f))
// File(path1.toURI()).writeBytes(scc.bytes)
// println ("Path = $path1")
// File(path3.toURI()).writeBytes(scc.bytes)
// println ("Path = $path3")
@Suppress("UNUSED")
data class C(val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) {

Some files were not shown because too many files have changed in this diff Show More