diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e69de29bb2..bf3094165c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -0,0 +1,205 @@ +# List of Contributors + +We'd like to thank the following people for contributing to Corda, either by +contributing to the design of Corda during the architecture review sessions of the +R3 Architecture Working Group and during design reviews since Corda has been +open-sourced, or by contributing code via pull requests. Some people have +moved to a different organisation since their contribution. Please forgive any +omissions, and create a pull request, or email , if you wish to +see changes to this list. + +* acetheultimate +* Adrian Fletcher (TD) +* agoldvarg +* Ajitha Thayaharan (BCS Technology International) +* Alberto Arri (R3) +* amiracam +* Andras Slemmer (R3) +* Andrius Dagys (R3) +* Andrzej Cichocki (R3) +* Andrzej Grzesik (R3) +* Anthony Keenan (R3) +* Anthony Woolley (Société Générale) +* Anton Semenov (Commerzbank) +* Antonio Cerrato (SEB) +* Antony Lewis (R3) +* anttiai +* Arijit Das (Northern Trust) +* Arnaud Stevens (Natixis) +* Arun Battu (BNY Mellon) +* Austin Moothart (R3) +* Balaji Bhanudas More (Synechron) +* Barry Childe (HSBC) +* Barry Flower (Westpac) +* Bart van den Bosch (KBC) +* Ben Wyeth (RBS) +* Benjamin Abineri (R3) +* Benoit Lafontaine (OCTO) +* Berit Bourgonje (ING) +* BitcoinErrorLog +* BMO +* Bob Crozier (AIA) +* Bogdan Paunescu (R3) +* Bruno dos Santos Carvalhal (Ordina Nederland) +* C-Otto +* Cais Manai (R3) +* Carl Worrall (BCS) +* Carlos Kuchovsky (BBVA) +* Cédric Wahl (Société Générale) +* Chaitanya Jadhav (HSBC) +* chalkido +* Chris Akers (R3) +* Chris Burlinchon (R3) +* Chris Rankin (R3) +* Christian Kaufmann +* Christian Sailer (R3) +* Christopher Saunders +* Christopher Swanson (US Bank) +* Clark Thompson (R3) +* Clay Ratliff (Thoughtworks) +* Clemens Wan (R3) +* Clinton Alexander (R3) +* Clyde D'Cruz (Persistent Systems Limited) +* cncorda +* Credit Suisse +* cyrsis +* Dan Newton (Accenture) +* Daniel Roig (SEB) +* Dave Hudson (R3) +* David John Grundy (Dankse Bank) +* David Lee (BCS) +* Dinesh Rivankar (Persistent Systems Limited) +* Dirk Hermans (KBC) +* dmytrobr +* Edward Greenwood (State Street) +* Elendu Uche (APPZONE) +* Emanuel Russo (NTT DATA Italy) +* Farzad Pezeshkpour (RBS) +* fracting +* Frederic Dalibard (Natixis) +* Garrett Macey (Wells Fargo) +* gary-rowe +* Gavin Thomas (R3) +* George Marcel Smetana (Bradesco) +* Giulio Katis (Westpac) +* Giuseppe Cardone (Intesa Sanpaolo) +* Guy Hochstetler (R3) +* Ian Cusden (UBS) +* Ian Grigg (R3) +* Igor Nitto (R3) +* Igor Panov +* Ivan Schasny (R3) +* James Brown (R3) +* James Carlyle (R3) +* Jared Harwayne-Gidansky (BNY Mellon) +* Jayavaradhan Sambedu (Société Générale) +* Joel Dudley (R3) +* Johan Hörmark (SEB) +* Johann Palychata (BNP Paribas) +* johnnyychiu +* Jonathan Sartin (R3) +* Jonathan Sphar (R3) +* Jose Coll (R3) +* Jose Luu (Natixis) +* Josh Lindl (BCS) +* Justin Chapman (Northern Trust) +* Kai-Michael Schramm +* Karel Hajek (Barclays Capital) +* karnauskas +* Kasia Streich (R3) +* Kat Baker (R3) +* Keerthi Nelaturu (Scotiabank) +* Khaild Ahmed (Northern Trust) +* Klaus Apolinario (Bradesco) +* Koen Vingerhoets (KBC) +* Kostas Chalkias (R3) +* Lars Stage Thomsen (Danske Bank) +* Lee Braine (Barclays) +* Lucas Salmen (Itau) +* Lulu Ren (S-Labs) +* Maksymilian Pawlak (R3) +* Marek Scocovsky (ABSA) +* marekdapps +* Mark Lauer (Westpac) +* Mark Oldfield (R3) +* Mark Raynes (Thomson Reuters) +* Mark Simpson (RBS) +* Mark Tiggas (Wells Fargo) +* Massimo Morini +* Mat Rizzo (R3) +* Matt Britton (BCS) +* Matthew Layton (TradeIX) +* Matthew Nesbit (R3) +* Matthijs van den Bos (ING) +* Michal Kit (R3) +* Micheal Hinstridge (Thoughtworks) +* Michele Sollecito (R3) +* Mike Hearn (R3) +* Mike Ward (R3) +* Mike Reichelt (US Bank) +* Mohamed Amine LEGHERABA +* Mustafa Ozturk (Natixis) +* Nick Skinner (Northern Trust) +* Nigel King (R3) +* Nitesh Solanki (Persistent Systems Limited) +* Nuam Athaweth (MUFG) +* Oscar Zibordi de Paiva (Scopus Soluções em TI) +* OP Financial +* Patrick Kuo (R3) +* Pekka Kaipio (OP Financial) +* Phillip Griffin +* Piotr Piskorski (Nordea) +* Przemyslaw Bak (R3) +* quiark +* RangerOfFire +* Rex Maudsley (Société Générale) +* Rhett Brewer (Goldman Sachs) +* Richard Crook (RBS) +* Richard Gendal Brown (R3) +* Richard Green (R3) +* Richard Green (Blocksure) +* Rick Parker (R3) +* Roberto Karpinski (Bradesco) +* Robin Green (CIBC) +* Rodrigo Bueno (Itau) +* Rodrigo Gonçalves (Itau Unibanco) +* Roger Willis (R3) +* Ross Burnett (Macquarie) +* Ross Nicoll (R3) +* Rui Hu (Nordea) +* s-matthew-english +* sadysnaat +* Sajindra Jayasena (Deutsche Bank) +* Saket Sharma (BNY Mellon) +* Sam Chadwick (Thomson Reuters) +* Sasmit Sahu +* Scott James +* Sean Zhang (Wells Fargo) +* Shams Asari (R3) +* Siddhartha Sengupta (Tradewind Markets) +* Simon Taylor (Barclays) +* Sofus Mortensen (Digital Asset Holdings) +* stevenroose +* Stanly Johnson (Servntire Global) +* Szymon Sztuka (R3) +* tb-pq +* Thiago Rafael Ferreira (Scopus Soluções em TI) +* Thomas O'Donnell (Macquarie) +* Thomas Schroeter (R3) +* Tim Swanson (R3) +* Timothy Smith +* Tittu Varghese (Servntire Global) +* Tom Menner (R3) +* tomconte +* Tommy Lillehagen (R3) +* tomtau +* Tudor Malene (R3) +* Tushar Singh Bora (Accenture) +* varunkm +* Venelin Stoykov (INDUSTRIA) +* verymahler +* Viktor Kolomeyko (R3) +* Vipin Bharathan +* Wawrzek Niewodniczanski (R3) +* Wei Wu Zhang (Commonwealth Bank of Australia) +* Zabrina Smith (Northern Trust) diff --git a/build.gradle b/build.gradle index 415cda53c1..263aba1889 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,7 @@ buildscript { ext.guava_version = constants.getProperty("guavaVersion") ext.caffeine_version = constants.getProperty("caffeineVersion") ext.metrics_version = constants.getProperty("metricsVersion") + ext.metrics_new_relic_version = constants.getProperty("metricsNewRelicVersion") ext.okhttp_version = '3.5.0' ext.netty_version = '4.1.22.Final' ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") diff --git a/constants.properties b/constants.properties index f51cdeb046..be65ec9d47 100644 --- a/constants.properties +++ b/constants.properties @@ -8,7 +8,7 @@ # Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. # -gradlePluginsVersion=4.0.28 +gradlePluginsVersion=4.0.29 kotlinVersion=1.2.51 platformVersion=4 guavaVersion=25.1-jre @@ -20,3 +20,4 @@ artifactoryPluginVersion=4.7.3 snakeYamlVersion=1.19 caffeineVersion=2.6.2 metricsVersion=3.2.5 +metricsNewRelicVersion=1.1.1 diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt index 03298f1751..91a8075b6d 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt @@ -27,9 +27,9 @@ sealed class NotaryError { /** Specifies which states have already been consumed in another transaction. */ val consumedStates: Map ) : NotaryError() { - override fun toString() = "Conflict notarising transaction $txId. " + - "Input states have been used in another transactions, count: ${consumedStates.size}, " + - "content: ${consumedStates.asSequence().joinToString(limit = 5) { it.key.toString() + "->" + it.value }}" + override fun toString() = "One or more input states have already been used in other transactions. Conflicting state count: ${consumedStates.size}, consumption details:\n" + + "${consumedStates.asSequence().joinToString(",\n", limit = 5) { it.key.toString() + " -> " + it.value }}.\n" + + "To find out if any of the conflicting transactions have been generated by this node you can use the hashLookup Corda shell command." } /** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */ diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index cf1f57aa14..62ca018372 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -108,7 +108,9 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) } @Suspendable - private fun getInputTransactions(tx: SignedTransaction): Set = tx.inputs.map { it.txhash }.toSet() + private fun getInputTransactions(tx: SignedTransaction): Set { + return tx.inputs.map { it.txhash }.toSet() + tx.references.map { it.txhash }.toSet() + } private class TransactionAuthorisationFilter(private val authorisedTransactions: MutableSet = mutableSetOf(), val acceptAll: Boolean = false) { fun isAuthorised(txId: SecureHash) = acceptAll || authorisedTransactions.contains(txId) diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt index 7a47a2eccc..28a1548e3d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt @@ -22,7 +22,7 @@ import net.corda.core.utilities.unwrap abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: TrustedAuthorityNotaryService) : FlowLogic() { companion object { // TODO: Determine an appropriate limit and also enforce in the network parameters and the transaction builder. - private const val maxAllowedInputs = 10_000 + private const val maxAllowedInputsAndReferences = 10_000 } @Suspendable @@ -51,9 +51,10 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: /** Checks whether the number of input states is too large. */ protected fun checkInputs(inputs: List) { - if (inputs.size > maxAllowedInputs) { + if (inputs.size > maxAllowedInputsAndReferences) { val error = NotaryError.TransactionInvalid( - IllegalArgumentException("A transaction cannot have more than $maxAllowedInputs inputs, received: ${inputs.size}") + IllegalArgumentException("A transaction cannot have more than $maxAllowedInputsAndReferences " + + "inputs or references, received: ${inputs.size}") ) throw NotaryInternalException(error) } diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt index 21a014c36c..fa1476c632 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt @@ -26,7 +26,14 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { * this method does not throw an exception when input states are present multiple times within the transaction. */ @JvmOverloads - fun commitInputStates(inputs: List, txId: SecureHash, caller: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List = emptyList()) { + fun commitInputStates( + inputs: List, + txId: SecureHash, + caller: Party, + requestSignature: NotarisationRequestSignature, + timeWindow: TimeWindow?, + references: List = emptyList() + ) { try { uniquenessProvider.commit(inputs, txId, caller, requestSignature, timeWindow, references) } catch (e: NotaryInternalException) { diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 817de0545a..42bf242dab 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -140,7 +140,7 @@ Signing the transaction Now that we have a valid transaction proposal, we need to sign it. Once the transaction is signed, no-one will be able to modify the transaction without invalidating this signature. This effectively makes the transaction immutable. -We sign the transaction using ``ServiceHub.toSignedTransaction``, which returns a ``SignedTransaction``. A +We sign the transaction using ``ServiceHub.signInitialTransaction``, which returns a ``SignedTransaction``. A ``SignedTransaction`` is an object that pairs a transaction with a list of signatures over that transaction. Finalising the transaction diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index a98cd6c077..49e0f7b600 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -109,10 +109,6 @@ We want to create an IOU of 99 with PartyB. We start the ``IOUFlow`` by typing: .. container:: codeset - .. code-block:: java - - start IOUFlow arg0: 99, arg1: "O=PartyB,L=New York,C=US" - .. code-block:: kotlin start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 616ad7bd78..402f939fb1 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -17,7 +17,7 @@ CorDapp onto a local test network of dummy nodes to test its functionality. CorDapps can be written in both Java and Kotlin, and will be providing the code in both languages in this tutorial. -Note that there's no need to download and install Corda itself. Corda V1.0's required libraries will be downloaded +Note that there's no need to download and install Corda itself. Corda's required libraries will be downloaded automatically from an online Maven repository. Downloading the template diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 7840dc6e6a..75af66b69a 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -286,12 +286,12 @@ Each node webserver exposes the following endpoints: There is also a web front-end served from ``/web/example``. -.. warning:: The content in ``web/example`` is only available for demonstration purposes and does not implement +.. warning:: The content in ``/web/example`` is only available for demonstration purposes and does not implement anti-XSS, anti-XSRF or other security techniques. Do not use this code in production. Creating an IOU via the endpoint ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -An IOU can be created by sending a PUT request to the ``api/example/create-iou`` endpoint directly, or by using the +An IOU can be created by sending a PUT request to the ``/api/example/create-iou`` endpoint directly, or by using the the web form served from ``/web/example``. To create an IOU between PartyA and PartyB, run the following command from the command line: diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst index a96e993d53..d50d4bc26f 100644 --- a/docs/source/upgrading-cordapps.rst +++ b/docs/source/upgrading-cordapps.rst @@ -297,21 +297,22 @@ s participants agree to the proposed upgrade. The following combinations of upgr * A state is upgraded while the contract stays the same. * The state and the contract are updated simultaneously. -The procedure for updating a state or a contract using a flag-day approach is quite simple: +Performing explicit contract and state upgrades +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* Update and test the state or contract. -* Produce a new CorDapp JAR file and distribute it to all the relevant parties. -* Each node operator stops their node, replaces the existing JAR with the new one, and restarts. They may wish to do - a node drain first to avoid the definition of states or contracts changing whilst a flow is in progress. -* Run the contract upgrade authorisation flow for each state that requires updating on every node. -* For each state, one node should run the contract upgrade initiation flow, which will contact the rest. +1. Preserve the existing state and contract definitions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Currently, all nodes must **permanently** keep **all** old state and contract definitions on their node's classpath -Update Process -~~~~~~~~~~~~~~ +.. note:: Once the contract-code-as-an-attachment feature has been implemented, nodes will only be required to keep the + old state and contract definitions on their node's classpath for the duration of the upgrade -Writing the new state and contract definitions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Start by updating the contract and/or state definitions. There are no restrictions on how states are updated. However, +Changing a state or contract's package constitutes a definition change. If you want to move a state or contract +definition to a new package, you must also preserve the definition in the old package + +2. Write the new state and contract definitions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Update the contract and/or state definitions. There are no restrictions on how states are updated. However, upgraded contracts must implement the ``UpgradedContract`` interface. This interface is defined as: .. sourcecode:: kotlin @@ -341,16 +342,40 @@ For example, in case of hash constraints the hash of the legacy JAR file should override val legacyContractConstraint: AttachmentConstraint get() = HashAttachmentConstraint(SecureHash.parse("E02BD2B9B010BBCE49C0D7C35BECEF2C79BEB2EE80D902B54CC9231418A4FA0C")) -Authorising the upgrade -^^^^^^^^^^^^^^^^^^^^^^^ -Once the new states and contracts are on the classpath for all the relevant nodes, the next step is for all nodes to -run the ``ContractUpgradeFlow.Authorise`` flow. This flow takes a ``StateAndRef`` of the state to update as well as a -reference to the new contract, which must implement the ``UpgradedContract`` interface. +3. Create the new CorDapp JAR +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Produce a new CorDapp JAR file. This JAR file should only contain the new contract and state definitions. + +4. Distribute the new CorDapp JAR +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Place the new CorDapp JAR file in the ``cordapps`` folder of all the relevant nodes. You can do this while the nodes are still +running. + +5. Stop the nodes +^^^^^^^^^^^^^^^^^ +Have each node operator stop their node. If you are also changing flow definitions, you should perform a +:ref:`node drain ` first to avoid the definition of states or contracts changing whilst a flow is +in progress. + +6. Re-run the network bootstrapper +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If you're using the network bootstrapper instead of a network map server and have defined any new contracts, you need to +re-run the network bootstrapper to whitelist the new contracts. See :doc:`network-bootstrapper`. + +7. Restart the nodes +^^^^^^^^^^^^^^^^^^^^ +Have each node operator restart their node. + +8. Authorise the upgrade +^^^^^^^^^^^^^^^^^^^^^^^^ +Now that new states and contracts are on the classpath for all the relevant nodes, the next step is for all node to run the +``ContractUpgradeFlow.Authorise`` flow. This flow takes a ``StateAndRef`` of the state to update as well as a reference +to the new contract, which must implement the ``UpgradedContract`` interface. At any point, a node administrator may de-authorise a contract upgrade by running the ``ContractUpgradeFlow.Deauthorise`` flow. -Performing the upgrade +9. Perform the upgrade ^^^^^^^^^^^^^^^^^^^^^^ Once all nodes have performed the authorisation process, a participant must be chosen to initiate the upgrade via the ``ContractUpgradeFlow.Initiate`` flow for each state object. This flow has the following signature: diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt index e1c61219b5..77d9088336 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt @@ -34,7 +34,7 @@ object SampleCashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, ve override val migrationResource = "sample-cash-v1.changelog-init" @Entity - @Table(name = "contract_cash_states_v1", indexes = [Index(name = "ccy_code_idx", columnList = "ccy_code"), Index(name = "pennies_idx", columnList = "pennies")]) + @Table(name = "contract_cash_states_v1", indexes = [Index(name = "ccy_code_idx1", columnList = "ccy_code"), Index(name = "pennies_idx1", columnList = "pennies")]) class PersistentCashState( @Column(name = "owner_key_hash", length = MAX_HASH_HEX_SIZE, nullable = false) var ownerHash: String, diff --git a/node/build.gradle b/node/build.gradle index d65efa0db7..e2fa641136 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -238,7 +238,7 @@ dependencies { // Jolokia JVM monitoring agent, required to push logs through slf4j compile "org.jolokia:jolokia-jvm:${jolokia_version}:agent" // Optional New Relic JVM reporter, used to push metrics to the configured account associated with a newrelic.yml configuration. See https://mvnrepository.com/artifact/com.palominolabs.metrics/metrics-new-relic - compile group: 'com.palominolabs.metrics', name: 'metrics-new-relic', version: '1.1.1' + compile "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}" // Allow access to simple SOCKS Server for integration testing testCompile("io.netty:netty-example:$netty_version") { diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index a1e6d591eb..9db8e05f91 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -56,6 +56,8 @@ import java.lang.management.ManagementFactory import java.net.InetAddress import java.nio.file.Path import java.nio.file.Paths +import java.time.DayOfWeek +import java.time.ZonedDateTime import java.util.* import kotlin.system.exitProcess @@ -539,8 +541,8 @@ open class NodeStartup(val args: Array) { "It's kind of like a block chain but\ncords sounded healthier than chains.", "Computer science and finance together.\nYou should see our crazy Christmas parties!", "I met my bank manager yesterday and asked\nto check my balance ... he pushed me over!", - "A banker with nobody around may find\nthemselves .... a-loan! ", - "Whenever I go near my bank I get\nwithdrawal symptoms ${Emoji.coolGuy}", + "A banker left to their own devices may find\nthemselves .... a-loan! ", + "Whenever I go near my bank\nI get withdrawal symptoms ${Emoji.coolGuy}", "There was an earthquake in California,\na local bank went into de-fault.", "I asked for insurance if the nearby\nvolcano erupted. They said I'd be covered.", "I had an account with a bank in the\nNorth Pole, but they froze all my assets ${Emoji.santaClaus}", @@ -551,11 +553,48 @@ open class NodeStartup(val args: Array) { "I won $3M on the lottery so I donated a quarter\nof it to charity. Now I have $2,999,999.75.", "There are two rules for financial success:\n1) Don't tell everything you know.", "Top tip: never say \"oops\", instead\nalways say \"Ah, Interesting!\"", - "Computers are useless. They can only\ngive you answers. -- Picasso" + "Computers are useless. They can only\ngive you answers. -- Picasso", + "Regular naps prevent old age, especially\nif you take them whilst driving.", + "Always borrow money from a pessimist.\nHe won't expect it back.", + "War does not determine who is right.\nIt determines who is left.", + "A bus stops at a bus station. A train stops at a\ntrain station. What happens at a workstation?", + "I got a universal remote control yesterday.\nI thought, this changes everything.", + "Did you ever walk into an office and\nthink, whiteboards are remarkable!", + "The good thing about lending out your time machine\nis that you basically get it back immediately.", + "I used to work in a shoe recycling\nshop. It was sole destroying.", + "What did the fish say\nwhen he hit a wall? Dam.", + "You should really try a seafood diet.\nIt's easy: you see food and eat it.", + "I recently sold my vacuum cleaner,\nall it was doing was gathering dust.", + "My professor accused me of plagiarism.\nHis words, not mine!", + "Change is inevitable, except\nfrom a vending machine.", + "If at first you don't succeed, destroy\nall the evidence that you tried.", + "If at first you don't succeed, \nthen we have something in common!", + "Moses had the first tablet that\ncould connect to the cloud.", + "How did my parents fight boredom before the internet?\nI asked my 17 siblings and they didn't know either.", + "Cats spend two thirds of their lives sleeping\nand the other third making viral videos.", + "The problem with troubleshooting\nis that trouble shoots back.", + "I named my dog 'Six Miles' so I can tell\npeople I walk Six Miles every day.", + "People used to laugh at me when I said I wanted\nto be a comedian. Well they're not laughing now!", + "My wife just found out I replaced our bed\nwith a trampoline; she hit the roof.", + "My boss asked me who is the stupid one, me or him?\nI said everyone knows he doesn't hire stupid people.", + "Don't trust atoms.\nThey make up everything.", + "Keep the dream alive:\nhit the snooze button.", + "Rest in peace, boiled water.\nYou will be mist.", + "When I discovered my toaster wasn't\nwaterproof, I was shocked.", + "Where do cryptographers go for\nentertainment? The security theatre.", + "How did the Java programmer get rich?\nThey inherited a factory.", + "Why did the developer quit his job?\nHe didn't get ar-rays." ) if (Emoji.hasEmojiTerminal) messages += "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}" + + + if (ZonedDateTime.now().dayOfWeek == DayOfWeek.FRIDAY) { + // Make it quite likely people see it. + repeat(20) { messages += "Ah, Friday.\nMy second favourite F-word." } + } + val (msg1, msg2) = messages.randomOrNull()!!.split('\n') println(Ansi.ansi().newline().fgBrightRed().a( @@ -563,7 +602,8 @@ open class NodeStartup(val args: Array) { """ / ____/ _________/ /___ _""").newline().a( """ / / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a( """/ /___ /_/ / / / /_/ / /_/ / """).fgBrightBlue().a(msg2).newline().fgBrightRed().a( - """\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold().a("--- ${versionInfo.vendor} ${versionInfo.releaseVersion} (${versionInfo.revision.take(7)}) -----------------------------------------------").newline().newline().reset()) + """\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold().a("--- ${versionInfo.vendor} ${versionInfo.releaseVersion} (${versionInfo.revision.take(7)}) -------------------------------------------------------------").newline().newline().reset()) + } } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index ac94064148..aa2517a3f5 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -75,43 +75,41 @@ interface ServiceHubInternal : ServiceHub { log.warn("Transactions recorded from outside of a state machine") } - if (statesToRecord != StatesToRecord.NONE) { - // When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states - // that do not involve us and that we cannot sign for. This will break coin selection and thus a warning - // is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net). - // - // The reason for this is three-fold: - // - // 1) We are putting in place the observer mode feature relatively quickly to meet specific customer - // launch target dates. - // - // 2) The right design for vaults which mix observations and relevant states isn't entirely clear yet. - // - // 3) If we get the design wrong it could create security problems and business confusions. - // - // Back in the bitcoinj days I did add support for "watching addresses" to the wallet code, which is the - // Bitcoin equivalent of observer nodes: - // - // https://bitcoinj.github.io/working-with-the-wallet#watching-wallets - // - // The ability to have a wallet containing both irrelevant and relevant states complicated everything quite - // dramatically, even methods as basic as the getBalance() API which required additional modes to let you - // query "balance I can spend" vs "balance I am observing". In the end it might have been better to just - // require the user to create an entirely separate wallet for observing with. - // - // In Corda we don't support a single node having multiple vaults (at the time of writing), and it's not - // clear that's the right way to go: perhaps adding an "origin" column to the VAULT_STATES table is a better - // solution. Then you could select subsets of states depending on where the report came from. - // - // The risk of doing this is that apps/developers may use 'canned SQL queries' not written by us that forget - // to add a WHERE clause for the origin column. Those queries will seem to work most of the time until - // they're run on an observer node and mix in irrelevant data. In the worst case this may result in - // erroneous data being reported to the user, which could cause security problems. - // - // Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely - // to make writes to the ledger very often or at all, we choose to punt this issue for the time being. - vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }) - } + // When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states + // that do not involve us and that we cannot sign for. This will break coin selection and thus a warning + // is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net). + // + // The reason for this is three-fold: + // + // 1) We are putting in place the observer mode feature relatively quickly to meet specific customer + // launch target dates. + // + // 2) The right design for vaults which mix observations and relevant states isn't entirely clear yet. + // + // 3) If we get the design wrong it could create security problems and business confusions. + // + // Back in the bitcoinj days I did add support for "watching addresses" to the wallet code, which is the + // Bitcoin equivalent of observer nodes: + // + // https://bitcoinj.github.io/working-with-the-wallet#watching-wallets + // + // The ability to have a wallet containing both irrelevant and relevant states complicated everything quite + // dramatically, even methods as basic as the getBalance() API which required additional modes to let you + // query "balance I can spend" vs "balance I am observing". In the end it might have been better to just + // require the user to create an entirely separate wallet for observing with. + // + // In Corda we don't support a single node having multiple vaults (at the time of writing), and it's not + // clear that's the right way to go: perhaps adding an "origin" column to the VAULT_STATES table is a better + // solution. Then you could select subsets of states depending on where the report came from. + // + // The risk of doing this is that apps/developers may use 'canned SQL queries' not written by us that forget + // to add a WHERE clause for the origin column. Those queries will seem to work most of the time until + // they're run on an observer node and mix in irrelevant data. In the worst case this may result in + // erroneous data being reported to the user, which could cause security problems. + // + // Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely + // to make writes to the ledger very often or at all, we choose to punt this issue for the time being. + vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index 75493c1cfb..1502945568 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -202,8 +202,8 @@ class StaffedFlowHospital { return Diagnosis.DISCHARGE } else { val errorMsg = "Maximum number of retries reached for flow ${flowFiber.snapshot().flowLogic.javaClass}. " + - "If the flow involves notarising a transaction, this usually means that the notary is being overloaded and " + - "unable to service requests fast enough. Please try again later." + "If the flow involves notarising a transaction, it means that no response was received from the notary." + + "This could be either due to the the notary being overloaded or unable to reach this node." newError.setMessage(errorMsg) log.warn(errorMsg) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index 368ec94a8a..4f6bc8953b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -149,10 +149,11 @@ class BFTNonValidatingNotaryService( return try { val id = transaction.id val inputs = transaction.inputs + val references = transaction.references val notary = transaction.notary val timeWindow = (transaction as? FilteredTransaction)?.timeWindow if (notary !in services.myInfo.legalIdentities) throw NotaryInternalException(NotaryError.WrongNotary) - commitInputStates(inputs, id, callerIdentity.name, requestSignature, timeWindow) + commitInputStates(inputs, id, callerIdentity.name, requestSignature, timeWindow, references) log.debug { "Inputs committed successfully, signing $id" } BFTSMaRt.ReplicaResponse.Signature(sign(id)) } catch (e: NotaryInternalException) { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 70da6f8a67..31286a61ea 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -233,17 +233,35 @@ object BFTSMaRt { */ abstract fun executeCommand(command: ByteArray): ByteArray? - protected fun commitInputStates(states: List, txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?) { + private fun checkConflict( + conflictingStates: LinkedHashMap, + states: List, + type: StateConsumptionDetails.ConsumedStateType + ) { + states.forEach { stateRef -> + commitLog[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.sha256(), type) } + } + } + + protected fun commitInputStates( + states: List, + txId: SecureHash, + callerName: CordaX500Name, + requestSignature: NotarisationRequestSignature, + timeWindow: TimeWindow?, + references: List = emptyList() + ) { log.debug { "Attempting to commit inputs for transaction: $txId" } services.database.transaction { logRequest(txId, callerName, requestSignature) val conflictingStates = LinkedHashMap() - for (state in states) { - commitLog[state]?.let { conflictingStates[state] = StateConsumptionDetails(it.sha256()) } - } + + checkConflict(conflictingStates, states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE) + checkConflict(conflictingStates, references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE) + if (conflictingStates.isNotEmpty()) { if (!isConsumedByTheSameTx(txId.sha256(), conflictingStates)) { - log.debug { "Failure, input states already committed: ${conflictingStates.keys}" } + log.debug { "Failure, input states or references already committed: ${conflictingStates.keys}" } throw NotaryInternalException(NotaryError.Conflict(txId, conflictingStates)) } } else { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index 1c71d50e13..3d9b3d75d9 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -34,7 +34,7 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut @Suspendable override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { val transaction = requestPayload.coreTransaction - checkInputs(transaction.inputs) + checkInputs(transaction.inputs + transaction.references) val request = NotarisationRequest(transaction.inputs, transaction.id) validateRequestSignature(request, requestPayload.requestSignature) val parts = extractParts(transaction) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 8064afe729..e20905854a 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -41,7 +41,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { try { val stx = requestPayload.signedTransaction - checkInputs(stx.inputs) + checkInputs(stx.inputs + stx.references) validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature) val notary = stx.notary checkNotary(notary) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index fba33f1c0c..fe21f9f003 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -43,7 +43,9 @@ abstract class AbstractQueryCriteriaParser, in P: val leftPredicates = parse(left) val rightPredicates = parse(right) - val orPredicate = criteriaBuilder.or(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) + val leftAnd = criteriaBuilder.and(*leftPredicates.toTypedArray()) + val rightAnd = criteriaBuilder.and(*rightPredicates.toTypedArray()) + val orPredicate = criteriaBuilder.or(leftAnd,rightAnd) predicateSet.add(orPredicate) return predicateSet @@ -183,8 +185,6 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() // incrementally build list of root entities (for later use in Sort parsing) private val rootEntities = mutableMapOf, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() @@ -340,8 +340,14 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() - val vaultFungibleStates = criteriaQuery.from(VaultSchemaV1.VaultFungibleStates::class.java) - rootEntities.putIfAbsent(VaultSchemaV1.VaultFungibleStates::class.java, vaultFungibleStates) + // ensure we re-use any existing instance of the same root entity + val entityStateClass = VaultSchemaV1.VaultFungibleStates::class.java + val vaultFungibleStates = + rootEntities.getOrElse(entityStateClass) { + val entityRoot = criteriaQuery.from(entityStateClass) + rootEntities[entityStateClass] = entityRoot + entityRoot + } val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultFungibleStates.get("stateRef")) predicateSet.add(joinPredicate) @@ -372,7 +378,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class - val joinLinearStateToParty = vaultFungibleStates.joinSet("participants") + val joinLinearStateToParty = vaultFungibleStates.joinSet("participants") predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) criteriaQuery.distinct(true) } @@ -384,11 +390,17 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() - val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) - rootEntities.putIfAbsent(VaultSchemaV1.VaultLinearStates::class.java, vaultLinearStates) + // ensure we re-use any existing instance of the same root entity + val entityStateClass = VaultSchemaV1.VaultLinearStates::class.java + val vaultLinearStates = + rootEntities.getOrElse(entityStateClass) { + val entityRoot = criteriaQuery.from(entityStateClass) + rootEntities[entityStateClass] = entityRoot + entityRoot + } val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) - joinPredicates.add(joinPredicate) + predicateSet.add(joinPredicate) // linear ids UUID criteria.uuid?.let { @@ -417,22 +429,28 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() - val entityClass = resolveEnclosingObjectFromExpression(criteria.expression) + val entityStateClass = resolveEnclosingObjectFromExpression(criteria.expression) try { - val entityRoot = criteriaQuery.from(entityClass) - rootEntities.putIfAbsent(entityClass, entityRoot) + // ensure we re-use any existing instance of the same root entity + val entityRoot = + rootEntities.getOrElse(entityStateClass) { + val entityRoot = criteriaQuery.from(entityStateClass) + rootEntities[entityStateClass] = entityRoot + entityRoot + } val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) - joinPredicates.add(joinPredicate) + predicateSet.add(joinPredicate) // resolve general criteria expressions - parseExpression(entityRoot, criteria.expression, predicateSet) + @Suppress("UNCHECKED_CAST") + parseExpression(entityRoot as Root, criteria.expression, predicateSet) } catch (e: Exception) { e.message?.let { message -> if (message.contains("Not an entity")) throw VaultQueryException(""" - Please register the entity '${entityClass.name}' + Please register the entity '${entityStateClass.name}' See https://docs.corda.net/api-persistence.html#custom-schema-registration for more information""") } throw VaultQueryException("Parsing error: ${e.message}") @@ -454,7 +472,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("stateRef"), entityRoot.get("stateRef")) - joinPredicates.add(joinPredicate) entityRoot } when (direction) { @@ -532,7 +548,6 @@ class HibernateQueryCriteriaParser(val contractStateType: Class DEFAULT_PAGE_SIZE) - throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") - + if (!skipPagingChecks && paging.isDefault && results.size > DEFAULT_PAGE_SIZE) { + throw VaultQueryException("There are ${results.size} results, which exceeds the limit of $DEFAULT_PAGE_SIZE for queries that do not specify paging. In order to retrieve these results, provide a `PageSpecification(pageNumber, pageSize)` to the method invoked.") + } val statesAndRefs: MutableList> = mutableListOf() val statesMeta: MutableList = mutableListOf() val otherResults: MutableList = mutableListOf() diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 7fa27bdbc3..1800dfc0d3 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -52,6 +52,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static net.corda.core.node.services.vault.Builder.sum; import static net.corda.core.node.services.vault.QueryCriteriaUtils.*; @@ -326,13 +327,13 @@ public class VaultQueryJavaTests { @Test public void trackDealStatesPagedSorted() { List dealIds = asList("123", "456", "789"); - UniqueIdentifier uid = - database.transaction(tx -> { - Vault states = vaultFiller.fillWithSomeTestLinearStates(10, null); - UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId(); - vaultFiller.fillWithSomeTestDeals(dealIds); - return _uid; - }); + UniqueIdentifier uid = new UniqueIdentifier("999"); + database.transaction(tx -> { + vaultFiller.fillWithSomeTestLinearStates(10, null); + vaultFiller.fillWithSomeTestLinearStates(1, null, emptyList(), uid); + vaultFiller.fillWithSomeTestDeals(dealIds); + return tx; + }); database.transaction(tx -> { // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") @@ -354,7 +355,7 @@ public class VaultQueryJavaTests { Vault.Page snapshot = results.getSnapshot(); // DOCEND VaultJavaQueryExample5 - assertThat(snapshot.getStates()).hasSize(13); + assertThat(snapshot.getStates()).hasSize(4); return tx; }); diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index a3474124f2..6d6c254ed8 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -14,6 +14,7 @@ import com.nhaarman.mockito_kotlin.* import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty @@ -631,6 +632,52 @@ class HibernateConfigurationTest { assertThat(queryResults).hasSize(10) } + /** + * Composite OR query + */ + @Test + fun `composite or query across VaultStates, VaultLinearStates and DummyLinearStates`() { + val uniqueID456 = UniqueIdentifier("456") + database.transaction { + vaultFiller.fillWithSomeTestLinearStates(1, externalId = "123", linearString = "123", linearNumber = 123, linearBoolean = true) + vaultFiller.fillWithSomeTestLinearStates(1, uniqueIdentifier = uniqueID456) + vaultFiller.fillWithSomeTestLinearStates(1, externalId = "789") + } + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val criteriaBuilder = sessionFactory.criteriaBuilder + val entityManager = sessionFactory.createEntityManager() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java) + + criteriaQuery.select(vaultStates) + val joinPredicate1 = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) + val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get("stateRef"), dummyLinearStates.get("stateRef"))) + + // and predicates on VaultLinearStates + val andLinearStatesPredicate1 = criteriaBuilder.and(criteriaBuilder.equal(vaultLinearStates.get("externalId"), uniqueID456.externalId)) + val andLinearStatesPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultLinearStates.get("uuid"), uniqueID456.id)) + val andLinearStatesPredicate = criteriaBuilder.and(andLinearStatesPredicate1, andLinearStatesPredicate2) + + // and predicates on PersistentDummyLinearState + val andDummyLinearStatesPredicate1 = criteriaBuilder.and(criteriaBuilder.equal(dummyLinearStates.get("linearString"), "123")) + val andDummyLinearStatesPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(dummyLinearStates.get("linearNumber"), 123L)) + val andDummyLinearStatesPredicate3 = criteriaBuilder.and(criteriaBuilder.equal(dummyLinearStates.get("linearBoolean"), true)) + val andDummyLinearStatesPredicate = criteriaBuilder.and(andDummyLinearStatesPredicate1, criteriaBuilder.and(andDummyLinearStatesPredicate2, andDummyLinearStatesPredicate3)) + + // or predicates + val orPredicates = criteriaBuilder.or(andLinearStatesPredicate, andDummyLinearStatesPredicate) + + criteriaQuery.where(joinPredicate1, joinPredicate2, orPredicates) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(2) + } + /** * Test a OneToOne table mapping */ diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index c75527f462..080b2bcd39 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -1186,7 +1186,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { @Test fun `pagination not specified but more than default results available`() { expectedEx.expect(VaultQueryException::class.java) - expectedEx.expectMessage("Please specify a `PageSpecification`") + expectedEx.expectMessage("provide a `PageSpecification(pageNumber, pageSize)`") database.transaction { vaultFiller.fillWithSomeTestCash(201.DOLLARS, notaryServices, 201, DUMMY_CASH_ISSUER) @@ -1460,11 +1460,12 @@ abstract class VaultQueryTestsBase : VaultQueryParties { @Test fun `unconsumed deal states sorted`() { database.transaction { - val linearStates = vaultFiller.fillWithSomeTestLinearStates(10) + vaultFiller.fillWithSomeTestLinearStates(10) + val uid = UniqueIdentifier("999") + vaultFiller.fillWithSomeTestLinearStates(1, uniqueIdentifier = uid) vaultFiller.fillWithSomeTestDeals(listOf("123", "456", "789")) - val uid = linearStates.states.first().state.data.linearId.id - val linearStateCriteria = LinearStateQueryCriteria(uuid = listOf(uid)) + val linearStateCriteria = LinearStateQueryCriteria(uuid = listOf(uid.id)) val dealStateCriteria = LinearStateQueryCriteria(externalId = listOf("123", "456", "789")) val compositeCriteria = linearStateCriteria or dealStateCriteria @@ -1972,6 +1973,63 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `composite query for fungible and linear states`() { + database.transaction { + vaultFiller.fillWithSomeTestLinearStates(1, "TEST1") + vaultFiller.fillWithSomeTestDeals(listOf("123")) + vaultFiller.fillWithSomeTestCash(100.DOLLARS, notaryServices, 1, DUMMY_CASH_ISSUER, services.myInfo.singleIdentity()) + vaultFiller.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices, DUMMY_OBLIGATION_ISSUER.ref(1)) + vaultFiller.fillWithDummyState() + // all contract states query + val results = vaultService.queryBy() + assertThat(results.states).hasSize(5) + // linear states only query + val linearStateCriteria = LinearStateQueryCriteria() + val resultsLSC = vaultService.queryBy(linearStateCriteria) + assertThat(resultsLSC.states).hasSize(2) + // fungible asset states only query + val fungibleAssetStateCriteria = FungibleAssetQueryCriteria() + val resultsFASC = vaultService.queryBy(fungibleAssetStateCriteria) + assertThat(resultsFASC.states).hasSize(2) + // composite OR query for both linear and fungible asset states (eg. all states in either Fungible and Linear states tables) + val resultsCompositeOr = vaultService.queryBy(fungibleAssetStateCriteria.or(linearStateCriteria)) + assertThat(resultsCompositeOr.states).hasSize(4) + // composite AND query for both linear and fungible asset states (eg. all states in both Fungible and Linear states tables) + val resultsCompositeAnd = vaultService.queryBy(fungibleAssetStateCriteria.and(linearStateCriteria)) + assertThat(resultsCompositeAnd.states).hasSize(0) + } + } + + @Test + fun `composite query for fungible and linear states for multiple participants`() { + database.transaction { + identitySvc.verifyAndRegisterIdentity(ALICE_IDENTITY) + identitySvc.verifyAndRegisterIdentity(BOB_IDENTITY) + identitySvc.verifyAndRegisterIdentity(CHARLIE_IDENTITY) + vaultFiller.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE)) + vaultFiller.fillWithSomeTestLinearStates(1, "TEST2", listOf(BOB)) + vaultFiller.fillWithSomeTestLinearStates(1, "TEST3", listOf(CHARLIE)) + vaultFiller.fillWithSomeTestCash(100.DOLLARS, notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices, DUMMY_OBLIGATION_ISSUER.ref(1)) + vaultFiller.fillWithDummyState() + // all contract states query + val results = vaultService.queryBy() + assertThat(results.states).hasSize(6) + // linear states by participants only query + val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE,BOB)) + val resultsLSC = vaultService.queryBy(linearStateCriteria) + assertThat(resultsLSC.states).hasSize(2) + // fungible asset states by participants only query + val fungibleAssetStateCriteria = FungibleAssetQueryCriteria(participants = listOf(services.myInfo.singleIdentity())) + val resultsFASC = vaultService.queryBy(fungibleAssetStateCriteria) + assertThat(resultsFASC.states).hasSize(2) + // composite query for both linear and fungible asset states by participants + val resultsComposite = vaultService.queryBy(linearStateCriteria.or(fungibleAssetStateCriteria)) + assertThat(resultsComposite.states).hasSize(4) + } + } + @Test fun `unconsumed linear heads where external id is null`() { database.transaction { diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle index 6ec41fe419..9ec53da0d8 100644 --- a/samples/cordapp-configuration/build.gradle +++ b/samples/cordapp-configuration/build.gradle @@ -16,14 +16,17 @@ apply plugin: 'net.corda.plugins.cordformation' dependencies { cordaCompile project(":core") cordaCompile project(":node-api") - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') - cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaRuntime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] - - directory "./build/nodes" + directory file("$buildDir/nodes") + nodeDefaults { + cordapps = [] + rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] + } node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] @@ -32,13 +35,12 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10003 adminPort 10004 } + rpcUsers = [] extraConfig = ['h2Settings.address' : 'localhost:10005'] } node { name "O=Bank A,L=London,C=GB" p2pPort 10006 - cordapps = [] - rpcUsers = ext.rpcUsers // This configures the default cordapp for this node projectCordapp { config "someStringValue=test" @@ -52,8 +54,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Bank B,L=New York,C=US" p2pPort 10010 - cordapps = [] - rpcUsers = ext.rpcUsers // This configures the default cordapp for this node projectCordapp { config project.file("src/config.conf") diff --git a/samples/cordapp-configuration/src/main/resources/log4j2.xml b/samples/cordapp-configuration/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..f82d09a079 --- /dev/null +++ b/samples/cordapp-configuration/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 29bae9965d..1a174398f7 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -58,8 +58,8 @@ dependencies { cordapp project(':samples:simm-valuation-demo:flows') // Corda integration dependencies - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') - cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaRuntime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') cordaCompile project(':webserver') @@ -87,20 +87,22 @@ dependencies { } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] - - directory "./build/nodes" + directory file("$buildDir/nodes") + nodeDefaults { + cordapp project(':finance') + cordapp project(':samples:simm-valuation-demo:contracts-states') + cordapp project(':samples:simm-valuation-demo:flows') + rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] + } node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 - cordapp project(':finance') - cordapp project(':samples:simm-valuation-demo:contracts-states') - cordapp project(':samples:simm-valuation-demo:flows') rpcSettings { address "localhost:10014" adminAddress "localhost:10015" } + rpcUsers = [] extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] @@ -116,10 +118,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10016") adminAddress("localhost:10017") } - cordapp project(':finance') - cordapp project(':samples:simm-valuation-demo:contracts-states') - cordapp project(':samples:simm-valuation-demo:flows') - rpcUsers = ext.rpcUsers extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] @@ -135,10 +133,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10026") adminAddress("localhost:10027") } - cordapp project(':finance') - cordapp project(':samples:simm-valuation-demo:contracts-states') - cordapp project(':samples:simm-valuation-demo:flows') - rpcUsers = ext.rpcUsers extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] @@ -154,10 +148,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10036") adminAddress("localhost:10037") } - cordapp project(':finance') - cordapp project(':samples:simm-valuation-demo:contracts-states') - cordapp project(':samples:simm-valuation-demo:flows') - rpcUsers = ext.rpcUsers extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt index 76ef9f4b4b..6bc3d6fd87 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt @@ -16,8 +16,10 @@ import net.corda.core.identity.AbstractParty /** * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. */ -data class DummyState( +data class DummyState @JvmOverloads constructor ( /** Some information that the state represents for test purposes. **/ - val magicNumber: Int = 0) : ContractState { - override val participants: List get() = emptyList() + val magicNumber: Int = 0, + override val participants: List = listOf()) : ContractState { + + fun copy(magicNumber: Int = this.magicNumber) = DummyState(magicNumber) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index 18dc65568f..c44a144efc 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -27,6 +27,8 @@ import net.corda.finance.contracts.DealState import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.Obligation import net.corda.finance.contracts.asset.OnLedgerAsset +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState import net.corda.testing.core.* import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.chooseIdentityAndCert @@ -106,6 +108,7 @@ class VaultFiller @JvmOverloads constructor( fun fillWithSomeTestLinearStates(numberToCreate: Int, externalId: String? = null, participants: List = emptyList(), + uniqueIdentifier: UniqueIdentifier? = null, linearString: String = "", linearNumber: Long = 0L, linearBoolean: Boolean = false, @@ -118,7 +121,7 @@ class VaultFiller @JvmOverloads constructor( // Issue a Linear state val dummyIssue = TransactionBuilder(notary = defaultNotary.party).apply { addOutputState(DummyLinearContract.State( - linearId = UniqueIdentifier(externalId), + linearId = uniqueIdentifier ?: UniqueIdentifier(externalId), participants = participants.plus(me), linearString = linearString, linearNumber = linearNumber, @@ -213,6 +216,23 @@ class VaultFiller @JvmOverloads constructor( return Vault(states) } + /** + * Records a dummy state in the Vault (useful for creating random states when testing vault queries) + */ + fun fillWithDummyState() : Vault { + val outputState = TransactionState( + data = DummyState(Random().nextInt(), participants = listOf(services.myInfo.singleIdentity())), + contract = DummyContract.PROGRAM_ID, + notary = defaultNotary.party + ) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, defaultNotary.party.owningKey) + val stxn = services.signInitialTransaction(builder) + services.recordTransactions(stxn) + return Vault(setOf(stxn.tx.outRef(0))) + } + /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt new file mode 100644 index 0000000000..e679c53002 --- /dev/null +++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt @@ -0,0 +1,96 @@ +package net.corda.tools.shell + +import co.paralleluniverse.fibers.Suspendable +import com.google.common.io.Files +import com.jcraft.jsch.ChannelExec +import com.jcraft.jsch.JSch +import com.jcraft.jsch.Session +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.node.services.Permissions +import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.bouncycastle.util.io.Streams +import org.junit.Ignore +import org.junit.Test +import kotlin.test.assertTrue + +class HashLookupCommandTest { + @Ignore + @Test + fun `hash lookup command returns correct response`() { + val user = User("u", "p", setOf(Permissions.all())) + + driver(DriverParameters(notarySpecs = emptyList(), extraCordappPackagesToScan = listOf("net.corda.testing.contracts"))) { + val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) + val node = nodeFuture.getOrThrow() + val txId = issueTransaction(node) + + val session = connectToShell(user, node) + + testCommand(session, command = "hashLookup ${txId.sha256()}", expected = "Found a matching transaction with Id: $txId") + testCommand(session, command = "hashLookup ${SecureHash.randomSHA256()}", expected = "No matching transaction found") + + session.disconnect() + } + } + + private fun testCommand(session: Session, command: String, expected: String) { + val channel = session.openChannel("exec") as ChannelExec + channel.setCommand(command) + channel.connect(5000) + + assertTrue(channel.isConnected) + + val response = String(Streams.readAll(channel.inputStream)) + val matchFound = response.lines().any { line -> + line.contains(expected) + } + channel.disconnect() + assertTrue(matchFound) + } + + private fun connectToShell(user: User, node: NodeHandle): Session { + val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress, + sshdPort = 2224) + + InteractiveShell.startShell(conf) + InteractiveShell.nodeInfo() + + val session = JSch().getSession("u", "localhost", 2224) + session.setConfig("StrictHostKeyChecking", "no") + session.setPassword("p") + session.connect() + + assertTrue(session.isConnected) + return session + } + + private fun issueTransaction(node: NodeHandle): SecureHash { + return node.rpc.startFlow(::DummyIssue).returnValue.get() + } + + @StartableByRPC + internal class DummyIssue : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + val me = serviceHub.myInfo.legalIdentities.first().ref(0) + val fakeNotary = me.party + val builder = DummyContract.generateInitial(1, fakeNotary as Party, me) + val stx = serviceHub.signInitialTransaction(builder) + serviceHub.recordTransactions(stx) + return stx.id + } + } +} \ No newline at end of file diff --git a/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java new file mode 100644 index 0000000000..038e60ce0d --- /dev/null +++ b/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java @@ -0,0 +1,59 @@ +package net.corda.tools.shell; + +import net.corda.core.crypto.SecureHash; +import net.corda.core.messaging.CordaRPCOps; +import net.corda.core.messaging.StateMachineTransactionMapping; +import org.crsh.cli.Argument; +import org.crsh.cli.Command; +import org.crsh.cli.Man; +import org.crsh.cli.Usage; +import org.crsh.text.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; + +public class HashLookupShellCommand extends InteractiveShellCommand { + private static Logger logger = LoggerFactory.getLogger(HashLookupShellCommand.class); + + @Command + @Man("Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" + + "This is mainly intended to be used for troubleshooting notarisation issues when a\n" + + "state is claimed to be already consumed by another transaction.\n\n" + + "Example usage: hash-lookup E470FD8A6350A74217B0A99EA5FB71F091C84C64AD0DE0E72ECC10421D03AAC9" + ) + public void main(@Usage("A hexadecimal SHA-256 hash value representing the hashed transaction Id") @Argument(unquote = false) String txIdHash) { + logger.info("Executing command \"hash-lookup\"."); + + if (txIdHash == null) { + out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Color.red); + return; + } + + CordaRPCOps proxy = ops(); + List mapping = proxy.stateMachineRecordedTransactionMappingSnapshot(); + + SecureHash txIdHashParsed; + try { + txIdHashParsed = SecureHash.parse(txIdHash); + } catch (IllegalArgumentException e) { + out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Color.red); + return; + } + + Optional match = mapping.stream() + .map(StateMachineTransactionMapping::getTransactionId) + .filter( + txId -> SecureHash.sha256(txId.getBytes()).equals(txIdHashParsed) + ) + .findFirst(); + + if (match.isPresent()) { + SecureHash found = match.get(); + out.println("Found a matching transaction with Id: " + found.toString()); + } else { + out.println("No matching transaction found", Color.red); + } + } +} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 5d26cd4b1e..1ab0a95bb3 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -142,6 +142,7 @@ object InteractiveShell { ExternalResolver.INSTANCE.addCommand("run", "Runs a method from the CordaRPCOps interface on the node.", RunShellCommand::class.java) ExternalResolver.INSTANCE.addCommand("flow", "Commands to work with flows. Flows are how you can change the ledger.", FlowShellCommand::class.java) ExternalResolver.INSTANCE.addCommand("start", "An alias for 'flow start'", StartShellCommand::class.java) + ExternalResolver.INSTANCE.addCommand("hashLookup", "Checks if a transaction with matching Id hash exists.", HashLookupShellCommand::class.java) shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password) } diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt index 2b45dd0d5a..f6068971bd 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt @@ -81,7 +81,7 @@ fun main(args: Array) { log.error(e.message) exitProcess(1) } catch (e: Exception) { - log.error("Exception during node startup", e) + log.error("Exception during webserver startup", e) exitProcess(1) } } \ No newline at end of file