From 6f05f639fb1429540e796284e919483794c8e500 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Tue, 2 Oct 2018 16:29:19 +0100 Subject: [PATCH 01/83] Added more docs for reference states to key concepts and api docs. (#4013) * Added more docs for reference states to key concepts and api docs. * Updated with requested changes --- docs/source/api-states.rst | 44 ++++++++++++++++++++++- docs/source/api-transactions.rst | 43 +++++++++++----------- docs/source/index.rst | 1 + docs/source/key-concepts-states.rst | 11 +++++- docs/source/key-concepts-transactions.rst | 14 +++++++- 5 files changed, 90 insertions(+), 23 deletions(-) diff --git a/docs/source/api-states.rst b/docs/source/api-states.rst index 6b3aea888d..ad05640b16 100644 --- a/docs/source/api-states.rst +++ b/docs/source/api-states.rst @@ -151,4 +151,46 @@ Where: * ``notary`` is the notary service for this state * ``encumbrance`` points to another state that must also appear as an input to any transaction consuming this state -* ``constraint`` is a constraint on which contract-code attachments can be used with this state \ No newline at end of file +* ``constraint`` is a constraint on which contract-code attachments can be used with this state + +Reference States +---------------- + +A reference input state is a ``ContractState`` which can be referred to in a transaction by the contracts of input and +output states but whose contract is not executed as part of the transaction verification process. Furthermore, +reference states are not consumed when the transaction is committed to the ledger but they are checked for +"current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal +state when it occurs in an input or output position. + +Reference data states enable many parties to reuse the same state in their transactions as reference data whilst +still allowing the reference data state owner the capability to update the state. A standard example would be the +creation of financial instrument reference data and the use of such reference data by parties holding the related +financial instruments. + +Just like regular input states, the chain of provenance for reference states is resolved and all dependency transactions +verified. This is because users of reference data must be satisfied that the data they are referring to is valid as per +the rules of the contract which governs it and that all previous participants of teh state assented to updates of it. + +**Known limitations:** + +*Notary change:* It is likely the case that users of reference states do not have permission to change the notary +assigned to a reference state. Even if users *did* have this permission the result would likely be a bunch of +notary change races. As such, if a reference state is added to a transaction which is assigned to a +different notary to the input and output states then all those inputs and outputs must be moved to the +notary which the reference state uses. + +If two or more reference states assigned to different notaries are added to a transaction then it follows that this +transaction cannot be committed to the ledger. This would also be the case for transactions not containing reference +states. There is an additional complication for transaction including reference states, however. It is unlikely that the +party using the reference states has the authority to change the notary for the state (in other words, the party using the +reference state would not be listed as a participant on it). Therefore, it is likely that a transaction containing +reference states with two different notaries cannot be committed to the ledger. + +As such, if reference states assigned to multiple different notaries are added to a transaction builder +then the check below will fail. + + .. warning:: Currently, encumbrances should not be used with reference states. In the case where a state is + encumbered by an encumbrance state, the encumbrance state should also be referenced in the same + transaction that references the encumbered state. This is because the data contained within the + encumbered state may take on a different meaning, and likely would do, once the encumbrance state + is taken into account. diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 3c286cc831..d3180c5c37 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -94,15 +94,6 @@ to "walk the chain" and verify that each input was generated through a valid seq Reference input states ~~~~~~~~~~~~~~~~~~~~~~ -A reference input state is a ``ContractState`` which can be referred to in a transaction by the contracts of input and -output states but whose contract is not executed as part of the transaction verification process. Furthermore, -reference states are not consumed when the transaction is committed to the ledger but they are checked for -"current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal -state when it occurs in an input or output position. - -Reference data states enable many parties to "reuse" the same state in their transactions as reference data whilst -still allowing the reference data state owner the capability to update the state. - A reference input state is added to a transaction as a ``ReferencedStateAndRef``. A ``ReferencedStateAndRef`` can be obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` method which returns a ``ReferencedStateAndRef``. @@ -123,20 +114,32 @@ obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` meth :end-before: DOCEND 55 :dedent: 12 -**Known limitations:** +**Handling of update races:** -*Notary change:* It is likely the case that users of reference states do not have permission to change the notary assigned -to a reference state. Even if users *did* have this permission the result would likely be a bunch of -notary change races. As such, if a reference state is added to a transaction which is assigned to a -different notary to the input and output states then all those inputs and outputs must be moved to the -notary which the reference state uses. +When using reference states in a transaction, it may be the case that a notarisation failure occurs. This is most likely +because the creator of the state (being used as a reference state in your transaction), has just updated it. -If two or more reference states assigned to different notaries are added to a transaction then it follows -that this transaction likely *cannot* be committed to the ledger as it unlikely that the party using the -reference state can change the assigned notary for one of the reference states. +Typically, the creator of such reference data will have implemented flows for syndicating the updates out to users. +However it is inevitable that there will be a delay between the state being used as a reference being consumed, and the +nodes using it receiving the update. -As such, if reference states assigned to multiple different notaries are added to a transaction builder -then the check below will fail. +This is where the ``WithReferencedStatesFlow`` comes in. Given a flow which uses reference states, the +``WithReferencedStatesFlow`` will execute the the flow as a subFlow. If the flow fails due to a ``NotaryError.Conflict`` +for a reference state, then it will be suspended until the state refs for the reference states are consumed. In this +case, a consumption means that: + +1. the owner of the reference state has updated the state with a valid, notarised transaction +2. the owner of the reference state has shared the update with the node attempting to run the flow which uses the + reference state +3. The node has successfully committed the transaction updating the reference state (and all the dependencies), and + added the updated reference state to the vault. + +At the point where the transaction updating the state being used as a reference is committed to storage and the vault +update occurs, then the ``WithReferencedStatesFlow`` will wake up and re-execute the provided flow. + +.. warning:: Caution should be taken when using this flow as it facilitates automated re-running of flows which use + reference states. The flow using reference states should include checks to ensure that the reference data is + reasonable, especially if the economics of the transaction depends upon the data contained within a reference state. Output states ^^^^^^^^^^^^^ diff --git a/docs/source/index.rst b/docs/source/index.rst index fc92e187ef..ac64315eb6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -79,6 +79,7 @@ We look forward to seeing what you can do with Corda! design/kafka-notary/design.md design/monitoring-management/design.md design/sgx-integration/design.md + design/reference-states/design.md design/sgx-infrastructure/design.md design/threat-model/corda-threat-model.md design/data-model-upgrades/signature-constraints.md diff --git a/docs/source/key-concepts-states.rst b/docs/source/key-concepts-states.rst index 05c0b2928e..b1c6addc29 100644 --- a/docs/source/key-concepts-states.rst +++ b/docs/source/key-concepts-states.rst @@ -59,4 +59,13 @@ is aware of, and which it considers to be relevant to itself: :align: center We can think of the ledger from each node's point of view as the set of all the current (i.e. non-historic) states that -it is aware of. \ No newline at end of file +it is aware of. + +Reference states +---------------- + +Not all states need to be updated by the parties which use them. In the case of reference data, there is a common pattern +where one party creates reference data, which is then used (but not updated) by other parties. For this use-case, the +states containing reference data are referred to as "reference states". Syntactically, reference states are no different +to regular states. However, they are treated different by Corda transactions. See :doc:`key-concepts-transactions` for +more details. \ No newline at end of file diff --git a/docs/source/key-concepts-transactions.rst b/docs/source/key-concepts-transactions.rst index 9f3cb8bfef..f1c1d65da5 100644 --- a/docs/source/key-concepts-transactions.rst +++ b/docs/source/key-concepts-transactions.rst @@ -33,7 +33,7 @@ Here is an example of an update transaction, with two inputs and two outputs: :scale: 25% :align: center -A transaction can contain any number of inputs and outputs of any type: +A transaction can contain any number of inputs, outputs and references of any type: * They can include many different state types (e.g. both cash and bonds) * They can be issuances (have zero inputs) or exits (have zero outputs) @@ -108,6 +108,18 @@ Each required signers should only sign the transaction if the following two cond If the transaction gathers all the required signatures but these conditions do not hold, the transaction's outputs will not be valid, and will not be accepted as inputs to subsequent transactions. +Reference states +---------------- + +As mentioned in :doc:`key-concepts-states`, some states need to be referred to by the contracts of other input or output +states but not updated/consumed. This is where reference states come in. When a state is added to the references list of +a transaction, instead of the inputs or outputs list, then it is treated as a *reference state*. There are two important +differences between regular states and reference states: + +* The specified notary for the transaction **does** check whether the reference states are current. However, reference + states are not consumed when the transaction containing them is committed to the ledger. +* The contracts for reference states are not executed for the transaction containing them. + Other transaction components ---------------------------- As well as input states and output states, transactions contain: From bc6ef74c6aa7e36db2933e6757d7e571a23cc668 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Tue, 2 Oct 2018 16:49:31 +0100 Subject: [PATCH 02/83] CordaPersistence class minor refactoring to align with Enterprise repo. (#4012) --- .../corda/nodeapi/internal/persistence/CordaPersistence.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 9bdbeccfcd..e1558971eb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -38,7 +38,8 @@ enum class TransactionIsolationLevel { /** * The JDBC constant value of the same name but prefixed with TRANSACTION_ defined in [java.sql.Connection]. */ - val jdbcValue: Int = java.sql.Connection::class.java.getField("TRANSACTION_$name").get(null) as Int + val jdbcString = "TRANSACTION_$name" + val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int } private val _contextDatabase = InheritableThreadLocal() @@ -63,6 +64,7 @@ class CordaPersistence( HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl) } } + val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas data class Boundary(val txId: UUID, val success: Boolean) From fa8761793f69dc9618dc6a30ba9a803ae99543e6 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Tue, 2 Oct 2018 17:55:22 +0100 Subject: [PATCH 03/83] NOTICK - Python script for creating test tickets in JIRA (#3979) Python script for creating 'Platform Test' and 'Platform Test Run' instances in JIRA for use in release testing. --- release-tools/testing/.gitignore | 1 + release-tools/testing/README.md | 83 +++++++ release-tools/testing/args.py | 71 ++++++ release-tools/testing/jira_manager.py | 187 +++++++++++++++ release-tools/testing/login_manager.py | 60 +++++ release-tools/testing/requirements.txt | 3 + release-tools/testing/test-manager | 312 +++++++++++++++++++++++++ 7 files changed, 717 insertions(+) create mode 100644 release-tools/testing/.gitignore create mode 100644 release-tools/testing/README.md create mode 100644 release-tools/testing/args.py create mode 100644 release-tools/testing/jira_manager.py create mode 100644 release-tools/testing/login_manager.py create mode 100644 release-tools/testing/requirements.txt create mode 100755 release-tools/testing/test-manager diff --git a/release-tools/testing/.gitignore b/release-tools/testing/.gitignore new file mode 100644 index 0000000000..0d20b6487c --- /dev/null +++ b/release-tools/testing/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/release-tools/testing/README.md b/release-tools/testing/README.md new file mode 100644 index 0000000000..0a1fbf8551 --- /dev/null +++ b/release-tools/testing/README.md @@ -0,0 +1,83 @@ +# Release Tools - Test Tracker and Generator + +## Introduction + +This command-line tool lets the user create and track tests in the [R3T](https://r3-cev.atlassian.net/projects/R3T) JIRA project. All generic test cases are captured as tickets of type **Platform Test Template** with a label "OS" for tests pertaining to **Corda Open Source**, "ENT" for **Corda Enterprise**, and "NS" for **Corda Network Services**. These tickets can be set to **Active** or **Inactive** status based on their relevance for a particular release. + +The tool creates a set of new release tests by cloning the current set of active test templates into a set of **Platform Test** tickets. These will each get assigned to the appropriate target version. Further, the tool lets the user create sub-tasks for each of the release tests, one for each release candidate. These steps are described in more detail further down. + +## List Test Cases + +To list the active test cases for a product, run the following command: + +```bash +$ ./test-manager list-tests +``` + +Where `` is either `OS`, `ENT` or `NS`. This will list the test cases that are currently applicable to Corda Open Source, Corda Enterprise and Corda Network Services, respectively. + +## Show Test Status + +To show the status of all test runs for a specific release or release candidate, run: + +```bash +$ ./test-manager status +``` + +Here, `` represents the target version, e.g., product `OS` and version `3.3` would represent Corda Open Source 3.3. `` is optional and will narrow down the report to only show the provided candidate version, e.g., `1` for `RC01`. + +## Create JIRA Version + +To create a new release version in JIRA, you can run the following command: + +```bash +$ ./test-manager create-version +``` + +Note that `` is optional. This command will create new versions in the following JIRA projects: `CORDA`, `ENT`, `ENM`, `CID` and `R3T`. + +## Create Release Tests + +To create the set of parent tests for a new release, you can run: + +```bash +$ ./test-manager create-release-tests +``` + +This will create the test cases, but none of the test run tickets for respective release candidates. Note also that "blocks"-links between active test templates will be carried across to the created test tickets. + +## Create Release Candidate Tests + +To create a set of test run tickets for a new release candidate, you can run: + +```bash +$ ./test-manager create-release-candidate-tests +``` + +This will create a new sub-task under each of the test tickets for `` ``, for release candidate ``. + +## Options + +Each command described above has a set of additional options. More specifically, if you want to use a particular JIRA user instead of being prompted for a user name every time, you can specify `--user `. For verbose logging, you can supply `--verbose` or `-v`. And to auto-reply to the prompt of whether to proceed or not, provide `--yes` or `-y`. + +There is also a useful dry-run option, `--dry-run` or `-d`, that lets you run through the command without creating any tickets or applying any changes to JIRA. + +## Example + +As an example, say you want to create test cases for Corda Network Services 1.0 RC01. You would then follow the following steps: + +```bash +$ ./test-manager create-version NS 1.0 # Create "Corda Network Services 1.0" - if it doesn't exist +$ ./test-manager create-version NS 1.0 1 # Create "Corda Network Services 1.0 RC01" - if it doesn't exist +$ ./test-manager create-release-tests NS 1.0 # Create test cases +$ ./test-manager create-release-candidate-tests NS 1.0 1 # Create test run for release candidate +``` + +Later, when it's time to test RC02, you simply run the following: + +```bash +$ ./test-manager create-version NS 1.0 2 +$ ./test-manager create-release-candidate-tests NS 1.0 2 +``` + +That's it. Voila, you've got yourself a whole new set of JIRA tickets :-) diff --git a/release-tools/testing/args.py b/release-tools/testing/args.py new file mode 100644 index 0000000000..9eb92a8a59 --- /dev/null +++ b/release-tools/testing/args.py @@ -0,0 +1,71 @@ +from __future__ import print_function +from argparse import Action, ArgumentParser +import sys, traceback + +# {{{ Representation of a command-line program +class Program: + + # Create a new command-line program represenation, provided an optional name and description + def __init__(self, name=None, description=None): + self.parser = ArgumentParser(name, description=description) + self.subparsers = self.parser.add_subparsers(title='commands') + self.arguments = [] + + # Enter program definition block + def __enter__(self): + return self + + # Add argument to the top-level command-line interface and all registered sub-commands + def add(self, name, *args, **kwargs): + self.parser.add_argument(name, *args, **kwargs) + self.arguments.append(([name] + list(args), kwargs)) + + # Add sub-command to the set of command-line options + def command(self, name, description, handler): + return Command(self, self.subparsers, name, description, handler) + + # Parse arguments from the command line, and run the associated command handler + def __exit__(self, type, value, tb): + args = self.parser.parse_args() + try: + if 'func' in args: + args.func(args) + else: + self.parser.print_help() + except KeyboardInterrupt: + print() + except Exception as error: + if args.verbose: + t, exception, tb = sys.exc_info() + self.parser.error('{}\n\n{}'.format(error.message, '\n'.join(traceback.format_tb(tb)))) + else: + self.parser.error(error.message) +# }}} + +# {{{ Representation of a sub-command of a command-line program +class Command: + + # Create a sub-command, provided a name, description and command handler + def __init__(self, program, subparsers, name, description, handler): + self.program = program + self.subparsers = subparsers + self.name = name + self.description = description + self.handler = handler + + # Enter sub-command definition block + def __enter__(self): + self.parser = self.subparsers.add_parser(self.name, description=self.description) + return self + + # Add argument to the CLI command + def add(self, name, *args, **kwargs): + self.parser.add_argument(name, *args, **kwargs) + + # Exit sub-command definition block and register default handler + def __exit__(self, type, value, traceback): + for (args, kwargs) in self.program.arguments: + self.parser.add_argument(*args, **kwargs) + self.parser.set_defaults(func=self.handler) + +# }}} diff --git a/release-tools/testing/jira_manager.py b/release-tools/testing/jira_manager.py new file mode 100644 index 0000000000..af57865398 --- /dev/null +++ b/release-tools/testing/jira_manager.py @@ -0,0 +1,187 @@ +# {{{ JIRA dependency +from jira import JIRA +from jira.exceptions import JIRAError +# }}} + +# {{{ Class for interacting with a hosted JIRA system +class Jira: + + # {{{ Constants + BLOCKS = 'Blocks' + DUPLICATE = 'Duplicate' + RELATES = 'Relates' + # }}} + + # {{{ init(address) - Initialise JIRA class, pointing it to the JIRA endpoint + def __init__(self, address='https://r3-cev.atlassian.net'): + self.address = address + self.jira = None + self.mock_key = 1 + self.custom_fields_by_name, self.custom_fields_by_key = {}, {} + # }}} + + # {{{ login(user, password) - Log in as a specific JIRA user + def login(self, user, password): + try: + self.jira = JIRA(self.address, auth=(user, password)) + for x in self.jira.fields(): + if x['custom']: + self.custom_fields_by_name[x['name']] = x['key'] + self.custom_fields_by_key[x['key']] = x['name'] + return self + except Exception as error: + message = error.message + if isinstance(error, JIRAError): + message = error.text if error.text and len(error.text) > 0 and not error.text.startswith(' 0 else query + while count == max_count: + try: + issues = self.jira.search_issues(query, maxResults=max_count, startAt=offset) + count = len(issues) + offset += count + for issue in issues: + index += 1 + yield Issue(self, index=index, issue=issue) + except JIRAError as error: + raise Exception('failed to run query "{}": {}'.format(query, error.text)) + # }}} + + # {{{ find(key) - Look up issue by key + def find(self, key): + try: + issue = self.jira.issue(key) + return Issue(self, issue=issue) + except JIRAError as error: + raise Exception('failed to look up issue "{}": {}'.format(key, error.text)) + # }}} + + # {{{ create(fields, dry_run) - Create a new issue + def create(self, fields, dry_run=False): + if dry_run: + return Issue(self, fields=fields) + try: + issue = self.jira.create_issue(fields) + return Issue(self, issue=issue) + except JIRAError as error: + raise Exception('failed to create issue: {}'.format(error.text)) + # }}} + + # {{{ link(issue_key, other_issue_key, relationship, dry_run) - Link one issue to another + def link(self, issue_key, other_issue_key, relationship=RELATES, dry_run=False): + if dry_run: + return + try: + self.jira.create_issue_link( + type=relationship, + inwardIssue=issue_key, + outwardIssue=other_issue_key, + comment={ + 'body': 'Linked {} to {}'.format(issue_key, other_issue_key), + } + ) + except JIRAError as error: + raise Exception('failed to link {} and {}: {}'.format(issue_key, other_issue_key, error.text)) + # }}} + +# }}} + +# {{{ Representation of a JIRA issue +class Issue: + + mock_index = 1 + + # {{{ init(jira, index, issue, key, fields) - Instantiate an abstract representation of an issue + def __init__(self, jira, index=0, issue=None, key=None, fields=None): + self._jira = jira + self._index = index + self._issue = issue + self._fields = fields + self._key = key if key else u'DRY-{:03d}'.format(Issue.mock_index) + if not key and not issue: + Issue.mock_index += 1 + # }}} + + # {{{ getattr(key) - Get attribute from issue + def __getattr__(self, key): + if key == 'index': + return self._index + if self._issue: + value = self._issue.__getattr__(key) + return WrappedDictionary(value) if key == 'fields' else value + elif self._fields: + if key == 'key': + return self._key + elif key == 'fields': + return WrappedDictionary(self._fields) + return None + # }}} + + # {{{ getitem(key) - Get item from issue + def __getitem__(self, key): + return self.__getattr__(key) + # }}} + + # {{{ str() - Get a string representation of the issue + def __str__(self): + summary = self.fields.summary.strip() + labels = self.fields.labels + if len(labels) > 0: + return u'[{}] {} ({})'.format(self.key, summary, ', '.join(labels)) + else: + return u'[{}] {}'.format(self.key, summary) + # }}} + + # {{{ clone(..., dry_run) - Create a clone of the issue, resetting any provided fields) + def clone(self, **kwargs): + dry_run = kwargs['dry_run'] if 'dry_run' in kwargs else False + fields = self.fields.to_dict() + whitelisted_fields = [ + 'project', 'summary', 'description', 'issuetype', 'labels', 'parent', 'priority', + self._jira.custom_fields_by_name['Target Version/s'], + ] + if 'parent' not in kwargs: + whitelisted_fields += [ + self._jira.custom_fields_by_name['Epic Link'], + self._jira.custom_fields_by_name['Preconditions'], + self._jira.custom_fields_by_name['Test Steps'], + self._jira.custom_fields_by_name['Acceptance Criteria'], + self._jira.custom_fields_by_name['Primary Test Environment'], + ] + for key in kwargs: + value = kwargs[key] + if key == 'parent' and type(value) in (str, unicode): + fields[key] = { 'key' : value } + elif key == 'version': + fields[self._jira.custom_fields_by_name['Target Version/s']] = [{ 'name' : value }] + else: + fields[key] = value + for key in [key for key in fields if key not in whitelisted_fields]: + del fields[key] + new_issue = self._jira.create(fields, dry_run=dry_run) + return new_issue + # }}} + +# }}} + +# {{{ Dictionary with attribute getters +class WrappedDictionary: + def __init__(self, dictionary): + self.dictionary = dictionary + + def __getattr__(self, key): + return self.__getitem__(key) + + def __getitem__(self, key): + return self.dictionary[key] + + def to_dict(self): + return dict(self.dictionary) + +# }}} diff --git a/release-tools/testing/login_manager.py b/release-tools/testing/login_manager.py new file mode 100644 index 0000000000..08a4e1412a --- /dev/null +++ b/release-tools/testing/login_manager.py @@ -0,0 +1,60 @@ +# {{{ Dependencies + +from __future__ import print_function +import sys + +try: + from getpass import getpass +except: + def getpass(message): return raw_input(message) + +try: + from keyring import get_password, set_password +except: + def get_password(account, user): return None + def set_password(account, user, password): pass + +# Python 2.x fix; raw_input was renamed to input in Python 3 +try: input = raw_input +except NameError: pass + +# }}} + +# {{{ prompt(message, secret) - Get input from user; if secret is true, hide the input +def prompt(message, secret=False): + try: + return getpass(message) if secret else input(message) + except: + print() + return '' +# }}} + +# {{{ confirm(message, auto_yes) - Request confirmation from user and proceed if the response is 'yes' +def confirm(message, auto_yes=False): + if auto_yes: + print(message.replace('?', '.')) + return + if not prompt(u'{} (y/N) '.format(message)).lower().strip().startswith('y'): + sys.exit(1) +# }}} + +# {{{ login(account, user, password, use_keyring) - Present user with login prompt and return the provided username and password. If use_keyring is true, use previously provided password (if any) +def login(account, user=None, password=None, use_keyring=True): + if not user: + user = prompt('Username: ') + user = u'{}@r3.com'.format(user) if '@' not in user else user + if not user: return (None, None) + else: + user = u'{}@r3.com'.format(user) if '@' not in user else user + print('Username: {}'.format(user)) + password = get_password(account, user) if password is None and use_keyring else password + if not password: + password = prompt('Password: ', secret=True) + if not password: return (None, None) + else: + print('Password: ********') + if use_keyring: + set_password(account, user, password) + print() + return (user, password) +# }}} diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt new file mode 100644 index 0000000000..b4bc32760d --- /dev/null +++ b/release-tools/testing/requirements.txt @@ -0,0 +1,3 @@ +jira==2.0.0 +keyring==13.1.0 +termcolor==1.1.0 diff --git a/release-tools/testing/test-manager b/release-tools/testing/test-manager new file mode 100755 index 0000000000..96bfd97a36 --- /dev/null +++ b/release-tools/testing/test-manager @@ -0,0 +1,312 @@ +#!/usr/bin/env python + +# {{{ Dependencies +from __future__ import print_function +import sys +from args import Program +from login_manager import login, confirm +from jira_manager import Jira +# }}} + +# {{{ Dependencies for printing coloured content to the console +try: + from termcolor import colored + def blue(message): return colored(message, 'blue') + def green(message): return colored(message, 'green') + def red(message): return colored(message, 'red') + def yellow(message): return colored(message, 'yellow') + def faint(message): return colored(message, 'white', attrs=['dark']) + def on_green(message): return colored(message, 'white', 'on_green') + def on_red(message): return colored(message, 'white', 'on_red') + def blue_on_white(message): return colored(message, 'blue', 'on_white') + def yellow_on_white(message): return colored(message, 'yellow', 'on_white') +except: + def blue(message): return u'[{}]'.format(message) + def green(message): return message + def red(message): return message + def yellow(message): return message + def faint(message): return message + def on_green(message): return message + def on_red(message): return message + def blue_on_white(message): return message + def yellow_on_white(message): return message +# }}} + +# {{{ Mapping from product code to product name +product_map = { + 'OS' : 'Corda', + 'ENT' : 'Corda Enterprise', + 'NS' : 'Corda Network Services', + 'TEST' : 'Corda', # for demo and test purposes +} +# }}} + +# {{{ JIRA queries +QUERY_LIST_TEST_CASES = \ + u'project = R3T AND type = "Platform Test Template" AND status = Active AND labels = "{}" ORDER BY key' +QUERY_LIST_TEST_INSTANCES = \ + u'project = R3T AND type = "Platform Test" AND labels = "{}" AND "Target Version/s" = "{}" ORDER BY key' +QUERY_LIST_TEST_INSTANCE_FOR_TICKET = \ + u'project = R3T AND type = "Platform Test" AND labels = "{}" AND "Target Version/s" = "{}" AND issue IN linkedIssues({})' +QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET = \ + u'project = R3T AND type = "Platform Test Run" AND labels = "{}" AND parent = {} ORDER BY "Target Version/S"' +QUERY_LIST_TEST_RUN_FOR_TICKET = \ + u'project = R3T AND type = "Platform Test Run" AND labels = "{}" AND "Target Version/s" = "{}" AND parent = {}' +QUERY_LIST_BLOCKING_TEST_CASES = \ + u'project = R3T AND type = "Platform Test Template" AND labels = "{}" AND issue IN linkedIssues({}, "Blocks")' +# }}} + +# {{{ list_test_cases() - List active test cases for a specific product +def list_test_cases(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + print(u'List of active test cases for {}:'.format(yellow(product_map[args.PRODUCT]))) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_CASES.format(args.PRODUCT)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + has_tests = True + if not has_tests: + print(u' - No active test cases found') + print() +# }}} + +# {{{ show_status() - Show the status of all test runs for a specific release or release candidate +def show_status(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + candidate = '{} RC{:02d}'.format(version, args.CANDIDATE) if args.CANDIDATE else version + if args.CANDIDATE: + print(u'Status of test runs for {} version {} release candidate {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow('RC{:02d}'.format(args.CANDIDATE)))) + else: + print(u'Status of test runs for {} version {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION))) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_INSTANCES.format(args.PRODUCT, version)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_INSTANCES, args.PRODUCT, version): + status = issue.fields.status['name'].lower() + if status == 'pass': + status = on_green('Pass') + elif status == 'fail': + status = on_red('Fail') + elif status == 'descope': + status = on_green('Descoped') + else: + status = '' + print(u' - {} {} {}'.format(blue(issue.key), issue.fields.summary, status)) + has_test_runs = False + if args.CANDIDATE: + if args.verbose: + print(faint(' [{}]'.format(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key)))) + run_list = jira.search(QUERY_LIST_TEST_RUN_FOR_TICKET, args.PRODUCT, candidate, issue.key) + else: + if args.verbose: + print(faint(' [{}]'.format(QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET.format(args.PRODUCT, issue.key)))) + run_list = jira.search(QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET, args.PRODUCT, issue.key) + for run in run_list: + has_test_runs = True + print() + status = run.fields.status['name'].lower() + if status == 'pass': + status = on_green('Pass ') + elif status == 'fail': + status = on_red('Fail ') + elif status == 'descope': + status = on_green('Descoped') + elif status == 'in progress': + status = yellow_on_white('Active ') + else: + status = blue_on_white('Open ') + print(u' {} {} ({})'.format(status, faint(run.fields[jira.custom_fields_by_name['Target Version/s']][0]['name']), blue(run.key))) + if not has_test_runs: + print() + print(u' - No release candidate tests found') + print() + has_tests = True + if not has_tests: + print(u' - No test cases found for the specified release') + print() +# }}} + +# {{{ create_version() - Create a new JIRA version +def create_version(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + version = '{} RC{:02d}'.format(version, args.CANDIDATE) if args.CANDIDATE else version + confirm(u'Create new version {}?'.format(yellow(version)), auto_yes=args.yes or args.dry_run) + print() + if not args.dry_run: + for project in ['CORDA', 'ENT', 'ENM', 'R3T', 'CID']: + print(u' - Creating version {} for project {} ...'.format(yellow(version), blue(project))) + try: + jira.jira.create_version(name=version, project=project, description=version) + print(u' {} - Created version for project {}'.format(green('SUCCESS'), blue(project))) + except Exception as error: + print(u' {} - Failed to version: {}'.format(red('FAIL'), error)) + print() + + +# }}} + +# {{{ create_release() - Create test cases for a specific version of a product +def create_release(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + confirm(u'Create test cases for {} version {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION)), auto_yes=args.yes or args.dry_run) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_CASES.format(args.PRODUCT)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + print() + has_tests = True + print(u' - Creating test case for version {} ...'.format(yellow(args.VERSION))) + if args.verbose: + print(faint(u' [{}]'.format(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key)))) + has_test_case_for_version = len(list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key)))) + if has_test_case_for_version: + print(u' {} - Test case for version already exists'.format(yellow('SKIPPED'))) + else: + try: + test_case = issue.clone(issuetype='Platform Test', version=version, dry_run=args.dry_run) + print(u' {} - Created ticket {}'.format(green('SUCCESS'), blue(test_case.key))) + except Exception as error: + print(u' {} - Failed to create ticket: {}'.format(red('FAIL'), error)) + print() + print(u' - Linking test case to template ...') + try: + jira.link(issue.key, test_case.key, dry_run=args.dry_run) + print(u' {} - Linked {} to {}'.format(green('SUCCESS'), blue(issue.key), blue(test_case.key))) + except Exception as error: + print(u' {} - Failed to link tickets: {}'.format(red('FAIL'), error)) + print() + print(u'Copying links from test templates for {} version {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION))) + print() + for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + print() + print(u' - Copying links for test case {} ...'.format(blue(issue.key))) + has_links = False + if args.verbose: + print(faint(u' [{}]'.format(QUERY_LIST_BLOCKING_TEST_CASES.format(args.PRODUCT, issue.key)))) + for blocking_issue in jira.search(QUERY_LIST_BLOCKING_TEST_CASES, args.PRODUCT, issue.key): + from_ticket = list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key))) + to_ticket = list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, blocking_issue.key))) + if len(from_ticket) == 0 or len(to_ticket) == 0: + continue + has_links = True + from_key = from_ticket[0].key + to_key = to_ticket[0].key + try: + jira.link(from_key, to_key, Jira.BLOCKS, dry_run=args.dry_run) + print(u' {} - Linked {} to {}'.format(green('SUCCESS'), blue(from_key), blue(to_key))) + except Exception as error: + print(u' {} - Failed to link tickets {} and {}: {}'.format(red('FAIL'), blue(from_key), blue(to_key), error)) + if not has_links: + print(u' {} - No relevant links found for ticket {}'.format(yellow('SKIPPED'), blue(issue.key))) + print() + if not has_tests: + print(u' - No active test cases found') + print() +# }}} + +# {{{ create_release_candidate() - Create test run tickets for a specific release candidate of a product +def create_release_candidate(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + CANDIDATE = args.CANDIDATE[0] + candidate = '{} RC{:02d}'.format(version, CANDIDATE) + confirm(u'Create test run tickets for {} version {} release candidate {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow('RC{:02d}'.format(CANDIDATE))), auto_yes=args.yes or args.dry_run) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_INSTANCES.format(args.PRODUCT, version)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_INSTANCES, args.PRODUCT, version): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + epic_field = jira.custom_fields_by_name['Epic Link'] + epic = issue.fields[epic_field] if epic_field in issue.fields.to_dict() else '' + labels = issue.fields.labels + [epic] + print() + has_tests = True + print(u' - Creating test run ticket for release candidate {} ...'.format(yellow('RC{:02d}'.format(CANDIDATE)))) + if args.verbose: + print(faint(u' [{}]'.format(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key)))) + has_test_instance_for_version = len(list(jira.search(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key)))) + if has_test_instance_for_version: + print(u' {} - Ticket for release candidate already exists'.format(yellow('SKIPPED'))) + else: + try: + test_case = issue.clone(issuetype='Platform Test Run', version=candidate, parent=issue.key, labels=labels, dry_run=args.dry_run) + print(u' {} - Created ticket {}'.format(green('SUCCESS'), blue(test_case.key))) + except Exception as error: + print(u' {} - Failed to create ticket: {}'.format(red('FAIL'), error)) + print() + if not has_tests: + print(u' - No active test cases found') + print() +# }}} + +# {{{ main() - Entry point +def main(): + with Program(description='tool for managing test cases and test runs in JIRA') as program: + + PRODUCTS = ['OS', 'ENT', 'NS', 'TEST'] + + program.add('--verbose', '-v', help='turn on verbose logging', action='store_true') + program.add('--yes', '-y', help='automatically answer "yes" to all prompts', action='store_true') + program.add('--user', '-u', help='the user name or email address used to log in to JIRA', type=str, metavar='USER') + program.add('--no-keyring', help='do not retrieve passwords persisted in the keyring', action='store_true') + + def mixin_dry_run(command): + command.add('--dry-run', '-d', help='run action without applying any changes to JIRA', action='store_true') + + def mixin_product(command): + command.add('PRODUCT', help='the product under test (OS, ENT, NS)', choices=PRODUCTS, metavar='PRODUCT') + + def mixin_version_and_product(command): + mixin_product(command) + command.add('VERSION', help='the target version of the release, e.g., 4.0', type=float) + + def mixin_candidate(command, optional=False): + if optional: + nargs = '?' + else: + nargs = 1 + command.add('CANDIDATE', help='the number of the release candidate, e.g., 1 for RC01', type=int, nargs=nargs) + + with program.command('list-tests', 'list test cases applicable to the provided specification', list_test_cases) as command: + mixin_product(command) + + with program.command('status', 'show the status of all test runs for a specific release or release candidate', show_status) as command: + mixin_version_and_product(command) + mixin_candidate(command, True) + + with program.command('create-version', 'create a new version in JIRA', create_version) as command: + mixin_dry_run(command) + mixin_version_and_product(command) + mixin_candidate(command, True) + + with program.command('create-release-tests', 'create test cases for a new release in JIRA', create_release) as command: + mixin_dry_run(command) + mixin_version_and_product(command) + + with program.command('create-release-candidate-tests', 'create test runs for a new release candidate in JIRA', create_release_candidate) as command: + mixin_dry_run(command) + mixin_version_and_product(command) + mixin_candidate(command) +# }}} + +if __name__ == '__main__': main() From 1e72298a461b755ef82c7a81db87660b4602a5d5 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Tue, 2 Oct 2018 20:45:50 +0100 Subject: [PATCH 04/83] CORDA-1915 Update to Network Bootstrapper for signed JARs (#4008) The cordapp and cordformation plugins (from v4.0.30) are going to have ability to sign JARs (in cordformation signing will be by default), to enable signature constraints to work out of box Network Bootstrapper will not whitelist contracts form signed JARs. For unsigned JARs the Network Bootstrapper behaviour is unchanged. --- docs/source/network-bootstrapper.rst | 8 +++++--- .../nodeapi/internal/network/NetworkBootstrapper.kt | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index a1d33d0034..4cc6ec559d 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -87,9 +87,11 @@ Any CorDapps provided when bootstrapping a network will be scanned for contracts The CorDapp JARs will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`). -By default the bootstrapper will whitelist all the contracts found in all the CorDapp JARs. To prevent certain -contracts from being whitelisted, add their fully qualified class name in the ``exclude_whitelist.txt``. These will instead -use the more restrictive ``HashAttachmentConstraint``. +By default the bootstrapper will whitelist all the contracts found in the unsigned CorDapp JARs (a JAR file not signed by jarSigner tool). +Whitelisted contracts are checked by `Zone constraints`, while contract classes from signed JARs will be checked by `Signature constraints`. +To prevent certain contracts from unsigned JARs from being whitelisted, add their fully qualified class name in the ``exclude_whitelist.txt``. +These will instead use the more restrictive ``HashAttachmentConstraint``. +Refer to :doc:`api-contract-constraints` to understand the implication of different constraint types before adding ``exclude_whitelist.txt`` files. For example: diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index d0c987d1be..0a80988f27 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -34,6 +34,7 @@ import java.time.Instant import java.util.* import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import java.util.jar.JarInputStream import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set @@ -208,7 +209,7 @@ internal constructor(private val initSerEnv: Boolean, println("Gathering notary identities") val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs) println("Generating contract implementations whitelist") - val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.map(contractsJarConverter)) + val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter)) val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs) if (newNetParams != existingNetParams) { println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams") @@ -398,4 +399,10 @@ internal constructor(private val initSerEnv: Boolean, return magic == amqpMagic && target == SerializationContext.UseCase.P2P } } + + private fun isSigned(file: Path): Boolean = file.read { + JarInputStream(it).use { + JarSignatureCollector.collectSigningParties(it).isNotEmpty() + } + } } From 149b6034e1b1bf5c71abd2f4910c0e14c6276efe Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 3 Oct 2018 08:59:31 +0100 Subject: [PATCH 05/83] CORDA-2016 Add unit tests to ensure SNI header generation will not be changed by accident (#4014) * Add test for SNI header to prevent changing it accidentally. * added hardcoded values test to ensure hashing function and corda x500 name format can't be changed --- .../internal/protonwrapper/netty/SSLHelper.kt | 2 +- .../protonwrapper/netty/SSLHelperTest.kt | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index 9850e6a8ce..2024be3359 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -167,5 +167,5 @@ internal fun x500toHostName(x500Name: CordaX500Name): String { val secureHash = SecureHash.sha256(x500Name.toString()) // RFC 1035 specifies a limit 255 bytes for hostnames with each label being 63 bytes or less. Due to this, the string // representation of the SHA256 hash is truncated to 32 characters. - return String.format(HOSTNAME_FORMAT, secureHash.toString().substring(0..32).toLowerCase()) + return String.format(HOSTNAME_FORMAT, secureHash.toString().take(32).toLowerCase()) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt new file mode 100644 index 0000000000..54d42edd0d --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt @@ -0,0 +1,35 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.testing.internal.configureTestSSL +import org.junit.Test +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SNIHostName +import javax.net.ssl.TrustManagerFactory +import kotlin.test.assertEquals + +class SSLHelperTest { + @Test + fun `ensure SNI header in correct format`() { + val legalName = CordaX500Name("Test", "London", "GB") + val sslConfig = configureTestSSL(legalName) + + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + keyManagerFactory.init(CertificateStore.fromFile(sslConfig.keyStore.path, sslConfig.keyStore.password, false)) + trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(CertificateStore.fromFile(sslConfig.trustStore.path, sslConfig.trustStore.password, false), false)) + + val sslHandler = createClientSslHelper(NetworkHostAndPort("localhost", 1234), setOf(legalName), keyManagerFactory, trustManagerFactory) + val legalNameHash = SecureHash.sha256(legalName.toString()).toString().take(32).toLowerCase() + + // These hardcoded values must not be changed, something is broken if you have to change these hardcoded values. + assertEquals("O=Test, L=London, C=GB", legalName.toString()) + assertEquals("f3df3c01a5f5aa5b9d394680cde3a414", legalNameHash) + assertEquals(1, sslHandler.engine().sslParameters.serverNames.size) + assertEquals("$legalNameHash.corda.net", (sslHandler.engine().sslParameters.serverNames.first() as SNIHostName).asciiName) + } +} \ No newline at end of file From ff9bf68e373bce75a81319f8bacd62b00f825502 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 3 Oct 2018 11:18:28 +0200 Subject: [PATCH 06/83] [ENT-2545]: Failing flow sent to Hospital does not log any ERROR's (fixed). (#4015) --- .../main/kotlin/net/corda/node/services/CoreFlowHandlers.kt | 2 ++ .../node/services/statemachine/FlowStateMachineImpl.kt | 2 +- .../corda/node/services/statemachine/StaffedFlowHospital.kt | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index c350b81849..df2a1ec019 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -20,6 +20,8 @@ class FinalityHandler(private val sender: FlowSession) : FlowLogic() { override fun call() { subFlow(ReceiveTransactionFlow(sender, true, StatesToRecord.ONLY_RELEVANT)) } + + internal fun sender(): Party = sender.counterparty } class NotaryChangeHandler(otherSideSession: FlowSession) : AbstractStateReplacementFlow.Acceptor(otherSideSession) { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 3b099e5c9a..4fcd24e7d6 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -218,7 +218,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, suspend(FlowIORequest.WaitForSessionConfirmations, maySkipCheckpoint = true) Try.Success(result) } catch (throwable: Throwable) { - logger.info("Flow threw exception... sending to flow hospital", throwable) + logger.info("Flow threw exception... sending it to flow hospital", throwable) Try.Failure(throwable) } val softLocksId = if (hasSoftLockedStates) logic.runId.uuid else null 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 1502945568..512497b6b9 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 @@ -217,7 +217,11 @@ class StaffedFlowHospital { */ object FinalityDoctor : Staff { override fun consult(flowFiber: FlowFiber, currentState: StateMachineState, newError: Throwable, history: MedicalHistory): Diagnosis { - return if (currentState.flowLogic is FinalityHandler) Diagnosis.OVERNIGHT_OBSERVATION else Diagnosis.NOT_MY_SPECIALTY + return (currentState.flowLogic as? FinalityHandler)?.let { logic -> Diagnosis.OVERNIGHT_OBSERVATION.also { warn(logic, flowFiber, currentState) } } ?: Diagnosis.NOT_MY_SPECIALTY + } + + private fun warn(flowLogic: FinalityHandler, flowFiber: FlowFiber, currentState: StateMachineState) { + log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying the flow by re-starting the node. State machine state: $currentState, initiating party was: ${flowLogic.sender().name}") } } } From beb4dc008f90a1dfc390e1137a09eacd861740aa Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 3 Oct 2018 12:31:20 +0200 Subject: [PATCH 07/83] [ENT-2545]: Fixed broken unit test. (#4019) --- .../corda/node/services/statemachine/RetryFlowMockTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt index 82f4d45a82..7822e6f136 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt @@ -3,6 +3,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.* +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.concurrent.flatMap @@ -15,6 +16,7 @@ import net.corda.node.services.FinalityHandler import net.corda.node.services.messaging.Message import net.corda.node.services.persistence.DBTransactionStorage import net.corda.nodeapi.internal.persistence.contextTransaction +import net.corda.testing.core.TestIdentity import net.corda.testing.node.internal.* import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -136,10 +138,10 @@ class RetryFlowMockTest { @Test fun `Patient records do not leak in hospital when using killFlow`() { // Make sure we have seen an update from the hospital, and thus the flow went there. + val alice = TestIdentity(CordaX500Name.parse("L=London,O=Alice Ltd,OU=Trade,C=GB")).party val records = nodeA.smm.flowHospital.track().updates.toBlocking().toIterable().iterator() val flow: FlowStateMachine = nodeA.services.startFlow(FinalityHandler(object : FlowSession() { - override val counterparty: Party - get() = TODO("not implemented") + override val counterparty = alice override fun getCounterpartyFlowInfo(maySkipCheckpoint: Boolean): FlowInfo { TODO("not implemented") From 7edc18f85deb0b18171dc63f37d1dfa8ea87e4c3 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 3 Oct 2018 13:41:25 +0100 Subject: [PATCH 08/83] CORDA-1997 Added constraint type information to vault states table. (#3975) * Added constraint type information to vault states table. * Added Vault Query criteria support for constraint data. * Added documentation and changelog entry. * Added missing @CordaSerializable. * Fix minor bug in test setup and parsing code. * Use binary encoding data types instead of serialize/deserialize. * Optimized storage of constraints data. Additional assertions on Vault Query constraint data contents (to validate encoding/decoding). Tested with CompositeKey containing 10 keys. * Addressing PR review feedback. * Query by constraints type and data. * Revert back accidentally removed code for contractStateType filtering. * Incorporating final PR review feedback. Use @JvmOverloads on constructor. * Make sure constraintInfo is class evolution friendly. --- .../corda/core/node/services/VaultService.kt | 74 +++++++-- .../core/node/services/vault/QueryCriteria.kt | 6 +- .../node/services/vault/QueryCriteriaUtils.kt | 3 +- docs/source/api-contract-constraints.rst | 2 + docs/source/api-vault-query.rst | 19 ++- docs/source/changelog.rst | 2 + .../vault/HibernateQueryCriteriaParser.kt | 35 ++++- .../node/services/vault/NodeVaultService.kt | 13 +- .../corda/node/services/vault/VaultSchema.kt | 12 +- .../vault-schema.changelog-master.xml | 1 + .../migration/vault-schema.changelog-v6.xml | 17 ++ .../node/services/vault/VaultQueryTests.kt | 147 ++++++++++++++++++ .../testing/internal/vault/VaultFiller.kt | 6 +- 13 files changed, 316 insertions(+), 21 deletions(-) create mode 100644 node/src/main/resources/migration/vault-schema.changelog-v6.xml diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 20d1984d8d..43b71bf966 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -5,6 +5,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* +import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic @@ -18,6 +19,7 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.toHexString import rx.Observable import java.time.Instant import java.util.* @@ -125,6 +127,44 @@ class Vault(val states: Iterable>) { RELEVANT, NOT_RELEVANT, ALL } + /** + * Contract constraint information associated with a [ContractState]. + * See [AttachmentConstraint] + */ + @CordaSerializable + data class ConstraintInfo(val constraint: AttachmentConstraint) { + @CordaSerializable + enum class Type { + ALWAYS_ACCEPT, HASH, CZ_WHITELISTED, SIGNATURE + } + fun type(): Type { + return when (constraint::class.java) { + AlwaysAcceptAttachmentConstraint::class.java -> Type.ALWAYS_ACCEPT + HashAttachmentConstraint::class.java -> Type.HASH + WhitelistedByZoneAttachmentConstraint::class.java -> Type.CZ_WHITELISTED + SignatureAttachmentConstraint::class.java -> Type.SIGNATURE + else -> throw IllegalArgumentException("Invalid constraint type: $constraint") + } + } + fun data(): ByteArray? { + return when (type()) { + Type.HASH -> (constraint as HashAttachmentConstraint).attachmentId.bytes + Type.SIGNATURE -> (constraint as SignatureAttachmentConstraint).key.encoded + else -> null + } + } + companion object { + fun constraintInfo(type: Type, data: ByteArray?): ConstraintInfo { + return when (type) { + Type.ALWAYS_ACCEPT -> ConstraintInfo(AlwaysAcceptAttachmentConstraint) + Type.HASH -> ConstraintInfo(HashAttachmentConstraint(SecureHash.parse(data!!.toHexString()))) + Type.CZ_WHITELISTED -> ConstraintInfo(WhitelistedByZoneAttachmentConstraint) + Type.SIGNATURE -> ConstraintInfo(SignatureAttachmentConstraint(Crypto.decodePublicKey(data!!))) + } + } + } + } + @CordaSerializable enum class UpdateType { GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE @@ -151,7 +191,7 @@ class Vault(val states: Iterable>) { val otherResults: List) @CordaSerializable - data class StateMetadata constructor( + data class StateMetadata @JvmOverloads constructor( val ref: StateRef, val contractStateClassName: String, val recordedTime: Instant, @@ -160,18 +200,9 @@ class Vault(val states: Iterable>) { val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?, - val relevancyStatus: Vault.RelevancyStatus? + val relevancyStatus: Vault.RelevancyStatus? = null, + val constraintInfo: ConstraintInfo? = null ) { - constructor(ref: StateRef, - contractStateClassName: String, - recordedTime: Instant, - consumedTime: Instant?, - status: Vault.StateStatus, - notary: AbstractParty?, - lockId: String?, - lockUpdateTime: Instant? - ) : this(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null) - fun copy( ref: StateRef = this.ref, contractStateClassName: String = this.contractStateClassName, @@ -184,6 +215,19 @@ class Vault(val states: Iterable>) { ): StateMetadata { return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null) } + fun copy( + ref: StateRef = this.ref, + contractStateClassName: String = this.contractStateClassName, + recordedTime: Instant = this.recordedTime, + consumedTime: Instant? = this.consumedTime, + status: Vault.StateStatus = this.status, + notary: AbstractParty? = this.notary, + lockId: String? = this.lockId, + lockUpdateTime: Instant? = this.lockUpdateTime, + relevancyStatus: Vault.RelevancyStatus? + ): StateMetadata { + return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, relevancyStatus, ConstraintInfo(AlwaysAcceptAttachmentConstraint)) + } } companion object { @@ -194,6 +238,12 @@ class Vault(val states: Iterable>) { } } +/** + * The maximum permissible size of contract constraint type data (for storage in vault states database table). + * Maximum value equates to a CompositeKey with 10 EDDSA_ED25519_SHA512 keys stored in. + */ +const val MAX_CONSTRAINT_DATA_SIZE = 563 + /** * A [VaultService] is responsible for securely and safely persisting the current state of a vault to storage. The * vault service vends immutable snapshots of the current vault for working with: if you build a transaction based diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 609434a60b..2ee555dee1 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -74,6 +74,8 @@ sealed class QueryCriteria : GenericQueryCriteria = emptySet() + open val constraints: Set = emptySet() abstract val contractStateTypes: Set>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) @@ -90,7 +92,9 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, val softLockingCondition: SoftLockingCondition? = null, val timeCondition: TimeCondition? = null, - override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL, + override val constraintTypes: Set = emptySet(), + override val constraints: Set = emptySet() ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index faac1f7fef..2492313d0b 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -184,7 +184,8 @@ data class Sort(val columns: Collection) : BaseSort() { STATE_STATUS("stateStatus"), RECORDED_TIME("recordedTime"), CONSUMED_TIME("consumedTime"), - LOCK_ID("lockId") + LOCK_ID("lockId"), + CONSTRAINT_TYPE("constraintType") } enum class LinearStateAttribute(val attributeName: String) : Attribute { diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 3863926981..df09bb5f26 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -51,6 +51,8 @@ upgrade approach is that you can upgrade states regardless of their constraint, anticipate a need to do so. But it requires everyone to sign, requires everyone to manually authorise the upgrade, consumes notary and ledger resources, and is just in general more complex. +.. _implicit_constraint_types: + How constraints work -------------------- diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index a012b84ea7..2243a6bfea 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -78,7 +78,8 @@ and/or composition and a rich set of operators to include: There are four implementations of this interface which can be chained together to define advanced filters. 1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, - CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). + CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED), + state constraints (see :ref:`Constraint Types `). .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft locked states). @@ -260,6 +261,22 @@ Query for unconsumed states for several contract state types: :end-before: DOCEND VaultQueryExample3 :dedent: 12 +Query for unconsumed states for specified contract state constraint types and sorted in ascending alphabetical order: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample30 + :end-before: DOCEND VaultQueryExample30 + :dedent: 12 + +Query for unconsumed states for specified contract state constraints (type and data): + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample31 + :end-before: DOCEND VaultQueryExample31 + :dedent: 12 + Query for unconsumed states for a given notary: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 78e75bd607..3017257931 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,8 @@ Unreleased * Introduce minimum and target platform version for CorDapps. +* Vault storage of contract state constraints metadata and associated vault query functions to retrieve and sort by constraint type. + * New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call. * Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive. 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 a3fa9aee01..f4d912ffb1 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 @@ -225,6 +225,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate + private val constraintPredicates = mutableSetOf() var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED @@ -508,7 +509,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class).values.map { (it as LiteralExpression).literal }.toSet() + if (existingTypes != criteria.constraintTypes) { + log.warn("Enriching previous attribute [${VaultSchemaV1.VaultStates::constraintType.name}] values [$existingTypes] with [${criteria.constraintTypes}]") + commonPredicates.replace(predicateID, criteriaBuilder.and(vaultStates.get(VaultSchemaV1.VaultStates::constraintType.name).`in`(criteria.constraintTypes.plus(existingTypes)))) + } + } else { + commonPredicates[predicateID] = criteriaBuilder.and(vaultStates.get(VaultSchemaV1.VaultStates::constraintType.name).`in`(criteria.constraintTypes)) + } + } + + // contract constraint information (type and data) + if (criteria.constraints.isNotEmpty()) { + criteria.constraints.forEach { constraint -> + val predicateConstraintType = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::constraintType.name), constraint.type()) + if (constraint.data() != null) { + val predicateConstraintData = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::constraintData.name), constraint.data()) + val compositePredicate = criteriaBuilder.and(predicateConstraintType, predicateConstraintData) + if (constraintPredicates.isNotEmpty()) { + val previousPredicate = constraintPredicates.last() + constraintPredicates.clear() + constraintPredicates.add(criteriaBuilder.or(previousPredicate, compositePredicate)) + } + else constraintPredicates.add(compositePredicate) + } + else constraintPredicates.add(criteriaBuilder.or(predicateConstraintType)) + } + } + return emptySet() } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index e3d11f0cab..1d20d8311f 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -11,8 +11,12 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord import net.corda.core.node.services.* import net.corda.core.node.services.vault.* +import net.corda.core.node.services.Vault.ConstraintInfo.Companion.constraintInfo import net.corda.core.schemas.PersistentStateRef +import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.transactions.* import net.corda.core.utilities.* import net.corda.node.services.api.SchemaService @@ -132,12 +136,15 @@ class NodeVaultService( // Adding a new column in the "VaultStates" table was considered the best approach. val keys = stateOnly.participants.map { it.owningKey } val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet()) + val constraintInfo = Vault.ConstraintInfo(stateAndRef.value.state.constraint) val stateToAdd = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, contractStateClassName = stateAndRef.value.state.data.javaClass.name, stateStatus = Vault.StateStatus.UNCONSUMED, recordedTime = clock.instant(), - relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT + relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT, + constraintType = constraintInfo.type(), + constraintData = constraintInfo.data() ) stateToAdd.stateRef = PersistentStateRef(stateAndRef.key) session.save(stateToAdd) @@ -513,7 +520,9 @@ class NodeVaultService( vaultState.notary, vaultState.lockId, vaultState.lockUpdateTime, - vaultState.relevancyStatus)) + vaultState.relevancyStatus, + constraintInfo(vaultState.constraintType, vaultState.constraintData) + )) } else { // TODO: improve typing of returned other results log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index e12f8fc7ac..8755eccd94 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.MAX_ISSUER_REF_SIZE import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.node.services.MAX_CONSTRAINT_DATA_SIZE import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState @@ -66,7 +67,16 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ @Column(name = "lock_timestamp", nullable = true) - var lockUpdateTime: Instant? = null + var lockUpdateTime: Instant? = null, + + /** refers to constraint type (none, hash, whitelisted, signature) associated with a contract state */ + @Column(name = "constraint_type", nullable = false) + var constraintType: Vault.ConstraintInfo.Type, + + /** associated constraint type data (if any) */ + @Column(name = "constraint_data", length = MAX_CONSTRAINT_DATA_SIZE, nullable = true) + @Type(type = "corda-wrapper-binary") + var constraintData: ByteArray? = null ) : PersistentState() @Entity diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index a9fbc181c5..0c3d274098 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -9,4 +9,5 @@ + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v6.xml b/node/src/main/resources/migration/vault-schema.changelog-v6.xml new file mode 100644 index 0000000000..71214f8050 --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v6.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + 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 0d9cbe9f68..99010acca7 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 @@ -9,6 +9,7 @@ import net.corda.core.internal.packageName import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* +import net.corda.core.node.services.Vault.ConstraintInfo.Type.* import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.* @@ -472,6 +473,125 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `query by contract states constraint type`() { + database.transaction { + // insert states with different constraint types + vaultFiller.fillWithSomeTestLinearStates(1).states.first().state.constraint + vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint + vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint).states.first().state.constraint + // hash constraint + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint + // signature constraint (single key) + val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) + val constraintSignature = linearStateSignature.states.first().state.constraint as SignatureAttachmentConstraint + // signature constraint (composite key) + val compositeKey = CompositeKey.Builder().addKeys(alice.publicKey, bob.publicKey, charlie.publicKey, bankOfCorda.publicKey, bigCorp.publicKey, megaCorp.publicKey, miniCorp.publicKey, cashNotary.publicKey, dummyNotary.publicKey, dummyCashIssuer.publicKey).build() + val linearStateSignatureCompositeKey = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(compositeKey)) + val constraintSignatureCompositeKey = linearStateSignatureCompositeKey.states.first().state.constraint as SignatureAttachmentConstraint + + // default Constraint Type is ALL + val results = vaultService.queryBy() + assertThat(results.states).hasSize(6) + + // search for states with Vault.ConstraintInfo.Type = ALWAYS_ACCEPT + val constraintTypeCriteria1 = VaultQueryCriteria(constraintTypes = setOf(ALWAYS_ACCEPT)) + val constraintResults1 = vaultService.queryBy(constraintTypeCriteria1) + assertThat(constraintResults1.states).hasSize(1) + + // search for states with [Vault.ConstraintInfo.Type] = HASH + val constraintTypeCriteria2 = VaultQueryCriteria(constraintTypes = setOf(HASH)) + val constraintResults2 = vaultService.queryBy(constraintTypeCriteria2) + assertThat(constraintResults2.states).hasSize(2) + assertThat(constraintResults2.states.map { it.state.constraint }).containsOnlyOnce(constraintHash) + + // search for states with [Vault.ConstraintInfo.Type] either HASH or CZ_WHITELISED + // DOCSTART VaultQueryExample30 + val constraintTypeCriteria = VaultQueryCriteria(constraintTypes = setOf(HASH, CZ_WHITELISTED)) + val sortAttribute = SortAttribute.Standard(Sort.VaultStateAttribute.CONSTRAINT_TYPE) + val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) + val constraintResults = vaultService.queryBy(constraintTypeCriteria, sorter) + // DOCEND VaultQueryExample30 + assertThat(constraintResults.states).hasSize(3) + + // search for states with [Vault.ConstraintInfo.Type] = SIGNATURE + val constraintTypeCriteria4 = VaultQueryCriteria(constraintTypes = setOf(SIGNATURE)) + val constraintResults4 = vaultService.queryBy(constraintTypeCriteria4) + assertThat(constraintResults4.states).hasSize(2) + assertThat(constraintResults4.states.map { it.state.constraint }).containsAll(listOf(constraintSignature, constraintSignatureCompositeKey)) + + // search for states with [Vault.ConstraintInfo.Type] = SIGNATURE or CZ_WHITELISED + val constraintTypeCriteria5 = VaultQueryCriteria(constraintTypes = setOf(SIGNATURE, CZ_WHITELISTED)) + val constraintResults5 = vaultService.queryBy(constraintTypeCriteria5) + assertThat(constraintResults5.states).hasSize(3) + } + } + + @Test + fun `query by contract states constraint type and data`() { + database.transaction { + // insert states with different constraint types + vaultFiller.fillWithSomeTestLinearStates(1).states.first().state.constraint + val alwaysAcceptConstraint = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint + vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) + // hash constraint + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint + // signature constraint (single key) + val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) + val constraintSignature = linearStateSignature.states.first().state.constraint as SignatureAttachmentConstraint + // signature constraint (composite key) + val compositeKey = CompositeKey.Builder().addKeys(alice.publicKey, bob.publicKey, charlie.publicKey, bankOfCorda.publicKey, bigCorp.publicKey, megaCorp.publicKey, miniCorp.publicKey, cashNotary.publicKey, dummyNotary.publicKey, dummyCashIssuer.publicKey).build() + val linearStateSignatureCompositeKey = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(compositeKey)) + val constraintSignatureCompositeKey = linearStateSignatureCompositeKey.states.first().state.constraint as SignatureAttachmentConstraint + + // default Constraint Type is ALL + val results = vaultService.queryBy() + assertThat(results.states).hasSize(6) + + // search for states with AlwaysAcceptAttachmentConstraint + val constraintCriteria1 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(AlwaysAcceptAttachmentConstraint))) + val constraintResults1 = vaultService.queryBy(constraintCriteria1) + assertThat(constraintResults1.states).hasSize(1) + assertThat(constraintResults1.states.first().state.constraint).isEqualTo(alwaysAcceptConstraint) + + // search for states for a specific HashAttachmentConstraint + val constraintsCriteria2 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintHash))) + val constraintResults2 = vaultService.queryBy(constraintsCriteria2) + assertThat(constraintResults2.states).hasSize(1) + assertThat(constraintResults2.states.first().state.constraint).isEqualTo(constraintHash) + + // search for states with a specific SignatureAttachmentConstraint constraint + val constraintCriteria3 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintSignatureCompositeKey))) + val constraintResults3 = vaultService.queryBy(constraintCriteria3) + assertThat(constraintResults3.states).hasSize(1) + assertThat(constraintResults3.states.first().state.constraint).isEqualTo(constraintSignatureCompositeKey) + + // search for states for given set of mixed constraint types + // DOCSTART VaultQueryExample31 + val constraintCriteria = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintSignature), + Vault.ConstraintInfo(constraintSignatureCompositeKey), Vault.ConstraintInfo(constraintHash))) + val constraintResults = vaultService.queryBy(constraintCriteria) + // DOCEND VaultQueryExample31 + assertThat(constraintResults.states).hasSize(3) + assertThat(constraintResults.states.map { it.state.constraint }).containsAll(listOf(constraintHash, constraintSignature, constraintSignatureCompositeKey)) + + // exercise enriched query + + // Base criteria + val baseCriteria = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(AlwaysAcceptAttachmentConstraint))) + + // Enrich and override QueryCriteria with additional default attributes + val enrichedCriteria = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintSignature))) + + // Execute query + val enrichedResults = services.vaultService.queryBy(baseCriteria and enrichedCriteria).states + assertThat(enrichedResults).hasSize(2) + assertThat(enrichedResults.map { it.state.constraint }).containsAll(listOf(constraintSignature, alwaysAcceptConstraint)) + } + } + @Test fun `consumed states`() { database.transaction { @@ -2211,6 +2331,33 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `sorted, enriched and overridden composite query with constraints handles defaults correctly`() { + database.transaction { + vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint( SecureHash.randomSHA256())) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint) + + // Base criteria + val baseCriteria = VaultQueryCriteria(constraintTypes = setOf(ALWAYS_ACCEPT)) + + // Enrich and override QueryCriteria with additional default attributes (contract constraints) + val enrichedCriteria = VaultQueryCriteria(constraintTypes = setOf(SIGNATURE, HASH, ALWAYS_ACCEPT)) // enrich + + // Sorting + val sortAttribute = SortAttribute.Standard(Sort.VaultStateAttribute.CONSTRAINT_TYPE) + val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) + + // Execute query + val results = services.vaultService.queryBy(baseCriteria and enrichedCriteria, sorter).states + assertThat(results).hasSize(3) + assertThat(results[0].state.constraint is AlwaysAcceptAttachmentConstraint) + assertThat(results[1].state.constraint is HashAttachmentConstraint) + assertThat(results[2].state.constraint is SignatureAttachmentConstraint) + } + } + @Test fun unconsumedCashStatesForSpending_single_issuer_reference() { database.transaction { 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 f5ae1f852a..eb46690305 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 @@ -102,7 +102,8 @@ class VaultFiller @JvmOverloads constructor( linearString: String = "", linearNumber: Long = 0L, linearBoolean: Boolean = false, - linearTimestamp: Instant = now()): Vault { + linearTimestamp: Instant = now(), + constraint: AttachmentConstraint = AutomaticHashConstraint): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair @@ -116,7 +117,8 @@ class VaultFiller @JvmOverloads constructor( linearString = linearString, linearNumber = linearNumber, linearBoolean = linearBoolean, - linearTimestamp = linearTimestamp), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + linearTimestamp = linearTimestamp), DUMMY_LINEAR_CONTRACT_PROGRAM_ID, + constraint = constraint) addCommand(dummyCommand()) } return@map services.signInitialTransaction(dummyIssue).withAdditionalSignature(issuerKey, signatureMetadata) From 3110c758474e26ddb30e2c03b957680ace2cb6db Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 3 Oct 2018 13:41:52 +0100 Subject: [PATCH 09/83] =?UTF-8?q?Network=20bootstrapper=20tool:=20optional?= =?UTF-8?q?=20configuration=20setting=20to=20specify=20the=20minimum=20pla?= =?UTF-8?q?t=E2=80=A6=20(#4005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Provide an optional configuration setting to specify the minimum platform version to use in the network params file. * Leave Cordform signature intact. * Leave previous Gradle Plugin called signature intact. * Incorporating feedback from PR review. * Added minimum platform version validation check. * Removed final 2 references to "default" * Added changelog entry. --- docs/source/changelog.rst | 2 ++ .../internal/network/NetworkBootstrapper.kt | 28 +++++++++++++------ .../network/NetworkBootstrapperTest.kt | 3 +- .../kotlin/net/corda/bootstrapper/Main.kt | 6 +++- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 3017257931..9b3e3bb571 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,8 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- +* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter + * Introduce minimum and target platform version for CorDapps. * Vault storage of contract state constraints metadata and associated vault query functions to retrieve and sort by constraint type. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 0a80988f27..49d0704c84 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -162,16 +162,23 @@ internal constructor(private val initSerEnv: Boolean, } /** Entry point for the tool */ - fun bootstrap(directory: Path, copyCordapps: Boolean) { + fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int) { + require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" } // Don't accidently include the bootstrapper jar as a CorDapp! val bootstrapperJar = javaClass.location.toPath() val cordappJars = directory.list { paths -> paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList() } - bootstrap(directory, cordappJars, copyCordapps, fromCordform = false) + bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion) } - private fun bootstrap(directory: Path, cordappJars: List, copyCordapps: Boolean, fromCordform: Boolean) { + private fun bootstrap( + directory: Path, + cordappJars: List, + copyCordapps: Boolean, + fromCordform: Boolean, + minimumPlatformVersion: Int = PLATFORM_VERSION + ) { directory.createDirectories() println("Bootstrapping local test network in $directory") if (!fromCordform) { @@ -210,7 +217,7 @@ internal constructor(private val initSerEnv: Boolean, val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs) println("Generating contract implementations whitelist") val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter)) - val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs) + val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion) if (newNetParams != existingNetParams) { println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams") } else { @@ -337,10 +344,13 @@ internal constructor(private val initSerEnv: Boolean, throw IllegalStateException(msg.toString()) } - private fun installNetworkParameters(notaryInfos: List, - whitelist: Map>, - existingNetParams: NetworkParameters?, - nodeDirs: List): NetworkParameters { + private fun installNetworkParameters( + notaryInfos: List, + whitelist: Map>, + existingNetParams: NetworkParameters?, + nodeDirs: List, + minimumPlatformVersion: Int + ): NetworkParameters { // TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize val netParams = if (existingNetParams != null) { if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos) { @@ -355,7 +365,7 @@ internal constructor(private val initSerEnv: Boolean, } } else { NetworkParameters( - minimumPlatformVersion = 4, + minimumPlatformVersion = minimumPlatformVersion, notaries = notaryInfos, modifiedTime = Instant.now(), maxMessageSize = 10485760, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index 0b265171e6..1871ea553f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -11,6 +11,7 @@ import net.corda.core.serialization.serialize import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig @@ -217,7 +218,7 @@ class NetworkBootstrapperTest { private fun bootstrap(copyCordapps: Boolean = true) { providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null } - bootstrapper.bootstrap(rootDir, copyCordapps) + bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION) } private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) { diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 09e6ca9972..9d382516fa 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -2,6 +2,7 @@ package net.corda.bootstrapper import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.start +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.network.NetworkBootstrapper import picocli.CommandLine.Option import java.nio.file.Path @@ -24,8 +25,11 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) private var noCopy: Boolean = false + @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters"]) + private var minimumPlatformVersion = PLATFORM_VERSION + override fun runProgram(): Int { - NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy) + NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy, minimumPlatformVersion = minimumPlatformVersion) return 0 //exit code } } \ No newline at end of file From 6f241b69fc9955bfa4850ad893a30d43191ef144 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 3 Oct 2018 17:24:03 +0200 Subject: [PATCH 10/83] [CORDA-2064]: Add "extraNetworkMapKeys" to node startup log. (#4021) --- node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 681d7ad264..74f3b0f40f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -378,7 +378,11 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { if (agentProperties.containsKey("sun.jdwp.listenerAddress")) { logger.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}") } - logger.info("Starting as node on ${conf.p2pAddress}") + var nodeStartedMessage = "Starting as node on ${conf.p2pAddress}" + if (conf.extraNetworkMapKeys.isNotEmpty()) { + nodeStartedMessage = "$nodeStartedMessage with additional Network Map keys ${conf.extraNetworkMapKeys.joinToString(prefix = "[", postfix = "]", separator = ", ")}" + } + logger.info(nodeStartedMessage) } protected open fun registerWithNetwork(conf: NodeConfiguration, versionInfo: VersionInfo, nodeRegistrationConfig: NodeRegistrationOption) { From 8629316dd1f80fbecc4aaf087441aa368b0dbad3 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 3 Oct 2018 17:27:04 +0200 Subject: [PATCH 11/83] [CORDA-2021]: Error message in log when keyStorePassword is incorrect is not accurately describing the problem (fixed). (#4022) --- .../net/corda/node/internal/AbstractNode.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2391cf7fca..3c0be454c7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -723,9 +723,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val identitiesKeyStore = configuration.signingCertificateStore.get() val trustStore = configuration.p2pSslOptions.trustStore.get() AllCertificateStores(trustStore, sslKeyStore, identitiesKeyStore) - } catch (e: KeyStoreException) { - log.warn("At least one of the keystores or truststore passwords does not match configuration.") - null } catch (e: IOException) { log.error("IO exception while trying to validate keystores and truststore", e) null @@ -736,11 +733,15 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun validateKeyStores(): X509Certificate { // Step 1. Check trustStore, sslKeyStore and identitiesKeyStore exist. - val certStores = requireNotNull(getCertificateStores()) { - "One or more keyStores (identity or TLS) or trustStore not found. " + - "Please either copy your existing keys and certificates from another node, " + - "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + - "Read more at: https://docs.corda.net/permissioning.html" + val certStores = try { + requireNotNull(getCertificateStores()) { + "One or more keyStores (identity or TLS) or trustStore not found. " + + "Please either copy your existing keys and certificates from another node, " + + "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + + "Read more at: https://docs.corda.net/permissioning.html" + } + } catch (e: KeyStoreException) { + throw IllegalArgumentException("At least one of the keystores or truststore passwords does not match configuration.") } // Step 2. Check that trustStore contains the correct key-alias entry. require(CORDA_ROOT_CA in certStores.trustStore) { From 3cf1450fc1839a7fbf134f75b3b95004367cdce1 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 3 Oct 2018 18:16:07 +0200 Subject: [PATCH 12/83] [CORDA-2066]: `setting-up-a-corda-network` docs file is misleading (fixed). (#4025) --- docs/source/corda-networks-index.rst | 2 +- docs/source/corda-test-networks.rst | 77 +++++++++ docs/source/setting-up-a-corda-network.rst | 190 --------------------- 3 files changed, 78 insertions(+), 191 deletions(-) create mode 100644 docs/source/corda-test-networks.rst delete mode 100644 docs/source/setting-up-a-corda-network.rst diff --git a/docs/source/corda-networks-index.rst b/docs/source/corda-networks-index.rst index 67f544d69c..d1bf86f03e 100644 --- a/docs/source/corda-networks-index.rst +++ b/docs/source/corda-networks-index.rst @@ -5,7 +5,7 @@ Networks :maxdepth: 1 joining-a-network - setting-up-a-corda-network + corda-test-networks running-a-notary permissioning network-map diff --git a/docs/source/corda-test-networks.rst b/docs/source/corda-test-networks.rst new file mode 100644 index 0000000000..f5c2991fcf --- /dev/null +++ b/docs/source/corda-test-networks.rst @@ -0,0 +1,77 @@ +.. _log4j2: http://logging.apache.org/log4j/2.x/ + +Corda networks +============== + +A Corda network consists of a number of machines running nodes. These nodes communicate using persistent protocols in +order to create and validate transactions. + +There are three broader categories of functionality one such node may have. These pieces of functionality are provided +as services, and one node may run several of them. + +* Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a + double-spend or not +* Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of + transactions +* Regular node: All nodes have a vault and may start protocols communicating with other nodes, notaries and oracles and + evolve their private ledger + +Bootstrap your own test network +------------------------------- + +Certificates +~~~~~~~~~~~~ + +Every node in a given Corda network must have an identity certificate signed by the network's root CA. See +:doc:`permissioning` for more information. + +Configuration +~~~~~~~~~~~~~ + +A node can be configured by adding/editing ``node.conf`` in the node's directory. For details see :doc:`corda-configuration-file`. + +An example configuration: + +.. literalinclude:: example-code/src/main/resources/example-node.conf +:language: cfg + + The most important fields regarding network configuration are: + + * ``p2pAddress``: This specifies a host and port to which Artemis will bind for messaging with other nodes. Note that the + address bound will **NOT** be ``my-corda-node``, but rather ``::`` (all addresses on all network interfaces). The hostname specified + is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the + resolvable name of a machine in a VPN. +* ``rpcAddress``: The address to which Artemis will bind for RPC calls. +* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` and ``rpcAddress`` if they are on the same machine. + +Starting the nodes +~~~~~~~~~~~~~~~~~~ + +You will first need to create the local network by bootstrapping it with the bootstrapper. Details of how to do that +can be found in :doc:`network-bootstrapper`. + +Once that's done you may now start the nodes in any order. You should see a banner, some log lines and eventually +``Node started up and registered``, indicating that the node is fully started. + +.. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound. + +In terms of process management there is no prescribed method. You may start the jars by hand or perhaps use systemd and friends. + +Logging +~~~~~~~ + +Only a handful of important lines are printed to the console. For +details/diagnosing problems check the logs. + +Logging is standard log4j2_ and may be configured accordingly. Logs +are by default redirected to files in ``NODE_DIRECTORY/logs/``. + +Connecting to the nodes +~~~~~~~~~~~~~~~~~~~~~~~ + +Once a node has started up successfully you may connect to it as a client to initiate protocols/query state etc. +Depending on your network setup you may need to tunnel to do this remotely. + +See the :doc:`tutorial-clientrpc-api` on how to establish an RPC link. + +Sidenote: A client is always associated with a single node with a single identity, which only sees their part of the ledger. diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst deleted file mode 100644 index 320cca5f0d..0000000000 --- a/docs/source/setting-up-a-corda-network.rst +++ /dev/null @@ -1,190 +0,0 @@ -.. _log4j2: http://logging.apache.org/log4j/2.x/ - -Setting up a Corda network -========================== - -.. contents:: - -Bootstrapping a development network ------------------------------------ - -When testing CorDapps during development, you should use the :doc:`bootstrapper tool ` to create -a local test network. - -Creating your own compatibility zone ------------------------------------- - -This section documents how to implement your own doorman and network map servers, which is the basic process required to -create a dedicated zone. At this time we do not provide tooling to do this, because the needs of each zone are different -and no generic, configurable doorman codebase has been written. - -Do you need a zone? -^^^^^^^^^^^^^^^^^^^ - -Think twice before going down this route: - -1. It isn't necessary for testing. -2. It isn't necessary for adding another layer of permissioning or 'know your customer' requirements onto your app. - -**Testing.** Creating a production-ready zone isn't necessary for testing as you can use the :doc:`network bootstrapper ` -tool to create all the certificates, keys, and distribute the needed map files to run many nodes. The bootstrapper can -create a network locally on your desktop/laptop but it also knows how to automate cloud providers via their APIs and -using Docker. In this way you can bring up a simulation of a real Corda network with different nodes on different -machines in the cloud for your own testing. Testing this way has several advantages, most obviously that you avoid -race conditions in your tests caused by nodes/tests starting before all map data has propagated to all nodes. -You can read more about the reasons for the creation of the bootstrapper tool -`in a blog post on the design thinking behind Corda's network map infrastructure `__. - -**Permissioning.** And creating a zone is also unnecessary for imposing permissioning requirements beyond that of the -base Corda network. You can control who can use your app by creating a *business network*. A business network is what we -call a coalition of nodes that have chosen to run a particular app within a given commercial context. Business networks -aren't represented in the Corda API at this time, partly because the technical side is so simple. You can create one -via a simple three step process: - -1. Distribute a list of X.500 names that are members of your business network, e.g. a simple way to do this is by - hosting a text file with one name per line on your website at a fixed HTTPS URL. You could also write a simple - request/response flow that serves the list over the Corda protocol itself, although this requires the business - network to have a node for itself. -2. Write a bit of code that downloads and caches the contents of this file on disk, and which loads it into memory in - the node. A good place to do this is in a class annotated with ``@CordaService``, because this class can expose - a ``Set`` field representing the membership of your service. -3. In your flows use ``serviceHub.findService`` to get a reference to your ``@CordaService`` class, read the list of - members and at the start of each flow, throw a FlowException if the counterparty isn't in the membership list. - -In this way you can impose a centrally controlled ACL that all members will collectively enforce. - -.. note:: A production-ready Corda network and a new iteration of the testnet will be available soon. - -Why create your own zone? -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The primary reason to create a zone and provide the associated infrastructure is control over *network parameters*. These -are settings that control Corda's operation, and on which all users in a network must agree. Failure to agree would create -the Corda equivalent of a blockchain "hard fork". Parameters control things like the root of identity, -how quickly users should upgrade, how long nodes can be offline before they are evicted from the system and so on. - -Creating a zone involves the following steps: - -1. Create the zone private keys and certificates. This procedure is conventional and no special knowledge is required: - any self-signed set of certificates can be used. A professional quality zone will probably keep the keys inside a - hardware security module (as the main Corda network and test networks do). -2. Write a network map server. -3. Optionally, create a doorman server. -4. Finally, you would select and generate your network parameter file. - -Writing a network map server -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This server implements a simple HTTP based protocol described in the ":doc:`network-map`" page. -The map server is responsible for gathering NodeInfo files from nodes, storing them, and distributing them back to the -nodes in the zone. By doing this it is also responsible for choosing who is in and who is out: having a signed -identity certificate is not enough to be a part of a Corda zone, you also need to be listed in the network map. -It can be thought of as a DNS equivalent. If you want to de-list a user, you would do it here. - -It is very likely that your map server won't be entirely standalone, but rather, integrated with whatever your master -user database is. - -The network map server also distributes signed network parameter files and controls the roll-out schedule for when they -become available for download and opt-in, and when they become enforced. This is again a policy decision you will -probably choose to place some simple UI or workflow tooling around, in particular to enforce restrictions on who can -edit the map or the parameters. - -Writing a doorman server -^^^^^^^^^^^^^^^^^^^^^^^^ - -This step is optional because your users can obtain a signed certificate in many different ways. The doorman protocol -is again a very simple HTTP based approach in which a node creates keys and requests a certificate, polling until it -gets back what it expects. However, you could also integrate this process with the rest of your signup process. For example, -by building a tool that's integrated with your payment flow (if payment is required to take part in your zone at all). -Alternatively you may wish to distribute USB smartcard tokens that generate the private key on first use, as is typically -seen in national PKIs. There are many options. - -If you do choose to make a doorman server, the bulk of the code you write will be workflow related. For instance, -related to keeping track of an applicant as they proceed through approval. You should also impose any naming policies -you have in the doorman process. If names are meant to match identities registered in government databases then that -should be enforced here, alternatively, if names can be self-selected or anonymous, you would only bother with a -deduplication check. Again it will likely be integrated with a master user database. - -Corda does not currently provide a doorman or network map service out of the box, partly because when stripped of the -zone specific policy there isn't much to them: just a basic HTTP server that most programmers will have favourite -frameworks for anyway. - -The protocol is: - -* If $URL = ``https://some.server.com/some/path`` -* Node submits a PKCS#10 certificate signing request using HTTP POST to ``$URL/certificate``. It will have a MIME - type of ``application/octet-stream``. The ``Platform-Version`` header is set to be "1.0" and the ``Client-Version`` header to reflect the node software version -* The server returns an opaque string that references this request (let's call it ``$requestid``, or an HTTP error if something went wrong -* The returned request ID should be persisted to disk, to handle zones where approval may take a long time due to manual - intervention being required -* The node starts polling ``$URL/$requestid`` using HTTP GET. The poll interval can be controlled by the server returning - a response with a ``Cache-Control`` header -* If the request is answered with a ``200 OK`` response, the body is expected to be a zip file. Each file is expected to - be a binary X.509 certificate, and the certs are expected to be in order -* If the request is answered with a ``204 No Content`` response, the node will try again later -* If the request is answered with a ``403 Not Authorized`` response, the node will treat that as request rejection and give up -* Other response codes will cause the node to abort with an exception - -You can use any standard key tools to create the required key pairs and certificates. The ``X509Utilities`` class in the -`Corda repository -`__ -shows how to generate the required key pairs and certificates using Bouncy Castle. - -Setting zone parameters -^^^^^^^^^^^^^^^^^^^^^^^ - -Zone parameters are stored in a file containing a Corda AMQP serialised ``SignedDataWithCert`` -object. It is easy to create such a file with a small Java or Kotlin program. The ``NetworkParameters`` object is a -simple data holder that could be read from e.g. a config file, or settings from a database. Signing and saving the -resulting file is just a few lines of code. A full example can be found in `NetworkParametersCopier.kt -`__, -but a flavour of it looks like this: - -.. container:: codeset - - .. sourcecode:: java - - NetworkParameters networkParameters = new NetworkParameters( - 4, // minPlatformVersion - Collections.emptyList(), // notaries - 1024 * 1024 * 20, // maxMessageSize - 1024 * 1024 * 15, // maxTransactionSize - Instant.now(), // modifiedTime - 2, // epoch - Collections.emptyMap() // whitelist - ); - CertificateAndKeyPair signingCertAndKeyPair = loadNetworkMapCA(); - SerializedBytes> bytes = SerializedBytes.from(netMapCA.sign(networkParameters)); - Files.copy(bytes.open(), Paths.get("params-file")); - - .. sourcecode:: kotlin - - val networkParameters = NetworkParameters( - minimumPlatformVersion = 4, - notaries = listOf(...), - maxMessageSize = 1024 * 1024 * 20 // 20mb, for example. - maxTransactionSize = 1024 * 1024 * 15, - modifiedTime = Instant.now(), - epoch = 2, - ... etc ... - ) - val signingCertAndKeyPair: CertificateAndKeyPair = loadNetworkMapCA() - val signedParams: SerializedBytes = signingCertAndKeyPair.sign(networkParameters).serialize() - signedParams.open().copyTo(Paths.get("/some/path")) - -Each individual parameter is documented in `the JavaDocs/KDocs for the NetworkParameters class -`__. The network map -certificate is usually chained off the root certificate, and can be created according to the instructions above. Each -time the zone parameters are changed, the epoch should be incremented. Epochs are essentially version numbers for the -parameters, and they therefore cannot go backwards. Once saved, the new parameters can be served by the network map server. - -Selecting parameter values -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -How to choose the parameters? This is the most complex question facing you as a new zone operator. Some settings may seem -straightforward and others may involve cost/benefit trade-offs specific to your business. For example, you could choose -to run a validating notary yourself, in which case you would (in the absence of SGX) see all the users' data. Or you could -run a non-validating notary, with BFT fault tolerance, which implies recruiting others to take part in the cluster. - -New network parameters will be added over time as Corda evolves. You will need to ensure that when your users upgrade, -all the new network parameters are being served. You can ask for advice on the `corda-dev mailing list `__. \ No newline at end of file From febe737a7a7b23b27d71fab4f485c8b4016ca44a Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 4 Oct 2018 10:45:55 +0100 Subject: [PATCH 13/83] Updates homepage link. (#4027) --- docs/source/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index ac64315eb6..9aaaeaaf73 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,7 +14,7 @@ follow our :doc:`quickstart guide `. If you want to start coding on Corda, then familiarise yourself with the :doc:`key concepts `, then read our :doc:`Hello, World! tutorial `. For the background behind Corda, read the non-technical -`introductory white paper`_ or for more detail, the `technical white paper`_. +`platform white paper`_ or for more detail, the `technical white paper`_. If you have questions or comments, then get in touch on `Slack `_ or ask a question on `Stack Overflow `_ . @@ -24,7 +24,7 @@ We look forward to seeing what you can do with Corda! .. note:: You can read this site offline. Either `download the PDF`_ or download the Corda source code, run ``gradle buildDocs`` and you will have a copy of this site in the ``docs/build/html`` directory. -.. _`introductory white paper`: _static/corda-introductory-whitepaper.pdf +.. _`platform white paper`: _static/corda-platform-whitepaper.pdf .. _`technical white paper`: _static/corda-technical-whitepaper.pdf .. _`download the PDF`: _static/corda-developer-site.pdf From 5d03a03ec44f3160881b2ba104e9b16f69e0edff Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 4 Oct 2018 11:48:24 +0100 Subject: [PATCH 14/83] CORDA-2030 - ensure the correct kotlin runtime library is used to resolve `AutoCloseable` (#4028) Fixes compatibility issue surfaced as "java.lang.NoClassDefFoundError: kotlin/jdk7/AutoCloseableKt" --- node/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/node/build.gradle b/node/build.gradle index 3a344867a3..ff7be256dc 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -79,6 +79,7 @@ dependencies { compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + runtime "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" From 064c72dfb14f9559eec5e91bbe5edf32a7c47e30 Mon Sep 17 00:00:00 2001 From: cburlinchon Date: Thu, 4 Oct 2018 11:55:18 +0100 Subject: [PATCH 15/83] ENT-2554: Add back hibernate dependency as required by explorer runDemoNodes (#4029) --- tools/explorer/build.gradle | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index fe52056591..e16e786058 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -11,13 +11,6 @@ processResources { from file("$rootDir/config/dev/log4j2.xml") } -configurations { - compile { - // We don't need Hibernate just for its @Type annotation. - exclude group: 'org.hibernate', module: 'hibernate-core' - } -} - dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" From 85d2a85e8515be4be46abd80e7fe9603bcb21634 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 4 Oct 2018 16:00:07 +0100 Subject: [PATCH 16/83] Safe parsing of min platform version and target version from CorDapp MANIFEST files (#4031) Also includes some cleanup --- .../core/internal/cordapp/CordappImpl.kt | 3 +- .../cordapp/JarScanningCordappLoader.kt | 8 ++-- .../node/internal/cordapp/ManifestUtils.kt | 37 ++++++++++--------- .../cordapp/JarScanningCordappLoaderTest.kt | 28 ++++++-------- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 38891ef254..2e1761afa8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -42,8 +42,7 @@ data class CordappImpl( // TODO Why a seperate Info class and not just have the fields directly in CordappImpl? data class Info(val shortName: String, val vendor: String, val version: String, val minimumPlatformVersion: Int, val targetPlatformVersion: Int) { companion object { - private const val UNKNOWN_VALUE = "Unknown" - + const val UNKNOWN_VALUE = "Unknown" val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE, 1, 1) } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index c691af76c7..96d1e4f435 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -112,10 +112,12 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: ) private fun loadCordapps(): List { - val cordapps = cordappJarPaths.map { scanCordapp(it).toCordapp(it) } + val cordapps = cordappJarPaths + .map { scanCordapp(it).toCordapp(it) } .filter { if (it.info.minimumPlatformVersion > versionInfo.platformVersion) { - logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") + logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum " + + "platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") false } else { true @@ -126,7 +128,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl { - val info = url.url.openStream().let(::JarInputStream).use { it.manifest }.toCordappInfo(CordappImpl.jarName(url.url)) + val info = url.url.openStream().let(::JarInputStream).use { it.manifest?.toCordappInfo(CordappImpl.jarName(url.url)) ?: CordappImpl.Info.UNKNOWN } return CordappImpl( findContractClassNames(this), findInitiatedFlows(this), diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt index c71a5bf7cb..6e10661c86 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt @@ -1,6 +1,7 @@ package net.corda.node.internal.cordapp import net.corda.core.internal.cordapp.CordappImpl +import net.corda.core.internal.cordapp.CordappImpl.Info.Companion.UNKNOWN_VALUE import java.util.jar.Attributes import java.util.jar.Manifest @@ -23,23 +24,23 @@ fun createTestManifest(name: String, title: String, version: String, vendor: Str return manifest } -operator fun Manifest.set(key: String, value: String) { - mainAttributes.putValue(key, value) +operator fun Manifest.set(key: String, value: String): String? { + return mainAttributes.putValue(key, value) } -fun Manifest?.toCordappInfo(defaultShortName: String): CordappImpl.Info { - var info = CordappImpl.Info.UNKNOWN - (this?.mainAttributes?.getValue("Name") ?: defaultShortName).let { shortName -> - info = info.copy(shortName = shortName) - } - this?.mainAttributes?.getValue("Implementation-Vendor")?.let { vendor -> - info = info.copy(vendor = vendor) - } - this?.mainAttributes?.getValue("Implementation-Version")?.let { version -> - info = info.copy(version = version) - } - val minPlatformVersion = this?.mainAttributes?.getValue("Min-Platform-Version")?.toInt() ?: 1 - val targetPlatformVersion = this?.mainAttributes?.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion - info = info.copy(minimumPlatformVersion = minPlatformVersion, targetPlatformVersion = targetPlatformVersion) - return info -} \ No newline at end of file +operator fun Manifest.get(key: String): String? = mainAttributes.getValue(key) + +fun Manifest.toCordappInfo(defaultShortName: String): CordappImpl.Info { + val shortName = this["Name"] ?: defaultShortName + val vendor = this["Implementation-Vendor"] ?: UNKNOWN_VALUE + val version = this["Implementation-Version"] ?: UNKNOWN_VALUE + val minPlatformVersion = this["Min-Platform-Version"]?.toIntOrNull() ?: 1 + val targetPlatformVersion = this["Target-Platform-Version"]?.toIntOrNull() ?: minPlatformVersion + return CordappImpl.Info( + shortName = shortName, + vendor = vendor, + version = version, + minimumPlatformVersion = minPlatformVersion, + targetPlatformVersion = targetPlatformVersion + ) +} diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index e71786f8b6..927852e1e8 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.* import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader -import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.testing.node.internal.cordappsForPackages import net.corda.testing.node.internal.getTimestampAsDirectoryName import net.corda.testing.node.internal.packageInDirectory @@ -45,7 +44,7 @@ class JarScanningCordappLoaderTest { } @Test - fun `test that classes that aren't in cordapps aren't loaded`() { + fun `classes that aren't in cordapps aren't loaded`() { // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp val loader = JarScanningCordappLoader.fromDirectories(listOf(Paths.get("."))) assertThat(loader.cordapps).containsOnly(loader.coreCordapp) @@ -56,10 +55,9 @@ class JarScanningCordappLoaderTest { val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)) - val actual = loader.cordapps.toTypedArray() - assertThat(actual).hasSize(2) + assertThat(loader.cordapps).hasSize(2) - val actualCordapp = actual.single { it != loader.coreCordapp } + val actualCordapp = loader.cordapps.single { it != loader.coreCordapp } assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") assertThat(actualCordapp.rpcFlows).isEmpty() @@ -113,7 +111,7 @@ class JarScanningCordappLoaderTest { fun `cordapp classloader sets target and min version to 1 if not specified`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) - loader.cordapps.filter { !it.info.shortName.equals("corda-core") }.forEach { + loader.cordapps.filter { it.info.shortName != "corda-core" }.forEach { assertThat(it.info.targetPlatformVersion).isEqualTo(1) assertThat(it.info.minimumPlatformVersion).isEqualTo(1) } @@ -126,7 +124,7 @@ class JarScanningCordappLoaderTest { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) // exclude the core cordapp - val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single() + val cordapp = loader.cordapps.single { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl") } assertThat(cordapp.info.targetPlatformVersion).isEqualTo(3) assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2) } @@ -137,17 +135,17 @@ class JarScanningCordappLoaderTest { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) // exclude the core cordapp - val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single() + val cordapp = loader.cordapps.single { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl") } assertThat(cordapp.info.targetPlatformVersion).isEqualTo(2) assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2) } @Test - fun `cordapp classloader does not load apps when their min platform version is greater than the platform version`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! + fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1)) // exclude the core cordapp - assertThat(loader.cordapps.size).isEqualTo(1) + assertThat(loader.cordapps).hasSize(1) } @Test @@ -155,7 +153,7 @@ class JarScanningCordappLoaderTest { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000)) // exclude the core cordapp - assertThat(loader.cordapps.size).isEqualTo(2) + assertThat(loader.cordapps).hasSize(2) } @Test @@ -163,11 +161,10 @@ class JarScanningCordappLoaderTest { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) // exclude the core cordapp - assertThat(loader.cordapps.size).isEqualTo(2) + assertThat(loader.cordapps).hasSize(2) } - private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { - + private fun cordappLoaderForPackages(packages: Iterable): CordappLoader { val cordapps = cordappsForPackages(packages) return testDirectory().let { directory -> cordapps.packageInDirectory(directory) @@ -176,7 +173,6 @@ class JarScanningCordappLoaderTest { } private fun testDirectory(): Path { - return Paths.get("build", getTimestampAsDirectoryName()) } } From 962e111389be6ec241c000bfde21c70154d01681 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 4 Oct 2018 17:44:24 +0200 Subject: [PATCH 17/83] Make the reference states design doc render better and more consistently with the other design docs. --- docs/source/design/reference-states/design.md | 84 +++---------------- 1 file changed, 10 insertions(+), 74 deletions(-) diff --git a/docs/source/design/reference-states/design.md b/docs/source/design/reference-states/design.md index 56e012499e..619bbdd094 100644 --- a/docs/source/design/reference-states/design.md +++ b/docs/source/design/reference-states/design.md @@ -1,42 +1,4 @@ -![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) - -# Design - -DOCUMENT MANAGEMENT ---- - -Design documents should follow the standard GitHub version management and pull request (PR) review workflow mechanism. - -## Document Control - -| Title | | -| -------------------- | ---------------------------------------- | -| Date | 27 March 2018 | -| Author | Roger Willis | -| Distribution | Matthew Nesbit, Rick Parker | -| Corda target version | open source and enterprise | -| JIRA reference | No JIRA's raised. | - -## Approvals - -#### Document Sign-off - -| Author | | -| ----------------- | ---------------------------------------- | -| Reviewer(s) | (GitHub PR reviewers) | -| Final approver(s) | (GitHub PR approver(s) from Design Approval Board) | - -#### Design Decisions - -There's only really one way to do this that satisfies our requirements - add a new input `StateAndRef` component group to the transaction classes. Other possible solutions are discussed below and why they are inappropriate. - -## Document History - -* [Version 1](https://github.com/corda/enterprise/blob/779aaefa5c09a6a28191496dd45252b6e207b7f7/docs/source/design/reference-states/design.md) (Received comments from Richard Brown and Mark Oldfield). -* [Version 2](https://github.com/corda/enterprise/blob/a87f1dcb22ba15081b0da92ba1501b6b81ae2baf/docs/source/design/reference-states/design.md) (Version presented to the DRB). - -HIGH LEVEL DESIGN ---- +# Reference states ## Overview @@ -44,13 +6,15 @@ See a prototype implementation here: https://github.com/corda/corda/pull/2889 There is an increasing need for Corda to support use-cases which require reference data which is issued and updated by specific parties, but available for use, by reference, in transactions built by other parties. -Why is this type of reference data required? +Why is this type of reference data required? A key benefit of blockchain systems is that everybody is sure they see the +same as their counterpart - and for this to work in situations where accurate processing depends on reference data +requires everybody to be operating on the same reference data. This, in turn, requires any given piece of reference data +to be uniquely identifiable and, requires that any given transaction must be certain to be operating on the most current +version of that reference data. In cases where the latter condition applies, only the notary can attest to this fact and +this, in turn, means the reference data must be in the form of an unconsumed state. -1. A key benefit of blockchain systems is that everybody is sure they see the same as their counterpart - and for this to work in situations where accurate processing depends on reference data requires everybody to be operating on the same reference data. -2. This, in turn, requires any given piece of reference data to be uniquely identifiable and, requires that any given transaction must be certain to be operating on the most current version of that reference data. -3. In cases where the latter condition applies, only the notary can attest to this fact and this, in turn, means the reference data must be in the form of an unconsumed state. - -This document outlines the approach for adding support for this type of reference data to the Corda transaction model via a new approach called "reference input states". +This document outlines the approach for adding support for this type of reference data to the Corda transaction model +via a new approach called "reference input states". ## Background @@ -71,16 +35,10 @@ However, neither of these solutions are optimal for reasons discussed in later s As such, this design document introduces the concept of a "reference input state" which is a better way to serve "periodically changing subjective reference data" on Corda. -*What is a "reference input state"?* - A reference input state is a `ContractState` which can be referred to in a transaction by the contracts of input and output states but whose contract is not executed as part of the transaction verification process and is not consumed when the transaction is committed to the ledger but _is_ checked for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal state when it occurs in an input or output position. -*What will reference input states enable?* - Reference data states will enable many parties to "reuse" the same state in their transactions as reference data whilst still allowing the reference data state owner the capability to update the state. When data distribution groups are available then reference state owners will be able to distribute updates to subscribers more easily. Currently, distribution would have to be performed manually. -*Roughly, how are reference input states implemented?* - Reference input states can be added to Corda by adding a new transaction component group that allows developers to add reference data `ContractState`s that are not consumed when the transaction is committed to the ledger. This eliminates the problems created by long chains of provenance, contention, and allows developers to use any `ContractState` for reference data. The feature should allow developers to add _any_ `ContractState` available in their vault, even if they are not a `participant` whilst nevertheless providing a guarantee that the state being used is the most recent version of that piece of information. ## Scope @@ -93,14 +51,6 @@ Non-goals (eg. out of scope) * Data distribution groups are required to realise the full potential of reference data states. This design document does not discuss data distribution groups. -## Timeline - -This work should be ready by the release of Corda V4. There is a prototype which is currently good enough for one of the firm's most critical projects, but more work is required: - -* to assess the impact of this change -* write tests -* write documentation - ## Requirements 1. Reference states can be any `ContractState` created by one or more `Party`s and subsequently updated by those `Party`s. E.g. `Cash`, `CompanyData`, `InterestRateSwap`, `FxRate`. Reference states can be `OwnableState`s, but it's more likely they will be `LinearState`s. @@ -222,18 +172,4 @@ It does the following: 4. If the subflow throws a NotaryException because it tried to finalise and failed, that exception is caught and examined. If the failure was due to a conflict on a referenced state, the flow suspends until that state has been updated in the vault (there is an API to do wait for transaction already, but here the flow must wait for a state update). 5. Then it re-does the initial calculation, re-creates the subflow with the new resolved tips using the factory, and re-runs it as a new subflow. -Care must be taken to handle progress tracking correctly in case of loops. - -## Complementary solutions - -See discussion of alternative approaches above in the "design decisions" section. - -## Final recommendation - -Proceed to Implementation - -TECHNICAL DESIGN ---- - -* Summary of changes to be included. -* Summary of outstanding issues to be included. \ No newline at end of file +Care must be taken to handle progress tracking correctly in case of loops. \ No newline at end of file From bffac331a37666415b91c0780d811669885a5d48 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 5 Oct 2018 09:28:00 +0100 Subject: [PATCH 18/83] Moved the PLATFORM_VERSION constant to core and added some missing usages (#4026) --- build.gradle | 1 - .../net/corda/client/rpc/CordaRPCClient.kt | 2 +- constants.properties | 6 ++++-- .../kotlin/net/corda/core/internal/CordaUtils.kt | 2 ++ .../kotlin/net/corda/core/NodeVersioningTest.kt | 16 ++++------------ .../corda/nodeapi/internal/NodeInfoConstants.kt | 1 - .../internal/network/NetworkBootstrapperTest.kt | 2 +- .../node/services/AttachmentLoadingTests.kt | 1 - .../main/kotlin/net/corda/node/VersionInfo.kt | 2 +- .../net/corda/node/internal/NodeStartup.kt | 2 +- .../internal/cordapp/CordappProviderImplTests.kt | 1 - .../net/corda/testing/node/MockServices.kt | 2 -- .../corda/testing/node/internal/DriverDSLImpl.kt | 4 +--- .../testing/node/internal/InternalMockNetwork.kt | 2 +- .../corda/testing/node/internal/NodeBasedTest.kt | 9 +++++---- .../main/kotlin/net/corda/bootstrapper/Main.kt | 2 +- 16 files changed, 22 insertions(+), 33 deletions(-) diff --git a/build.gradle b/build.gradle index 233c8be5c1..36872b5fef 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,6 @@ buildscript { // Our version: bump this on release. ext.corda_release_version = "4.0-SNAPSHOT" - // Increment this on any release that changes public APIs anywhere in the Corda platform ext.corda_platform_version = constants.getProperty("platformVersion") ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 116e6baf84..db445caa55 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -13,7 +13,7 @@ import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport -import net.corda.nodeapi.internal.PLATFORM_VERSION +import net.corda.core.internal.PLATFORM_VERSION import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import java.time.Duration diff --git a/constants.properties b/constants.properties index 8d23d66503..d472994d01 100644 --- a/constants.properties +++ b/constants.properties @@ -1,7 +1,9 @@ gradlePluginsVersion=4.0.29 kotlinVersion=1.2.51 -# When adjusting platformVersion upwards please also modify CordaRPCClientConfiguration.minimumServerProtocolVersion \ -# if there have been any RPC changes. Also please modify InternalMockNetwork.kt:MOCK_VERSION_INFO and NodeBasedTest.startNode +# ***************************************************************# +# When incrementing platformVersion make sure to update # +# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. # +# ***************************************************************# platformVersion=4 guavaVersion=25.1-jre proguardVersion=6.0.3 diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index 35ea7158ef..c7651da4cb 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -17,6 +17,8 @@ import org.slf4j.MDC // *Internal* Corda-specific utilities +const val PLATFORM_VERSION = 4 + fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) { val currentMinPlatformVersion = networkParameters.minimumPlatformVersion if (currentMinPlatformVersion < requiredMinPlatformVersion) { diff --git a/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt b/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt index 20f7e8cbfd..e118fe0326 100644 --- a/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt +++ b/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt @@ -10,11 +10,9 @@ import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.internal.config.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess -import net.corda.testing.common.internal.ProjectStructure import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Paths -import java.util.* import java.util.concurrent.atomic.AtomicInteger import java.util.jar.JarFile import kotlin.streams.toList @@ -23,12 +21,6 @@ class NodeVersioningTest { private companion object { val user = User("user1", "test", permissions = setOf("ALL")) val port = AtomicInteger(15100) - - val expectedPlatformVersion = (ProjectStructure.projectRootDir / "constants.properties").read { - val constants = Properties() - constants.load(it) - constants.getProperty("platformVersion").toInt() - } } private val factory = NodeProcess.Factory() @@ -45,7 +37,7 @@ class NodeVersioningTest { @Test fun `platform version in manifest file`() { val manifest = JarFile(factory.cordaJar.toFile()).manifest - assertThat(manifest.mainAttributes.getValue("Corda-Platform-Version").toInt()).isEqualTo(expectedPlatformVersion) + assertThat(manifest.mainAttributes.getValue("Corda-Platform-Version").toInt()).isEqualTo(PLATFORM_VERSION) } @Test @@ -60,9 +52,9 @@ class NodeVersioningTest { factory.create(aliceConfig).use { alice -> alice.connect().use { val rpc = it.proxy - assertThat(rpc.protocolVersion).isEqualTo(expectedPlatformVersion) - assertThat(rpc.nodeInfo().platformVersion).isEqualTo(expectedPlatformVersion) - assertThat(rpc.startFlow(NodeVersioningTest::GetPlatformVersionFlow).returnValue.getOrThrow()).isEqualTo(expectedPlatformVersion) + assertThat(rpc.protocolVersion).isEqualTo(PLATFORM_VERSION) + assertThat(rpc.nodeInfo().platformVersion).isEqualTo(PLATFORM_VERSION) + assertThat(rpc.startFlow(NodeVersioningTest::GetPlatformVersionFlow).returnValue.getOrThrow()).isEqualTo(PLATFORM_VERSION) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt index 79cb6844de..371ed2cd57 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt @@ -2,4 +2,3 @@ package net.corda.nodeapi.internal // TODO: Add to Corda node.conf to allow customisation const val NODE_INFO_DIRECTORY = "additional-node-infos" -const val PLATFORM_VERSION = 4 diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index 1871ea553f..a3ddb3cc93 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -11,7 +11,7 @@ import net.corda.core.serialization.serialize import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY -import net.corda.nodeapi.internal.PLATFORM_VERSION +import net.corda.core.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index d21de023cb..b9e5029fc9 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -26,7 +26,6 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME diff --git a/node/src/main/kotlin/net/corda/node/VersionInfo.kt b/node/src/main/kotlin/net/corda/node/VersionInfo.kt index b3d0ec0fbb..01492ee396 100644 --- a/node/src/main/kotlin/net/corda/node/VersionInfo.kt +++ b/node/src/main/kotlin/net/corda/node/VersionInfo.kt @@ -1,6 +1,6 @@ package net.corda.node -import net.corda.nodeapi.internal.PLATFORM_VERSION +import net.corda.core.internal.PLATFORM_VERSION /** * Encapsulates various pieces of version information of the node. 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 74f3b0f40f..1b089a7c63 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -28,7 +28,7 @@ import net.corda.node.utilities.registration.NodeRegistrationException import net.corda.node.utilities.registration.NodeRegistrationHelper import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore -import net.corda.nodeapi.internal.PLATFORM_VERSION +import net.corda.core.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index d622f834f8..77d97f5715 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -4,7 +4,6 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import net.corda.core.node.services.AttachmentStorage import net.corda.node.VersionInfo -import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index be91c42c1f..112cf3f19f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -28,10 +28,8 @@ import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService -import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity import net.corda.testing.internal.DEV_ROOT_CA diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 850cf8f8d5..92615f3fd0 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -28,7 +28,7 @@ import net.corda.node.services.Permissions import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NodeRegistrationHelper -import net.corda.nodeapi.internal.PLATFORM_VERSION +import net.corda.core.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook @@ -54,10 +54,8 @@ import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages import okhttp3.OkHttpClient import okhttp3.Request -import rx.Observable import rx.Subscription import rx.schedulers.Schedulers -import rx.subjects.AsyncSubject import java.lang.management.ManagementFactory import java.net.ConnectException import java.net.URL diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index cebb7f6d30..9d6ce49198 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -74,7 +74,7 @@ import java.time.Clock import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -val MOCK_VERSION_INFO = VersionInfo(4, "Mock release", "Mock revision", "Mock Vendor") +val MOCK_VERSION_INFO = VersionInfo(PLATFORM_VERSION, "Mock release", "Mock revision", "Mock Vendor") data class MockNodeArgs( val config: NodeConfiguration, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index b21df970db..66a4f2408d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -2,6 +2,7 @@ package net.corda.testing.node.internal import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.createDirectories @@ -31,12 +32,12 @@ import java.nio.file.Path import java.util.concurrent.Executors import kotlin.concurrent.thread -// TODO Some of the logic here duplicates what's in the driver - the reason why it's not straightforward to replace it by using DriverDSLImpl in `init()` and `stopAllNodes()` is because of the platform version passed to nodes (driver doesn't support this, and it's a property of the Corda JAR) +// TODO Some of the logic here duplicates what's in the driver - the reason why it's not straightforward to replace it by +// using DriverDSLImpl in `init()` and `stopAllNodes()` is because of the platform version passed to nodes (driver doesn't +// support this, and it's a property of the Corda JAR) abstract class NodeBasedTest(private val cordappPackages: List = emptyList()) { companion object { private val WHITESPACE = "\\s++".toRegex() - - private val logger = loggerFor() } @Rule @@ -85,7 +86,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi @JvmOverloads fun startNode(legalName: CordaX500Name, - platformVersion: Int = 4, + platformVersion: Int = PLATFORM_VERSION, rpcUsers: List = emptyList(), configOverrides: Map = emptyMap()): NodeWithInfo { val baseDirectory = baseDirectory(legalName).createDirectories() diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 9d382516fa..e3a1967e2e 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -2,7 +2,7 @@ package net.corda.bootstrapper import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.start -import net.corda.nodeapi.internal.PLATFORM_VERSION +import net.corda.core.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.network.NetworkBootstrapper import picocli.CommandLine.Option import java.nio.file.Path From d75fd7bd8a16757a6e92b3178ef64a684e2147bf Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 5 Oct 2018 11:01:27 +0100 Subject: [PATCH 19/83] CORDA-2030: Resolve build warnings created by adding kotlin-stdlib-jre8 to node. (#4033) --- testing/test-utils/build.gradle | 5 ++++- tools/network-bootstrapper/build.gradle | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/testing/test-utils/build.gradle b/testing/test-utils/build.gradle index 36c44a076b..715c795317 100644 --- a/testing/test-utils/build.gradle +++ b/testing/test-utils/build.gradle @@ -9,7 +9,10 @@ description 'Testing utilities for Corda' dependencies { compile project(':test-common') - compile project(':node') + compile(project(':node')) { + // The Node only needs this for binary compatibility with Cordapps written in Kotlin 1.1. + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jre8' + } compile project(':client:mock') compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" diff --git a/tools/network-bootstrapper/build.gradle b/tools/network-bootstrapper/build.gradle index 674b97f229..316be6e723 100644 --- a/tools/network-bootstrapper/build.gradle +++ b/tools/network-bootstrapper/build.gradle @@ -16,6 +16,9 @@ configurations { compile { exclude group: "log4j", module: "log4j" exclude group: "org.apache.logging.log4j" + + // The Node only needs this for binary compatibility with Cordapps written in Kotlin 1.1. + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jre8' } } From fa4c54a080faec87f42899ff51a2ceb64dac81fb Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 5 Oct 2018 12:01:16 +0100 Subject: [PATCH 20/83] [CORDA-2063] Ensure signatures and BC operations always use newSecureRandom (#4020) * special handling for Sphincs due a BC implementation issue * delete all sign operations from DJVM and stub out BC's default RNG * copy Crypto signing functions to deterministic.crypto.CryptoSignUtils as they are required for testing transaction signatures. --- .../deterministic/crypto/CryptoSignUtils.kt | 68 +++++++++++++++++++ .../crypto/TransactionSignatureTest.kt | 7 +- .../kotlin/net/corda/core/crypto/Crypto.kt | 27 +++++++- .../net/corda/core/crypto/CryptoUtils.kt | 5 ++ .../net/corda/core/internal/InternalUtils.kt | 4 ++ .../core/transactions/SignedTransaction.kt | 1 + 6 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt new file mode 100644 index 0000000000..9351d06bc3 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt @@ -0,0 +1,68 @@ +@file:JvmName("CryptoSignUtils") + +package net.corda.deterministic.crypto + +import net.corda.core.crypto.* +import net.corda.core.crypto.Crypto.findSignatureScheme +import net.corda.core.crypto.Crypto.isSupportedSignatureScheme +import net.corda.core.serialization.serialize +import java.security.* + +/** + * This is a slightly modified copy of signing utils from net.corda.core.crypto.Crypto, which are normally removed from DJVM. + * However, we need those for TransactionSignatureTest. + */ +object CryptoSignUtils { + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) + fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray { + return doSign(findSignatureScheme(schemeCodeName), privateKey, clearData) + } + + /** + * Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature]. + * @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto]. + * @param privateKey the signer's [PrivateKey]. + * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the digital signature (in [ByteArray]) on the input message. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) + fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } + require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" } + val signature = Signature.getInstance(signatureScheme.signatureName, signatureScheme.providerName) + signature.initSign(privateKey) + signature.update(clearData) + return signature.sign() + } + + /** + * Generic way to sign [SignableData] objects with a [PrivateKey]. + * [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as + * a timestamp or partial and blind signature indicators. + * @param keyPair the signer's [KeyPair]. + * @param signableData a [SignableData] object that adds extra information to a transaction. + * @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and + * the signature metadata. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) + fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature { + val sigKey: SignatureScheme = findSignatureScheme(keyPair.private) + val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public) + require(sigKey == sigMetaData) { + "Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}." + } + val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes) + return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata) + } +} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt index 4825d4787f..0e9191f308 100644 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt @@ -37,7 +37,7 @@ class TransactionSignatureTest { // Sign the meta object. val transactionSignature: TransactionSignature = CheatingSecurityProvider().use { - keyPair.sign(signableData) + CryptoSignUtils.doSign(keyPair, signableData) } // Check auto-verification. @@ -52,7 +52,7 @@ class TransactionSignatureTest { fun `Signature metadata full failure clearData has changed`() { val signableData = SignableData(testBytes.sha256(), SignatureMetadata(1, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) val transactionSignature = CheatingSecurityProvider().use { - keyPair.sign(signableData) + CryptoSignUtils.doSign(keyPair, signableData) } Crypto.doVerify((testBytes + testBytes).sha256(), transactionSignature) } @@ -137,7 +137,8 @@ class TransactionSignatureTest { private fun signOneTx(txId: SecureHash, keyPair: KeyPair): TransactionSignature { val signableData = SignableData(txId, SignatureMetadata(3, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) return CheatingSecurityProvider().use { - keyPair.sign(signableData) + CryptoSignUtils.doSign(keyPair, signableData) + } } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index a172f31747..29465d2808 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -2,6 +2,7 @@ package net.corda.core.crypto import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM +import net.corda.core.StubOutForDJVM import net.corda.core.crypto.internal.* import net.corda.core.serialization.serialize import net.i2p.crypto.eddsa.EdDSAEngine @@ -23,6 +24,7 @@ import org.bouncycastle.asn1.sec.SECObjectIdentifiers import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x9.X9ObjectIdentifiers +import org.bouncycastle.crypto.CryptoServicesRegistrar import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey @@ -381,6 +383,7 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ + @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(privateKey: PrivateKey, clearData: ByteArray): ByteArray = doSign(findSignatureScheme(privateKey), privateKey, clearData) @@ -395,6 +398,7 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ + @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray { @@ -411,6 +415,7 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ + @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { @@ -419,7 +424,16 @@ object Crypto { } require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" } val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) - signature.initSign(privateKey) + // Note that deterministic signature schemes, such as EdDSA, do not require extra randomness, but we have to + // ensure that non-deterministic algorithms (i.e., ECDSA) use non-blocking SecureRandom implementations (if possible). + // TODO consider updating this when the related BC issue for Sphincs is fixed. + if (signatureScheme != SPHINCS256_SHA256) { + signature.initSign(privateKey, newSecureRandom()) + } else { + // Special handling for Sphincs, due to a BC implementation issue. + // As Sphincs is deterministic, it does not require RNG input anyway. + signature.initSign(privateKey) + } signature.update(clearData) return signature.sign() } @@ -436,6 +450,7 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ + @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature { @@ -1017,5 +1032,15 @@ object Crypto { @JvmStatic fun registerProviders() { providerMap + // Adding our non-blocking newSecureRandom as default for any BouncyCastle operations + // (applies only when a SecureRandom is not specifically defined, i.e., if we call + // signature.initSign(privateKey) instead of signature.initSign(privateKey, newSecureRandom() + // for a BC algorithm, i.e., ECDSA). + setBouncyCastleRNG() + } + + @StubOutForDJVM + private fun setBouncyCastleRNG() { + CryptoServicesRegistrar.setSecureRandom(newSecureRandom()) } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 1cc29b5fdd..535f0013ef 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -24,6 +24,7 @@ import java.security.* * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ +@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign)) @@ -36,6 +37,7 @@ fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ +@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { return DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes) @@ -49,10 +51,12 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignat * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ +@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: ByteArray): DigitalSignature.WithKey = private.sign(bytesToSign, public) /** Helper function to sign the bytes of [bytesToSign] with a key pair. */ +@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: OpaqueBytes): DigitalSignature.WithKey = sign(bytesToSign.bytes) @@ -64,6 +68,7 @@ fun KeyPair.sign(bytesToSign: OpaqueBytes): DigitalSignature.WithKey = sign(byte * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ +@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSign(this, signableData) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 078cfcaa11..9814502433 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -457,11 +457,13 @@ $trustAnchor""", e, this, e.index) } } +@DeleteForDJVM inline fun T.signWithCert(signer: (SerializedBytes) -> DigitalSignatureWithCert): SignedDataWithCert { val serialised = serialize() return SignedDataWithCert(serialised, signer(serialised)) } +@DeleteForDJVM fun T.signWithCert(privateKey: PrivateKey, certificate: X509Certificate): SignedDataWithCert { return signWithCert { val signature = Crypto.doSign(privateKey, it.bytes) @@ -469,10 +471,12 @@ fun T.signWithCert(privateKey: PrivateKey, certificate: X509Certificat } } +@DeleteForDJVM inline fun SerializedBytes.sign(signer: (SerializedBytes) -> DigitalSignature.WithKey): SignedData { return SignedData(this, signer(this)) } +@DeleteForDJVM fun SerializedBytes.sign(keyPair: KeyPair): SignedData = SignedData(this, keyPair.sign(this.bytes)) fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index f035192a48..601efcd8cf 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -91,6 +91,7 @@ data class SignedTransaction(val txBits: SerializedBytes, return descriptions } + @DeleteForDJVM @VisibleForTesting fun withAdditionalSignature(keyPair: KeyPair, signatureMetadata: SignatureMetadata): SignedTransaction { val signableData = SignableData(tx.id, signatureMetadata) From 0621efe7c6a29c35c7d54aa715908ed0961f9873 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 5 Oct 2018 14:11:56 +0100 Subject: [PATCH 21/83] Do not remove entropyToKeyPair from DJVM (it is deterministic anyway and we might use it in tests) (#4036) --- .../net/corda/deterministic/crypto/TransactionSignatureTest.kt | 1 - core/src/main/kotlin/net/corda/core/crypto/Crypto.kt | 2 -- core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt | 1 - 3 files changed, 4 deletions(-) diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt index 0e9191f308..ac5e7971eb 100644 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt @@ -138,7 +138,6 @@ class TransactionSignatureTest { val signableData = SignableData(txId, SignatureMetadata(3, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) return CheatingSecurityProvider().use { CryptoSignUtils.doSign(keyPair, signableData) - } } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 29465d2808..e131cdc7df 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -809,7 +809,6 @@ object Crypto { * @return a new [KeyPair] from an entropy input. * @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input. */ - @DeleteForDJVM @JvmStatic fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair { return when (signatureScheme) { @@ -825,7 +824,6 @@ object Crypto { * @param entropy a [BigInteger] value. * @return a new [KeyPair] from an entropy input. */ - @DeleteForDJVM @JvmStatic fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 535f0013ef..a58665c069 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -150,7 +150,6 @@ fun generateKeyPair(): KeyPair = Crypto.generateKeyPair() * @param entropy a [BigInteger] value. * @return a deterministically generated [KeyPair] for the [Crypto.DEFAULT_SIGNATURE_SCHEME]. */ -@DeleteForDJVM fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEntropy(entropy) /** From 39434dcbecdd2cd656e40622530e84d02443b8e2 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 5 Oct 2018 18:05:10 +0100 Subject: [PATCH 22/83] Assorted set of clean ups (#4039) --- client/rpc/.attach_pid14784 | 0 .../core/flows/ContractUpgradeFlowTest.kt | 2 +- .../flows/WithReferencedStatesFlowTests.kt | 52 ++++++++--------- .../corda/core/flows/mixins/WithFinality.kt | 27 +++++---- .../docs/java/tutorial/twoparty/IOUFlow.java | 6 +- .../kotlin/tutorial/helloworld/IOUState.kt | 4 +- .../docs/kotlin/tutorial/twoparty/IOUFlow.kt | 8 ++- .../tutorial/twoparty/IOUFlowResponder.kt | 4 +- .../docs/{ => kotlin}/CustomVaultQueryTest.kt | 3 +- .../FxTransactionBuildTutorialTest.kt | 10 +--- .../WorkflowTransactionBuildTutorialTest.kt | 6 +- docs/source/hello-world-flow.rst | 3 +- .../net/corda/loadtest/tests/NotaryTest.kt | 57 ------------------- 13 files changed, 61 insertions(+), 121 deletions(-) delete mode 100644 client/rpc/.attach_pid14784 rename docs/source/example-code/src/test/kotlin/net/corda/docs/{ => kotlin}/CustomVaultQueryTest.kt (98%) rename docs/source/example-code/src/test/kotlin/net/corda/docs/{ => kotlin}/FxTransactionBuildTutorialTest.kt (92%) rename docs/source/example-code/src/test/kotlin/net/corda/docs/{ => kotlin}/WorkflowTransactionBuildTutorialTest.kt (95%) delete mode 100644 tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt diff --git a/client/rpc/.attach_pid14784 b/client/rpc/.attach_pid14784 deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index ead8da3ae4..8d30b83b62 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -52,7 +52,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { @Test fun `2 parties contract upgrade`() { // Create dummy contract. - val signedByA = aliceNode.signDummyContract(alice.ref(1),0, bob.ref(1)) + val signedByA = aliceNode.signDummyContract(alice.ref(1), 0, bob.ref(1)) val stx = bobNode.addSignatureTo(signedByA) aliceNode.finalise(stx, bob) diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt index 2263f85b39..a6d007b549 100644 --- a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -24,14 +24,14 @@ import org.junit.After import org.junit.Test import kotlin.test.assertEquals - // A dummy reference state contract. internal class RefState : Contract { companion object { - val CONTRACT_ID = "net.corda.core.flows.RefState" + const val CONTRACT_ID = "net.corda.core.flows.RefState" } override fun verify(tx: LedgerTransaction) = Unit + data class State(val owner: Party, val version: Int = 0, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState { override val participants: List get() = listOf(owner) fun update() = copy(version = version + 1) @@ -46,34 +46,32 @@ internal class CreateRefState : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryIdentities.first() - return subFlow(FinalityFlow( - transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { - addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID) - addCommand(RefState.Create(), listOf(ourIdentity.owningKey)) - }) - )) + val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { + addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID) + addCommand(RefState.Create(), listOf(ourIdentity.owningKey)) + }) + return subFlow(FinalityFlow(stx)) } } // A flow to update a specific reference state. -internal class UpdateRefState(val stateAndRef: StateAndRef) : FlowLogic() { +internal class UpdateRefState(private val stateAndRef: StateAndRef) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryIdentities.first() - return subFlow(FinalityFlow( - transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { - addInputState(stateAndRef) - addOutputState((stateAndRef.state.data as RefState.State).update(), RefState.CONTRACT_ID) - addCommand(RefState.Update(), listOf(ourIdentity.owningKey)) - }) - )) + val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { + addInputState(stateAndRef) + addOutputState((stateAndRef.state.data as RefState.State).update(), RefState.CONTRACT_ID) + addCommand(RefState.Update(), listOf(ourIdentity.owningKey)) + }) + return subFlow(FinalityFlow(stx)) } } // A set of flows to share a stateref with all other nodes in the mock network. internal object ShareRefState { @InitiatingFlow - class Initiator(val stateAndRef: StateAndRef) : FlowLogic() { + class Initiator(private val stateAndRef: StateAndRef) : FlowLogic() { @Suspendable override fun call() { val sessions = serviceHub.networkMapCache.allNodes.flatMap { it.legalIdentities }.map { initiateFlow(it) } @@ -85,7 +83,7 @@ internal object ShareRefState { } @InitiatedBy(ShareRefState.Initiator::class) - class Responder(val otherSession: FlowSession) : FlowLogic() { + class Responder(private val otherSession: FlowSession) : FlowLogic() { @Suspendable override fun call() { logger.info("Receiving dependencies.") @@ -99,7 +97,7 @@ internal object ShareRefState { } // A flow to use a reference state in another transaction. -internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic() { +internal class UseRefState(private val linearId: UniqueIdentifier) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryIdentities.first() @@ -108,14 +106,12 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic(query).states.single() - return subFlow(FinalityFlow( - transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { - @Suppress("DEPRECATION") // To be removed when feature is finalised. - addReferenceState(referenceState.referenced()) - addOutputState(DummyState(), DummyContract.PROGRAM_ID) - addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey)) - }) - )) + val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { + addReferenceState(referenceState.referenced()) + addOutputState(DummyState(), DummyContract.PROGRAM_ID) + addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey)) + }) + return subFlow(FinalityFlow(stx)) } } @@ -165,4 +161,4 @@ class WithReferencedStatesFlowTests { assertEquals(updatedRefState.ref, result.tx.references.single()) } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt b/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt index 4e2ac3c5c5..038e360644 100644 --- a/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt +++ b/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt @@ -1,37 +1,42 @@ package net.corda.core.flows.mixins import co.paralleluniverse.fibers.Suspendable +import com.natpryce.hamkrest.MatchResult import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.equalTo import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party +import net.corda.core.internal.FlowStateMachine import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.TestStartedNode interface WithFinality : WithMockNet { - //region Operations - fun TestStartedNode.finalise(stx: SignedTransaction, vararg additionalParties: Party) = - startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet())) + fun TestStartedNode.finalise(stx: SignedTransaction, vararg additionalParties: Party): FlowStateMachine { + return startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet())) + } - fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction) = - services.validatedTransactions.getTransaction(stx.id)!! + fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction { + return services.validatedTransactions.getTransaction(stx.id)!! + } - fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party) = - startFlow(::FinalityInvoker, stx, parties.toSet()) - .andRunNetwork() + fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party): FlowHandle { + return startFlow(::FinalityInvoker, stx, parties.toSet()).andRunNetwork() + } //endregion //region Matchers fun visibleTo(other: TestStartedNode) = object : Matcher { override val description = "has a transaction visible to ${other.info.singleIdentity()}" - override fun invoke(actual: SignedTransaction) = - equalTo(actual)(other.getValidatedTransaction(actual)) + override fun invoke(actual: SignedTransaction): MatchResult { + return equalTo(actual)(other.getValidatedTransaction(actual)) + } } //endregion @@ -41,4 +46,4 @@ interface WithFinality : WithMockNet { @Suspendable override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients)) } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java index 5ad1a12ce2..a7dd8c6c22 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java @@ -66,11 +66,11 @@ public class IOUFlow extends FlowLogic { final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Creating a session with the other party. - FlowSession otherpartySession = initiateFlow(otherParty); + FlowSession otherPartySession = initiateFlow(otherParty); // Obtaining the counterparty's signature. SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( - signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker())); + signedTx, ImmutableList.of(otherPartySession), CollectSignaturesFlow.tracker())); // Finalising the transaction. subFlow(new FinalityFlow(fullySignedTx)); @@ -78,4 +78,4 @@ public class IOUFlow extends FlowLogic { return null; // DOCEND 02 } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt index f79e3311cd..b66604a504 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt @@ -1,3 +1,5 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + package net.corda.docs.kotlin.tutorial.helloworld import net.corda.core.contracts.ContractState @@ -12,4 +14,4 @@ class IOUState(val value: Int, val borrower: Party) : ContractState { override val participants get() = listOf(lender, borrower) } -// DOCEND 01 \ No newline at end of file +// DOCEND 01 diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt index 22714acd33..bffd19472d 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt @@ -1,3 +1,5 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + package net.corda.docs.kotlin.tutorial.twoparty // DOCSTART 01 @@ -48,13 +50,13 @@ class IOUFlow(val iouValue: Int, val signedTx = serviceHub.signInitialTransaction(txBuilder) // Creating a session with the other party. - val otherpartySession = initiateFlow(otherParty) + val otherPartySession = initiateFlow(otherParty) // Obtaining the counterparty's signature. - val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker())) + val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherPartySession), CollectSignaturesFlow.tracker())) // Finalising the transaction. subFlow(FinalityFlow(fullySignedTx)) // DOCEND 02 } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt index 45e792abe0..c6ac0704b4 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package net.corda.docs.kotlin.tutorial.twoparty import co.paralleluniverse.fibers.Suspendable @@ -30,4 +32,4 @@ class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { subFlow(signTransactionFlow) } } -// DOCEND 01 \ No newline at end of file +// DOCEND 01 diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/CustomVaultQueryTest.kt similarity index 98% rename from docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt rename to docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/CustomVaultQueryTest.kt index e5a5ec8786..3738e977a3 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/CustomVaultQueryTest.kt @@ -1,4 +1,4 @@ -package net.corda.docs +package net.corda.docs.kotlin import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState @@ -8,7 +8,6 @@ import net.corda.core.node.services.vault.* import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.docs.java.tutorial.helloworld.IOUFlow -import net.corda.docs.kotlin.TopupIssuerFlow import net.corda.finance.* import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorialTest.kt similarity index 92% rename from docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt rename to docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorialTest.kt index 430fc025c8..1a0808fe91 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorialTest.kt @@ -1,18 +1,12 @@ -package net.corda.docs +package net.corda.docs.kotlin import net.corda.core.identity.Party import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.docs.kotlin.ForeignExchangeFlow -import net.corda.docs.kotlin.ForeignExchangeRemoteFlow -import net.corda.finance.DOLLARS -import net.corda.finance.GBP -import net.corda.finance.POUNDS -import net.corda.finance.USD +import net.corda.finance.* import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow -import net.corda.finance.issuedBy import net.corda.testing.core.singleIdentity import net.corda.testing.node.MockNetwork import net.corda.testing.node.StartedMockNode diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorialTest.kt similarity index 95% rename from docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt rename to docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorialTest.kt index ccc0793663..d19fa0c33b 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorialTest.kt @@ -1,4 +1,4 @@ -package net.corda.docs +package net.corda.docs.kotlin import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef @@ -9,10 +9,6 @@ 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.docs.kotlin.SubmitCompletionFlow -import net.corda.docs.kotlin.SubmitTradeApprovalFlow -import net.corda.docs.kotlin.TradeApprovalContract -import net.corda.docs.kotlin.WorkflowState import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.node.MockNetwork diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index b50ff584ec..1454b96498 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -73,7 +73,8 @@ annotation out will lead to some very weird error messages! There are also a few more annotations, on the ``FlowLogic`` subclass itself: - * ``@InitiatingFlow`` means that this flow can be started directly by the node + * ``@InitiatingFlow`` means that this flow is part of a flow pair and that it triggers the other side to run the + the counterpart flow. * ``@StartableByRPC`` allows the node owner to start this flow via an RPC call Let's walk through the steps of ``FlowLogic.call`` itself. This is where we actually describe the procedure for diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt deleted file mode 100644 index a8cff6abb9..0000000000 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package net.corda.loadtest.tests - -import net.corda.client.mock.Generator -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowException -import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.concurrent.thenMatch -import net.corda.core.messaging.startFlow -import net.corda.core.transactions.SignedTransaction -import net.corda.loadtest.LoadTest -import net.corda.loadtest.NodeConnection -import net.corda.testing.contracts.DummyContract -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.TestIdentity -import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestIdentityService -import org.slf4j.LoggerFactory - -private val log = LoggerFactory.getLogger("NotaryTest") -private val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) -private val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1) -private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) -private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) -private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) - -data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTransaction, val node: NodeConnection) - -val dummyNotarisationTest = LoadTest( - "Notarising dummy transactions", - generate = { _, _ -> - val issuerServices = MockServices(emptyList(), megaCorp.name, makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity), dummyCashIssuer.keyPair) - val generateTx = Generator.pickOne(simpleNodes).flatMap { node -> - Generator.int().map { - val issueBuilder = DummyContract.generateInitial(it, notary.info.legalIdentities[0], DUMMY_CASH_ISSUER) // TODO notary choice - val issueTx = issuerServices.signInitialTransaction(issueBuilder) - val asset = issueTx.tx.outRef(0) - val moveBuilder = DummyContract.move(asset, dummyCashIssuer.party) - val moveTx = issuerServices.signInitialTransaction(moveBuilder) - NotariseCommand(issueTx, moveTx, node) - } - } - Generator.replicate(10, generateTx) - }, - interpret = { _, _ -> }, - execute = { (issueTx, moveTx, node) -> - try { - val proxy = node.proxy - val issueFlow = proxy.startFlow(::FinalityFlow, issueTx) - issueFlow.returnValue.thenMatch({ - proxy.startFlow(::FinalityFlow, moveTx) - }, {}) - } catch (e: FlowException) { - log.error("Failure", e) - } - }, - gatherRemoteState = {} -) From c88d3d8c1bac16931d34a24384953c1401bbccb5 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 8 Oct 2018 12:49:05 +0100 Subject: [PATCH 23/83] CORDA-2030: Resolve build warnings about kotlin-stdlib-jre8 in unit tests too. (#4043) --- confidential-identities/build.gradle | 1 - core/build.gradle | 1 - 2 files changed, 2 deletions(-) diff --git a/confidential-identities/build.gradle b/confidential-identities/build.gradle index 6b5616d000..38a2f43807 100644 --- a/confidential-identities/build.gradle +++ b/confidential-identities/build.gradle @@ -20,7 +20,6 @@ dependencies { testCompile "com.google.guava:guava-testlib:$guava_version" // Bring in the MockNode infrastructure for writing protocol unit tests. - testCompile project(":node") testCompile project(":node-driver") // AssertJ: for fluent assertions for testing diff --git a/core/build.gradle b/core/build.gradle index 671b7b616d..05e9716dc9 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -61,7 +61,6 @@ dependencies { testCompile "com.google.guava:guava-testlib:$guava_version" // Bring in the MockNode infrastructure for writing protocol unit tests. - testCompile project(":node") testCompile project(":node-driver") compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" From d9ea19855f52fd2972cf1328e50e336f55665567 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Mon, 8 Oct 2018 13:39:28 +0100 Subject: [PATCH 24/83] CORDA-2006: Simplify checkpoint serialization (#4042) * CORDA-2006: Simplify checkpoint serialization * Supply rule to KryoTest --- .../amqp/AMQPClientSerializationScheme.kt | 3 +- .../CheckpointSerializationFactory.kt | 74 --------------- .../verifier/LocalSerializationRule.kt | 8 +- .../internal/CheckpointSerializationAPI.kt | 95 ++++--------------- .../internal/SerializationEnvironment.kt | 65 +++++++++---- .../flows/SerializationApiInJavaTest.java | 6 +- .../internal/network/NetworkBootstrapper.kt | 4 +- .../corda/node/internal/CheckpointVerifier.kt | 3 +- .../kotlin/net/corda/node/internal/Node.kt | 15 +-- ...nScheme.kt => KryoCheckpointSerializer.kt} | 5 +- .../SingleThreadedStateMachineManager.kt | 2 +- .../transactions/RaftTransactionCommitLog.kt | 6 +- .../node/serialization/kryo/KryoTests.kt | 74 +++++++-------- .../internal/SerializeAsTokenContextImpl.kt | 6 +- .../LambdaCheckpointSerializationTest.java | 17 ++-- .../ContractAttachmentSerializerTest.kt | 31 +++--- .../internal/SerializationTokenTest.kt | 28 +++--- .../testing/core/SerializationTestHelpers.kt | 10 +- .../CheckpointSerializationTestHelpers.kt | 11 +-- .../InternalSerializationTestHelpers.kt | 15 ++- .../net/corda/blobinspector/BlobInspector.kt | 6 +- .../kotlin/net/corda/demobench/DemoBench.kt | 4 +- .../serialization/SerializationHelper.kt | 9 +- 23 files changed, 186 insertions(+), 311 deletions(-) delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt rename node/src/main/kotlin/net/corda/node/serialization/kryo/{KryoSerializationScheme.kt => KryoCheckpointSerializer.kt} (97%) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt index df12645479..a0e2bfc307 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt @@ -6,7 +6,6 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext.* import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.internal.SerializationEnvironment -import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.serialization.internal.* import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme @@ -35,7 +34,7 @@ class AMQPClientSerializationScheme( } fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment { - return SerializationEnvironmentImpl( + return SerializationEnvironment.with( SerializationFactoryImpl().apply { registerScheme(AMQPClientSerializationScheme(emptyList())) }, diff --git a/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt deleted file mode 100644 index dbb6fb54c0..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt +++ /dev/null @@ -1,74 +0,0 @@ -package net.corda.core.serialization.internal - -import net.corda.core.KeepForDJVM -import net.corda.core.serialization.SerializedBytes -import net.corda.core.utilities.ByteSequence -import java.io.NotSerializableException - -/** - * A deterministic version of [CheckpointSerializationFactory] that does not use thread-locals to manage serialization - * context. - */ -@KeepForDJVM -class CheckpointSerializationFactory( - private val scheme: CheckpointSerializationScheme -) { - - val defaultContext: CheckpointSerializationContext get() = _currentContext ?: effectiveSerializationEnv.checkpointContext - - private val creator: List = Exception().stackTrace.asList() - - /** - * 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. - */ - @Throws(NotSerializableException::class) - fun deserialize(byteSequence: ByteSequence, clazz: Class, context: CheckpointSerializationContext): T { - return withCurrentContext(context) { scheme.deserialize(byteSequence, clazz, context) } - } - - /** - * Serialize an object to bytes using the preferred serialization format version from the context. - * - * @param obj The object to be serialized. - * @param context A context that configures various parameters to serialization, including the serialization format version. - */ - fun serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes { - return withCurrentContext(context) { scheme.serialize(obj, context) } - } - - override fun toString(): String { - return "${this.javaClass.name} scheme=$scheme ${creator.joinToString("\n")}" - } - - override fun equals(other: Any?): Boolean { - return other is CheckpointSerializationFactory && other.scheme == this.scheme - } - - override fun hashCode(): Int = scheme.hashCode() - - private var _currentContext: CheckpointSerializationContext? = null - - /** - * Change the current context inside the block to that supplied. - */ - fun withCurrentContext(context: CheckpointSerializationContext?, block: () -> T): T { - val priorContext = _currentContext - if (context != null) _currentContext = context - try { - return block() - } finally { - if (context != null) _currentContext = priorContext - } - } - - companion object { - /** - * A default factory for serialization/deserialization. - */ - val defaultFactory: CheckpointSerializationFactory get() = effectiveSerializationEnv.checkpointSerializationFactory - } -} \ No newline at end of file diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt index 15848a4be4..e388c7c6d9 100644 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt @@ -4,7 +4,7 @@ import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext.UseCase.P2P import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.serialization.internal.* import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme @@ -58,13 +58,11 @@ class LocalSerializationRule(private val label: String) : TestRule { _contextSerializationEnv.set(null) } - private fun createTestSerializationEnv(): SerializationEnvironmentImpl { + private fun createTestSerializationEnv(): SerializationEnvironment { val factory = SerializationFactoryImpl(mutableMapOf()).apply { registerScheme(AMQPSerializationScheme(emptySet(), AccessOrderLinkedHashMap(128))) } - return object : SerializationEnvironmentImpl(factory, AMQP_P2P_CONTEXT) { - override fun toString() = "testSerializationEnv($label)" - } + return SerializationEnvironment.with(factory, AMQP_P2P_CONTEXT) } private class AMQPSerializationScheme( diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt index 448d1ab25f..6769b73b03 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt @@ -13,75 +13,12 @@ import java.io.NotSerializableException object CheckpointSerializationDefaults { @DeleteForDJVM val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext - val CHECKPOINT_SERIALIZATION_FACTORY get() = effectiveSerializationEnv.checkpointSerializationFactory -} - -/** - * A class for serializing and deserializing objects at checkpoints, using Kryo serialization. - */ -@KeepForDJVM -class CheckpointSerializationFactory( - private val scheme: CheckpointSerializationScheme -) { - - val defaultContext: CheckpointSerializationContext get() = _currentContext.get() ?: effectiveSerializationEnv.checkpointContext - - private val creator: List = Exception().stackTrace.asList() - - /** - * 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. - */ - fun deserialize(byteSequence: ByteSequence, clazz: Class, context: CheckpointSerializationContext): T { - return withCurrentContext(context) { scheme.deserialize(byteSequence, clazz, context) } - } - - /** - * Serialize an object to bytes using the preferred serialization format version from the context. - * - * @param obj The object to be serialized. - * @param context A context that configures various parameters to serialization, including the serialization format version. - */ - fun serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes { - return withCurrentContext(context) { scheme.serialize(obj, context) } - } - - override fun toString(): String { - return "${this.javaClass.name} scheme=$scheme ${creator.joinToString("\n")}" - } - - override fun equals(other: Any?): Boolean { - return other is CheckpointSerializationFactory && other.scheme == this.scheme - } - - override fun hashCode(): Int = scheme.hashCode() - - private val _currentContext = ThreadLocal() - - /** - * Change the current context inside the block to that supplied. - */ - fun withCurrentContext(context: CheckpointSerializationContext?, block: () -> T): T { - val priorContext = _currentContext.get() - if (context != null) _currentContext.set(context) - try { - return block() - } finally { - if (context != null) _currentContext.set(priorContext) - } - } - - companion object { - val defaultFactory: CheckpointSerializationFactory get() = effectiveSerializationEnv.checkpointSerializationFactory - } + val CHECKPOINT_SERIALIZER get() = effectiveSerializationEnv.checkpointSerializer } @KeepForDJVM @DoNotImplement -interface CheckpointSerializationScheme { +interface CheckpointSerializer { @Throws(NotSerializableException::class) fun deserialize(byteSequence: ByteSequence, clazz: Class, context: CheckpointSerializationContext): T @@ -167,32 +104,36 @@ interface CheckpointSerializationContext { /* * Convenience extension method for deserializing a ByteSequence, utilising the default factory. */ -inline fun ByteSequence.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, - context: CheckpointSerializationContext): T { - return serializationFactory.deserialize(this, T::class.java, context) +@JvmOverloads +inline fun ByteSequence.checkpointDeserialize( + context: CheckpointSerializationContext = effectiveSerializationEnv.checkpointContext): T { + return effectiveSerializationEnv.checkpointSerializer.deserialize(this, T::class.java, context) } /** * Convenience extension method for deserializing SerializedBytes with type matching, utilising the default factory. */ -inline fun SerializedBytes.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, - context: CheckpointSerializationContext): T { - return serializationFactory.deserialize(this, T::class.java, context) +@JvmOverloads +inline fun SerializedBytes.checkpointDeserialize( + context: CheckpointSerializationContext = effectiveSerializationEnv.checkpointContext): T { + return effectiveSerializationEnv.checkpointSerializer.deserialize(this, T::class.java, context) } /** * Convenience extension method for deserializing a ByteArray, utilising the default factory. */ -inline fun ByteArray.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, - context: CheckpointSerializationContext): T { +@JvmOverloads +inline fun ByteArray.checkpointDeserialize( + context: CheckpointSerializationContext = effectiveSerializationEnv.checkpointContext): T { require(isNotEmpty()) { "Empty bytes" } - return this.sequence().checkpointDeserialize(serializationFactory, context) + return this.sequence().checkpointDeserialize(context) } /** * Convenience extension method for serializing an object of type T, utilising the default factory. */ -fun T.checkpointSerialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, - context: CheckpointSerializationContext): SerializedBytes { - return serializationFactory.serialize(this, context) +@JvmOverloads +fun T.checkpointSerialize( + context: CheckpointSerializationContext = effectiveSerializationEnv.checkpointContext): SerializedBytes { + return effectiveSerializationEnv.checkpointSerializer.serialize(this, context) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt index 441cd52be4..b213b6322d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt @@ -11,38 +11,63 @@ import net.corda.core.serialization.SerializationFactory @KeepForDJVM interface SerializationEnvironment { + + companion object { + fun with( + serializationFactory: SerializationFactory, + p2pContext: SerializationContext, + rpcServerContext: SerializationContext? = null, + rpcClientContext: SerializationContext? = null, + storageContext: SerializationContext? = null, + + checkpointContext: CheckpointSerializationContext? = null, + checkpointSerializer: CheckpointSerializer? = null + ): SerializationEnvironment = + SerializationEnvironmentImpl( + serializationFactory = serializationFactory, + p2pContext = p2pContext, + optionalRpcServerContext = rpcServerContext, + optionalRpcClientContext = rpcClientContext, + optionalStorageContext = storageContext, + optionalCheckpointContext = checkpointContext, + optionalCheckpointSerializer = checkpointSerializer + ) + } + val serializationFactory: SerializationFactory - val checkpointSerializationFactory: CheckpointSerializationFactory val p2pContext: SerializationContext val rpcServerContext: SerializationContext val rpcClientContext: SerializationContext val storageContext: SerializationContext + + val checkpointSerializer: CheckpointSerializer val checkpointContext: CheckpointSerializationContext } @KeepForDJVM -open class SerializationEnvironmentImpl( +private class SerializationEnvironmentImpl( override val serializationFactory: SerializationFactory, override val p2pContext: SerializationContext, - rpcServerContext: SerializationContext? = null, - rpcClientContext: SerializationContext? = null, - storageContext: SerializationContext? = null, - checkpointContext: CheckpointSerializationContext? = null, - checkpointSerializationFactory: CheckpointSerializationFactory? = null) : SerializationEnvironment { - // Those that are passed in as null are never inited: - override lateinit var rpcServerContext: SerializationContext - override lateinit var rpcClientContext: SerializationContext - override lateinit var storageContext: SerializationContext - override lateinit var checkpointContext: CheckpointSerializationContext - override lateinit var checkpointSerializationFactory: CheckpointSerializationFactory + private val optionalRpcServerContext: SerializationContext? = null, + private val optionalRpcClientContext: SerializationContext? = null, + private val optionalStorageContext: SerializationContext? = null, + private val optionalCheckpointContext: CheckpointSerializationContext? = null, + private val optionalCheckpointSerializer: CheckpointSerializer? = null) : SerializationEnvironment { - init { - rpcServerContext?.let { this.rpcServerContext = it } - rpcClientContext?.let { this.rpcClientContext = it } - storageContext?.let { this.storageContext = it } - checkpointContext?.let { this.checkpointContext = it } - checkpointSerializationFactory?.let { this.checkpointSerializationFactory = it } - } + override val rpcServerContext: SerializationContext get() = optionalRpcServerContext ?: + throw UnsupportedOperationException("RPC server serialization not supported in this environment") + + override val rpcClientContext: SerializationContext get() = optionalRpcClientContext ?: + throw UnsupportedOperationException("RPC client serialization not supported in this environment") + + override val storageContext: SerializationContext get() = optionalStorageContext ?: + throw UnsupportedOperationException("Storage serialization not supported in this environment") + + override val checkpointContext: CheckpointSerializationContext get() = optionalCheckpointContext ?: + throw UnsupportedOperationException("Checkpoint serialization not supported in this environment") + + override val checkpointSerializer: CheckpointSerializer get() = optionalCheckpointSerializer ?: + throw UnsupportedOperationException("Checkpoint serialization not supported in this environment") } private val _nodeSerializationEnv = SimpleToggleField("nodeSerializationEnv", true) diff --git a/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java b/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java index 55e66c1766..9f48a7ba0a 100644 --- a/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java @@ -1,7 +1,5 @@ package net.corda.core.flows; -import net.corda.core.serialization.internal.CheckpointSerializationDefaults; -import net.corda.core.serialization.internal.CheckpointSerializationFactory; import net.corda.core.serialization.SerializationDefaults; import net.corda.core.serialization.SerializationFactory; import net.corda.testing.core.SerializationEnvironmentRule; @@ -32,12 +30,10 @@ public class SerializationApiInJavaTest { SerializationDefaults defaults = SerializationDefaults.INSTANCE; SerializationFactory factory = defaults.getSERIALIZATION_FACTORY(); - CheckpointSerializationDefaults checkpointDefaults = CheckpointSerializationDefaults.INSTANCE; - CheckpointSerializationFactory checkpointSerializationFactory = checkpointDefaults.getCHECKPOINT_SERIALIZATION_FACTORY(); serialize("hello", factory, defaults.getP2P_CONTEXT()); serialize("hello", factory, defaults.getRPC_SERVER_CONTEXT()); serialize("hello", factory, defaults.getRPC_CLIENT_CONTEXT()); serialize("hello", factory, defaults.getSTORAGE_CONTEXT()); - checkpointSerialize("hello", checkpointSerializationFactory, checkpointDefaults.getCHECKPOINT_CONTEXT()); + checkpointSerialize("hello"); } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 49d0704c84..1747cc2333 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -14,7 +14,7 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize -import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.utilities.days import net.corda.core.utilities.getOrThrow @@ -393,7 +393,7 @@ internal constructor(private val initSerEnv: Boolean, // We need to to set serialization env, because generation of parameters is run from Cordform. private fun initialiseSerialization() { - _contextSerializationEnv.set(SerializationEnvironmentImpl( + _contextSerializationEnv.set(SerializationEnvironment.with( SerializationFactoryImpl().apply { registerScheme(AMQPParametersSerializationScheme) }, diff --git a/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt b/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt index 53f22b3147..48b8c71971 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt @@ -4,7 +4,6 @@ import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.node.ServiceHub -import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.internal.CheckpointSerializationDefaults import net.corda.core.serialization.internal.checkpointDeserialize import net.corda.node.services.api.CheckpointStorage @@ -21,7 +20,7 @@ object CheckpointVerifier { */ fun verifyCheckpointsCompatible(checkpointStorage: CheckpointStorage, currentCordapps: List, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List) { val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext( - CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) + CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) ) checkpointStorage.getAllCheckpoints().forEach { (_, serializedCheckpoint) -> diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index f1c9140938..3db80bdf25 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -21,8 +21,7 @@ import net.corda.core.messaging.RPCOps import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub -import net.corda.core.serialization.internal.CheckpointSerializationFactory -import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger @@ -38,7 +37,7 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.node.serialization.kryo.KryoSerializationScheme +import net.corda.node.serialization.kryo.KryoCheckpointSerializer import net.corda.node.services.Permissions import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal @@ -470,17 +469,19 @@ open class Node(configuration: NodeConfiguration, private fun initialiseSerialization() { if (!initialiseSerialization) return val classloader = cordappLoader.appClassLoader - nodeSerializationEnv = SerializationEnvironmentImpl( + nodeSerializationEnv = SerializationEnvironment.with( SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps)) registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps)) }, - checkpointSerializationFactory = CheckpointSerializationFactory(KryoSerializationScheme), p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), rpcServerContext = AMQP_RPC_SERVER_CONTEXT.withClassLoader(classloader), + rpcClientContext = if (configuration.shouldInitCrashShell()) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null, //even Shell embeded in the node connects via RPC to the node storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader), - checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader), - rpcClientContext = if (configuration.shouldInitCrashShell()) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null) //even Shell embeded in the node connects via RPC to the node + + checkpointSerializer = KryoCheckpointSerializer, + checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader) + ) } /** Starts a blocking event loop for message dispatch. */ diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoCheckpointSerializer.kt similarity index 97% rename from node/src/main/kotlin/net/corda/node/serialization/kryo/KryoSerializationScheme.kt rename to node/src/main/kotlin/net/corda/node/serialization/kryo/KryoCheckpointSerializer.kt index 22edf8258e..cd74dafb4c 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoSerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoCheckpointSerializer.kt @@ -12,10 +12,9 @@ import com.esotericsoftware.kryo.serializers.ClosureSerializer import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* import net.corda.core.serialization.internal.CheckpointSerializationContext -import net.corda.core.serialization.internal.CheckpointSerializationScheme +import net.corda.core.serialization.internal.CheckpointSerializer import net.corda.core.utilities.ByteSequence import net.corda.serialization.internal.* -import java.security.PublicKey import java.util.concurrent.ConcurrentHashMap val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0)) @@ -31,7 +30,7 @@ private object AutoCloseableSerialisationDetector : Serializer() override fun read(kryo: Kryo, input: Input, type: Class) = throw IllegalStateException("Should not reach here!") } -object KryoSerializationScheme : CheckpointSerializationScheme { +object KryoCheckpointSerializer : CheckpointSerializer { private val kryoPoolsForContexts = ConcurrentHashMap, KryoPool>() private fun getPool(context: CheckpointSerializationContext): KryoPool { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index a47799e0aa..60fb528bcb 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -127,7 +127,7 @@ class SingleThreadedStateMachineManager( override fun start(tokenizableServices: List) { checkQuasarJavaAgentPresence() val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext( - CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) + CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) ) this.checkpointSerializationContext = checkpointSerializationContext this.actionExecutor = makeActionExecutor(checkpointSerializationContext) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt index 72fa52bdc1..c35ae146ab 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt @@ -22,7 +22,7 @@ import net.corda.core.internal.notary.isConsumedByTheSameTx import net.corda.core.internal.notary.validateTimeWindow import net.corda.core.serialization.* import net.corda.core.serialization.internal.CheckpointSerializationDefaults -import net.corda.core.serialization.internal.CheckpointSerializationFactory + import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.contextLogger @@ -201,7 +201,7 @@ class RaftTransactionCommitLog( class CordaKryoSerializer : TypeSerializer { private val context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withEncoding(CordaSerializationEncoding.SNAPPY) - private val factory = CheckpointSerializationFactory.defaultFactory + private val checkpointSerializer = CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER override fun write(obj: T, buffer: BufferOutput<*>, serializer: Serializer) { val serialized = obj.checkpointSerialize(context = context) @@ -213,7 +213,7 @@ class RaftTransactionCommitLog( val size = buffer.readInt() val serialized = ByteArray(size) buffer.read(serialized) - return factory.deserialize(ByteSequence.of(serialized), type, context) + return checkpointSerializer.deserialize(ByteSequence.of(serialized), type, context) } } } diff --git a/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt b/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt index 5598f38f67..abfec25bc2 100644 --- a/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt +++ b/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt @@ -13,7 +13,6 @@ import net.corda.core.crypto.* import net.corda.core.internal.FetchDataFlow import net.corda.core.serialization.* import net.corda.core.serialization.internal.CheckpointSerializationContext -import net.corda.core.serialization.internal.CheckpointSerializationFactory import net.corda.core.serialization.internal.checkpointDeserialize import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.ByteSequence @@ -23,11 +22,13 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.serialization.internal.* import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.TestIdentity +import net.corda.testing.core.internal.CheckpointSerializationEnvironmentRule import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.* import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -48,12 +49,12 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun compression() = arrayOf(null) + CordaSerializationEncoding.values() } - private lateinit var factory: CheckpointSerializationFactory + @get:Rule + val serializationRule = CheckpointSerializationEnvironmentRule() private lateinit var context: CheckpointSerializationContext @Before fun setup() { - factory = CheckpointSerializationFactory(KryoSerializationScheme) context = CheckpointSerializationContextImpl( javaClass.classLoader, AllWhitelist, @@ -69,15 +70,15 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun `simple data class`() { val birthday = Instant.parse("1984-04-17T00:30:00.00Z") val mike = Person("mike", birthday) - val bits = mike.checkpointSerialize(factory, context) - assertThat(bits.checkpointDeserialize(factory, context)).isEqualTo(Person("mike", birthday)) + val bits = mike.checkpointSerialize(context) + assertThat(bits.checkpointDeserialize(context)).isEqualTo(Person("mike", birthday)) } @Test fun `null values`() { val bob = Person("bob", null) - val bits = bob.checkpointSerialize(factory, context) - assertThat(bits.checkpointDeserialize(factory, context)).isEqualTo(Person("bob", null)) + val bits = bob.checkpointSerialize(context) + assertThat(bits.checkpointDeserialize(context)).isEqualTo(Person("bob", null)) } @Test @@ -85,10 +86,10 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { val noReferencesContext = context.withoutReferences() val obj : ByteSequence = Ints.toByteArray(0x01234567).sequence() val originalList : ArrayList = ArrayList().apply { this += obj } - val deserialisedList = originalList.checkpointSerialize(factory, noReferencesContext).checkpointDeserialize(factory, noReferencesContext) + val deserialisedList = originalList.checkpointSerialize(noReferencesContext).checkpointDeserialize(noReferencesContext) originalList += obj deserialisedList += obj - assertThat(deserialisedList.checkpointSerialize(factory, noReferencesContext)).isEqualTo(originalList.checkpointSerialize(factory, noReferencesContext)) + assertThat(deserialisedList.checkpointSerialize(noReferencesContext)).isEqualTo(originalList.checkpointSerialize(noReferencesContext)) } @Test @@ -105,14 +106,14 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { this += instant this += instant } - assertThat(listWithSameInstances.checkpointSerialize(factory, noReferencesContext)).isEqualTo(listWithCopies.checkpointSerialize(factory, noReferencesContext)) + assertThat(listWithSameInstances.checkpointSerialize(noReferencesContext)).isEqualTo(listWithCopies.checkpointSerialize(noReferencesContext)) } @Test fun `cyclic object graph`() { val cyclic = Cyclic(3) - val bits = cyclic.checkpointSerialize(factory, context) - assertThat(bits.checkpointDeserialize(factory, context)).isEqualTo(cyclic) + val bits = cyclic.checkpointSerialize(context) + assertThat(bits.checkpointDeserialize(context)).isEqualTo(cyclic) } @Test @@ -124,7 +125,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { signature.verify(bitsToSign) assertThatThrownBy { signature.verify(wrongBits) } - val deserialisedKeyPair = keyPair.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val deserialisedKeyPair = keyPair.checkpointSerialize(context).checkpointDeserialize(context) val deserialisedSignature = deserialisedKeyPair.sign(bitsToSign) deserialisedSignature.verify(bitsToSign) assertThatThrownBy { deserialisedSignature.verify(wrongBits) } @@ -132,28 +133,28 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `write and read Kotlin object singleton`() { - val serialised = TestSingleton.checkpointSerialize(factory, context) - val deserialised = serialised.checkpointDeserialize(factory, context) + val serialised = TestSingleton.checkpointSerialize(context) + val deserialised = serialised.checkpointDeserialize(context) assertThat(deserialised).isSameAs(TestSingleton) } @Test fun `check Kotlin EmptyList can be serialised`() { - val deserialisedList: List = emptyList().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val deserialisedList: List = emptyList().checkpointSerialize(context).checkpointDeserialize(context) assertEquals(0, deserialisedList.size) assertEquals(Collections.emptyList().javaClass, deserialisedList.javaClass) } @Test fun `check Kotlin EmptySet can be serialised`() { - val deserialisedSet: Set = emptySet().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val deserialisedSet: Set = emptySet().checkpointSerialize(context).checkpointDeserialize(context) assertEquals(0, deserialisedSet.size) assertEquals(Collections.emptySet().javaClass, deserialisedSet.javaClass) } @Test fun `check Kotlin EmptyMap can be serialised`() { - val deserialisedMap: Map = emptyMap().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val deserialisedMap: Map = emptyMap().checkpointSerialize(context).checkpointDeserialize(context) assertEquals(0, deserialisedMap.size) assertEquals(Collections.emptyMap().javaClass, deserialisedMap.javaClass) } @@ -161,7 +162,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `InputStream serialisation`() { val rubbish = ByteArray(12345) { (it * it * 0.12345).toByte() } - val readRubbishStream: InputStream = rubbish.inputStream().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val readRubbishStream: InputStream = rubbish.inputStream().checkpointSerialize(context).checkpointDeserialize(context) for (i in 0..12344) { assertEquals(rubbish[i], readRubbishStream.read().toByte()) } @@ -171,7 +172,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `InputStream serialisation does not write trailing garbage`() { val byteArrays = listOf("123", "456").map { it.toByteArray() } - val streams = byteArrays.map { it.inputStream() }.checkpointSerialize(factory, context).checkpointDeserialize(factory, context).iterator() + val streams = byteArrays.map { it.inputStream() }.checkpointSerialize(context).checkpointDeserialize(context).iterator() byteArrays.forEach { assertArrayEquals(it, streams.next().readBytes()) } assertFalse(streams.hasNext()) } @@ -182,8 +183,8 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { val testBytes = testString.toByteArray() val meta = SignableData(testBytes.sha256(), SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID)) - val serializedMetaData = meta.checkpointSerialize(factory, context).bytes - val meta2 = serializedMetaData.checkpointDeserialize(factory, context) + val serializedMetaData = meta.checkpointSerialize(context).bytes + val meta2 = serializedMetaData.checkpointDeserialize(context) assertEquals(meta2, meta) } @@ -191,7 +192,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun `serialize - deserialize Logger`() { val storageContext: CheckpointSerializationContext = context val logger = LoggerFactory.getLogger("aName") - val logger2 = logger.checkpointSerialize(factory, storageContext).checkpointDeserialize(factory, storageContext) + val logger2 = logger.checkpointSerialize(storageContext).checkpointDeserialize(storageContext) assertEquals(logger.name, logger2.name) assertTrue(logger === logger2) } @@ -203,7 +204,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { SecureHash.sha256(rubbish), rubbish.size, rubbish.inputStream() - ).checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + ).checkpointSerialize(context).checkpointDeserialize(context) for (i in 0..12344) { assertEquals(rubbish[i], readRubbishStream.read().toByte()) } @@ -230,8 +231,8 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 )) - val serializedBytes = expected.checkpointSerialize(factory, context) - val actual = serializedBytes.checkpointDeserialize(factory, context) + val serializedBytes = expected.checkpointSerialize(context) + val actual = serializedBytes.checkpointDeserialize(context) assertEquals(expected, actual) } @@ -278,14 +279,13 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { } } Tmp() - val factory = CheckpointSerializationFactory(KryoSerializationScheme) val context = CheckpointSerializationContextImpl( javaClass.classLoader, AllWhitelist, emptyMap(), true, null) - pt.checkpointSerialize(factory, context) + pt.checkpointSerialize(context) } @Test @@ -293,7 +293,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { val exception = IllegalArgumentException("fooBar") val toBeSuppressedOnSenderSide = IllegalStateException("bazz1") exception.addSuppressed(toBeSuppressedOnSenderSide) - val exception2 = exception.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val exception2 = exception.checkpointSerialize(context).checkpointDeserialize(context) assertEquals(exception.message, exception2.message) assertEquals(1, exception2.suppressed.size) @@ -308,7 +308,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `serialize - deserialize Exception no suppressed`() { val exception = IllegalArgumentException("fooBar") - val exception2 = exception.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val exception2 = exception.checkpointSerialize(context).checkpointDeserialize(context) assertEquals(exception.message, exception2.message) assertEquals(0, exception2.suppressed.size) @@ -322,7 +322,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun `serialize - deserialize HashNotFound`() { val randomHash = SecureHash.randomSHA256() val exception = FetchDataFlow.HashNotFound(randomHash) - val exception2 = exception.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) + val exception2 = exception.checkpointSerialize(context).checkpointDeserialize(context) assertEquals(randomHash, exception2.requested) } @@ -330,17 +330,17 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun `compression has the desired effect`() { compression ?: return val data = ByteArray(12345).also { Random(0).nextBytes(it) }.let { it + it } - val compressed = data.checkpointSerialize(factory, context) + val compressed = data.checkpointSerialize(context) assertEquals(.5, compressed.size.toDouble() / data.size, .03) - assertArrayEquals(data, compressed.checkpointDeserialize(factory, context)) + assertArrayEquals(data, compressed.checkpointDeserialize(context)) } @Test fun `a particular encoding can be banned for deserialization`() { compression ?: return doReturn(false).whenever(context.encodingWhitelist).acceptEncoding(compression) - val compressed = "whatever".checkpointSerialize(factory, context) - catchThrowable { compressed.checkpointDeserialize(factory, context) }.run { + val compressed = "whatever".checkpointSerialize(context) + catchThrowable { compressed.checkpointDeserialize(context) }.run { assertSame(KryoException::class.java, javaClass) assertEquals(encodingNotPermittedFormat.format(compression), message) } @@ -351,8 +351,8 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { class Holder(val holder: ByteArray) val obj = Holder(ByteArray(20000)) - val uncompressedSize = obj.checkpointSerialize(factory, context.withEncoding(null)).size - val compressedSize = obj.checkpointSerialize(factory, context.withEncoding(CordaSerializationEncoding.SNAPPY)).size + val uncompressedSize = obj.checkpointSerialize(context.withEncoding(null)).size + val compressedSize = obj.checkpointSerialize(context.withEncoding(CordaSerializationEncoding.SNAPPY)).size // If these need fixing, sounds like Kryo wire format changed and checkpoints might not surive an upgrade. assertEquals(20222, uncompressedSize) assertEquals(1111, compressedSize) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt index 785ce47597..025e27a38a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt @@ -5,7 +5,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.node.ServiceHub import net.corda.core.serialization.* import net.corda.core.serialization.internal.CheckpointSerializationContext -import net.corda.core.serialization.internal.CheckpointSerializationFactory +import net.corda.core.serialization.internal.CheckpointSerializer val serializationContextKey = SerializeAsTokenContext::class.java @@ -70,8 +70,8 @@ class SerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: Ser */ @DeleteForDJVM class CheckpointSerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: SerializeAsTokenContext.() -> Unit) : SerializeAsTokenContext { - constructor(toBeTokenized: Any, serializationFactory: CheckpointSerializationFactory, context: CheckpointSerializationContext, serviceHub: ServiceHub) : this(serviceHub, { - serializationFactory.serialize(toBeTokenized, context.withTokenContext(this)) + constructor(toBeTokenized: Any, serializer: CheckpointSerializer, context: CheckpointSerializationContext, serviceHub: ServiceHub) : this(serviceHub, { + serializer.serialize(toBeTokenized, context.withTokenContext(this)) }) private val classNameToSingleton = mutableMapOf() diff --git a/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java b/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java index feab89ad92..0c9c9f2b5a 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java +++ b/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java @@ -2,14 +2,14 @@ package net.corda.serialization.internal; import net.corda.core.serialization.*; import net.corda.core.serialization.internal.CheckpointSerializationContext; -import net.corda.core.serialization.internal.CheckpointSerializationFactory; +import net.corda.core.serialization.internal.CheckpointSerializer; import net.corda.node.serialization.kryo.CordaClosureSerializer; -import net.corda.testing.core.SerializationEnvironmentRule; import net.corda.testing.core.internal.CheckpointSerializationEnvironmentRule; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import java.io.NotSerializableException; import java.io.Serializable; import java.util.Collections; import java.util.concurrent.Callable; @@ -23,12 +23,11 @@ public final class LambdaCheckpointSerializationTest { public final CheckpointSerializationEnvironmentRule testCheckpointSerialization = new CheckpointSerializationEnvironmentRule(); - private CheckpointSerializationFactory factory; private CheckpointSerializationContext context; + private CheckpointSerializer serializer; @Before public void setup() { - factory = testCheckpointSerialization.getCheckpointSerializationFactory(); context = new CheckpointSerializationContextImpl( getClass().getClassLoader(), AllWhitelist.INSTANCE, @@ -36,6 +35,8 @@ public final class LambdaCheckpointSerializationTest { true, null ); + + serializer = testCheckpointSerialization.getCheckpointSerializer(); } @Test @@ -63,11 +64,11 @@ public final class LambdaCheckpointSerializationTest { assertThat(throwable).hasMessage(CordaClosureSerializer.ERROR_MESSAGE); } - private SerializedBytes serialize(final T target) { - return factory.serialize(target, context); + private SerializedBytes serialize(final T target) throws NotSerializableException { + return serializer.serialize(target, context); } - private T deserialize(final SerializedBytes bytes, final Class type) { - return factory.deserialize(bytes, type, context); + private T deserialize(final SerializedBytes bytes, final Class type) throws NotSerializableException { + return serializer.deserialize(bytes, type, context); } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt index 73b799217d..e3426a1fd9 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt @@ -4,11 +4,9 @@ import net.corda.core.contracts.ContractAttachment import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.* import net.corda.core.serialization.internal.CheckpointSerializationContext -import net.corda.core.serialization.internal.CheckpointSerializationFactory import net.corda.core.serialization.internal.checkpointDeserialize import net.corda.core.serialization.internal.checkpointSerialize import net.corda.testing.contracts.DummyContract -import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.internal.CheckpointSerializationEnvironmentRule import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices @@ -27,24 +25,25 @@ class ContractAttachmentSerializerTest { @JvmField val testCheckpointSerialization = CheckpointSerializationEnvironmentRule() - private lateinit var factory: CheckpointSerializationFactory - private lateinit var context: CheckpointSerializationContext private lateinit var contextWithToken: CheckpointSerializationContext private val mockServices = MockServices(emptyList(), CordaX500Name("MegaCorp", "London", "GB"), rigorousMock()) @Before fun setup() { - factory = testCheckpointSerialization.checkpointSerializationFactory - context = testCheckpointSerialization.checkpointSerializationContext - contextWithToken = context.withTokenContext(CheckpointSerializeAsTokenContextImpl(Any(), factory, context, mockServices)) + contextWithToken = testCheckpointSerialization.checkpointSerializationContext.withTokenContext( + CheckpointSerializeAsTokenContextImpl( + Any(), + testCheckpointSerialization.checkpointSerializer, + testCheckpointSerialization.checkpointSerializationContext, + mockServices)) } @Test fun `write contract attachment and read it back`() { val contractAttachment = ContractAttachment(GeneratedAttachment(EMPTY_BYTE_ARRAY), DummyContract.PROGRAM_ID) // no token context so will serialize the whole attachment - val serialized = contractAttachment.checkpointSerialize(factory, context) - val deserialized = serialized.checkpointDeserialize(factory, context) + val serialized = contractAttachment.checkpointSerialize() + val deserialized = serialized.checkpointDeserialize() assertEquals(contractAttachment.id, deserialized.attachment.id) assertEquals(contractAttachment.contract, deserialized.contract) @@ -59,8 +58,8 @@ class ContractAttachmentSerializerTest { mockServices.attachments.importAttachment(attachment.open(), "test", null) val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) - val deserialized = serialized.checkpointDeserialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(contextWithToken) + val deserialized = serialized.checkpointDeserialize(contextWithToken) assertEquals(contractAttachment.id, deserialized.attachment.id) assertEquals(contractAttachment.contract, deserialized.contract) @@ -76,7 +75,7 @@ class ContractAttachmentSerializerTest { mockServices.attachments.importAttachment(attachment.open(), "test", null) val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(contextWithToken) assertThat(serialized.size).isLessThan(largeAttachmentSize) } @@ -88,8 +87,8 @@ class ContractAttachmentSerializerTest { // don't importAttachment in mockService val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) - val deserialized = serialized.checkpointDeserialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(contextWithToken) + val deserialized = serialized.checkpointDeserialize(contextWithToken) assertThatThrownBy { deserialized.attachment.open() }.isInstanceOf(MissingAttachmentsException::class.java) } @@ -100,8 +99,8 @@ class ContractAttachmentSerializerTest { // don't importAttachment in mockService val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) - serialized.checkpointDeserialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(contextWithToken) + serialized.checkpointDeserialize(contextWithToken) // MissingAttachmentsException thrown if we try to open attachment } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt index 7f2bad6854..49e5d1f2ed 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt @@ -5,7 +5,6 @@ import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.io.Output import net.corda.core.serialization.* import net.corda.core.serialization.internal.CheckpointSerializationContext -import net.corda.core.serialization.internal.CheckpointSerializationFactory import net.corda.core.serialization.internal.checkpointDeserialize import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.OpaqueBytes @@ -14,7 +13,6 @@ import net.corda.node.serialization.kryo.CordaKryo import net.corda.node.serialization.kryo.DefaultKryoCustomizer import net.corda.node.serialization.kryo.kryoMagic import net.corda.testing.internal.rigorousMock -import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.internal.CheckpointSerializationEnvironmentRule import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -28,12 +26,10 @@ class SerializationTokenTest { @JvmField val testCheckpointSerialization = CheckpointSerializationEnvironmentRule() - private lateinit var factory: CheckpointSerializationFactory private lateinit var context: CheckpointSerializationContext @Before fun setup() { - factory = testCheckpointSerialization.checkpointSerializationFactory context = testCheckpointSerialization.checkpointSerializationContext.withWhitelisted(SingletonSerializationToken::class.java) } @@ -49,16 +45,16 @@ class SerializationTokenTest { override fun equals(other: Any?) = other is LargeTokenizable && other.bytes.size == this.bytes.size } - private fun serializeAsTokenContext(toBeTokenized: Any) = CheckpointSerializeAsTokenContextImpl(toBeTokenized, factory, context, rigorousMock()) + private fun serializeAsTokenContext(toBeTokenized: Any) = CheckpointSerializeAsTokenContextImpl(toBeTokenized, testCheckpointSerialization.checkpointSerializer, context, rigorousMock()) @Test fun `write token and read tokenizable`() { val tokenizableBefore = LargeTokenizable() val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.checkpointSerialize(factory, testContext) + val serializedBytes = tokenizableBefore.checkpointSerialize(testContext) assertThat(serializedBytes.size).isLessThan(tokenizableBefore.numBytes) - val tokenizableAfter = serializedBytes.checkpointDeserialize(factory, testContext) + val tokenizableAfter = serializedBytes.checkpointDeserialize(testContext) assertThat(tokenizableAfter).isSameAs(tokenizableBefore) } @@ -69,8 +65,8 @@ class SerializationTokenTest { val tokenizableBefore = UnitSerializeAsToken() val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.checkpointSerialize(factory, testContext) - val tokenizableAfter = serializedBytes.checkpointDeserialize(factory, testContext) + val serializedBytes = tokenizableBefore.checkpointSerialize(testContext) + val tokenizableAfter = serializedBytes.checkpointDeserialize(testContext) assertThat(tokenizableAfter).isSameAs(tokenizableBefore) } @@ -79,7 +75,7 @@ class SerializationTokenTest { val tokenizableBefore = UnitSerializeAsToken() val context = serializeAsTokenContext(emptyList()) val testContext = this.context.withTokenContext(context) - tokenizableBefore.checkpointSerialize(factory, testContext) + tokenizableBefore.checkpointSerialize(testContext) } @Test(expected = UnsupportedOperationException::class) @@ -87,14 +83,14 @@ class SerializationTokenTest { val tokenizableBefore = UnitSerializeAsToken() val context = serializeAsTokenContext(emptyList()) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.toToken(serializeAsTokenContext(emptyList())).checkpointSerialize(factory, testContext) - serializedBytes.checkpointDeserialize(factory, testContext) + val serializedBytes = tokenizableBefore.toToken(serializeAsTokenContext(emptyList())).checkpointSerialize(testContext) + serializedBytes.checkpointDeserialize(testContext) } @Test(expected = KryoException::class) fun `no context set`() { val tokenizableBefore = UnitSerializeAsToken() - tokenizableBefore.checkpointSerialize(factory, context) + tokenizableBefore.checkpointSerialize(context) } @Test(expected = KryoException::class) @@ -112,7 +108,7 @@ class SerializationTokenTest { kryo.writeObject(it, emptyList()) } val serializedBytes = SerializedBytes(stream.toByteArray()) - serializedBytes.checkpointDeserialize(factory, testContext) + serializedBytes.checkpointDeserialize(testContext) } private class WrongTypeSerializeAsToken : SerializeAsToken { @@ -128,7 +124,7 @@ class SerializationTokenTest { val tokenizableBefore = WrongTypeSerializeAsToken() val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.checkpointSerialize(factory, testContext) - serializedBytes.checkpointDeserialize(factory, testContext) + val serializedBytes = tokenizableBefore.checkpointSerialize(testContext) + serializedBytes.checkpointDeserialize(testContext) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt index 514b23a855..526f1251b6 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt @@ -3,9 +3,7 @@ package net.corda.testing.core import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doAnswer import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.DoNotImplement import net.corda.core.internal.staticField -import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.testing.common.internal.asContextEnv @@ -40,7 +38,7 @@ class SerializationEnvironmentRule(private val inheritable: Boolean = false) : T /** Do not call, instead use [SerializationEnvironmentRule] as a [org.junit.Rule]. */ fun run(taskLabel: String, task: (SerializationEnvironment) -> T): T { - return SerializationEnvironmentRule().apply { init(taskLabel) }.runTask(task) + return SerializationEnvironmentRule().apply { init() }.runTask(task) } } @@ -48,14 +46,14 @@ class SerializationEnvironmentRule(private val inheritable: Boolean = false) : T val serializationFactory get() = env.serializationFactory override fun apply(base: Statement, description: Description): Statement { - init(description.toString()) + init() return object : Statement() { override fun evaluate() = runTask { base.evaluate() } } } - private fun init(envLabel: String) { - env = createTestSerializationEnv(envLabel) + private fun init() { + env = createTestSerializationEnv() } private fun runTask(task: (SerializationEnvironment) -> T): T { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/CheckpointSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/CheckpointSerializationTestHelpers.kt index eb92d12cf6..69b634e107 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/CheckpointSerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/CheckpointSerializationTestHelpers.kt @@ -39,7 +39,7 @@ class CheckpointSerializationEnvironmentRule(private val inheritable: Boolean = /** Do not call, instead use [SerializationEnvironmentRule] as a [org.junit.Rule]. */ fun run(taskLabel: String, task: (SerializationEnvironment) -> T): T { - return CheckpointSerializationEnvironmentRule().apply { init(taskLabel) }.runTask(task) + return CheckpointSerializationEnvironmentRule().apply { init() }.runTask(task) } } @@ -47,14 +47,14 @@ class CheckpointSerializationEnvironmentRule(private val inheritable: Boolean = private lateinit var env: SerializationEnvironment override fun apply(base: Statement, description: Description): Statement { - init(description.toString()) + init() return object : Statement() { override fun evaluate() = runTask { base.evaluate() } } } - private fun init(envLabel: String) { - env = createTestSerializationEnv(envLabel) + private fun init() { + env = createTestSerializationEnv() } private fun runTask(task: (SerializationEnvironment) -> T): T { @@ -65,7 +65,6 @@ class CheckpointSerializationEnvironmentRule(private val inheritable: Boolean = } } - val checkpointSerializationFactory get() = env.checkpointSerializationFactory val checkpointSerializationContext get() = env.checkpointContext - + val checkpointSerializer get() = env.checkpointSerializer } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt index 53bee6f798..3257cca802 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt @@ -4,11 +4,10 @@ import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.whenever import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.core.DoNotImplement -import net.corda.core.serialization.internal.CheckpointSerializationFactory import net.corda.core.serialization.internal.* import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.node.serialization.kryo.KryoSerializationScheme +import net.corda.node.serialization.kryo.KryoCheckpointSerializer import net.corda.serialization.internal.* import net.corda.testing.core.SerializationEnvironmentRule import java.util.concurrent.ConcurrentHashMap @@ -30,22 +29,20 @@ fun withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, s } } -internal fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl { +internal fun createTestSerializationEnv(): SerializationEnvironment { val factory = SerializationFactoryImpl().apply { registerScheme(AMQPClientSerializationScheme(emptyList())) registerScheme(AMQPServerSerializationScheme(emptyList())) } - return object : SerializationEnvironmentImpl( + return SerializationEnvironment.with( factory, AMQP_P2P_CONTEXT, AMQP_RPC_SERVER_CONTEXT, AMQP_RPC_CLIENT_CONTEXT, AMQP_STORAGE_CONTEXT, KRYO_CHECKPOINT_CONTEXT, - CheckpointSerializationFactory(KryoSerializationScheme) - ) { - override fun toString() = "testSerializationEnv($label)" - } + KryoCheckpointSerializer + ) } /** @@ -54,7 +51,7 @@ internal fun createTestSerializationEnv(label: String): SerializationEnvironment */ fun setGlobalSerialization(armed: Boolean): GlobalSerializationEnvironment { return if (armed) { - object : GlobalSerializationEnvironment, SerializationEnvironment by createTestSerializationEnv("") { + object : GlobalSerializationEnvironment, SerializationEnvironment by createTestSerializationEnv() { override fun unset() { _globalSerializationEnv.set(null) inVMExecutors.remove(this) diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index 570a9cbe2e..e65a7441be 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -8,11 +8,10 @@ import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.ExitCodes import net.corda.cliutils.start import net.corda.core.internal.isRegularFile -import net.corda.core.internal.rootMessage import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize -import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.utilities.base64ToByteArray import net.corda.core.utilities.hexToByteArray @@ -22,7 +21,6 @@ import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme import net.corda.serialization.internal.amqp.DeserializationInput import net.corda.serialization.internal.amqp.amqpMagic import org.slf4j.event.Level -import picocli.CommandLine import picocli.CommandLine.* import java.io.PrintStream import java.net.MalformedURLException @@ -128,7 +126,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised private fun initialiseSerialization() { // Deserialise with the lenient carpenter as we only care for the AMQP field getters - _contextSerializationEnv.set(SerializationEnvironmentImpl( + _contextSerializationEnv.set(SerializationEnvironment.with( SerializationFactoryImpl().apply { registerScheme(AMQPInspectorSerializationScheme) }, diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt index 86430dadf3..dfecae05ac 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt @@ -2,7 +2,7 @@ package net.corda.demobench import javafx.scene.image.Image import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme -import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.demobench.views.DemoBenchView import net.corda.serialization.internal.AMQP_P2P_CONTEXT @@ -56,7 +56,7 @@ class DemoBench : App(DemoBenchView::class) { } private fun initialiseSerialization() { - nodeSerializationEnv = SerializationEnvironmentImpl( + nodeSerializationEnv = SerializationEnvironment.with( SerializationFactoryImpl().apply { registerScheme(AMQPClientSerializationScheme(emptyList())) }, diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt index ce978e4131..d0efa8d492 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt @@ -1,9 +1,10 @@ package net.corda.bootstrapper.serialization -import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT +import net.corda.node.serialization.kryo.KryoCheckpointSerializer import net.corda.serialization.internal.AMQP_P2P_CONTEXT import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT import net.corda.serialization.internal.SerializationFactoryImpl @@ -14,14 +15,16 @@ class SerializationEngine { synchronized(this) { if (nodeSerializationEnv == null) { val classloader = this::class.java.classLoader - nodeSerializationEnv = SerializationEnvironmentImpl( + nodeSerializationEnv = SerializationEnvironment.with( SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme(emptyList())) }, p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), rpcServerContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader), - checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader) + + checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader), + checkpointSerializer = KryoCheckpointSerializer ) } } From d987f18871657b11c5fe5e42205368bb070abb3a Mon Sep 17 00:00:00 2001 From: josecoll Date: Mon, 8 Oct 2018 13:56:02 +0100 Subject: [PATCH 25/83] Gradle cache friendly Junit test fix. (#4044) --- core-deterministic/testing/data/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core-deterministic/testing/data/build.gradle b/core-deterministic/testing/data/build.gradle index 59992d92eb..03da2a8977 100644 --- a/core-deterministic/testing/data/build.gradle +++ b/core-deterministic/testing/data/build.gradle @@ -22,6 +22,9 @@ test { // Running this class is the whole point, so include it explicitly. includeTestsMatching "net.corda.deterministic.data.GenerateData" } + // force execution of these tests to generate artifacts required by other module (eg. VerifyTransactionTest) + // note: required by Gradle Build Cache. + outputs.upToDateWhen { false } } assemble.finalizedBy test From b8e88232b45601293c13a1394afdf2177a9ef248 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 8 Oct 2018 14:19:56 +0100 Subject: [PATCH 26/83] NOTICK Create semantic version numbers (#4045) --- release-tools/testing/test-manager | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-tools/testing/test-manager b/release-tools/testing/test-manager index 96bfd97a36..57c55c2259 100755 --- a/release-tools/testing/test-manager +++ b/release-tools/testing/test-manager @@ -278,7 +278,7 @@ def main(): def mixin_version_and_product(command): mixin_product(command) - command.add('VERSION', help='the target version of the release, e.g., 4.0', type=float) + command.add('VERSION', help='the target version of the release, e.g., 4.0', type=str) def mixin_candidate(command, optional=False): if optional: From d1adb09ca9c1653899ab17b9b3994fd6917296c0 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 8 Oct 2018 15:26:44 +0100 Subject: [PATCH 27/83] NOTICK Python 3 compat, ENM version name, skip closed tests (#4046) --- release-tools/testing/args.py | 7 +++++-- release-tools/testing/jira_manager.py | 7 +++++++ release-tools/testing/test-manager | 10 ++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/release-tools/testing/args.py b/release-tools/testing/args.py index 9eb92a8a59..58ed3f0b90 100644 --- a/release-tools/testing/args.py +++ b/release-tools/testing/args.py @@ -5,7 +5,7 @@ import sys, traceback # {{{ Representation of a command-line program class Program: - # Create a new command-line program represenation, provided an optional name and description + # Create a new command-line program representation, provided an optional name and description def __init__(self, name=None, description=None): self.parser = ArgumentParser(name, description=description) self.subparsers = self.parser.add_subparsers(title='commands') @@ -39,7 +39,10 @@ class Program: t, exception, tb = sys.exc_info() self.parser.error('{}\n\n{}'.format(error.message, '\n'.join(traceback.format_tb(tb)))) else: - self.parser.error(error.message) + try: + self.parser.error(error.message) + except AttributeError: + self.parser.error(str(error)) # }}} # {{{ Representation of a sub-command of a command-line program diff --git a/release-tools/testing/jira_manager.py b/release-tools/testing/jira_manager.py index af57865398..5f9e66fa60 100644 --- a/release-tools/testing/jira_manager.py +++ b/release-tools/testing/jira_manager.py @@ -3,6 +3,13 @@ from jira import JIRA from jira.exceptions import JIRAError # }}} +# {{{ Python 2 and 3 interoperability +try: + unicode('') +except NameError: + unicode = str +# }}} + # {{{ Class for interacting with a hosted JIRA system class Jira: diff --git a/release-tools/testing/test-manager b/release-tools/testing/test-manager index 57c55c2259..bfea84eb68 100755 --- a/release-tools/testing/test-manager +++ b/release-tools/testing/test-manager @@ -36,7 +36,8 @@ except: product_map = { 'OS' : 'Corda', 'ENT' : 'Corda Enterprise', - 'NS' : 'Corda Network Services', + 'NS' : 'ENM', + 'ENM' : 'ENM', 'TEST' : 'Corda', # for demo and test purposes } # }}} @@ -235,10 +236,15 @@ def create_release_candidate(args): print() has_tests = False for issue in jira.search(QUERY_LIST_TEST_INSTANCES, args.PRODUCT, version): - print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + test_status = issue.fields.status['name'] + print(u' - {} {} ({})'.format(blue(issue.key), issue.fields.summary, test_status)) epic_field = jira.custom_fields_by_name['Epic Link'] epic = issue.fields[epic_field] if epic_field in issue.fields.to_dict() else '' labels = issue.fields.labels + [epic] + if test_status in ['Pass', 'Fail', 'Descope']: + print(u' {} - Parent test is marked as {}'.format(yellow('SKIPPED'), test_status)) + print() + continue print() has_tests = True print(u' - Creating test run ticket for release candidate {} ...'.format(yellow('RC{:02d}'.format(CANDIDATE)))) From 5d84640d1f666bb60a0660bd06a27f6884fa02d8 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Tue, 9 Oct 2018 09:48:54 +0100 Subject: [PATCH 28/83] Add missing validation in the OpaqueBytesSubSequence.init (#4047) --- .../main/kotlin/net/corda/core/utilities/ByteArrays.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 0407bd95bb..7190e0c770 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -29,7 +29,7 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si */ abstract val bytes: ByteArray - /** Returns a [ByteArrayInputStream] of the bytes */ + /** Returns a [ByteArrayInputStream] of the bytes. */ fun open() = ByteArrayInputStream(_bytes, offset, size) /** @@ -41,8 +41,6 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si */ @Suppress("MemberVisibilityCanBePrivate") fun subSequence(offset: Int, size: Int): ByteSequence { - require(offset >= 0) - require(offset + size <= this.size) // Intentionally use bytes rather than _bytes, to mirror the copy-or-not behaviour of that property. return if (offset == 0 && size == this.size) this else of(bytes, this.offset + offset, size) } @@ -108,7 +106,7 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si return Integer.signum(unsignedThis - unsignedOther) } } - // First min bytes is the same, so now resort to size + // First min bytes is the same, so now resort to size. return Integer.signum(this.size - other.size) } @@ -190,12 +188,12 @@ fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this) fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this) /** - * Class is public for serialization purposes + * Class is public for serialization purposes. */ @KeepForDJVM class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) { init { require(offset >= 0 && offset < bytes.size) - require(size >= 0 && size <= bytes.size) + require(size >= 0 && offset + size <= bytes.size) } } From 9c8a1cd14ad8544e7ca8e8db2320ec152aabd676 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Tue, 9 Oct 2018 15:49:24 +0200 Subject: [PATCH 29/83] CORDA-2028 - Fix use of required paramers/options on command line (#4040) * Make required parameters work with --install-shell-extensions and make errors look a bit more errorey * Make blobinspector required parameter work the way it used to * Fix compilation Error --- .../net/corda/blobinspector/BlobInspector.kt | 7 +-- .../corda/blobinspector/BlobInspectorTest.kt | 2 +- .../net/corda/cliutils/CordaCliWrapper.kt | 59 +++++++++++++------ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index e65a7441be..8887247bf6 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -34,8 +34,8 @@ fun main(args: Array) { } class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised binary blobs to text") { - @Parameters(index = "*..0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) - var source: MutableList = mutableListOf() + @Parameters(index = "0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) + var source: URL? = null @Option(names = ["--format"], paramLabel = "type", description = ["Output format. Possible values: [YAML, JSON]"]) private var formatType: OutputFormatType = OutputFormatType.YAML @@ -61,8 +61,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised } fun run(out: PrintStream): Int { - require(source.count() == 1) { "You must specify URL or file path to the blob" } - val inputBytes = source.first().readBytes() + val inputBytes = source!!.readBytes() val bytes = parseToBinaryRelaxed(inputFormatType, inputBytes) ?: throw IllegalArgumentException("Error: this input does not appear to be encoded in Corda's AMQP extended format, sorry.") diff --git a/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt index 04cc72d621..65e1223c4f 100644 --- a/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt +++ b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt @@ -53,7 +53,7 @@ class BlobInspectorTest { } private fun run(resourceName: String): String { - blobInspector.source = mutableListOf(javaClass.getResource(resourceName)) + blobInspector.source = javaClass.getResource(resourceName) val writer = StringWriter() blobInspector.run(PrintStream(WriterOutputStream(writer, UTF_8))) val output = writer.toString() diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index f30c3ed05d..5a35be8089 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -21,8 +21,6 @@ import java.util.concurrent.Callable interface Validated { companion object { val logger = contextLogger() - const val RED = "\u001B[31m" - const val RESET = "\u001B[0m" } /** @@ -36,8 +34,8 @@ interface Validated { fun validate() { val errors = validator() if (errors.isNotEmpty()) { - logger.error(RED + "Exceptions when parsing command line arguments:") - logger.error(errors.joinToString("\n") + RESET) + logger.error(ShellConstants.RED + "Exceptions when parsing command line arguments:") + logger.error(errors.joinToString("\n") + ShellConstants.RESET) CommandLine(this).usage(System.err) exitProcess(ExitCodes.FAILURE) } @@ -55,6 +53,11 @@ object CordaSystemUtils { fun getOsName(): String = System.getProperty(OS_NAME) } +object ShellConstants { + const val RED = "\u001B[31m" + const val RESET = "\u001B[0m" +} + fun CordaCliWrapper.start(args: Array) { this.args = args @@ -66,27 +69,47 @@ fun CordaCliWrapper.start(args: Array) { cmd.registerConverter(Path::class.java) { Paths.get(it).toAbsolutePath().normalize() } cmd.commandSpec.name(alias) cmd.commandSpec.usageMessage().description(description) + cmd.commandSpec.parser().collectErrors(true) try { - val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { Help.Ansi.ON } else { Help.Ansi.AUTO } - val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), - DefaultExceptionHandler>().useErr(System.err).useAnsi(defaultAnsiMode), - *args) - // If an error code has been returned, use this and exit - results?.firstOrNull()?.let { - if (it is Int) { - exitProcess(it) - } else { + val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { + Help.Ansi.ON + } else { + Help.Ansi.AUTO + } + + val results = cmd.parse(*args) + val app = cmd.getCommand() + if (cmd.isUsageHelpRequested) { + cmd.usage(System.out, defaultAnsiMode) + exitProcess(ExitCodes.SUCCESS) + } + if (cmd.isVersionHelpRequested) { + cmd.printVersionHelp(System.out, defaultAnsiMode) + exitProcess(ExitCodes.SUCCESS) + } + if (app.installShellExtensionsParser.installShellExtensions) { + System.out.println("Install shell extensions: ${app.installShellExtensionsParser.installShellExtensions}") + // ignore any parsing errors and run the program + exitProcess(app.call()) + } + val allErrors = results.flatMap { it.parseResult?.errors() ?: emptyList() } + if (allErrors.any()) { + val parameterExceptions = allErrors.asSequence().filter { it is ParameterException } + if (parameterExceptions.any()) { + System.err.println("${ShellConstants.RED}${parameterExceptions.map{ it.message }.joinToString()}${ShellConstants.RESET}") + parameterExceptions.filter { it is UnmatchedArgumentException}.forEach { (it as UnmatchedArgumentException).printSuggestions(System.out) } + usage(cmd, System.out, defaultAnsiMode) exitProcess(ExitCodes.FAILURE) } + throw allErrors.first() } - // If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully - exitProcess(ExitCodes.SUCCESS) - } catch (e: ExecutionException) { + exitProcess(app.call()) + } catch (e: Exception) { val throwable = e.cause ?: e if (this.verbose) { throwable.printStackTrace() } else { - System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}") + System.err.println("${ShellConstants.RED}${throwable.rootMessage ?: "Use --verbose for more details"}${ShellConstants.RESET}") } exitProcess(ExitCodes.FAILURE) } @@ -126,7 +149,7 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal var loggingLevel: Level = Level.INFO @Mixin - private lateinit var installShellExtensionsParser: InstallShellExtensionsParser + lateinit var installShellExtensionsParser: InstallShellExtensionsParser // This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before). // Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used. From b6f2532ce67ce02376f9bc7e20690876e6c6d91f Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Tue, 9 Oct 2018 14:54:31 +0100 Subject: [PATCH 30/83] Corda 1922 serialize states with calculated values (#3938) * Introduce SerializeForCarpenter annotation * Apply SerializableComputedProperty annotation to Cash.exitKeys, fix bugs * info -> trace * Remove annotation from FungibleAsset, as we do not know whether all implementing classes will provide the property as a calculated value * Remove redundant import * Explicit lambda params * Restore explicit import for Enum valueOf * Moving and rescoping * More meaningful error message * Add java test and documentation * Fix accidentally broken unit test * Ignore superclass annotation if property not calculated in implementing class * Exclude calculated properties from Jackson serialisation * Fix broken test --- .../client/jackson/internal/CordaModule.kt | 2 +- .../net/corda/core/contracts/FungibleAsset.kt | 2 + ...lizable.kt => SerializationAnnotations.kt} | 10 +- docs/source/serialization.rst | 25 ++- .../net/corda/finance/contracts/asset/Cash.kt | 1 + .../vault/VaultSoftLockManagerTest.kt | 3 +- .../internal/amqp/DeserializationInput.kt | 4 +- .../internal/amqp/EvolutionSerializer.kt | 13 +- .../internal/amqp/ObjectSerializer.kt | 23 ++- .../internal/amqp/PropertyDescriptor.kt | 52 ++++- .../internal/amqp/PropertySerializers.kt | 44 +++-- .../internal/amqp/SerializationHelper.kt | 47 +++-- .../internal/amqp/SerializerFactory.kt | 187 +++++++++--------- .../internal/carpenter/MetaCarpenter.kt | 5 + ...aCalculatedValuesToClassCarpenterTest.java | 107 ++++++++++ ... => EvolutionSerializerProviderTesting.kt} | 4 +- .../internal/amqp/FingerPrinterTesting.kt | 2 +- .../internal/amqp/RoundTripTests.kt | 59 ++++++ .../internal/amqp/SerializationOutputTests.kt | 2 +- .../internal/amqp/testutils/AMQPTestUtils.kt | 2 +- .../CalculatedValuesToClassCarpenterTests.kt | 101 ++++++++++ .../carpenter/ClassCarpenterTestUtils.kt | 3 +- ...berCompositeSchemaToClassCarpenterTests.kt | 17 +- .../InheritanceSchemaToClassCarpenterTests.kt | 13 -- ...berCompositeSchemaToClassCarpenterTests.kt | 6 +- ...berCompositeSchemaToClassCarpenterTests.kt | 24 +-- 26 files changed, 554 insertions(+), 204 deletions(-) rename core/src/main/kotlin/net/corda/core/serialization/{CordaSerializable.kt => SerializationAnnotations.kt} (76%) create mode 100644 serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java rename serialization/src/test/kotlin/net/corda/serialization/internal/amqp/{EvolutionSerializerGetterTesting.kt => EvolutionSerializerProviderTesting.kt} (85%) create mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index 2212638551..e56711a50d 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -98,7 +98,7 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier() val ctor = constructorForDeserialization(beanClass) val amqpProperties = propertiesForSerialization(ctor, beanClass, serializerFactory) .serializationOrder - .map { it.serializer.name } + .mapNotNull { if (it.isCalculated) null else it.serializer.name } val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName }) (amqpProperties - propertyRenames.values).let { check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" } diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index d28362a34b..ddcd04be56 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -3,6 +3,7 @@ package net.corda.core.contracts import net.corda.core.KeepForDJVM import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty +import net.corda.core.serialization.SerializableCalculatedProperty import java.security.PublicKey /** @@ -38,6 +39,7 @@ interface FungibleAsset : OwnableState { * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their * owner to sign, some (i.e. cash) also require the issuer. */ + @get:SerializableCalculatedProperty val exitKeys: Collection /** diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAnnotations.kt similarity index 76% rename from core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt rename to core/src/main/kotlin/net/corda/core/serialization/SerializationAnnotations.kt index ce80444256..95fd23e60f 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAnnotations.kt @@ -19,4 +19,12 @@ import java.lang.annotation.Inherited @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @Inherited -annotation class CordaSerializable \ No newline at end of file +annotation class CordaSerializable + + +/** + * Used to annotate methods which expose calculated values that we want to be serialized for use by the class carpenter. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION) +annotation class SerializableCalculatedProperty \ No newline at end of file diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index c015eb317e..00f4baa07a 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -556,10 +556,33 @@ be able to use reflection over the deserialized data, for scripting languages th ensuring classes not on the classpath can be deserialized without loading potentially malicious code. If the original class implements some interfaces then the carpenter will make sure that all of the interface methods are -backed by feilds. If that's not the case then an exception will be thrown during deserialization. This check can +backed by fields. If that's not the case then an exception will be thrown during deserialization. This check can be turned off with ``SerializationContext.withLenientCarpenter``. This can be useful if only the field getters are needed, say in an object viewer. +Calculated values +````````````````` + +In some cases, for example the `exitKeys` field in ``FungibleState``, a property in an interface may normally be implemented +as a *calculated* value, with a "getter" method for reading it but neither a corresponding constructor parameter nor a +"setter" method for writing it. In this case, it will not automatically be included among the properties to be serialized, +since the receiving class would ordinarily be able to re-calculate it on demand. However, a synthesized class will not +have the method implementation which knows how to calculate the value, and a cast to the interface will fail because the +property is not serialized and so the "getter" method present in the interface will not be synthesized. + +The solution is to annotate the method with the ``SerializableCalculatedProperty`` annotation, which will cause the value +exposed by the method to be read and transmitted during serialization, but discarded during normal deserialization. The +synthesized class will then include a backing field together with a "getter" for the serialized calculated value, and will +remain compatible with the interface. + +If the annotation is added to the method in the *interface*, then all implementing classes must calculate the value and +none may have a corresponding backing field; alternatively, it can be added to the overriding method on each implementing +class where the value is calculated and there is no backing field. If the field is a Kotlin ``val``, then the annotation +should be targeted at its getter method, e.g. ``@get:SerializableCalculatedProperty``. + +Future enhancements +``````````````````` + Possible future enhancements include: #. Java singleton support. We will add support for identifying classes which are singletons and identifying the diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 73681e44b7..58d5bae36c 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -16,6 +16,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState +import net.corda.core.serialization.SerializableCalculatedProperty import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 6784d43362..93e60859fe 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -34,6 +34,7 @@ import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow import org.junit.After import org.junit.Test +import java.security.PublicKey import java.util.* import java.util.concurrent.atomic.AtomicBoolean import kotlin.reflect.jvm.jvmName @@ -127,7 +128,7 @@ class VaultSoftLockManagerTest { override val owner get() = participants[0] override fun withNewOwner(newOwner: AbstractParty) = throw UnsupportedOperationException() override val amount get() = Amount(1, Issued(PartyAndReference(owner, OpaqueBytes.of(1)), Unit)) - override val exitKeys get() = throw UnsupportedOperationException() + override val exitKeys get() = emptyList() override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty) = throw UnsupportedOperationException() override fun equals(other: Any?) = other is FungibleAssetImpl && participants == other.participants override fun hashCode() = participants.hashCode() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index b9c50f7250..0cea2003f7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -117,7 +117,7 @@ class DeserializationInput constructor( des { val envelope = getEnvelope(bytes, context.encodingWhitelist) - logger.trace("deserialize blob scheme=\"${envelope.schema.toString()}\"") + logger.trace("deserialize blob scheme=\"${envelope.schema}\"") clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz, context)) @@ -149,7 +149,7 @@ class DeserializationInput constructor( if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) { // It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference. val objectIndex = (obj.described as UnsignedInteger).toInt() - if (objectIndex !in 0..objectHistory.size) + if (objectIndex >= objectHistory.size) throw AMQPNotSerializableException( type, "Retrieval of existing reference failed. Requested index $objectIndex " + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 79a3f85c00..4604af4505 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -125,7 +125,7 @@ abstract class EvolutionSerializer( // any particular NonNullable annotation type to indicate cross // compiler nullability val isKotlin = (new.type.javaClass.declaredAnnotations.any { - it.annotationClass.qualifiedName == "kotlin.Metadata" + it.annotationClass.qualifiedName == "kotlin.Metadata" }) constructor.parameters.withIndex().forEach { @@ -270,8 +270,8 @@ class EvolutionSerializerViaSetters( * be an object that returns an [EvolutionSerializer]. Of course, any implementation that * extends this class can be written to invoke whatever behaviour is desired. */ -abstract class EvolutionSerializerGetterBase { - abstract fun getEvolutionSerializer( +interface EvolutionSerializerProvider { + fun getEvolutionSerializer( factory: SerializerFactory, typeNotation: TypeNotation, newSerializer: AMQPSerializer, @@ -283,12 +283,12 @@ abstract class EvolutionSerializerGetterBase { * between the received schema and the class as it exists now on the class path, */ @KeepForDJVM -class EvolutionSerializerGetter : EvolutionSerializerGetterBase() { +object DefaultEvolutionSerializerProvider : EvolutionSerializerProvider { override fun getEvolutionSerializer(factory: SerializerFactory, typeNotation: TypeNotation, newSerializer: AMQPSerializer, schemas: SerializationSchemas): AMQPSerializer { - return factory.serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { + return factory.registerByDescriptor(typeNotation.descriptor.name!!) { when (typeNotation) { is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory) is RestrictedType -> { @@ -310,5 +310,4 @@ class EvolutionSerializerGetter : EvolutionSerializerGetterBase() { } } } -} - +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt index dc142fce16..9ae529b608 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt @@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp import net.corda.core.internal.isConcreteClass import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType @@ -61,7 +62,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS context: SerializationContext, debugIndent: Int) = ifThrowsAppend({ clazz.typeName } ) { - if (propertySerializers.size != javaConstructor?.parameterCount && + if (propertySerializers.deserializableSize != javaConstructor?.parameterCount && javaConstructor?.parameterCount ?: 0 > 0 ) { throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects " @@ -86,8 +87,9 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS input: DeserializationInput, context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) { if (obj is List<*>) { - if (obj.size > propertySerializers.size) { - throw AMQPNotSerializableException(type, "Too many properties in described type $typeName") + if (obj.size != propertySerializers.size) { + throw AMQPNotSerializableException(type, "${obj.size} objects to deserialize, but " + + "${propertySerializers.size} properties in described type $typeName") } return if (propertySerializers.byConstructor) { @@ -109,8 +111,19 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS return construct(propertySerializers.serializationOrder .zip(obj) - .map { Pair(it.first.initialPosition, it.first.serializer.readProperty(it.second, schemas, input, context)) } - .sortedWith(compareBy({ it.first })) + .mapNotNull { (accessor, obj) -> + // Ensure values get read out of input no matter what + val value = accessor.serializer.readProperty(obj, schemas, input, context) + + when(accessor) { + is PropertyAccessorConstructor -> accessor.initialPosition to value + is CalculatedPropertyAccessor -> null + else -> throw UnsupportedOperationException( + "${accessor::class.simpleName} accessor not supported " + + "for constructor-based object building") + } + } + .sortedWith(compareBy { it.first }) .map { it.second }) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt index 882f067eba..3de78db04e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt @@ -3,6 +3,7 @@ package net.corda.serialization.internal.amqp import com.google.common.reflect.TypeToken import net.corda.core.KeepForDJVM import net.corda.core.internal.isPublic +import net.corda.core.serialization.SerializableCalculatedProperty import net.corda.serialization.internal.amqp.MethodClassifier.* import java.lang.reflect.Field import java.lang.reflect.Method @@ -84,7 +85,7 @@ private val propertyMethodRegex = Regex("(?get|set|is)(?\\p{Lu}.*)") * take a single parameter of a type compatible with exampleProperty and isExampleProperty must * return a boolean */ -fun Class.propertyDescriptors(): Map { +internal fun Class.propertyDescriptors(): Map { val fieldProperties = superclassChain().declaredFields().byFieldName() return superclassChain().declaredMethods() @@ -96,9 +97,23 @@ fun Class.propertyDescriptors(): Map { .validated() } +/** + * Obtain [PropertyDescriptor]s for those calculated properties of a class which are annotated with + * [SerializableCalculatedProperty] + */ +internal fun Class.calculatedPropertyDescriptors(): Map = + superclassChain().withInterfaces().declaredMethods() + .thatArePublic() + .thatAreCalculated() + .toCalculatedProperties() + // Generate the sequence of classes starting with this class and ascending through it superclasses. private fun Class<*>.superclassChain() = generateSequence(this, Class<*>::getSuperclass) +private fun Sequence>.withInterfaces() = flatMap { + sequenceOf(it) + it.genericInterfaces.asSequence().map { it.asClass() } +} + // Obtain the fields declared by all classes in this sequence of classes. private fun Sequence>.declaredFields() = flatMap { it.declaredFields.asSequence() } @@ -108,18 +123,45 @@ private fun Sequence>.declaredMethods() = flatMap { it.declaredMethods. // Map a sequence of fields by field name. private fun Sequence.byFieldName() = map { it.name to it }.toMap() -// Select only those methods that are public (and are not the "getClass" method) +// Select only those methods that are public (and are not the "getClass" method). private fun Sequence.thatArePublic() = filter { it.isPublic && it.name != "getClass" } +// Select only those methods that are annotated with [SerializableCalculatedProperty]. +private fun Sequence.thatAreCalculated() = filter { + it.isAnnotationPresent(SerializableCalculatedProperty::class.java) +} + +// Convert a sequence of calculated property methods to a map of property descriptors by property name. +private fun Sequence.toCalculatedProperties(): Map { + val methodsByName = mutableMapOf() + for (method in this) { + val propertyNamedMethod = getPropertyNamedMethod(method) + ?: throw IllegalArgumentException("Calculated property method must have a name beginning with 'get' or 'is'") + + require(propertyNamedMethod.hasValidSignature()) { + "Calculated property name must have no parameters, and a non-void return type" + } + + val propertyName = propertyNamedMethod.fieldName.decapitalize() + methodsByName.compute(propertyName) { _, existingMethod -> + if (existingMethod == null) method + else leastGenericBy({ genericReturnType }, existingMethod, method) + } + } + return methodsByName.mapValues { (_, method) -> PropertyDescriptor(null, null, method) } +} + // Select only those methods that are isX/getX/setX methods -private fun Sequence.thatArePropertyMethods() = map { method -> - propertyMethodRegex.find(method.name)?.let { result -> +private fun Sequence.thatArePropertyMethods() = mapNotNull(::getPropertyNamedMethod) + +private fun getPropertyNamedMethod(method: Method): PropertyNamedMethod? { + return propertyMethodRegex.find(method.name)?.let { result -> PropertyNamedMethod( result.groups[2]!!.value, MethodClassifier.valueOf(result.groups[1]!!.value.toUpperCase()), method) } -}.filterNotNull() +} // Pick only those methods whose signatures are valid, discarding the remainder without warning. private fun Sequence.withValidSignature() = filter { it.hasValidSignature() } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt index c56071a84e..65a8998a14 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt @@ -1,6 +1,7 @@ package net.corda.serialization.internal.amqp import net.corda.core.KeepForDJVM +import net.corda.core.serialization.SerializableCalculatedProperty import net.corda.core.utilities.loggerFor import java.io.NotSerializableException import java.lang.reflect.Field @@ -19,9 +20,9 @@ abstract class PropertyReader { * Accessor for those properties of a class that have defined getter functions. */ @KeepForDJVM -class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() { +class PublicPropertyReader(private val readMethod: Method) : PropertyReader() { init { - readMethod?.isAccessible = true + readMethod.isAccessible = true } private fun Method.returnsNullable(): Boolean { @@ -47,10 +48,10 @@ class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() { } override fun read(obj: Any?): Any? { - return readMethod!!.invoke(obj) + return readMethod.invoke(obj) } - override fun isNullable(): Boolean = readMethod?.returnsNullable() ?: false + override fun isNullable(): Boolean = readMethod.returnsNullable() } /** @@ -112,7 +113,6 @@ class EvolutionPropertyReader : PropertyReader() { * making the property accessible. */ abstract class PropertyAccessor( - val initialPosition: Int, open val serializer: PropertySerializer) { companion object : Comparator { override fun compare(p0: PropertyAccessor?, p1: PropertyAccessor?): Int { @@ -120,13 +120,15 @@ abstract class PropertyAccessor( } } + open val isCalculated get() = false + /** * Override to control how the property is set on the object. */ abstract fun set(instance: Any, obj: Any?) override fun toString(): String { - return "${serializer.name}($initialPosition)" + return serializer.name } } @@ -135,9 +137,8 @@ abstract class PropertyAccessor( * is serialized and deserialized via JavaBean getter and setter style methods. */ class PropertyAccessorGetterSetter( - initialPosition: Int, getter: PropertySerializer, - private val setter: Method) : PropertyAccessor(initialPosition, getter) { + private val setter: Method) : PropertyAccessor(getter) { init { /** * Play nicely with Java interop, public methods aren't marked as accessible @@ -159,16 +160,34 @@ class PropertyAccessorGetterSetter( * of the object the property belongs to. */ class PropertyAccessorConstructor( - initialPosition: Int, - override val serializer: PropertySerializer) : PropertyAccessor(initialPosition, serializer) { + val initialPosition: Int, + override val serializer: PropertySerializer) : PropertyAccessor(serializer) { /** - * Because the property should be being set on the obejct through the constructor any + * Because the property should be being set on the object through the constructor any * calls to the explicit setter should be an error. */ override fun set(instance: Any, obj: Any?) { NotSerializableException("Attempting to access a setter on an object being instantiated " + "via its constructor.") } + + override fun toString(): String = + "${serializer.name}($initialPosition)" +} + +/** + * Implementation of [PropertyAccessor] representing a calculated property of an object that is serialized + * so that it can be used by the class carpenter, but ignored on deserialisation as there is no setter or + * constructor parameter to receive its value. + * + * This will only be created for calculated properties that are accessible via no-argument methods annotated + * with [SerializableCalculatedProperty]. + */ +class CalculatedPropertyAccessor(override val serializer: PropertySerializer): PropertyAccessor(serializer) { + override val isCalculated: Boolean + get() = true + + override fun set(instance: Any, obj: Any?) = Unit // do nothing, as it's a calculated value } /** @@ -186,7 +205,7 @@ abstract class PropertySerializers( val serializationOrder: List) { companion object { fun make(serializationOrder: List) = - when (serializationOrder.firstOrNull()) { + when (serializationOrder.find { !it.isCalculated }) { is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder) is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder) null -> PropertySerializersNoProperties() @@ -198,6 +217,7 @@ abstract class PropertySerializers( val size get() = serializationOrder.size abstract val byConstructor: Boolean + val deserializableSize = serializationOrder.count { !it.isCalculated } } class PropertySerializersNoProperties : PropertySerializers(emptyList()) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index f2820cc505..aed85b9825 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -3,10 +3,7 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken import net.corda.core.internal.isConcreteClass -import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.ConstructorForDeserialization -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.* import org.apache.qpid.proton.codec.Data import java.lang.reflect.* import java.lang.reflect.Field @@ -68,12 +65,33 @@ fun propertiesForSerialization( kotlinConstructor: KFunction?, type: Type, factory: SerializerFactory): PropertySerializers = PropertySerializers.make( - if (kotlinConstructor != null) { - propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) - } else { - propertiesForSerializationFromAbstract(type.asClass(), type, factory) - }.sortedWith(PropertyAccessor) - ) + getValueProperties(kotlinConstructor, type, factory) + .addCalculatedProperties(factory, type) + .sortedWith(PropertyAccessor)) + +fun getValueProperties(kotlinConstructor: KFunction?, type: Type, factory: SerializerFactory) + : List = + if (kotlinConstructor != null) { + propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) + } else { + propertiesForSerializationFromAbstract(type.asClass(), type, factory) + } + +private fun List.addCalculatedProperties(factory: SerializerFactory, type: Type) + : List { + val nonCalculated = map { it.serializer.name }.toSet() + return this + type.asClass().calculatedPropertyDescriptors().mapNotNull { (name, descriptor) -> + if (name in nonCalculated) null else { + val calculatedPropertyMethod = descriptor.getter + ?: throw IllegalStateException("Property $name is not a calculated property") + CalculatedPropertyAccessor(PropertySerializer.make( + name, + PublicPropertyReader(calculatedPropertyMethod), + calculatedPropertyMethod.genericReturnType, + factory)) + } + } +} /** * From a constructor, determine which properties of a class are to be serialized. @@ -145,7 +163,7 @@ fun propertiesForSerializationFromSetters( properties: Map, type: Type, factory: SerializerFactory): List = - properties.asSequence().withIndex().map { (index, entry) -> + properties.asSequence().map { entry -> val (name, property) = entry val getter = property.getter @@ -154,7 +172,6 @@ fun propertiesForSerializationFromSetters( if (getter == null || setter == null) return@map null PropertyAccessorGetterSetter( - index, PropertySerializer.make( name, PublicPropertyReader(getter), @@ -191,9 +208,9 @@ private fun propertiesForSerializationFromAbstract( clazz: Class<*>, type: Type, factory: SerializerFactory): List = - clazz.propertyDescriptors().asSequence().withIndex().map { (index, entry) -> + clazz.propertyDescriptors().asSequence().withIndex().mapNotNull { (index, entry) -> val (name, property) = entry - if (property.getter == null || property.field == null) return@map null + if (property.getter == null || property.field == null) return@mapNotNull null val getter = property.getter val returnType = resolveTypeVariables(getter.genericReturnType, type) @@ -201,7 +218,7 @@ private fun propertiesForSerializationFromAbstract( PropertyAccessorConstructor( index, PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory)) - }.filterNotNull().toList() + }.toList() internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List = exploreType(type, serializerFactory).toList() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 8c869e2eda..cfce4fa76e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -7,10 +7,7 @@ import net.corda.core.StubOutForDJVM import net.corda.core.internal.kotlinObjectInstance import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.ClassWhitelist -import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.debug -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.trace +import net.corda.core.utilities.* import net.corda.serialization.internal.carpenter.* import org.apache.qpid.proton.amqp.* import java.io.NotSerializableException @@ -30,7 +27,7 @@ data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type /** * Factory of serializers designed to be shared across threads and invocations. * - * @property evolutionSerializerGetter controls how evolution serializers are generated by the factory. The normal + * @property evolutionSerializerProvider controls how evolution serializers are generated by the factory. The normal * use case is an [EvolutionSerializer] type is returned. However, in some scenarios, primarily testing, this * can be altered to fit the requirements of the test. * @property onlyCustomSerializers used for testing, when set will cause the factory to throw a @@ -52,7 +49,7 @@ data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type open class SerializerFactory( val whitelist: ClassWhitelist, val classCarpenter: ClassCarpenter, - private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), + private val evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider, val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, private val serializersByType: MutableMap>, val serializersByDescriptor: MutableMap>, @@ -64,13 +61,13 @@ open class SerializerFactory( @DeleteForDJVM constructor(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter, - evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), + evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider, fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, classCarpenter, - evolutionSerializerGetter, + evolutionSerializerProvider, fingerPrinterConstructor, ConcurrentHashMap(), ConcurrentHashMap(), @@ -84,13 +81,13 @@ open class SerializerFactory( constructor(whitelist: ClassWhitelist, carpenterClassLoader: ClassLoader, lenientCarpenter: Boolean = false, - evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), + evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider, fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter), - evolutionSerializerGetter, + evolutionSerializerProvider, fingerPrinterConstructor, onlyCustomSerializers) @@ -98,12 +95,6 @@ open class SerializerFactory( val classloader: ClassLoader get() = classCarpenter.classloader - private fun getEvolutionSerializer(typeNotation: TypeNotation, - newSerializer: AMQPSerializer, - schemas: SerializationSchemas): AMQPSerializer { - return evolutionSerializerGetter.getEvolutionSerializer(this, typeNotation, newSerializer, schemas) - } - /** * Look up, and manufacture if necessary, a serializer for the given type. * @@ -117,11 +108,11 @@ open class SerializerFactory( val declaredClass = declaredType.asClass() val actualType: Type = if (actualClass == null) declaredType - else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType val serializer = when { - // Declared class may not be set to Collection, but actual class could be a collection. - // In this case use of CollectionSerializer is perfectly appropriate. + // Declared class may not be set to Collection, but actual class could be a collection. + // In this case use of CollectionSerializer is perfectly appropriate. (Collection::class.java.isAssignableFrom(declaredClass) || (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) && !EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { @@ -130,8 +121,8 @@ open class SerializerFactory( CollectionSerializer(declaredTypeAmended, this) } } - // Declared class may not be set to Map, but actual class could be a map. - // In this case use of MapSerializer is perfectly appropriate. + // Declared class may not be set to Map, but actual class could be a map. + // In this case use of MapSerializer is perfectly appropriate. (Map::class.java.isAssignableFrom(declaredClass) || (actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> { val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) @@ -142,8 +133,8 @@ open class SerializerFactory( Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { logger.trace { "class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + - "declaredType=${declaredType.typeName} " + - "isEnum=${declaredType::class.java.isEnum}" + "declaredType=${declaredType.typeName} " + + "isEnum=${declaredType::class.java.isEnum}" } serializersByType.computeIfAbsent(actualClass ?: declaredClass) { @@ -203,33 +194,86 @@ open class SerializerFactory( * if not, use the [ClassCarpenter] to generate a class to use in its place. */ private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) { - val metaSchema = CarpenterMetaSchema.newInstance() - for (typeNotation in schemaAndDescriptor.schemas.schema.types) { - logger.trace { "descriptor=${schemaAndDescriptor.typeDescriptor}, typeNotation=${typeNotation.name}" } + val requiringCarpentry = schemaAndDescriptor.schemas.schema.types.mapNotNull { typeNotation -> try { - val serialiser = processSchemaEntry(typeNotation) - // if we just successfully built a serializer for the type but the type fingerprint - // doesn't match that of the serialised object then we are dealing with different - // instance of the class, as such we need to build an EvolutionSerializer - if (serialiser.typeDescriptor != typeNotation.descriptor.name) { - logger.trace { "typeNotation=${typeNotation.name} action=\"requires Evolution\"" } - getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas) - } + getOrRegisterSerializer(schemaAndDescriptor, typeNotation) + return@mapNotNull null } catch (e: ClassNotFoundException) { if (sentinel) { logger.error("typeNotation=${typeNotation.name} error=\"after Carpentry attempt failed to load\"") throw e } - else { - logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" } - } - metaSchema.buildFor(typeNotation, classloader) + logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" } + return@mapNotNull typeNotation + } + }.toList() + + if (requiringCarpentry.isEmpty()) return + + runCarpentry(schemaAndDescriptor, CarpenterMetaSchema.buildWith(classloader, requiringCarpentry)) + } + + private fun getOrRegisterSerializer(schemaAndDescriptor: FactorySchemaAndDescriptor, typeNotation: TypeNotation) { + logger.trace { "descriptor=${schemaAndDescriptor.typeDescriptor}, typeNotation=${typeNotation.name}" } + val serialiser = processSchemaEntry(typeNotation) + + // if we just successfully built a serializer for the type but the type fingerprint + // doesn't match that of the serialised object then we may be dealing with different + // instance of the class, and such we need to build an EvolutionSerializer + if (serialiser.typeDescriptor == typeNotation.descriptor.name) return + + logger.trace { "typeNotation=${typeNotation.name} action=\"requires Evolution\"" } + evolutionSerializerProvider.getEvolutionSerializer(this, typeNotation, serialiser, schemaAndDescriptor.schemas) + } + + private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) { + // java.lang.Class (whether a class or interface) + is CompositeType -> { + logger.trace("typeNotation=${typeNotation.name} amqpType=CompositeType") + processCompositeType(typeNotation) + } + // Collection / Map, possibly with generics + is RestrictedType -> { + logger.trace("typeNotation=${typeNotation.name} amqpType=RestrictedType") + processRestrictedType(typeNotation) + } + } + + // TODO: class loader logic, and compare the schema. + private fun processRestrictedType(typeNotation: RestrictedType) = + get(null, typeForName(typeNotation.name, classloader)) + + private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer { + // TODO: class loader logic, and compare the schema. + val type = typeForName(typeNotation.name, classloader) + return get(type.asClass(), type) + } + + private fun typeForName(name: String, classloader: ClassLoader): Type = when { + name.endsWith("[]") -> { + val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader) + if (elementType is ParameterizedType || elementType is GenericArrayType) { + DeserializedGenericArrayType(elementType) + } else if (elementType is Class<*>) { + java.lang.reflect.Array.newInstance(elementType, 0).javaClass + } else { + throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name") } } - - if (metaSchema.isNotEmpty()) { - runCarpentry(schemaAndDescriptor, metaSchema) - } + name.endsWith("[p]") -> // There is no need to handle the ByteArray case as that type is coercible automatically + // to the binary type and is thus handled by the main serializer and doesn't need a + // special case for a primitive array of bytes + when (name) { + "int[p]" -> IntArray::class.java + "char[p]" -> CharArray::class.java + "boolean[p]" -> BooleanArray::class.java + "float[p]" -> FloatArray::class.java + "double[p]" -> DoubleArray::class.java + "short[p]" -> ShortArray::class.java + "long[p]" -> LongArray::class.java + else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name") + } + else -> DeserializedParameterizedType.make(name, classloader) } @StubOutForDJVM @@ -251,31 +295,6 @@ open class SerializerFactory( processSchema(schemaAndDescriptor, true) } - private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) { - // java.lang.Class (whether a class or interface) - is CompositeType -> { - logger.trace("typeNotation=${typeNotation.name} amqpType=CompositeType") - processCompositeType(typeNotation) - } - // Collection / Map, possibly with generics - is RestrictedType -> { - logger.trace("typeNotation=${typeNotation.name} amqpType=RestrictedType") - processRestrictedType(typeNotation) - } - } - - // TODO: class loader logic, and compare the schema. - private fun processRestrictedType(typeNotation: RestrictedType) = get(null, - typeForName(typeNotation.name, classloader)) - - private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer { - // TODO: class loader logic, and compare the schema. - val type = typeForName(typeNotation.name, classloader) - return get( - type.asClass(), - type) - } - private fun makeClassSerializer( clazz: Class<*>, type: Type, @@ -352,6 +371,9 @@ open class SerializerFactory( return MapSerializer(declaredType, this) } + fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer): AMQPSerializer = + serializersByDescriptor.computeIfAbsent(name) { _ -> serializerCreator() } + companion object { private val logger = contextLogger() @@ -405,35 +427,6 @@ open class SerializerFactory( is TypeVariable<*> -> "?" else -> throw AMQPNotSerializableException(type, "Unable to render type $type to a string.") } - - private fun typeForName(name: String, classloader: ClassLoader): Type { - return if (name.endsWith("[]")) { - val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader) - if (elementType is ParameterizedType || elementType is GenericArrayType) { - DeserializedGenericArrayType(elementType) - } else if (elementType is Class<*>) { - java.lang.reflect.Array.newInstance(elementType, 0).javaClass - } else { - throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name") - } - } else if (name.endsWith("[p]")) { - // There is no need to handle the ByteArray case as that type is coercible automatically - // to the binary type and is thus handled by the main serializer and doesn't need a - // special case for a primitive array of bytes - when (name) { - "int[p]" -> IntArray::class.java - "char[p]" -> CharArray::class.java - "boolean[p]" -> BooleanArray::class.java - "float[p]" -> FloatArray::class.java - "double[p]" -> DoubleArray::class.java - "short[p]" -> ShortArray::class.java - "long[p]" -> LongArray::class.java - else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name") - } - } else { - DeserializedParameterizedType.make(name, classloader) - } - } } object AnyType : WildcardType { @@ -443,4 +436,4 @@ open class SerializerFactory( override fun toString(): String = "?" } -} +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt index c85bb6ebd6..b5e6593286 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt @@ -32,6 +32,11 @@ data class CarpenterMetaSchema( val dependencies: MutableMap>>, val dependsOn: MutableMap>) { companion object CarpenterSchemaConstructor { + fun buildWith(classLoader: ClassLoader, types: List) = + newInstance().apply { + types.forEach { buildFor(it, classLoader) } + } + fun newInstance(): CarpenterMetaSchema { return CarpenterMetaSchema(mutableListOf(), mutableMapOf(), mutableMapOf()) } diff --git a/serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java b/serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java new file mode 100644 index 0000000000..1171d7713d --- /dev/null +++ b/serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java @@ -0,0 +1,107 @@ +package net.corda.serialization.internal.carpenter; + +import net.corda.core.serialization.SerializableCalculatedProperty; +import net.corda.core.serialization.SerializationContext; +import net.corda.core.serialization.SerializationFactory; +import net.corda.core.serialization.SerializedBytes; +import net.corda.serialization.internal.AllWhitelist; +import net.corda.serialization.internal.amqp.*; +import net.corda.serialization.internal.amqp.Schema; +import net.corda.testing.core.SerializationEnvironmentRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static java.util.Collections.singletonList; +import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactoryNoEvolution; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class JavaCalculatedValuesToClassCarpenterTest extends AmqpCarpenterBase { + public JavaCalculatedValuesToClassCarpenterTest() { + super(AllWhitelist.INSTANCE); + } + + public interface Parent { + @SerializableCalculatedProperty + int getDoubled(); + } + + public static final class C implements Parent { + private final int i; + + public C(int i) { + this.i = i; + } + + @SerializableCalculatedProperty + public String getSquared() { + return Integer.toString(i * i); + } + + @Override + public int getDoubled() { + return i * 2; + } + + public int getI() { + return i; + } + } + + @Rule + public final SerializationEnvironmentRule serializationEnvironmentRule = new SerializationEnvironmentRule(); + private SerializationContext context; + + @Before + public void initSerialization() { + SerializationFactory factory = serializationEnvironmentRule.getSerializationFactory(); + context = factory.getDefaultContext(); + } + + @Test + public void calculatedValues() throws Exception { + SerializerFactory factory = testDefaultFactoryNoEvolution(); + SerializedBytes serialized = serialise(new C(2)); + ObjectAndEnvelope objAndEnv = new DeserializationInput(factory) + .deserializeAndReturnEnvelope(serialized, C.class, context); + + C amqpObj = objAndEnv.getObj(); + Schema schema = objAndEnv.getEnvelope().getSchema(); + + assertEquals(2, amqpObj.getI()); + assertEquals("4", amqpObj.getSquared()); + assertEquals(2, schema.getTypes().size()); + assertTrue(schema.getTypes().get(0) instanceof CompositeType); + + CompositeType concrete = (CompositeType) schema.getTypes().get(0); + assertEquals(3, concrete.getFields().size()); + assertEquals("doubled", concrete.getFields().get(0).getName()); + assertEquals("int", concrete.getFields().get(0).getType()); + assertEquals("i", concrete.getFields().get(1).getName()); + assertEquals("int", concrete.getFields().get(1).getType()); + assertEquals("squared", concrete.getFields().get(2).getName()); + assertEquals("string", concrete.getFields().get(2).getType()); + + assertEquals(0, AMQPSchemaExtensions.carpenterSchema(schema, ClassLoader.getSystemClassLoader()).getSize()); + Schema mangledSchema = ClassCarpenterTestUtilsKt.mangleNames(schema, singletonList(C.class.getTypeName())); + CarpenterMetaSchema l2 = AMQPSchemaExtensions.carpenterSchema(mangledSchema, ClassLoader.getSystemClassLoader()); + String mangledClassName = ClassCarpenterTestUtilsKt.mangleName(C.class.getTypeName()); + + assertEquals(1, l2.getSize()); + net.corda.serialization.internal.carpenter.Schema carpenterSchema = l2.getCarpenterSchemas().stream() + .filter(s -> s.getName().equals(mangledClassName)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No schema found for mangled class name " + mangledClassName)); + + Class pinochio = new ClassCarpenterImpl(AllWhitelist.INSTANCE).build(carpenterSchema); + Object p = pinochio.getConstructors()[0].newInstance(4, 2, "4"); + + assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.getI()); + assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.getSquared()); + assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.getDoubled()); + + Parent upcast = (Parent) p; + assertEquals(upcast.getDoubled(), amqpObj.getDoubled()); + } +} diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerGetterTesting.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerProviderTesting.kt similarity index 85% rename from serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerGetterTesting.kt rename to serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerProviderTesting.kt index 74ab702687..7239e6a467 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerGetterTesting.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerProviderTesting.kt @@ -3,12 +3,12 @@ package net.corda.serialization.internal.amqp import java.io.NotSerializableException /** - * An implementation of [EvolutionSerializerGetterBase] that disables all evolution within a + * An implementation of [EvolutionSerializerProvider] that disables all evolution within a * [SerializerFactory]. This is most useful in testing where it is known that evolution should not be * occurring and where bugs may be hidden by transparent invocation of an [EvolutionSerializer]. This * prevents that by simply throwing an exception whenever such a serializer is requested. */ -class EvolutionSerializerGetterTesting : EvolutionSerializerGetterBase() { +object FailIfEvolutionAttempted : EvolutionSerializerProvider { override fun getEvolutionSerializer(factory: SerializerFactory, typeNotation: TypeNotation, newSerializer: AMQPSerializer, diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt index 023f291394..6d88f3ccf2 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt @@ -42,7 +42,7 @@ class FingerPrinterTestingTests { val factory = SerializerFactory( AllWhitelist, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter = EvolutionSerializerGetterTesting(), + evolutionSerializerProvider = FailIfEvolutionAttempted, fingerPrinterConstructor = { _ -> FingerPrinterTesting() }) val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L)) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/RoundTripTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/RoundTripTests.kt index 381032cfbd..5f9ee087b3 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/RoundTripTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/RoundTripTests.kt @@ -1,6 +1,7 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.ConstructorForDeserialization +import net.corda.core.serialization.SerializableCalculatedProperty import net.corda.serialization.internal.amqp.testutils.deserialize import net.corda.serialization.internal.amqp.testutils.serialize import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution @@ -61,4 +62,62 @@ class RoundTripTests { val newC = DeserializationInput(factory).deserialize(bytes) newC.copy(l = (newC.l + "d")) } + + @Test + fun calculatedValues() { + data class C(val i: Int) { + @get:SerializableCalculatedProperty + val squared = i * i + } + + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(C(2)) + val deserialized = DeserializationInput(factory).deserialize(bytes) + assertThat(deserialized.squared).isEqualTo(4) + } + + @Test + fun calculatedFunction() { + class C { + var i: Int = 0 + @SerializableCalculatedProperty + fun getSquared() = i * i + } + + val instance = C().apply { i = 2 } + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(instance) + val deserialized = DeserializationInput(factory).deserialize(bytes) + assertThat(deserialized.getSquared()).isEqualTo(4) + } + + interface I { + @get:SerializableCalculatedProperty + val squared: Int + } + + @Test + fun inheritedCalculatedFunction() { + class C: I { + var i: Int = 0 + override val squared get() = i * i + } + + val instance = C().apply { i = 2 } + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(instance) + val deserialized = DeserializationInput(factory).deserialize(bytes) as I + assertThat(deserialized.squared).isEqualTo(4) + } + + @Test + fun inheritedCalculatedFunctionIsNotCalculated() { + class C(override val squared: Int): I + + val instance = C(2) + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(instance) + val deserialized = DeserializationInput(factory).deserialize(bytes) as I + assertThat(deserialized.squared).isEqualTo(2) + } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt index 37bf6e8b47..a0259ca36c 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt @@ -210,7 +210,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi return SerializerFactory( AllWhitelist, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter = EvolutionSerializerGetterTesting() + evolutionSerializerProvider = FailIfEvolutionAttempted ) } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt index e3820e2d6b..ea81448da7 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt @@ -23,7 +23,7 @@ fun testDefaultFactoryNoEvolution(): SerializerFactory { return SerializerFactory( AllWhitelist, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter = EvolutionSerializerGetterTesting()) + evolutionSerializerProvider = FailIfEvolutionAttempted) } fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader()) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt new file mode 100644 index 0000000000..893f81833b --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt @@ -0,0 +1,101 @@ +package net.corda.serialization.internal.carpenter + +import net.corda.core.serialization.SerializableCalculatedProperty +import net.corda.serialization.internal.AllWhitelist +import net.corda.serialization.internal.amqp.CompositeType +import net.corda.serialization.internal.amqp.DeserializationInput +import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope +import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution +import org.junit.Test +import kotlin.test.assertEquals + +class CalculatedValuesToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { + + interface Parent { + @get:SerializableCalculatedProperty + val doubled: Int + } + + @Test + fun calculatedValues() { + data class C(val i: Int): Parent { + @get:SerializableCalculatedProperty + val squared = (i * i).toString() + + override val doubled get() = i * 2 + } + + val factory = testDefaultFactoryNoEvolution() + val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(2))) + val amqpObj = obj.obj + val serSchema = obj.envelope.schema + + assertEquals(2, amqpObj.i) + assertEquals("4", amqpObj.squared) + assertEquals(2, serSchema.types.size) + require(serSchema.types[0] is CompositeType) + + val concrete = serSchema.types[0] as CompositeType + assertEquals(3, concrete.fields.size) + assertEquals("doubled", concrete.fields[0].name) + assertEquals("int", concrete.fields[0].type) + assertEquals("i", concrete.fields[1].name) + assertEquals("int", concrete.fields[1].type) + assertEquals("squared", concrete.fields[2].name) + assertEquals("string", concrete.fields[2].type) + + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) + assertEquals(0, l1.size) + val mangleSchema = serSchema.mangleNames(listOf((classTestName("C")))) + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) + val aName = mangleName(classTestName("C")) + + assertEquals(1, l2.size) + val aSchema = l2.carpenterSchemas.find { it.name == aName }!! + + val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) + val p = pinochio.constructors[0].newInstance(4, 2, "4") + + assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.i) + assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.squared) + assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled) + + val upcast = p as Parent + assertEquals(upcast.doubled, amqpObj.doubled) + } + + @Test + fun implementingClassDoesNotCalculateValue() { + class C(override val doubled: Int): Parent + + val factory = testDefaultFactoryNoEvolution() + val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(5))) + val amqpObj = obj.obj + val serSchema = obj.envelope.schema + + assertEquals(2, serSchema.types.size) + require(serSchema.types[0] is CompositeType) + + val concrete = serSchema.types[0] as CompositeType + assertEquals(1, concrete.fields.size) + assertEquals("doubled", concrete.fields[0].name) + assertEquals("int", concrete.fields[0].type) + + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) + assertEquals(0, l1.size) + val mangleSchema = serSchema.mangleNames(listOf((classTestName("C")))) + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) + val aName = mangleName(classTestName("C")) + + assertEquals(1, l2.size) + val aSchema = l2.carpenterSchemas.find { it.name == aName }!! + + val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) + val p = pinochio.constructors[0].newInstance(5) + + assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled) + + val upcast = p as Parent + assertEquals(upcast.doubled, amqpObj.doubled) + } +} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt index 22f93a5b16..5c3701dc49 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt @@ -1,6 +1,7 @@ package net.corda.serialization.internal.carpenter import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializedBytes import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.amqp.Field import net.corda.serialization.internal.amqp.Schema @@ -47,7 +48,7 @@ open class AmqpCarpenterBase(whitelist: ClassWhitelist) { var cc = ClassCarpenterImpl(whitelist = whitelist) var factory = SerializerFactoryExternalCarpenter(cc) - fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) + fun serialise(obj: T): SerializedBytes = SerializationOutput(factory).serialize(obj) @Suppress("NOTHING_TO_INLINE") inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt index cca81755e9..5e5b262a6f 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt @@ -30,9 +30,7 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { val b = B(A(testA), testB) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - require(obj.obj is B) - - val amqpObj = obj.obj as B + val amqpObj = obj.obj assertEquals(testB, amqpObj.b) assertEquals(testA, amqpObj.a.a) @@ -92,8 +90,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"))) - require(obj.obj is B) - amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) } @@ -111,8 +107,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { val b = B(A(testA), testB) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - require(obj.obj is B) - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B"))) val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) @@ -138,9 +132,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { val b = B(A(testA), testB) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - - require(obj.obj is B) - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) @@ -198,8 +189,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { val c = C(B(testA, testB), testC) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) - require(obj.obj is C) - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) @@ -224,8 +213,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { val c = C(B(testA, testB), testC) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) - require(obj.obj is C) - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) @@ -250,8 +237,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { val c = C(B(testA, testB), testC) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) - require(obj.obj is C) - val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) TestMetaCarpenter(carpenterSchema.carpenterSchema( ClassLoader.getSystemClassLoader()), ClassCarpenterImpl(whitelist = AllWhitelist)) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt index b7e09664f6..6a1a2c6e3e 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt @@ -44,7 +44,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { assertEquals(testJ, a.j) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - assertTrue(obj.obj is A) val serSchema = obj.envelope.schema assertEquals(2, serSchema.types.size) val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) @@ -84,8 +83,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - assertTrue(obj.obj is A) - val serSchema = obj.envelope.schema assertEquals(2, serSchema.types.size) @@ -128,8 +125,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { val a = A(testI, testII) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - assertTrue(obj.obj is A) - val serSchema = obj.envelope.schema assertEquals(3, serSchema.types.size) @@ -176,8 +171,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { val a = A(testI, testIII) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - assertTrue(obj.obj is A) - val serSchema = obj.envelope.schema assertEquals(3, serSchema.types.size) @@ -225,8 +218,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { val b = B(a, testIIII) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - assertTrue(obj.obj is B) - val serSchema = obj.envelope.schema // Expected classes are @@ -281,8 +272,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { val b = B(a, testIIII) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - assertTrue(obj.obj is B) - val serSchema = obj.envelope.schema // The classes we're expecting to find: @@ -305,8 +294,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { val a = A(testI) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - assertTrue(obj.obj is A) - val serSchema = obj.envelope.schema // The classes we're expecting to find: diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt index 8b50f9d996..3a560566bb 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt @@ -21,8 +21,7 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhi val a = A(testA, testB) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(testA, amqpObj.a) assertEquals(testB, amqpObj.b) @@ -65,8 +64,7 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhi val a = A(testA, testB) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(testA, amqpObj.a) assertEquals(testB, amqpObj.b) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt index abcf831091..5642b0b824 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt @@ -17,9 +17,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh val test = 10 val a = A(test) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(test, amqpObj.a) assertEquals(1, obj.envelope.schema.types.size) @@ -53,9 +51,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh val a = A(test) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(test, amqpObj.a) assertEquals(1, obj.envelope.schema.types.size) @@ -83,9 +79,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh val test = 10L val a = A(test) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(test, amqpObj.a) assertEquals(1, obj.envelope.schema.types.size) @@ -118,9 +112,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh val test = 10.toShort() val a = A(test) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(test, amqpObj.a) assertEquals(1, obj.envelope.schema.types.size) @@ -153,9 +145,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh val test = 10.0 val a = A(test) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(test, amqpObj.a) assertEquals(1, obj.envelope.schema.types.size) @@ -188,9 +178,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh val test = 10.0F val a = A(test) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - require(obj.obj is A) - val amqpObj = obj.obj as A + val amqpObj = obj.obj assertEquals(test, amqpObj.a) assertEquals(1, obj.envelope.schema.types.size) From 9ebeac1ad87448a17e45519161e76e756ea0f854 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 10 Oct 2018 10:04:22 +0100 Subject: [PATCH 31/83] CORDA-535: Extract notary implementations into CorDapps (#3978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move Raft and BFT notaries into separate modules * Move schemas * Fix tests & demos * Modified logic for creating notary services: Added a new field 'className' to the notary configuration. The node now loads the specified implementation via reflection. The default className value points to the simple notary implementation for backwards compatibility. Relevant schemas are loaded in a similar fashion. For backwards compatibility purposes the default SimpleNotaryService will remain built-in to node, but its cordapp will be generated on startup – so the loading of notary services is streamlined. * Move test namedcache factory to test utils --- .idea/compiler.xml | 6 +- build.gradle | 4 +- .../kotlin/net/corda/core/cordapp/Cordapp.kt | 2 + .../core/internal/cordapp/CordappImpl.kt | 13 ++- .../net/corda/core/flows/AttachmentTests.kt | 2 +- .../AttachmentSerializationTest.kt | 2 +- experimental/notary-bft-smart/build.gradle | 35 ++++++++ .../net/corda/notary/bftsmart}/BFTSMaRt.kt | 11 +-- .../corda/notary/bftsmart}/BFTSMaRtConfig.kt | 3 +- .../notary/bftsmart/BftSmartNotaryService.kt | 28 +++++-- .../net/corda/notary/bftsmart/Schema.kt | 20 +++++ .../notary-bft-smart.changelog-init.xml | 45 +++++++++++ .../notary-bft-smart.changelog-master.xml | 9 +++ .../notary-bft-smart.changelog-pkey.xml | 11 +++ .../notary-bft-smart.changelog-v1.xml | 10 +++ .../notary/bftsmart}/system.config.printf | 0 .../notary/bftsmart}/BFTNotaryServiceTests.kt | 10 ++- .../notary/bftsmart}/BFTSMaRtConfigTests.kt | 4 +- experimental/notary-raft/build.gradle | 35 ++++++++ .../corda/notary/raft/RaftNotaryService.kt | 46 +++++++++++ .../notary/raft}/RaftTransactionCommitLog.kt | 9 ++- .../notary/raft}/RaftUniquenessProvider.kt | 4 +- .../kotlin/net/corda/notary/raft/Schema.kt | 19 +++++ .../migration/notary-raft.changelog-init.xml | 46 +++++++++++ .../notary-raft.changelog-master.xml | 11 +++ .../migration/notary-raft.changelog-pkey.xml | 11 +++ .../migration/notary-raft.changelog-v1.xml | 11 +++ .../notary/raft}/RaftNotaryServiceTests.kt | 2 +- .../raft}/RaftTransactionCommitLogTests.kt | 6 +- node/build.gradle | 9 --- .../distributed/DistributedServiceTests.kt | 2 +- .../messaging/ArtemisMessagingTest.kt | 2 +- .../network/PersistentNetworkMapCacheTest.kt | 2 +- .../services/messaging/P2PMessagingTest.kt | 1 + .../net/corda/node/internal/AbstractNode.kt | 81 ++++++++++--------- .../kotlin/net/corda/node/internal/Node.kt | 8 -- .../net/corda/node/internal/NodeStartup.kt | 14 +++- .../cordapp/JarScanningCordappLoader.kt | 69 ++++++++-------- .../node/internal/cordapp/VirtualCordapps.kt | 60 ++++++++++++++ .../node/services/config/NodeConfiguration.kt | 3 +- .../node/services/schema/NodeSchemaService.kt | 20 +---- .../RaftNonValidatingNotaryService.kt | 26 ------ .../RaftValidatingNotaryService.kt | 26 ------ .../transactions/SimpleNotaryService.kt | 27 ++++++- .../transactions/ValidatingNotaryService.kt | 17 ---- .../migration/node-notary.changelog-init.xml | 21 +---- .../migration/node-notary.changelog-pkey.xml | 5 -- .../migration/node-notary.changelog-v1.xml | 4 - .../cordapp/JarScanningCordappLoaderTest.kt | 24 +++--- .../node/messaging/TwoPartyTradeFlowTests.kt | 21 ++--- .../net/corda/node/services/TimedFlowTests.kt | 5 +- .../PersistentIdentityServiceTests.kt | 2 +- .../AppendOnlyPersistentMapTest.kt | 2 +- .../persistence/DBTransactionStorageTests.kt | 5 +- .../HibernateColumnConverterTests.kt | 2 +- .../persistence/NodeAttachmentServiceTest.kt | 2 +- .../services/schema/NodeSchemaServiceTest.kt | 12 +-- .../transactions/MaxTransactionSizeTests.kt | 2 +- .../PersistentUniquenessProviderTests.kt | 4 +- .../vault/VaultSoftLockManagerTest.kt | 2 +- samples/notary-demo/build.gradle | 57 ++++++------- settings.gradle | 2 + testing/node-driver/build.gradle | 2 + .../testing/node/internal/DriverDSLImpl.kt | 1 + .../node/internal/InternalMockNetwork.kt | 39 +++------ .../internal}/TestingNamedCacheFactory.kt | 3 +- 66 files changed, 637 insertions(+), 362 deletions(-) create mode 100644 experimental/notary-bft-smart/build.gradle rename {node/src/main/kotlin/net/corda/node/services/transactions => experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart}/BFTSMaRt.kt (97%) rename {node/src/main/kotlin/net/corda/node/services/transactions => experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart}/BFTSMaRtConfig.kt (97%) rename node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt => experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt (87%) create mode 100644 experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/Schema.kt create mode 100644 experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-init.xml create mode 100644 experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-master.xml create mode 100644 experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-pkey.xml create mode 100644 experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-v1.xml rename {node/src/main/resources/net/corda/node/services/transactions => experimental/notary-bft-smart/src/main/resources/net/corda/notary/bftsmart}/system.config.printf (100%) rename {node/src/integration-test/kotlin/net/corda/node/services => experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart}/BFTNotaryServiceTests.kt (96%) rename {node/src/test/kotlin/net/corda/node/services/transactions => experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart}/BFTSMaRtConfigTests.kt (92%) create mode 100644 experimental/notary-raft/build.gradle create mode 100644 experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt rename {node/src/main/kotlin/net/corda/node/services/transactions => experimental/notary-raft/src/main/kotlin/net/corda/notary/raft}/RaftTransactionCommitLog.kt (96%) rename {node/src/main/kotlin/net/corda/node/services/transactions => experimental/notary-raft/src/main/kotlin/net/corda/notary/raft}/RaftUniquenessProvider.kt (98%) create mode 100644 experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/Schema.kt create mode 100644 experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-init.xml create mode 100644 experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-master.xml create mode 100644 experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-pkey.xml create mode 100644 experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-v1.xml rename {node/src/integration-test/kotlin/net/corda/node/services => experimental/notary-raft/src/test/kotlin/net/corda/notary/raft}/RaftNotaryServiceTests.kt (99%) rename {node/src/integration-test/kotlin/net/corda/node/services/transactions => experimental/notary-raft/src/test/kotlin/net/corda/notary/raft}/RaftTransactionCommitLogTests.kt (98%) create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt rename {node/src/test/kotlin/net/corda/node/utilities => testing/test-utils/src/main/kotlin/net/corda/testing/internal}/TestingNamedCacheFactory.kt (95%) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index d981d5fd35..4cfe9a6d84 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -157,8 +157,12 @@ + + + + @@ -235,4 +239,4 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 36872b5fef..f3cc3c97ca 100644 --- a/build.gradle +++ b/build.gradle @@ -361,7 +361,9 @@ bintrayConfig { 'corda-tools-blob-inspector', 'corda-tools-explorer', 'corda-tools-network-bootstrapper', - 'corda-tools-cliutils' + 'corda-tools-cliutils', + 'corda-notary-raft', + 'corda-notary-bft-smart' ] license { name = 'Apache-2.0' diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index ddf369b668..9ca4e04e36 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -4,6 +4,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.internal.notary.NotaryService import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist @@ -48,4 +49,5 @@ interface Cordapp { val jarPath: URL val cordappClasses: List val jarHash: SecureHash.SHA256 + val notaryService: Class? } diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 2e1761afa8..8ab16a5190 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -4,6 +4,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.toPath import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationCustomSerializer @@ -25,7 +26,10 @@ data class CordappImpl( override val allFlows: List>>, override val jarPath: URL, val info: Info, - override val jarHash: SecureHash.SHA256) : Cordapp { + override val jarHash: SecureHash.SHA256, + override val notaryService: Class? = null, + /** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */ + val isLoaded: Boolean = true) : Cordapp { override val name: String = jarName(jarPath) companion object { @@ -37,7 +41,10 @@ data class CordappImpl( * * TODO: Also add [SchedulableFlow] as a Cordapp class */ - override val cordappClasses: List = (rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames + override val cordappClasses: List = run { + val classList = rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass } + notaryService + classList.mapNotNull { it?.name } + contractClassNames + } // TODO Why a seperate Info class and not just have the fields directly in CordappImpl? data class Info(val shortName: String, val vendor: String, val version: String, val minimumPlatformVersion: Int, val targetPlatformVersion: Int) { @@ -48,4 +55,4 @@ data class CordappImpl( fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, version).any { it == UNKNOWN_VALUE } } -} +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index eaabb2d987..e2a2e22b71 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -128,7 +128,7 @@ class AttachmentTests : WithMockNet { // Makes a node that doesn't do sanity checking at load time. private fun makeBadNode(name: CordaX500Name) = mockNet.createNode( InternalMockNodeParameters(legalName = makeUnique(name)), - nodeFactory = { args, _ -> + nodeFactory = { args -> object : InternalMockNetwork.MockNode(args) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 43c35ca0dd..3ba769ce88 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -161,7 +161,7 @@ class AttachmentSerializationTest { } private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { - client = mockNet.restartNode(client) { args, _ -> + client = mockNet.restartNode(client) { args -> object : InternalMockNetwork.MockNode(args) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad } } diff --git a/experimental/notary-bft-smart/build.gradle b/experimental/notary-bft-smart/build.gradle new file mode 100644 index 0000000000..b4ff48b444 --- /dev/null +++ b/experimental/notary-bft-smart/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'kotlin' +apply plugin: 'kotlin-jpa' +apply plugin: 'idea' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.publish-utils' + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + cordaCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + // Corda integration dependencies + cordaCompile project(':node') + + // BFT-SMaRt + compile 'commons-codec:commons-codec:1.10' + compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' + + testCompile "junit:junit:$junit_version" + testCompile project(':node-driver') +} + +idea { + module { + downloadJavadoc = true // defaults to false + downloadSources = true + } +} + +publish { + name 'corda-notary-bft-smart' +} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRt.kt similarity index 97% rename from node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt rename to experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRt.kt index 885746ecb6..a825b3c1a5 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRt.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.transactions +package net.corda.notary.bftsmart import bftsmart.communication.ServerCommunicationSystem import bftsmart.communication.client.netty.NettyClientServerCommunicationSystemClientSide @@ -34,10 +34,11 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.ServiceHubInternal -import net.corda.node.services.transactions.BFTSMaRt.Client -import net.corda.node.services.transactions.BFTSMaRt.Replica +import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.currentDBSession +import net.corda.notary.bftsmart.BFTSMaRt.Client +import net.corda.notary.bftsmart.BFTSMaRt.Replica import java.nio.file.Path import java.security.PublicKey import java.util.* @@ -78,7 +79,7 @@ object BFTSMaRt { fun waitUntilAllReplicasHaveInitialized() } - class Client(config: BFTSMaRtConfig, private val clientId: Int, private val cluster: Cluster, private val notaryService: BFTNonValidatingNotaryService) : SingletonSerializeAsToken() { + class Client(config: BFTSMaRtConfig, private val clientId: Int, private val cluster: Cluster, private val notaryService: BftSmartNotaryService) : SingletonSerializeAsToken() { companion object { private val log = contextLogger() } @@ -178,7 +179,7 @@ object BFTSMaRt { abstract class Replica(config: BFTSMaRtConfig, replicaId: Int, createMap: () -> AppendOnlyPersistentMap, + BftSmartNotaryService.CommittedState, PersistentStateRef>, protected val services: ServiceHubInternal, protected val notaryIdentityKey: PublicKey) : DefaultRecoverable() { companion object { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt similarity index 97% rename from node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt rename to experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt index e642dc858f..489f190d6f 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt @@ -1,10 +1,11 @@ -package net.corda.node.services.transactions +package net.corda.notary.bftsmart import net.corda.core.internal.div import net.corda.core.internal.writer import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.node.services.transactions.PathManager import java.io.PrintWriter import java.net.InetAddress import java.net.Socket diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt similarity index 87% rename from node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt rename to experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt index 156640c443..57560699cb 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.transactions +package net.corda.notary.bftsmart import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.SettableFuture @@ -10,6 +10,7 @@ import net.corda.core.identity.Party import net.corda.core.internal.notary.NotaryInternalException import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.verifySignature +import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -21,6 +22,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.config.BFTSMaRtConfiguration +import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import java.security.PublicKey @@ -33,16 +35,30 @@ import kotlin.concurrent.thread * * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity. */ -class BFTNonValidatingNotaryService( +class BftSmartNotaryService( override val services: ServiceHubInternal, - override val notaryIdentityKey: PublicKey, - private val bftSMaRtConfig: BFTSMaRtConfiguration, - cluster: BFTSMaRt.Cluster + override val notaryIdentityKey: PublicKey ) : NotaryService() { companion object { private val log = contextLogger() } + private val notaryConfig = services.configuration.notary + ?: throw IllegalArgumentException("Failed to register ${this::class.java}: notary configuration not present") + + private val bftSMaRtConfig = notaryConfig.bftSMaRt + ?: throw IllegalArgumentException("Failed to register ${this::class.java}: raft configuration not present") + + private val cluster: BFTSMaRt.Cluster = makeBFTCluster(notaryIdentityKey, bftSMaRtConfig) + + protected open fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster { + return object : BFTSMaRt.Cluster { + override fun waitUntilAllReplicasHaveInitialized() { + log.warn("A BFT replica may still be initializing, in which case the upcoming consensus change may cause it to spin.") + } + } + } + private val client: BFTSMaRt.Client private val replicaHolder = SettableFuture.create() @@ -71,7 +87,7 @@ class BFTNonValidatingNotaryService( override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ServiceFlow(otherPartySession, this) - private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic() { + private class ServiceFlow(val otherSideSession: FlowSession, val service: BftSmartNotaryService) : FlowLogic() { @Suspendable override fun call(): Void? { val payload = otherSideSession.receive().unwrap { it } diff --git a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/Schema.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/Schema.kt new file mode 100644 index 0000000000..360c1af5df --- /dev/null +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/Schema.kt @@ -0,0 +1,20 @@ +package net.corda.notary.bftsmart + +import net.corda.core.schemas.MappedSchema +import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.notary.bftsmart.BftSmartNotaryService + +object BftSmartNotarySchema + +object BftSmartNotarySchemaV1 : MappedSchema( + schemaFamily = BftSmartNotarySchema.javaClass, + version = 1, + mappedTypes = listOf( + PersistentUniquenessProvider.BaseComittedState::class.java, + PersistentUniquenessProvider.Request::class.java, + BftSmartNotaryService.CommittedState::class.java + ) +) { + override val migrationResource: String? + get() = "notary-bft-smart.changelog-master" +} \ No newline at end of file diff --git a/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-init.xml b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-init.xml new file mode 100644 index 0000000000..1b8dbf7464 --- /dev/null +++ b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-init.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-master.xml b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-master.xml new file mode 100644 index 0000000000..fdf0632cf8 --- /dev/null +++ b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-master.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-pkey.xml b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-pkey.xml new file mode 100644 index 0000000000..59a5b1fb50 --- /dev/null +++ b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-pkey.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-v1.xml b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-v1.xml new file mode 100644 index 0000000000..57b9adbf93 --- /dev/null +++ b/experimental/notary-bft-smart/src/main/resources/migration/notary-bft-smart.changelog-v1.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/node/src/main/resources/net/corda/node/services/transactions/system.config.printf b/experimental/notary-bft-smart/src/main/resources/net/corda/notary/bftsmart/system.config.printf similarity index 100% rename from node/src/main/resources/net/corda/node/services/transactions/system.config.printf rename to experimental/notary-bft-smart/src/main/resources/net/corda/notary/bftsmart/system.config.printf diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTNotaryServiceTests.kt similarity index 96% rename from node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt rename to experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTNotaryServiceTests.kt index c906474274..1924470588 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTNotaryServiceTests.kt @@ -1,4 +1,4 @@ -package net.corda.node.services +package net.corda.notary.bftsmart import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever @@ -23,8 +23,6 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig -import net.corda.node.services.transactions.minClusterSize -import net.corda.node.services.transactions.minCorrectReplicas import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters @@ -84,7 +82,11 @@ class BFTNotaryServiceTests { val nodes = replicaIds.map { replicaId -> mockNet.createUnstartedNode(InternalMockNodeParameters(configOverrides = { - val notary = NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces)) + val notary = NotaryConfig( + validating = false, + bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces), + className = "net.corda.notary.bftsmart.BftSmartNotaryService" + ) doReturn(notary).whenever(it).notary })) } + mockNet.createUnstartedNode() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt b/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfigTests.kt similarity index 92% rename from node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt rename to experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfigTests.kt index 1837953266..cdf5364b83 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt +++ b/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfigTests.kt @@ -1,7 +1,7 @@ -package net.corda.node.services.transactions +package net.corda.notary.bftsmart import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.services.transactions.BFTSMaRtConfig.Companion.portIsClaimedFormat +import net.corda.notary.bftsmart.BFTSMaRtConfig.Companion.portIsClaimedFormat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import kotlin.test.assertEquals diff --git a/experimental/notary-raft/build.gradle b/experimental/notary-raft/build.gradle new file mode 100644 index 0000000000..b07659ef2f --- /dev/null +++ b/experimental/notary-raft/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'kotlin' +apply plugin: 'idea' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.publish-utils' + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + cordaCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + // Corda integration dependencies + cordaCompile project(':node') + + // Java Atomix: RAFT library + compile 'io.atomix.copycat:copycat-client:1.2.8' + compile 'io.atomix.copycat:copycat-server:1.2.8' + compile 'io.atomix.catalyst:catalyst-netty:1.2.1' + + testCompile "junit:junit:$junit_version" + testCompile project(':node-driver') +} + +idea { + module { + downloadJavadoc = true // defaults to false + downloadSources = true + } +} + +publish { + name 'corda-notary-raft' +} diff --git a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt new file mode 100644 index 0000000000..3913551802 --- /dev/null +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt @@ -0,0 +1,46 @@ +package net.corda.notary.raft + +import net.corda.core.flows.FlowSession +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService +import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.transactions.NonValidatingNotaryFlow +import net.corda.node.services.transactions.ValidatingNotaryFlow +import java.security.PublicKey + +/** A highly available notary service using the Raft algorithm to achieve consensus. */ +class RaftNotaryService( + override val services: ServiceHubInternal, + override val notaryIdentityKey: PublicKey +) : TrustedAuthorityNotaryService() { + private val notaryConfig = services.configuration.notary + ?: throw IllegalArgumentException("Failed to register ${this::class.java}: notary configuration not present") + + override val uniquenessProvider = with(services) { + val raftConfig = notaryConfig.raft + ?: throw IllegalArgumentException("Failed to register ${this::class.java}: raft configuration not present") + RaftUniquenessProvider( + configuration.baseDirectory, + configuration.p2pSslOptions, + database, + clock, + monitoringService.metrics, + services.cacheFactory, + raftConfig + ) + } + + override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow { + return if (notaryConfig.validating) { + ValidatingNotaryFlow(otherPartySession, this) + } else NonValidatingNotaryFlow(otherPartySession, this) + } + + override fun start() { + uniquenessProvider.start() + } + + override fun stop() { + uniquenessProvider.stop() + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftTransactionCommitLog.kt similarity index 96% rename from node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt rename to experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftTransactionCommitLog.kt index c35ae146ab..a6fcd0b8a3 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftTransactionCommitLog.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.transactions +package net.corda.notary.raft import io.atomix.catalyst.buffer.BufferInput import io.atomix.catalyst.buffer.BufferOutput @@ -27,6 +27,7 @@ import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.currentDBSession @@ -111,7 +112,7 @@ class RaftTransactionCommitLog( } } - private fun logRequest(commitCommand: RaftTransactionCommitLog.Commands.CommitTransaction) { + private fun logRequest(commitCommand: Commands.CommitTransaction) { val request = PersistentUniquenessProvider.Request( consumingTxHash = commitCommand.txId.toString(), partyName = commitCommand.requestingParty, @@ -192,8 +193,8 @@ class RaftTransactionCommitLog( registerAbstract(SecureHash::class.java, CordaKryoSerializer::class.java) registerAbstract(TimeWindow::class.java, CordaKryoSerializer::class.java) registerAbstract(NotaryError::class.java, CordaKryoSerializer::class.java) - register(RaftTransactionCommitLog.Commands.CommitTransaction::class.java, CordaKryoSerializer::class.java) - register(RaftTransactionCommitLog.Commands.Get::class.java, CordaKryoSerializer::class.java) + register(Commands.CommitTransaction::class.java, CordaKryoSerializer::class.java) + register(Commands.Get::class.java, CordaKryoSerializer::class.java) register(StateRef::class.java, CordaKryoSerializer::class.java) register(LinkedHashMap::class.java, CordaKryoSerializer::class.java) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt similarity index 98% rename from node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt rename to experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt index 2192343fa1..c7670d0c18 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.transactions +package net.corda.notary.raft import com.codahale.metrics.Gauge import com.codahale.metrics.MetricRegistry @@ -26,12 +26,12 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.config.RaftConfig -import net.corda.node.services.transactions.RaftTransactionCommitLog.Commands.CommitTransaction import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import net.corda.notary.raft.RaftTransactionCommitLog.Commands.CommitTransaction import java.nio.file.Path import java.time.Clock import java.util.concurrent.CompletableFuture diff --git a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/Schema.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/Schema.kt new file mode 100644 index 0000000000..3a93164875 --- /dev/null +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/Schema.kt @@ -0,0 +1,19 @@ +package net.corda.notary.raft + +import net.corda.core.schemas.MappedSchema +import net.corda.node.services.transactions.PersistentUniquenessProvider + +object RaftNotarySchema + +object RaftNotarySchemaV1 : MappedSchema( + schemaFamily = RaftNotarySchema.javaClass, + version = 1, + mappedTypes = listOf( + PersistentUniquenessProvider.BaseComittedState::class.java, + PersistentUniquenessProvider.Request::class.java, + RaftUniquenessProvider.CommittedState::class.java + ) +) { + override val migrationResource: String? + get() = "notary-raft.changelog-master" +} \ No newline at end of file diff --git a/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-init.xml b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-init.xml new file mode 100644 index 0000000000..5fe85ce568 --- /dev/null +++ b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-init.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-master.xml b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-master.xml new file mode 100644 index 0000000000..555344167e --- /dev/null +++ b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-master.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-pkey.xml b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-pkey.xml new file mode 100644 index 0000000000..24cf747d58 --- /dev/null +++ b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-pkey.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-v1.xml b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-v1.xml new file mode 100644 index 0000000000..f95c8e7923 --- /dev/null +++ b/experimental/notary-raft/src/main/resources/migration/notary-raft.changelog-v1.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftNotaryServiceTests.kt similarity index 99% rename from node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt rename to experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftNotaryServiceTests.kt index 43090937cc..35f6fc3f89 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftNotaryServiceTests.kt @@ -1,4 +1,4 @@ -package net.corda.node.services +package net.corda.notary.raft import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef diff --git a/node/src/integration-test/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLogTests.kt b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftTransactionCommitLogTests.kt similarity index 98% rename from node/src/integration-test/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLogTests.kt rename to experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftTransactionCommitLogTests.kt index 38389ef595..8b0f31f12d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLogTests.kt +++ b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftTransactionCommitLogTests.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.transactions +package net.corda.notary.raft import io.atomix.catalyst.transport.Address import io.atomix.copycat.client.ConnectionStrategies @@ -16,7 +16,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME @@ -154,7 +154,7 @@ class RaftTransactionCommitLogTests { private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) - val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(includeNotarySchemas = true)) + val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(extraSchemas = setOf(RaftNotarySchemaV1))) databases.add(database) val stateMachineFactory = { RaftTransactionCommitLog(database, Clock.systemUTC(), { RaftUniquenessProvider.createMap(TestingNamedCacheFactory()) }) } diff --git a/node/build.gradle b/node/build.gradle index ff7be256dc..b100797903 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -153,18 +153,9 @@ dependencies { // We only need this dependency to compile our Caplet against. compileOnly "co.paralleluniverse:capsule:$capsule_version" - // Java Atomix: RAFT library - compile 'io.atomix.copycat:copycat-client:1.2.8' - compile 'io.atomix.copycat:copycat-server:1.2.8' - compile 'io.atomix.catalyst:catalyst-netty:1.2.1' - // OkHTTP: Simple HTTP library. compile "com.squareup.okhttp3:okhttp:$okhttp_version" - // BFT-SMaRt - compile 'commons-codec:commons-codec:1.10' - compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' - // Apache Shiro: authentication, authorization and session management. compile "org.apache.shiro:shiro-core:${shiro_version}" diff --git a/node/src/integration-test/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt index 9bebdc794c..1bb2148e96 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/distributed/DistributedServiceTests.kt @@ -42,7 +42,7 @@ class DistributedServiceTests { invokeRpc(CordaRPCOps::stateMachinesFeed)) ) driver(DriverParameters( - extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), + extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas", "net.corda.notary.raft"), notarySpecs = listOf( NotarySpec( DUMMY_NOTARY_NAME, diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index fef890d2ec..4a427940cf 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -14,7 +14,6 @@ import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME @@ -26,6 +25,7 @@ import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.internal.MOCK_VERSION_INFO +import net.corda.testing.internal.TestingNamedCacheFactory import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 1eea4f49db..8f35b7a543 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -5,11 +5,11 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.configureDatabase import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.internal.TestingNamedCacheFactory import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.junit.After diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index 004f27a026..f23aeb1a4d 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -40,6 +40,7 @@ class P2PMessagingTest { private fun startDriverWithDistributedService(dsl: DriverDSL.(List) -> Unit) { driver(DriverParameters( startNodesInProcess = true, + extraCordappPackagesToScan = listOf("net.corda.notary.raft"), notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2))) )) { dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) }) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 3c0be454c7..bea2d435ae 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -34,9 +34,7 @@ import net.corda.node.CordaClock import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.classloading.requireAnnotation -import net.corda.node.internal.cordapp.CordappConfigFileProvider -import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.node.internal.cordapp.CordappProviderInternal +import net.corda.node.internal.cordapp.* import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy import net.corda.node.internal.rpc.proxies.ExceptionMaskingRpcOpsProxy import net.corda.node.internal.rpc.proxies.ExceptionSerialisingRpcOpsProxy @@ -115,7 +113,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val platformClock: CordaClock, cacheFactoryPrototype: NamedCacheFactory, protected val versionInfo: VersionInfo, - protected val cordappLoader: CordappLoader, protected val serverThread: AffinityExecutor.ServiceAffinityExecutor, private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { @@ -141,7 +138,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null).tokenize() + protected val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo) + val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize() val identityService = PersistentIdentityService(cacheFactory).tokenize() val database: CordaPersistence = createCordaPersistence( configuration.database, @@ -498,6 +496,24 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ) } + private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader { + val generatedCordapps = mutableListOf(VirtualCordapp.generateCoreCordapp(versionInfo)) + if (isRunningSimpleNotaryService(configuration)) { + // For backwards compatibility purposes the single node notary implementation is built-in: a virtual + // CorDapp will be generated. + generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo) + } + return JarScanningCordappLoader.fromDirectories( + configuration.cordappDirectories, + versionInfo, + extraCordapps = generatedCordapps + ) + } + + private fun isRunningSimpleNotaryService(configuration: NodeConfiguration): Boolean { + return configuration.notary != null && configuration.notary?.className == SimpleNotaryService::class.java.name + } + private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause) private fun installCordaServices(myNotaryIdentity: PartyAndCertificate?) { @@ -780,17 +796,32 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } private fun makeNotaryService(myNotaryIdentity: PartyAndCertificate?): NotaryService? { - return configuration.notary?.let { - makeCoreNotaryService(it, myNotaryIdentity).also { - it.tokenize() - runOnStop += it::stop - installCoreFlow(NotaryFlow.Client::class, it::createServiceFlow) - log.info("Running core notary: ${it.javaClass.name}") - it.start() + return configuration.notary?.let { notaryConfig -> + val serviceClass = getNotaryServiceClass(notaryConfig.className) + log.info("Starting notary service: $serviceClass") + + val notaryKey = myNotaryIdentity?.owningKey + ?: throw IllegalArgumentException("Unable to start notary service $serviceClass: notary identity not found") + val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true } + val service = constructor.newInstance(services, notaryKey) as NotaryService + + service.run { + tokenize() + runOnStop += ::stop + installCoreFlow(NotaryFlow.Client::class, ::createServiceFlow) + start() } + return service } } + private fun getNotaryServiceClass(className: String): Class { + val loadedImplementations = cordappLoader.cordapps.mapNotNull { it.notaryService } + log.debug("Notary service implementations found: ${loadedImplementations.joinToString(", ")}") + return loadedImplementations.firstOrNull { it.name == className } + ?: throw IllegalArgumentException("The notary service implementation specified in the configuration: $className is not found. Available implementations: ${loadedImplementations.joinToString(", ")}}") + } + protected open fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal { // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with @@ -798,32 +829,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return PersistentKeyManagementService(cacheFactory, identityService, database) } - private fun makeCoreNotaryService(notaryConfig: NotaryConfig, myNotaryIdentity: PartyAndCertificate?): NotaryService { - val notaryKey = myNotaryIdentity?.owningKey - ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service") - return notaryConfig.run { - when { - raft != null -> { - val uniquenessProvider = RaftUniquenessProvider(configuration.baseDirectory, configuration.p2pSslOptions, database, platformClock, monitoringService.metrics, cacheFactory, raft) - (if (validating) ::RaftValidatingNotaryService else ::RaftNonValidatingNotaryService)(services, notaryKey, uniquenessProvider) - } - bftSMaRt != null -> { - if (validating) throw IllegalArgumentException("Validating BFTSMaRt notary not supported") - BFTNonValidatingNotaryService(services, notaryKey, bftSMaRt, makeBFTCluster(notaryKey, bftSMaRt)) - } - else -> (if (validating) ::ValidatingNotaryService else ::SimpleNotaryService)(services, notaryKey) - } - } - } - - protected open fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster { - return object : BFTSMaRt.Cluster { - override fun waitUntilAllReplicasHaveInitialized() { - log.warn("A BFT replica may still be initializing, in which case the upcoming consensus change may cause it to spin.") - } - } - } - open fun stop() { // TODO: We need a good way of handling "nice to have" shutdown events, especially those that deal with the // network, including unsubscribing from updates from remote services. Possibly some sort of parameter to stop() diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 3db80bdf25..0af843dd5b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -28,10 +28,8 @@ import net.corda.core.utilities.contextLogger import net.corda.node.CordaClock import net.corda.node.SimpleClock import net.corda.node.VersionInfo -import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.artemis.ArtemisBroker import net.corda.node.internal.artemis.BrokerAddresses -import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser @@ -87,14 +85,12 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) { open class Node(configuration: NodeConfiguration, versionInfo: VersionInfo, private val initialiseSerialization: Boolean = true, - cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo), cacheFactoryPrototype: NamedCacheFactory = DefaultNamedCacheFactory() ) : AbstractNode( configuration, createClock(configuration), cacheFactoryPrototype, versionInfo, - cordappLoader, // Under normal (non-test execution) it will always be "1" AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1) ) { @@ -132,10 +128,6 @@ open class Node(configuration: NodeConfiguration, private val sameVmNodeCounter = AtomicInteger() - private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader { - return JarScanningCordappLoader.fromDirectories(configuration.cordappDirectories, versionInfo) - } - // TODO: make this configurable. const val MAX_RPC_MESSAGE_SIZE = 10485760 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 1b089a7c63..110a29cdca 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -21,7 +21,6 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfigurationImpl import net.corda.node.services.config.shouldStartLocalShell import net.corda.node.services.config.shouldStartSSHDaemon -import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NodeRegistrationException @@ -256,7 +255,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { } val nodeInfo = node.start() - logLoadedCorDapps(node.services.cordappProvider.cordapps) + val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded } + logLoadedCorDapps(loadedCodapps) node.nodeReadyFuture.thenMatch({ // Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits. @@ -411,6 +411,16 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) } + /** This filter is required for BFT-Smart to work as it only supports Java serialization. */ + // TODO: move this filter out of the node, allow Cordapps to specify filters. + private fun bftSMaRtSerialFilter(clazz: Class<*>): Boolean = clazz.name.let { + it.startsWith("bftsmart.") + || it.startsWith("java.security.") + || it.startsWith("java.util.") + || it.startsWith("java.lang.") + || it.startsWith("java.net.") + } + protected open fun getVersionInfo(): VersionInfo { return VersionInfo( PLATFORM_VERSION, diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 96d1e4f435..e47badd63e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -9,6 +9,8 @@ import net.corda.core.flows.* import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappInfoResolver +import net.corda.core.internal.notary.NotaryService +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationCustomSerializer @@ -36,9 +38,12 @@ import kotlin.streams.toList * @property cordappJarPaths The classpath of cordapp JARs */ class JarScanningCordappLoader private constructor(private val cordappJarPaths: List, - private val versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() { + private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, + extraCordapps: List) : CordappLoaderTemplate() { - override val cordapps: List by lazy { loadCordapps() + coreCordapp } + override val cordapps: List by lazy { + loadCordapps() + extraCordapps + } override val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader) @@ -58,9 +63,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * * @param corDappDirectories Directories used to scan for CorDapp JARs. */ - fun fromDirectories(corDappDirectories: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader { + fun fromDirectories(corDappDirectories: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList()): JarScanningCordappLoader { logger.info("Looking for CorDapps in ${corDappDirectories.distinct().joinToString(", ", "[", "]")}") - return JarScanningCordappLoader(corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }, versionInfo) + val paths = corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } + return JarScanningCordappLoader(paths, versionInfo, extraCordapps) } /** @@ -68,11 +74,12 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. */ - fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader { - return JarScanningCordappLoader(scanJars.map { it.restricted() }, versionInfo) + fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList()): JarScanningCordappLoader { + val paths = scanJars.map { it.restricted() } + return JarScanningCordappLoader(paths, versionInfo, extraCordapps) } - private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) + private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) private fun jarUrlsInDirectory(directory: Path): List { @@ -85,32 +92,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } } } - - /** A list of the core RPC flows present in Corda */ - private val coreRPCFlows = listOf( - ContractUpgradeFlow.Initiate::class.java, - ContractUpgradeFlow.Authorise::class.java, - ContractUpgradeFlow.Deauthorise::class.java) } - - /** A Cordapp representing the core package which is not scanned automatically. */ - @VisibleForTesting - internal val coreCordapp = CordappImpl( - contractClassNames = listOf(), - initiatedFlows = listOf(), - rpcFlows = coreRPCFlows, - serviceFlows = listOf(), - schedulableFlows = listOf(), - services = listOf(), - serializationWhitelists = listOf(), - serializationCustomSerializers = listOf(), - customSchemas = setOf(), - info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion), - allFlows = listOf(), - jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location - jarHash = SecureHash.allOnesHash - ) - private fun loadCordapps(): List { val cordapps = cordappJarPaths .map { scanCordapp(it).toCordapp(it) } @@ -126,7 +108,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) } return cordapps } - private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl { val info = url.url.openStream().let(::JarInputStream).use { it.manifest?.toCordappInfo(CordappImpl.jarName(url.url)) ?: CordappImpl.Info.UNKNOWN } return CordappImpl( @@ -142,10 +123,21 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: findAllFlows(this), url.url, info, - getJarHash(url.url) + getJarHash(url.url), + findNotaryService(this) ) } + private fun findNotaryService(scanResult: RestrictedScanResult): Class? { + // Note: we search for implementations of both NotaryService and TrustedAuthorityNotaryService as + // the scanner won't find subclasses deeper down the hierarchy if any intermediate class is not + // present in the CorDapp. + val result = scanResult.getClassesWithSuperclass(NotaryService::class) + + scanResult.getClassesWithSuperclass(TrustedAuthorityNotaryService::class) + logger.info("Found notary service CorDapp implementations: " + result.joinToString(", ")) + return result.firstOrNull() + } + private fun getJarHash(url: URL): SecureHash.SHA256 = url.openStream().readFully().sha256() private fun findServices(scanResult: RestrictedScanResult): List> { @@ -202,7 +194,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } private fun findCustomSchemas(scanResult: RestrictedScanResult): Set { - return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet() + return scanResult.getClassesWithSuperclass(MappedSchema::class).instances().toSet() } private val cachedScanResult = LRUMap(1000) @@ -243,18 +235,21 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: val qualifiedNamePrefix: String get() = rootPackageName?.let { "$it." } ?: "" } + private fun List>.instances(): List { + return map { it.kotlin.objectOrNewInstance() } + } + private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) { fun getNamesOfClassesImplementing(type: KClass<*>): List { return scanResult.getNamesOfClassesImplementing(type.java) .filter { it.startsWith(qualifiedNamePrefix) } } - fun getClassesWithSuperclass(type: KClass): List { + fun getClassesWithSuperclass(type: KClass): List> { return scanResult.getNamesOfSubclassesOf(type.java) .filter { it.startsWith(qualifiedNamePrefix) } .mapNotNull { loadClass(it, type) } .filterNot { Modifier.isAbstract(it.modifiers) } - .map { it.kotlin.objectOrNewInstance() } } fun getClassesImplementing(type: KClass): List { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt new file mode 100644 index 0000000000..8fa3d24222 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt @@ -0,0 +1,60 @@ +package net.corda.node.internal.cordapp + +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.ContractUpgradeFlow +import net.corda.core.internal.cordapp.CordappImpl +import net.corda.core.internal.location +import net.corda.node.VersionInfo +import net.corda.node.services.transactions.NodeNotarySchemaV1 +import net.corda.node.services.transactions.SimpleNotaryService + +internal object VirtualCordapp { + /** A list of the core RPC flows present in Corda */ + private val coreRpcFlows = listOf( + ContractUpgradeFlow.Initiate::class.java, + ContractUpgradeFlow.Authorise::class.java, + ContractUpgradeFlow.Deauthorise::class.java + ) + + /** A Cordapp representing the core package which is not scanned automatically. */ + fun generateCoreCordapp(versionInfo: VersionInfo): CordappImpl { + return CordappImpl( + contractClassNames = listOf(), + initiatedFlows = listOf(), + rpcFlows = coreRpcFlows, + serviceFlows = listOf(), + schedulableFlows = listOf(), + services = listOf(), + serializationWhitelists = listOf(), + serializationCustomSerializers = listOf(), + customSchemas = setOf(), + info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion), + allFlows = listOf(), + jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location + jarHash = SecureHash.allOnesHash, + notaryService = null, + isLoaded = false + ) + } + + /** A Cordapp for the built-in notary service implementation. */ + fun generateSimpleNotaryCordapp(versionInfo: VersionInfo): CordappImpl { + return CordappImpl( + contractClassNames = listOf(), + initiatedFlows = listOf(), + rpcFlows = listOf(), + serviceFlows = listOf(), + schedulableFlows = listOf(), + services = listOf(), + serializationWhitelists = listOf(), + serializationCustomSerializers = listOf(), + customSchemas = setOf(NodeNotarySchemaV1), + info = CordappImpl.Info("corda-notary", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion), + allFlows = listOf(), + jarPath = SimpleNotaryService::class.java.location, + jarHash = SecureHash.allOnesHash, + notaryService = SimpleNotaryService::class.java, + isLoaded = false + ) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 1d7f89f7bc..28e73e4600 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -123,7 +123,8 @@ data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null, val custom: Boolean = false, - val serviceLegalName: CordaX500Name? = null + val serviceLegalName: CordaX500Name? = null, + val className: String = "net.corda.node.services.transactions.SimpleNotaryService" ) { init { require(raft == null || bftSMaRt == null || !custom) { diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 3fdaa4b798..f75a6ad83a 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -16,9 +16,7 @@ import net.corda.node.services.messaging.P2PMessageDeduplicator import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.PersistentUniquenessProvider -import net.corda.node.services.transactions.RaftUniquenessProvider import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.VaultSchemaV1 @@ -29,7 +27,7 @@ import net.corda.node.services.vault.VaultSchemaV1 * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService(private val extraSchemas: Set = emptySet(), includeNotarySchemas: Boolean = false) : SchemaService, SingletonSerializeAsToken() { +class NodeSchemaService(private val extraSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { // Core Entities used by a Node object NodeCore @@ -47,26 +45,12 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() override val migrationResource = "node-core.changelog-master" } - // Entities used by a Notary - object NodeNotary - - object NodeNotaryV1 : MappedSchema(schemaFamily = NodeNotary.javaClass, version = 1, - mappedTypes = listOf(PersistentUniquenessProvider.BaseComittedState::class.java, - PersistentUniquenessProvider.Request::class.java, - PersistentUniquenessProvider.CommittedState::class.java, - RaftUniquenessProvider.CommittedState::class.java, - BFTNonValidatingNotaryService.CommittedState::class.java - )) { - override val migrationResource = "node-notary.changelog-master" - } - // Required schemas are those used by internal Corda services private val requiredSchemas: Map = mapOf(Pair(CommonSchemaV1, SchemaOptions()), Pair(VaultSchemaV1, SchemaOptions()), Pair(NodeInfoSchemaV1, SchemaOptions()), - Pair(NodeCoreV1, SchemaOptions())) + - if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap() + Pair(NodeCoreV1, SchemaOptions())) fun internalSchemas() = requiredSchemas.keys + extraSchemas.filter { schema -> // when mapped schemas from the finance module are present, they are considered as internal ones schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt deleted file mode 100644 index 158d86b3c7..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.flows.FlowSession -import net.corda.core.internal.notary.NotaryServiceFlow -import net.corda.core.internal.notary.TrustedAuthorityNotaryService -import net.corda.core.node.ServiceHub -import java.security.PublicKey - -/** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService( - override val services: ServiceHub, - override val notaryIdentityKey: PublicKey, - override val uniquenessProvider: RaftUniquenessProvider -) : TrustedAuthorityNotaryService() { - override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow { - return NonValidatingNotaryFlow(otherPartySession, this) - } - - override fun start() { - uniquenessProvider.start() - } - - override fun stop() { - uniquenessProvider.stop() - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt deleted file mode 100644 index f0cb8c1f8e..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.flows.FlowSession -import net.corda.core.internal.notary.NotaryServiceFlow -import net.corda.core.internal.notary.TrustedAuthorityNotaryService -import net.corda.core.node.ServiceHub -import java.security.PublicKey - -/** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService( - override val services: ServiceHub, - override val notaryIdentityKey: PublicKey, - override val uniquenessProvider: RaftUniquenessProvider -) : TrustedAuthorityNotaryService() { - override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow { - return ValidatingNotaryFlow(otherPartySession, this) - } - - override fun start() { - uniquenessProvider.start() - } - - override fun stop() { - uniquenessProvider.stop() - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index 51909ea5b1..d382b2feae 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -3,15 +3,38 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowSession import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.internal.notary.TrustedAuthorityNotaryService +import net.corda.core.schemas.MappedSchema import net.corda.node.services.api.ServiceHubInternal import java.security.PublicKey -/** A simple Notary service that does not perform transaction validation */ +/** An embedded notary service that uses the node's database to store committed states. */ class SimpleNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { + private val notaryConfig = services.configuration.notary + ?: throw IllegalArgumentException("Failed to register ${this::class.java}: notary configuration not present") + override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = NonValidatingNotaryFlow(otherPartySession, this) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow { + return if (notaryConfig.validating) { + log.info("Starting in validating mode") + ValidatingNotaryFlow(otherPartySession, this) + } else { + log.info("Starting in non-validating mode") + NonValidatingNotaryFlow(otherPartySession, this) + } + } override fun start() {} override fun stop() {} } + +// Entities used by a Notary +object NodeNotarySchema + +object NodeNotarySchemaV1 : MappedSchema(schemaFamily = NodeNotarySchema.javaClass, version = 1, + mappedTypes = listOf(PersistentUniquenessProvider.BaseComittedState::class.java, + PersistentUniquenessProvider.Request::class.java, + PersistentUniquenessProvider.CommittedState::class.java + )) { + override val migrationResource = "node-notary.changelog-master" +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 6e39a3ea1e..e69de29bb2 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -1,17 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.flows.FlowSession -import net.corda.core.internal.notary.NotaryServiceFlow -import net.corda.core.internal.notary.TrustedAuthorityNotaryService -import net.corda.node.services.api.ServiceHubInternal -import java.security.PublicKey - -/** A Notary service that validates the transaction chain of the submitted transaction before committing it */ -class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { - override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory) - - override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = ValidatingNotaryFlow(otherPartySession, this) - - override fun start() {} - override fun stop() {} -} diff --git a/node/src/main/resources/migration/node-notary.changelog-init.xml b/node/src/main/resources/migration/node-notary.changelog-init.xml index fed5c691b8..8d0f1bcb6f 100644 --- a/node/src/main/resources/migration/node-notary.changelog-init.xml +++ b/node/src/main/resources/migration/node-notary.changelog-init.xml @@ -27,18 +27,6 @@ - - - - - - - - - - - - @@ -58,18 +46,11 @@ - - - + - - - diff --git a/node/src/main/resources/migration/node-notary.changelog-pkey.xml b/node/src/main/resources/migration/node-notary.changelog-pkey.xml index 64a99cc978..c4d7c59376 100644 --- a/node/src/main/resources/migration/node-notary.changelog-pkey.xml +++ b/node/src/main/resources/migration/node-notary.changelog-pkey.xml @@ -13,9 +13,4 @@ - - - - \ No newline at end of file diff --git a/node/src/main/resources/migration/node-notary.changelog-v1.xml b/node/src/main/resources/migration/node-notary.changelog-v1.xml index 0856d543b4..3002133bad 100644 --- a/node/src/main/resources/migration/node-notary.changelog-v1.xml +++ b/node/src/main/resources/migration/node-notary.changelog-v1.xml @@ -7,10 +7,6 @@ - - - - \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 927852e1e8..4dd255f414 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -47,7 +47,7 @@ class JarScanningCordappLoaderTest { fun `classes that aren't in cordapps aren't loaded`() { // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp val loader = JarScanningCordappLoader.fromDirectories(listOf(Paths.get("."))) - assertThat(loader.cordapps).containsOnly(loader.coreCordapp) + assertThat(loader.cordapps).isEmpty() } @Test @@ -55,9 +55,9 @@ class JarScanningCordappLoaderTest { val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)) - assertThat(loader.cordapps).hasSize(2) + assertThat(loader.cordapps).hasSize(1) - val actualCordapp = loader.cordapps.single { it != loader.coreCordapp } + val actualCordapp = loader.cordapps.single() assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") assertThat(actualCordapp.rpcFlows).isEmpty() @@ -73,8 +73,8 @@ class JarScanningCordappLoaderTest { val loader = cordappLoaderForPackages(listOf(testScanPackage)) val actual = loader.cordapps.toTypedArray() - // One core cordapp, one cordapp from this source tree. In gradle it will also pick up the node jar. - assertThat(actual.size == 2 || actual.size == 3).isTrue() + // One cordapp from this source tree. In gradle it will also pick up the node jar. + assertThat(actual.size == 0 || actual.size == 1).isTrue() val actualCordapp = actual.single { !it.initiatedFlows.isEmpty() } assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java) @@ -111,7 +111,7 @@ class JarScanningCordappLoaderTest { fun `cordapp classloader sets target and min version to 1 if not specified`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) - loader.cordapps.filter { it.info.shortName != "corda-core" }.forEach { + loader.cordapps.forEach { assertThat(it.info.targetPlatformVersion).isEqualTo(1) assertThat(it.info.minimumPlatformVersion).isEqualTo(1) } @@ -123,8 +123,7 @@ class JarScanningCordappLoaderTest { // make sure classloader extracts correct values val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) - // exclude the core cordapp - val cordapp = loader.cordapps.single { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl") } + val cordapp = loader.cordapps.first() assertThat(cordapp.info.targetPlatformVersion).isEqualTo(3) assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2) } @@ -144,24 +143,21 @@ class JarScanningCordappLoaderTest { fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1)) - // exclude the core cordapp - assertThat(loader.cordapps).hasSize(1) + assertThat(loader.cordapps).hasSize(0) } @Test fun `cordapp classloader does load apps when their min platform version is less than the platform version`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000)) - // exclude the core cordapp - assertThat(loader.cordapps).hasSize(2) + assertThat(loader.cordapps).hasSize(1) } @Test fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) - // exclude the core cordapp - assertThat(loader.cordapps).hasSize(2) + assertThat(loader.cordapps).hasSize(1) } private fun cordappLoaderForPackages(packages: Iterable): CordappLoader { diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 8303cacefd..30e4a518ba 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -313,20 +313,11 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // of gets and puts. private fun makeNodeWithTracking(name: CordaX500Name): TestStartedNode { // Create a node in the mock network ... - return mockNet.createNode(InternalMockNodeParameters(legalName = name), nodeFactory = { args, cordappLoader -> - if (cordappLoader != null) { - object : InternalMockNetwork.MockNode(args, cordappLoader) { - // That constructs a recording tx storage - override fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { - return RecordingTransactionStorage(database, super.makeTransactionStorage(transactionCacheSizeBytes)) - } - } - } else { - object : InternalMockNetwork.MockNode(args) { - // That constructs a recording tx storage - override fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { - return RecordingTransactionStorage(database, super.makeTransactionStorage(transactionCacheSizeBytes)) - } + return mockNet.createNode(InternalMockNodeParameters(legalName = name), nodeFactory = { args -> + object : InternalMockNetwork.MockNode(args) { + // That constructs a recording tx storage + override fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { + return RecordingTransactionStorage(database, super.makeTransactionStorage(transactionCacheSizeBytes)) } } }) @@ -611,7 +602,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fillUpForBuyer(bobError, issuer, bob, notary).second } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(aliceError, issuer, alice,1200.DOLLARS `issued by` issuer, null, notary).second + fillUpForSeller(aliceError, issuer, alice, 1200.DOLLARS `issued by` issuer, null, notary).second } insertFakeTransactions(bobsBadCash, bobNode, bob, notaryNode, bankNode) diff --git a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt index 1edf477911..71eef309ad 100644 --- a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt @@ -22,6 +22,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.seconds +import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.config.FlowTimeoutConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NotaryConfig @@ -90,6 +91,7 @@ class TimedFlowTests { whenever(it.custom).thenReturn(true) whenever(it.isClusterConfig).thenReturn(true) whenever(it.validating).thenReturn(true) + whenever(it.className).thenReturn(TestNotaryService::class.java.name) } val notaryNodes = (0 until CLUSTER_SIZE).map { @@ -176,8 +178,7 @@ class TimedFlowTests { }.bufferUntilSubscribed().toBlocking().toFuture() } - @CordaService - private class TestNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { + private class TestNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val uniquenessProvider = mock() override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = TestNotaryFlow(otherPartySession, this) override fun start() {} diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index 091a47d168..186cfb13cc 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -8,7 +8,7 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.node.internal.configureDatabase -import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.x509Certificates diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt index 5ed16e74a8..b52d8f39a8 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt @@ -5,7 +5,7 @@ import net.corda.core.utilities.loggerFor import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index f0c7c95859..44c92b6630 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -9,7 +9,7 @@ import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.node.internal.configureDatabase import net.corda.node.services.transactions.PersistentUniquenessProvider -import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* @@ -154,7 +154,8 @@ class DBTransactionStorageTests { } private fun newTransactionStorage(cacheSizeBytesOverride: Long? = null) { - transactionStorage = DBTransactionStorage(database, TestingNamedCacheFactory(cacheSizeBytesOverride ?: 1024)) + transactionStorage = DBTransactionStorage(database, TestingNamedCacheFactory(cacheSizeBytesOverride + ?: 1024)) } private fun assertTransactionIsRetrievable(transaction: SignedTransaction) { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt index 0c5213eb63..4ce3b354e5 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt @@ -9,7 +9,7 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService -import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.core.BOC_NAME import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 70c827b496..e4f6549e48 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -15,7 +15,7 @@ import net.corda.core.node.services.vault.Sort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.configureDatabase import net.corda.node.services.transactions.PersistentUniquenessProvider -import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.internal.LogHelper diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index e258334897..2dad47df2c 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -10,20 +10,18 @@ import net.corda.core.schemas.PersistentState import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.schema.NodeSchemaService.NodeCoreV1 -import net.corda.node.services.schema.NodeSchemaService.NodeNotaryV1 import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 -import net.corda.testing.node.internal.cordappsForPackages import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.cordappsForPackages import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType import org.junit.Ignore import org.junit.Test import javax.persistence.* import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeSchemaServiceTest { @@ -47,7 +45,6 @@ class NodeSchemaServiceTest { // check against NodeCore schemas assertTrue(schemaService.schemaOptions.containsKey(NodeCoreV1)) - assertFalse(schemaService.schemaOptions.containsKey(NodeNotaryV1)) mockNet.stopNodes() } @@ -57,9 +54,8 @@ class NodeSchemaServiceTest { val mockNotaryNode = mockNet.notaryNodes.first() val schemaService = mockNotaryNode.services.schemaService - // check against NodeCore + NodeNotary Schemas + // check against NodeCore Schema assertTrue(schemaService.schemaOptions.containsKey(NodeCoreV1)) - assertTrue(schemaService.schemaOptions.containsKey(NodeNotaryV1)) mockNet.stopNodes() } @@ -97,7 +93,6 @@ class NodeSchemaServiceTest { val mappedSchemas = result.returnValue.getOrThrow() // check against NodeCore schemas assertTrue(mappedSchemas.contains(NodeCoreV1.name)) - assertFalse(mappedSchemas.contains(NodeNotaryV1.name)) // still gets loaded due TODO restriction } } @@ -107,9 +102,8 @@ class NodeSchemaServiceTest { driver(DriverParameters(startNodesInProcess = true)) { val notary = defaultNotaryNode.getOrThrow() val mappedSchemas = notary.rpc.startFlow(::MappedSchemasFlow).returnValue.getOrThrow() - // check against NodeCore + NodeNotary Schemas + // check against NodeCore Schema assertTrue(mappedSchemas.contains(NodeCoreV1.name)) - assertTrue(mappedSchemas.contains(NodeNotaryV1.name)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt index 14ff42b13e..28db2d7d13 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt @@ -37,7 +37,7 @@ class MaxTransactionSizeTests { @Before fun setup() { - mockNet = MockNetwork(listOf("net.corda.testing.contracts", "net.corda.node.services.transactions"), networkParameters = testNetworkParameters(maxTransactionSize = 3_000_000)) + mockNet = MockNetwork(listOf("net.corda.testing.contracts"), networkParameters = testNetworkParameters(maxTransactionSize = 3_000_000)) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notaryNode = mockNet.defaultNotaryNode diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 4bf28c246c..65401708d4 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -10,7 +10,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.notary.NotaryInternalException import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.SerializationEnvironmentRule @@ -39,7 +39,7 @@ class PersistentUniquenessProviderTests { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(includeNotarySchemas = true)) + database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(extraSchemas = setOf(NodeNotarySchemaV1))) } @After diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 93e60859fe..073800fd0b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -82,7 +82,7 @@ class VaultSoftLockManagerTest { private val mockVault = rigorousMock().also { doNothing().whenever(it).softLockRelease(any(), anyOrNull()) } - private val mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(ContractImpl::class.packageName), defaultFactory = { args, _ -> + private val mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(ContractImpl::class.packageName), defaultFactory = { args -> object : InternalMockNetwork.MockNode(args) { override fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, database: CordaPersistence): VaultServiceInternal { val node = this diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 586d67109b..7f52642b53 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -4,10 +4,8 @@ apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' -apply plugin: 'maven-publish' configurations { integrationTestCompile.extendsFrom testCompile @@ -16,7 +14,6 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - testCompile "junit:junit:$junit_version" // Corda integration dependencies cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') @@ -24,26 +21,11 @@ dependencies { cordaCompile project(':core') cordaCompile project(':client:jfx') cordaCompile project(':client:rpc') - cordaCompile project(':node-driver') -} + cordaCompile project(':test-utils') -idea { - module { - downloadJavadoc = true // defaults to false - downloadSources = true - } -} - -publishing { - publications { - jarAndSources(MavenPublication) { - from components.java - artifactId 'notarydemo' - - artifact sourceJar - artifact javadocJar - } - } + // Notary implementations + cordapp project(':experimental:notary-raft') + cordapp project(':experimental:notary-bft-smart') } task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom']) @@ -99,9 +81,11 @@ task deployNodesCustom(type: Cordform, dependsOn: 'jar') { } task deployNodesRaft(type: Cordform, dependsOn: 'jar') { + def className = "net.corda.notary.raft.RaftNotaryService" directory file("$buildDir/nodes/nodesRaft") nodeDefaults { extraConfig = [h2Settings: [address: "localhost:0"]] + cordapp project(':experimental:notary-raft') } node { name "O=Alice Corp,L=Madrid,C=ES" @@ -124,7 +108,8 @@ task deployNodesRaft(type: Cordform, dependsOn: 'jar') { serviceLegalName: "O=Raft,L=Zurich,C=CH", raft: [ nodeAddress: "localhost:10008" - ] + ], + className: className ] } node { @@ -140,7 +125,8 @@ task deployNodesRaft(type: Cordform, dependsOn: 'jar') { raft: [ nodeAddress: "localhost:10012", clusterAddresses: ["localhost:10008"] - ] + ], + className: className ] } node { @@ -156,16 +142,19 @@ task deployNodesRaft(type: Cordform, dependsOn: 'jar') { raft: [ nodeAddress: "localhost:10016", clusterAddresses: ["localhost:10008"] - ] + ], + className: className ] } } task deployNodesBFT(type: Cordform, dependsOn: 'jar') { def clusterAddresses = ["localhost:11000", "localhost:11010", "localhost:11020", "localhost:11030"] + def className = "net.corda.notary.bftsmart.BftSmartNotaryService" directory file("$buildDir/nodes/nodesBFT") nodeDefaults { extraConfig = [h2Settings: [address: "localhost:0"]] + cordapp project(':experimental:notary-bft-smart') } node { name "O=Alice Corp,L=Madrid,C=ES" @@ -189,7 +178,8 @@ task deployNodesBFT(type: Cordform, dependsOn: 'jar') { bftSMaRt: [ replicaId: 0, clusterAddresses: clusterAddresses - ] + ], + className: className ] } node { @@ -203,9 +193,10 @@ task deployNodesBFT(type: Cordform, dependsOn: 'jar') { validating: false, serviceLegalName: "O=BFT,L=Zurich,C=CH", bftSMaRt: [ - replicaId: 0, + replicaId: 1, clusterAddresses: clusterAddresses - ] + ], + className: className ] } node { @@ -219,9 +210,10 @@ task deployNodesBFT(type: Cordform, dependsOn: 'jar') { validating: false, serviceLegalName: "O=BFT,L=Zurich,C=CH", bftSMaRt: [ - replicaId: 0, + replicaId: 2, clusterAddresses: clusterAddresses - ] + ], + className: className ] } node { @@ -235,9 +227,10 @@ task deployNodesBFT(type: Cordform, dependsOn: 'jar') { validating: false, serviceLegalName: "O=BFT,L=Zurich,C=CH", bftSMaRt: [ - replicaId: 0, + replicaId: 3, clusterAddresses: clusterAddresses - ] + ], + className: className ] } } diff --git a/settings.gradle b/settings.gradle index 026a092f27..118033c379 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,8 @@ include 'experimental:behave' include 'experimental:quasar-hook' include 'experimental:kryo-hook' include 'experimental:corda-utils' +include 'experimental:notary-raft' +include 'experimental:notary-bft-smart' include 'jdk8u-deterministic' include 'test-common' include 'test-cli' diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index 102249761e..cdcb25d649 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -25,6 +25,8 @@ sourceSets { } dependencies { + // Bundling in the Raft notary service for tests involving distributed notaries + compile project(':experimental:notary-raft') compile project(':test-utils') // Integration test helpers diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 92615f3fd0..bd562e41f5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -473,6 +473,7 @@ class DriverDSLImpl( val config = NotaryConfig( validating = spec.validating, serviceLegalName = spec.name, + className = "net.corda.notary.raft.RaftNotaryService", raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) return config.toConfigMap() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 9d6ce49198..05ebe55eae 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -28,14 +28,15 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.hours import net.corda.core.utilities.seconds import net.corda.node.VersionInfo -import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.AbstractNode import net.corda.node.internal.InitiatedFlowFactory -import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StartedNodeServices -import net.corda.node.services.config.* +import net.corda.node.services.config.FlowTimeoutConfiguration +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.config.VerifierType import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.keys.KeyManagementServiceInternal @@ -43,8 +44,6 @@ import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.MessagingService import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.BFTNonValidatingNotaryService -import net.corda.node.services.transactions.BFTSMaRt import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.DefaultNamedCacheFactory import net.corda.nodeapi.internal.DevIdentityGenerator @@ -69,7 +68,6 @@ import java.math.BigInteger import java.nio.file.Path import java.nio.file.Paths import java.security.KeyPair -import java.security.PublicKey import java.time.Clock import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -149,7 +147,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe val notarySpecs: List = defaultParameters.notarySpecs, val testDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), val networkParameters: NetworkParameters = testNetworkParameters(), - val defaultFactory: (MockNodeArgs, CordappLoader?) -> MockNode = { args, cordappLoader -> cordappLoader?.let { MockNode(args, it) } ?: MockNode(args) }, + val defaultFactory: (MockNodeArgs) -> MockNode = { args -> MockNode(args) }, val cordappsForAllNodes: Set = emptySet(), val autoVisibleNodes: Boolean = true) : AutoCloseable { init { @@ -276,12 +274,11 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } } - open class MockNode(args: MockNodeArgs, cordappLoader: CordappLoader = JarScanningCordappLoader.fromDirectories(args.config.cordappDirectories, args.version)) : AbstractNode( + open class MockNode(args: MockNodeArgs) : AbstractNode( args.config, TestClock(Clock.systemUTC()), DefaultNamedCacheFactory(), args.version, - cordappLoader, args.network.getServerThread(args.id), args.network.busyLatch ) { @@ -424,27 +421,13 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe var acceptableLiveFiberCountOnStop: Int = 0 override fun acceptableLiveFiberCountOnStop(): Int = acceptableLiveFiberCountOnStop - - override fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster { - return object : BFTSMaRt.Cluster { - override fun waitUntilAllReplicasHaveInitialized() { - val clusterNodes = mockNet.nodes.map { it.started!! }.filter { notaryKey in it.info.legalIdentities.map { it.owningKey } } - if (clusterNodes.size != bftSMaRtConfig.clusterAddresses.size) { - throw IllegalStateException("Unable to enumerate all nodes in BFT cluster.") - } - clusterNodes.forEach { - (it.notaryService as BFTNonValidatingNotaryService).waitUntilReplicaHasInitialized() - } - } - } - } } fun createUnstartedNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters()): MockNode { return createUnstartedNode(parameters, defaultFactory) } - fun createUnstartedNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters(), nodeFactory: (MockNodeArgs, CordappLoader?) -> MockNode): MockNode { + fun createUnstartedNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters(), nodeFactory: (MockNodeArgs) -> MockNode): MockNode { return createNodeImpl(parameters, nodeFactory, false) } @@ -453,11 +436,11 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } /** Like the other [createNode] but takes a [nodeFactory] and propagates its [MockNode] subtype. */ - fun createNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters(), nodeFactory: (MockNodeArgs, CordappLoader?) -> MockNode): TestStartedNode { + fun createNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters(), nodeFactory: (MockNodeArgs) -> MockNode): TestStartedNode { return uncheckedCast(createNodeImpl(parameters, nodeFactory, true).started)!! } - private fun createNodeImpl(parameters: InternalMockNodeParameters, nodeFactory: (MockNodeArgs, CordappLoader?) -> MockNode, start: Boolean): MockNode { + private fun createNodeImpl(parameters: InternalMockNodeParameters, nodeFactory: (MockNodeArgs) -> MockNode, start: Boolean): MockNode { val id = parameters.forcedID ?: nextNodeId++ val baseDirectory = baseDirectory(id) val certificatesDirectory = baseDirectory / "certificates" @@ -474,7 +457,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe val cordappDirectories = sharedCorDappsDirectories + TestCordappDirectories.cached(cordapps) doReturn(cordappDirectories).whenever(config).cordappDirectories - val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version), JarScanningCordappLoader.fromDirectories(cordappDirectories, parameters.version)) + val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version)) _nodes += node if (start) { node.start() @@ -482,7 +465,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe return node } - fun restartNode(node: TestStartedNode, nodeFactory: (MockNodeArgs, CordappLoader?) -> MockNode): TestStartedNode { + fun restartNode(node: TestStartedNode, nodeFactory: (MockNodeArgs) -> MockNode): TestStartedNode { node.internals.disableDBCloseOnStop() node.dispose() return createNode( diff --git a/node/src/test/kotlin/net/corda/node/utilities/TestingNamedCacheFactory.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt similarity index 95% rename from node/src/test/kotlin/net/corda/node/utilities/TestingNamedCacheFactory.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt index 4582246e09..7908d5dd27 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/TestingNamedCacheFactory.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt @@ -1,4 +1,4 @@ -package net.corda.node.utilities +package net.corda.testing.internal import com.codahale.metrics.MetricRegistry import com.github.benmanes.caffeine.cache.Cache @@ -9,6 +9,7 @@ import net.corda.core.internal.buildNamed import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.config.MB import net.corda.node.services.config.NodeConfiguration +import net.corda.node.utilities.NamedCacheFactory class TestingNamedCacheFactory private constructor(private val sizeOverride: Long, private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : NamedCacheFactory, SingletonSerializeAsToken() { constructor(sizeOverride: Long = 1024) : this(sizeOverride, null, null) From 554b1fa371eb35c9009df83b65afa852602f317b Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Wed, 10 Oct 2018 10:35:18 +0100 Subject: [PATCH 32/83] [CORDA-2084] EdDSA, SPHINCS-256 and RSA PKCS#1 are deterministic, no RNG required. (#4051) --- .../kotlin/net/corda/core/crypto/Crypto.kt | 20 +- .../net/corda/core/crypto/CryptoUtilsTest.kt | 222 ++++++++++-------- 2 files changed, 138 insertions(+), 104 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index e131cdc7df..9da4417c7d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -424,15 +424,19 @@ object Crypto { } require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" } val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) - // Note that deterministic signature schemes, such as EdDSA, do not require extra randomness, but we have to - // ensure that non-deterministic algorithms (i.e., ECDSA) use non-blocking SecureRandom implementations (if possible). - // TODO consider updating this when the related BC issue for Sphincs is fixed. - if (signatureScheme != SPHINCS256_SHA256) { - signature.initSign(privateKey, newSecureRandom()) - } else { - // Special handling for Sphincs, due to a BC implementation issue. - // As Sphincs is deterministic, it does not require RNG input anyway. + // Note that deterministic signature schemes, such as EdDSA, original SPHINCS-256 and RSA PKCS#1, do not require + // extra randomness, but we have to ensure that non-deterministic algorithms (i.e., ECDSA) use non-blocking + // SecureRandom implementation. Also, SPHINCS-256 implementation in BouncyCastle 1.60 fails with + // ClassCastException if we invoke initSign with a SecureRandom as an input. + // TODO Although we handle the above issue here, consider updating to BC 1.61+ which provides a fix. + if (signatureScheme == EDDSA_ED25519_SHA512 + || signatureScheme == SPHINCS256_SHA256 + || signatureScheme == RSA_SHA256) { signature.initSign(privateKey) + } else { + // The rest of the algorithms will require a SecureRandom input (i.e., ECDSA or any new algorithm for which + // we don't know if it's deterministic). + signature.initSign(privateKey, newSecureRandom()) } signature.update(clearData) return signature.sign() diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index a8f156b77a..dbfa620d15 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -1,6 +1,12 @@ package net.corda.core.crypto import com.google.common.collect.Sets +import net.corda.core.crypto.Crypto.ECDSA_SECP256K1_SHA256 +import net.corda.core.crypto.Crypto.ECDSA_SECP256R1_SHA256 +import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 +import net.corda.core.crypto.Crypto.RSA_SHA256 +import net.corda.core.crypto.Crypto.SPHINCS256_SHA256 +import net.corda.core.utilities.OpaqueBytes import net.i2p.crypto.eddsa.EdDSAKey import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey @@ -30,17 +36,20 @@ import kotlin.test.* */ class CryptoUtilsTest { - private val testBytes = "Hello World".toByteArray() + companion object { + private val testBytes = "Hello World".toByteArray() + private val test100ZeroBytes = ByteArray(100) + } // key generation test @Test fun `Generate key pairs`() { // testing supported algorithms - val rsaKeyPair = Crypto.generateKeyPair(Crypto.RSA_SHA256) - val ecdsaKKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) - val ecdsaRKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) - val eddsaKeyPair = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) - val sphincsKeyPair = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + val rsaKeyPair = Crypto.generateKeyPair(RSA_SHA256) + val ecdsaKKeyPair = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) + val ecdsaRKeyPair = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256) + val eddsaKeyPair = Crypto.generateKeyPair(EDDSA_ED25519_SHA512) + val sphincsKeyPair = Crypto.generateKeyPair(SPHINCS256_SHA256) // not null private keys assertNotNull(rsaKeyPair.private) @@ -69,7 +78,7 @@ class CryptoUtilsTest { @Test fun `RSA full process keygen-sign-verify`() { - val keyPair = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val keyPair = Crypto.generateKeyPair(RSA_SHA256) val (privKey, pubKey) = keyPair // test for some data val signedData = Crypto.doSign(privKey, testBytes) @@ -101,8 +110,8 @@ class CryptoUtilsTest { } // test for zero bytes data - val signedDataZeros = Crypto.doSign(privKey, ByteArray(100)) - val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, ByteArray(100)) + val signedDataZeros = Crypto.doSign(privKey, test100ZeroBytes) + val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, test100ZeroBytes) assertTrue(verificationZeros) // test for 1MB of data (I successfully tested it locally for 1GB as well) @@ -124,7 +133,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256k1 full process keygen-sign-verify`() { - val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPair = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val (privKey, pubKey) = keyPair // test for some data val signedData = Crypto.doSign(privKey, testBytes) @@ -156,8 +165,8 @@ class CryptoUtilsTest { } // test for zero bytes data - val signedDataZeros = Crypto.doSign(privKey, ByteArray(100)) - val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, ByteArray(100)) + val signedDataZeros = Crypto.doSign(privKey, test100ZeroBytes) + val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, test100ZeroBytes) assertTrue(verificationZeros) // test for 1MB of data (I successfully tested it locally for 1GB as well) @@ -179,7 +188,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256r1 full process keygen-sign-verify`() { - val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val keyPair = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256) val (privKey, pubKey) = keyPair // test for some data val signedData = Crypto.doSign(privKey, testBytes) @@ -211,8 +220,8 @@ class CryptoUtilsTest { } // test for zero bytes data - val signedDataZeros = Crypto.doSign(privKey, ByteArray(100)) - val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, ByteArray(100)) + val signedDataZeros = Crypto.doSign(privKey, test100ZeroBytes) + val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, test100ZeroBytes) assertTrue(verificationZeros) // test for 1MB of data (I successfully tested it locally for 1GB as well) @@ -234,7 +243,7 @@ class CryptoUtilsTest { @Test fun `EDDSA ed25519 full process keygen-sign-verify`() { - val keyPair = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val keyPair = Crypto.generateKeyPair(EDDSA_ED25519_SHA512) val (privKey, pubKey) = keyPair // test for some data val signedData = Crypto.doSign(privKey, testBytes) @@ -266,8 +275,8 @@ class CryptoUtilsTest { } // test for zero bytes data - val signedDataZeros = Crypto.doSign(privKey, ByteArray(100)) - val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, ByteArray(100)) + val signedDataZeros = Crypto.doSign(privKey, test100ZeroBytes) + val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, test100ZeroBytes) assertTrue(verificationZeros) // test for 1MB of data (I successfully tested it locally for 1GB as well) @@ -289,7 +298,7 @@ class CryptoUtilsTest { @Test fun `SPHINCS-256 full process keygen-sign-verify`() { - val keyPair = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + val keyPair = Crypto.generateKeyPair(SPHINCS256_SHA256) val (privKey, pubKey) = keyPair // test for some data val signedData = Crypto.doSign(privKey, testBytes) @@ -321,8 +330,8 @@ class CryptoUtilsTest { } // test for zero bytes data - val signedDataZeros = Crypto.doSign(privKey, ByteArray(100)) - val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, ByteArray(100)) + val signedDataZeros = Crypto.doSign(privKey, test100ZeroBytes) + val verificationZeros = Crypto.doVerify(pubKey, signedDataZeros, test100ZeroBytes) assertTrue(verificationZeros) // test for 1MB of data (I successfully tested it locally for 1GB as well) @@ -354,7 +363,7 @@ class CryptoUtilsTest { @Test fun `RSA encode decode keys - required for serialization`() { // Generate key pair. - val keyPair = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val keyPair = Crypto.generateKeyPair(RSA_SHA256) val (privKey, pubKey) = keyPair // Encode and decode private key. @@ -369,7 +378,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256k1 encode decode keys - required for serialization`() { // Generate key pair. - val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPair = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val (privKey, pubKey) = keyPair // Encode and decode private key. @@ -384,7 +393,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256r1 encode decode keys - required for serialization`() { // Generate key pair. - val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val keyPair = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256) val (privKey, pubKey) = keyPair // Encode and decode private key. @@ -399,7 +408,7 @@ class CryptoUtilsTest { @Test fun `EdDSA encode decode keys - required for serialization`() { // Generate key pair. - val keyPair = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val keyPair = Crypto.generateKeyPair(EDDSA_ED25519_SHA512) val (privKey, pubKey) = keyPair // Encode and decode private key. @@ -414,7 +423,7 @@ class CryptoUtilsTest { @Test fun `SPHINCS-256 encode decode keys - required for serialization`() { // Generate key pair. - val keyPair = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + val keyPair = Crypto.generateKeyPair(SPHINCS256_SHA256) val privKey: BCSphincs256PrivateKey = keyPair.private as BCSphincs256PrivateKey val pubKey: BCSphincs256PublicKey = keyPair.public as BCSphincs256PublicKey @@ -443,7 +452,7 @@ class CryptoUtilsTest { @Test fun `RSA scheme finder by key type`() { - val keyPairRSA = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val keyPairRSA = Crypto.generateKeyPair(RSA_SHA256) val (privRSA, pubRSA) = keyPairRSA assertEquals(privRSA.algorithm, "RSA") assertEquals(pubRSA.algorithm, "RSA") @@ -451,7 +460,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256k1 scheme finder by key type`() { - val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPair = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val (privKey, pubKey) = keyPair // Encode and decode private key. @@ -466,7 +475,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256r1 scheme finder by key type`() { - val keyPairR1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val keyPairR1 = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256) val (privR1, pubR1) = keyPairR1 assertEquals(privR1.algorithm, "ECDSA") assertEquals((privR1 as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1")) @@ -476,7 +485,7 @@ class CryptoUtilsTest { @Test fun `EdDSA scheme finder by key type`() { - val keyPairEd = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val keyPairEd = Crypto.generateKeyPair(EDDSA_ED25519_SHA512) val (privEd, pubEd) = keyPairEd assertEquals(privEd.algorithm, "EdDSA") @@ -487,7 +496,7 @@ class CryptoUtilsTest { @Test fun `SPHINCS-256 scheme finder by key type`() { - val keyPairSP = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + val keyPairSP = Crypto.generateKeyPair(SPHINCS256_SHA256) val (privSP, pubSP) = keyPairSP assertEquals(privSP.algorithm, "SPHINCS-256") assertEquals(pubSP.algorithm, "SPHINCS-256") @@ -495,7 +504,7 @@ class CryptoUtilsTest { @Test fun `Automatic EdDSA key-type detection and decoding`() { - val keyPairEd = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val keyPairEd = Crypto.generateKeyPair(EDDSA_ED25519_SHA512) val (privEd, pubEd) = keyPairEd val encodedPrivEd = privEd.encoded val encodedPubEd = pubEd.encoded @@ -511,7 +520,7 @@ class CryptoUtilsTest { @Test fun `Automatic ECDSA secp256k1 key-type detection and decoding`() { - val keyPairK1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPairK1 = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val (privK1, pubK1) = keyPairK1 val encodedPrivK1 = privK1.encoded val encodedPubK1 = pubK1.encoded @@ -527,7 +536,7 @@ class CryptoUtilsTest { @Test fun `Automatic ECDSA secp256r1 key-type detection and decoding`() { - val keyPairR1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val keyPairR1 = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256) val (privR1, pubR1) = keyPairR1 val encodedPrivR1 = privR1.encoded val encodedPubR1 = pubR1.encoded @@ -543,7 +552,7 @@ class CryptoUtilsTest { @Test fun `Automatic RSA key-type detection and decoding`() { - val keyPairRSA = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val keyPairRSA = Crypto.generateKeyPair(RSA_SHA256) val (privRSA, pubRSA) = keyPairRSA val encodedPrivRSA = privRSA.encoded val encodedPubRSA = pubRSA.encoded @@ -559,7 +568,7 @@ class CryptoUtilsTest { @Test fun `Automatic SPHINCS-256 key-type detection and decoding`() { - val keyPairSP = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + val keyPairSP = Crypto.generateKeyPair(SPHINCS256_SHA256) val (privSP, pubSP) = keyPairSP val encodedPrivSP = privSP.encoded val encodedPubSP = pubSP.encoded @@ -575,12 +584,12 @@ class CryptoUtilsTest { @Test fun `Failure test between K1 and R1 keys`() { - val keyPairK1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPairK1 = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val privK1 = keyPairK1.private val encodedPrivK1 = privK1.encoded val decodedPrivK1 = Crypto.decodePrivateKey(encodedPrivK1) - val keyPairR1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val keyPairR1 = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256) val privR1 = keyPairR1.private val encodedPrivR1 = privR1.encoded val decodedPrivR1 = Crypto.decodePrivateKey(encodedPrivR1) @@ -590,7 +599,7 @@ class CryptoUtilsTest { @Test fun `Decoding Failure on randomdata as key`() { - val keyPairK1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPairK1 = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val privK1 = keyPairK1.private val encodedPrivK1 = privK1.encoded @@ -610,7 +619,7 @@ class CryptoUtilsTest { @Test fun `Decoding Failure on malformed keys`() { - val keyPairK1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPairK1 = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val privK1 = keyPairK1.private val encodedPrivK1 = privK1.encoded @@ -630,25 +639,25 @@ class CryptoUtilsTest { @Test fun `Check ECDSA public key on curve`() { - val keyPairK1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val keyPairK1 = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val pubK1 = keyPairK1.public as BCECPublicKey - assertTrue(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256K1_SHA256, pubK1)) + assertTrue(Crypto.publicKeyOnCurve(ECDSA_SECP256K1_SHA256, pubK1)) // use R1 curve for check. - assertFalse(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256R1_SHA256, pubK1)) + assertFalse(Crypto.publicKeyOnCurve(ECDSA_SECP256R1_SHA256, pubK1)) // use ed25519 curve for check. - assertFalse(Crypto.publicKeyOnCurve(Crypto.EDDSA_ED25519_SHA512, pubK1)) + assertFalse(Crypto.publicKeyOnCurve(EDDSA_ED25519_SHA512, pubK1)) } @Test fun `Check EdDSA public key on curve`() { - val keyPairEdDSA = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val keyPairEdDSA = Crypto.generateKeyPair(EDDSA_ED25519_SHA512) val pubEdDSA = keyPairEdDSA.public - assertTrue(Crypto.publicKeyOnCurve(Crypto.EDDSA_ED25519_SHA512, pubEdDSA)) + assertTrue(Crypto.publicKeyOnCurve(EDDSA_ED25519_SHA512, pubEdDSA)) // Use R1 curve for check. - assertFalse(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256R1_SHA256, pubEdDSA)) + assertFalse(Crypto.publicKeyOnCurve(ECDSA_SECP256R1_SHA256, pubEdDSA)) // Check for point at infinity. - val pubKeySpec = EdDSAPublicKeySpec((Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3), Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec) - assertFalse(Crypto.publicKeyOnCurve(Crypto.EDDSA_ED25519_SHA512, EdDSAPublicKey(pubKeySpec))) + val pubKeySpec = EdDSAPublicKeySpec((EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3), EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec) + assertFalse(Crypto.publicKeyOnCurve(EDDSA_ED25519_SHA512, EdDSAPublicKey(pubKeySpec))) } @Test(expected = IllegalArgumentException::class) @@ -658,12 +667,12 @@ class CryptoUtilsTest { val pairSun = keyGen.generateKeyPair() val pubSun = pairSun.public // Should fail as pubSun is not a BCECPublicKey. - Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256R1_SHA256, pubSun) + Crypto.publicKeyOnCurve(ECDSA_SECP256R1_SHA256, pubSun) } @Test fun `ECDSA secp256R1 deterministic key generation`() { - val (priv, pub) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (priv, pub) = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256) val (dpriv, dpub) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) // Check scheme. @@ -673,11 +682,11 @@ class CryptoUtilsTest { assertTrue(dpub is BCECPublicKey) assertEquals((dpriv as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1")) assertEquals((dpub as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1")) - assertEquals(Crypto.findSignatureScheme(dpriv), Crypto.ECDSA_SECP256R1_SHA256) - assertEquals(Crypto.findSignatureScheme(dpub), Crypto.ECDSA_SECP256R1_SHA256) + assertEquals(Crypto.findSignatureScheme(dpriv), ECDSA_SECP256R1_SHA256) + assertEquals(Crypto.findSignatureScheme(dpub), ECDSA_SECP256R1_SHA256) // Validate public key. - assertTrue(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256R1_SHA256, dpub)) + assertTrue(Crypto.publicKeyOnCurve(ECDSA_SECP256R1_SHA256, dpub)) // Try to sign/verify. val signedData = Crypto.doSign(dpriv, testBytes) @@ -704,7 +713,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256K1 deterministic key generation`() { - val (priv, pub) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (priv, pub) = Crypto.generateKeyPair(ECDSA_SECP256K1_SHA256) val (dpriv, dpub) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) // Check scheme. @@ -714,11 +723,11 @@ class CryptoUtilsTest { assertTrue(dpub is BCECPublicKey) assertEquals((dpriv as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1")) assertEquals((dpub as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1")) - assertEquals(Crypto.findSignatureScheme(dpriv), Crypto.ECDSA_SECP256K1_SHA256) - assertEquals(Crypto.findSignatureScheme(dpub), Crypto.ECDSA_SECP256K1_SHA256) + assertEquals(Crypto.findSignatureScheme(dpriv), ECDSA_SECP256K1_SHA256) + assertEquals(Crypto.findSignatureScheme(dpub), ECDSA_SECP256K1_SHA256) // Validate public key. - assertTrue(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256K1_SHA256, dpub)) + assertTrue(Crypto.publicKeyOnCurve(ECDSA_SECP256K1_SHA256, dpub)) // Try to sign/verify. val signedData = Crypto.doSign(dpriv, testBytes) @@ -745,7 +754,7 @@ class CryptoUtilsTest { @Test fun `EdDSA ed25519 deterministic key generation`() { - val (priv, pub) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (priv, pub) = Crypto.generateKeyPair(EDDSA_ED25519_SHA512) val (dpriv, dpub) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) // Check scheme. @@ -755,11 +764,11 @@ class CryptoUtilsTest { assertTrue(dpub is EdDSAPublicKey) assertEquals((dpriv as EdDSAKey).params, EdDSANamedCurveTable.getByName("ED25519")) assertEquals((dpub as EdDSAKey).params, EdDSANamedCurveTable.getByName("ED25519")) - assertEquals(Crypto.findSignatureScheme(dpriv), Crypto.EDDSA_ED25519_SHA512) - assertEquals(Crypto.findSignatureScheme(dpub), Crypto.EDDSA_ED25519_SHA512) + assertEquals(Crypto.findSignatureScheme(dpriv), EDDSA_ED25519_SHA512) + assertEquals(Crypto.findSignatureScheme(dpub), EDDSA_ED25519_SHA512) // Validate public key. - assertTrue(Crypto.publicKeyOnCurve(Crypto.EDDSA_ED25519_SHA512, dpub)) + assertTrue(Crypto.publicKeyOnCurve(EDDSA_ED25519_SHA512, dpub)) // Try to sign/verify. val signedData = Crypto.doSign(dpriv, testBytes) @@ -786,110 +795,131 @@ class CryptoUtilsTest { @Test fun `EdDSA ed25519 keyPair from entropy`() { - val keyPairPositive = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("10")) + val keyPairPositive = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("10")) assertEquals("DLBL3iHCp9uRReWhhCGfCsrxZZpfAm9h9GLbfN8ijqXTq", keyPairPositive.public.toStringShort()) - val keyPairNegative = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("-10")) + val keyPairNegative = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("-10")) assertEquals("DLC5HXnYsJAFqmM9hgPj5G8whQ4TpyE9WMBssqCayLBwA2", keyPairNegative.public.toStringShort()) - val keyPairZero = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("0")) + val keyPairZero = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("0")) assertEquals("DL4UVhGh4tqu1G86UVoGNaDDNCMsBtNHzE6BSZuNNJN7W2", keyPairZero.public.toStringShort()) - val keyPairOne = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("1")) + val keyPairOne = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("1")) assertEquals("DL8EZUdHixovcCynKMQzrMWBnXQAcbVDHi6ArPphqwJVzq", keyPairOne.public.toStringShort()) - val keyPairBiggerThan256bits = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("2").pow(258).minus(BigInteger.TEN)) + val keyPairBiggerThan256bits = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("2").pow(258).minus(BigInteger.TEN)) assertEquals("DLB9K1UiBrWonn481z6NzkqoWHjMBXpfDeaet3wiwRNWSU", keyPairBiggerThan256bits.public.toStringShort()) // The underlying implementation uses the first 256 bytes of the entropy. Thus, 2^258-10 and 2^258-50 and 2^514-10 have the same impact. - val keyPairBiggerThan256bitsV2 = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("2").pow(258).minus(BigInteger("50"))) + val keyPairBiggerThan256bitsV2 = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("2").pow(258).minus(BigInteger("50"))) assertEquals("DLB9K1UiBrWonn481z6NzkqoWHjMBXpfDeaet3wiwRNWSU", keyPairBiggerThan256bitsV2.public.toStringShort()) - val keyPairBiggerThan512bits = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("2").pow(514).minus(BigInteger.TEN)) + val keyPairBiggerThan512bits = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("2").pow(514).minus(BigInteger.TEN)) assertEquals("DLB9K1UiBrWonn481z6NzkqoWHjMBXpfDeaet3wiwRNWSU", keyPairBiggerThan512bits.public.toStringShort()) // Try another big number. - val keyPairBiggerThan258bits = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger("2").pow(259).plus(BigInteger.ONE)) + val keyPairBiggerThan258bits = Crypto.deriveKeyPairFromEntropy(EDDSA_ED25519_SHA512, BigInteger("2").pow(259).plus(BigInteger.ONE)) assertEquals("DL5tEFVMXMGrzwjfCAW34JjkhsRkPfFyJ38iEnmpB6L2Z9", keyPairBiggerThan258bits.public.toStringShort()) } @Test fun `ECDSA R1 keyPair from entropy`() { - val keyPairPositive = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("10")) + val keyPairPositive = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("10")) assertEquals("DLHDcxuSt9J3cbjd2Dsx4rAgYYA7BAP7A8VLrFiq1tH9yy", keyPairPositive.public.toStringShort()) // The underlying implementation uses the hash of entropy if it is out of range 2 < entropy < N, where N the order of the group. - val keyPairNegative = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("-10")) + val keyPairNegative = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("-10")) assertEquals("DLBASmjiMZuu1g3EtdHJxfSueXE8PRoUWbkdU61Qcnpamt", keyPairNegative.public.toStringShort()) - val keyPairZero = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("0")) + val keyPairZero = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("0")) assertEquals("DLH2FEHEnsT3MpCJt2gfyNjpqRqcBxeupK4YRPXvDsVEkb", keyPairZero.public.toStringShort()) // BigIntenger.Zero is out or range, so 1 and hash(1.toByteArray) would have the same impact. val zeroHashed = BigInteger(1, BigInteger("0").toByteArray().sha256().bytes) // Check oneHashed < N (order of the group), otherwise we would need an extra hash. - assertEquals(-1, zeroHashed.compareTo((Crypto.ECDSA_SECP256R1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) - val keyPairZeroHashed = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, zeroHashed) + assertEquals(-1, zeroHashed.compareTo((ECDSA_SECP256R1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) + val keyPairZeroHashed = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, zeroHashed) assertEquals("DLH2FEHEnsT3MpCJt2gfyNjpqRqcBxeupK4YRPXvDsVEkb", keyPairZeroHashed.public.toStringShort()) - val keyPairOne = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("1")) + val keyPairOne = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("1")) assertEquals("DLHrtKwjv6onq9HcrQDJPs8Cgtai5mZU5ZU6sb1ivJjx3z", keyPairOne.public.toStringShort()) // BigIntenger.ONE is out or range, so 1 and hash(1.toByteArray) would have the same impact. val oneHashed = BigInteger(1, BigInteger("1").toByteArray().sha256().bytes) // Check oneHashed < N (order of the group), otherwise we would need an extra hash. - assertEquals(-1, oneHashed.compareTo((Crypto.ECDSA_SECP256R1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) - val keyPairOneHashed = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, oneHashed) + assertEquals(-1, oneHashed.compareTo((ECDSA_SECP256R1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) + val keyPairOneHashed = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, oneHashed) assertEquals("DLHrtKwjv6onq9HcrQDJPs8Cgtai5mZU5ZU6sb1ivJjx3z", keyPairOneHashed.public.toStringShort()) // 2 is in the range. - val keyPairTwo = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("2")) + val keyPairTwo = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("2")) assertEquals("DLFoz6txJ3vHcKNSM1vFxHJUoEQ69PorBwW64dHsAnEoZB", keyPairTwo.public.toStringShort()) // Try big numbers that are out of range. - val keyPairBiggerThan256bits = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("2").pow(258).minus(BigInteger.TEN)) + val keyPairBiggerThan256bits = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("2").pow(258).minus(BigInteger.TEN)) assertEquals("DLBv6fZqaCTbE4L7sgjbt19biXHMgU9CzR5s8g8XBJjZ11", keyPairBiggerThan256bits.public.toStringShort()) - val keyPairBiggerThan256bitsV2 = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("2").pow(258).minus(BigInteger("50"))) + val keyPairBiggerThan256bitsV2 = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("2").pow(258).minus(BigInteger("50"))) assertEquals("DLANmjhGSVdLyghxcPHrn3KuGatscf6LtvqifUDxw7SGU8", keyPairBiggerThan256bitsV2.public.toStringShort()) - val keyPairBiggerThan512bits = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("2").pow(514).minus(BigInteger.TEN)) + val keyPairBiggerThan512bits = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("2").pow(514).minus(BigInteger.TEN)) assertEquals("DL9sKwMExBTD3MnJN6LWGqo496Erkebs9fxZtXLVJUBY9Z", keyPairBiggerThan512bits.public.toStringShort()) - val keyPairBiggerThan258bits = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger("2").pow(259).plus(BigInteger.ONE)) + val keyPairBiggerThan258bits = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256R1_SHA256, BigInteger("2").pow(259).plus(BigInteger.ONE)) assertEquals("DLBwjWwPJSF9E7b1NWaSbEJ4oK8CF7RDGWd648TiBhZoL1", keyPairBiggerThan258bits.public.toStringShort()) } @Test fun `ECDSA K1 keyPair from entropy`() { - val keyPairPositive = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("10")) + val keyPairPositive = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("10")) assertEquals("DL6pYKUgH17az8MLdonvvUtUPN8TqwpCGcdgLr7vg3skCU", keyPairPositive.public.toStringShort()) // The underlying implementation uses the hash of entropy if it is out of range 2 <= entropy < N, where N the order of the group. - val keyPairNegative = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("-10")) + val keyPairNegative = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("-10")) assertEquals("DLnpXhxece69Nyqgm3pPt3yV7ESQYDJKoYxs1hKgfBAEu", keyPairNegative.public.toStringShort()) - val keyPairZero = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("0")) + val keyPairZero = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("0")) assertEquals("DLBC28e18T6KsYwjTFfUWJfhvHjvYVapyVf6antnqUkbgd", keyPairZero.public.toStringShort()) // BigIntenger.Zero is out or range, so 1 and hash(1.toByteArray) would have the same impact. val zeroHashed = BigInteger(1, BigInteger("0").toByteArray().sha256().bytes) // Check oneHashed < N (order of the group), otherwise we would need an extra hash. - assertEquals(-1, zeroHashed.compareTo((Crypto.ECDSA_SECP256K1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) - val keyPairZeroHashed = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, zeroHashed) + assertEquals(-1, zeroHashed.compareTo((ECDSA_SECP256K1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) + val keyPairZeroHashed = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, zeroHashed) assertEquals("DLBC28e18T6KsYwjTFfUWJfhvHjvYVapyVf6antnqUkbgd", keyPairZeroHashed.public.toStringShort()) - val keyPairOne = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("1")) + val keyPairOne = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("1")) assertEquals("DLBimRXdEQhJUTpL6f9ri9woNdsze6mwkRrhsML13Eh7ET", keyPairOne.public.toStringShort()) // BigIntenger.ONE is out or range, so 1 and hash(1.toByteArray) would have the same impact. val oneHashed = BigInteger(1, BigInteger("1").toByteArray().sha256().bytes) // Check oneHashed < N (order of the group), otherwise we would need an extra hash. - assertEquals(-1, oneHashed.compareTo((Crypto.ECDSA_SECP256K1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) - val keyPairOneHashed = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, oneHashed) + assertEquals(-1, oneHashed.compareTo((ECDSA_SECP256K1_SHA256.algSpec as ECNamedCurveParameterSpec).n)) + val keyPairOneHashed = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, oneHashed) assertEquals("DLBimRXdEQhJUTpL6f9ri9woNdsze6mwkRrhsML13Eh7ET", keyPairOneHashed.public.toStringShort()) // 2 is in the range. - val keyPairTwo = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("2")) + val keyPairTwo = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("2")) assertEquals("DLG32UWaevGw9YY7w1Rf9mmK88biavgpDnJA9bG4GapVPs", keyPairTwo.public.toStringShort()) // Try big numbers that are out of range. - val keyPairBiggerThan256bits = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("2").pow(258).minus(BigInteger.TEN)) + val keyPairBiggerThan256bits = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("2").pow(258).minus(BigInteger.TEN)) assertEquals("DLGHsdv2xeAuM7n3sBc6mFfiphXe6VSf3YxqvviKDU6Vbd", keyPairBiggerThan256bits.public.toStringShort()) - val keyPairBiggerThan256bitsV2 = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("2").pow(258).minus(BigInteger("50"))) + val keyPairBiggerThan256bitsV2 = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("2").pow(258).minus(BigInteger("50"))) assertEquals("DL9yJfiNGqteRrKPjGUkRQkeqzuQ4kwcYQWMCi5YKuUHrk", keyPairBiggerThan256bitsV2.public.toStringShort()) - val keyPairBiggerThan512bits = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("2").pow(514).minus(BigInteger.TEN)) + val keyPairBiggerThan512bits = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("2").pow(514).minus(BigInteger.TEN)) assertEquals("DL3Wr5EQGrMTaKBy5XMvG8rvSfKX1AYZLCRU8kixGbxt1E", keyPairBiggerThan512bits.public.toStringShort()) - val keyPairBiggerThan258bits = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger("2").pow(259).plus(BigInteger.ONE)) + val keyPairBiggerThan258bits = Crypto.deriveKeyPairFromEntropy(ECDSA_SECP256K1_SHA256, BigInteger("2").pow(259).plus(BigInteger.ONE)) assertEquals("DL7NbssqvuuJ4cqFkkaVYu9j1MsVswESGgCfbqBS9ULwuM", keyPairBiggerThan258bits.public.toStringShort()) } + + @Test + fun `Ensure deterministic signatures of EdDSA, SPHINCS-256 and RSA PKCS1`() { + listOf(EDDSA_ED25519_SHA512, SPHINCS256_SHA256, RSA_SHA256) + .forEach { testDeterministicSignatures(it) } + } + + private fun testDeterministicSignatures(signatureScheme: SignatureScheme) { + val privateKey = Crypto.generateKeyPair(signatureScheme).private + val signedData1stTime = Crypto.doSign(privateKey, testBytes) + val signedData2ndTime = Crypto.doSign(privateKey, testBytes) + assertEquals(OpaqueBytes(signedData1stTime), OpaqueBytes(signedData2ndTime)) + + // Try for the special case of signing a zero array. + val signedZeroArray1stTime = Crypto.doSign(privateKey, test100ZeroBytes) + val signedZeroArray2ndTime = Crypto.doSign(privateKey, test100ZeroBytes) + assertEquals(OpaqueBytes(signedZeroArray1stTime), OpaqueBytes(signedZeroArray2ndTime)) + + // Just in case, test that signatures of different messages are not the same. + assertNotEquals(OpaqueBytes(signedData1stTime), OpaqueBytes(signedZeroArray1stTime)) + } } From b8b2cc772d27ab78330557f4deb13be112a63642 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 10 Oct 2018 13:31:29 +0100 Subject: [PATCH 33/83] CORDA-535: Remove the old mechanism for loading custom notary service implementations. All notary service implementations are now assumed to be loaded from CorDapps. --- docs/source/tutorial-custom-notary.rst | 13 ++-- .../net/corda/node/internal/AbstractNode.kt | 75 +++++-------------- .../node/services/config/NodeConfiguration.kt | 5 +- .../net/corda/node/services/TimedFlowTests.kt | 1 - samples/notary-demo/build.gradle | 5 +- .../corda/notarydemo/MyCustomNotaryService.kt | 5 +- 6 files changed, 32 insertions(+), 72 deletions(-) diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index cd102e484f..cf7b78a0ff 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -4,13 +4,12 @@ Writing a custom notary service (experimental) ============================================== .. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. The APIs - for writing a custom notary may change in the future. Additionally, customising Raft or BFT notaries is not yet - fully supported. If you want to write your own Raft notary you will have to implement a custom database connector - (or use a separate database for the notary), and use a custom configuration file. + for writing a custom notary may change in the future. -Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it -with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary -service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``. +The first step is to create a service class in your CorDapp that extends the ``NotaryService`` abstract class. +This will ensure that it is recognised as a notary service. +The custom notary service class should provide a constructor with two parameters of types ``ServiceHubInternal`` and ``PublicKey``. +Note that ``ServiceHubInternal`` does not provide any API stability guarantees. .. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin @@ -32,5 +31,5 @@ To enable the service, add the following to the node configuration: notary : { validating : true # Set to false if your service is non-validating - custom : true + className : "net.corda.notarydemo.MyCustomValidatingNotaryService" # The fully qualified name of your service class } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index bea2d435ae..141c92778b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -42,9 +42,12 @@ import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.api.* -import net.corda.node.services.config.* +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.shell.toShellConfig +import net.corda.node.services.config.shouldInitCrashShell import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver import net.corda.node.services.identity.PersistentIdentityService @@ -59,7 +62,8 @@ import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.* import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.* -import net.corda.node.services.transactions.* +import net.corda.node.services.transactions.InMemoryTransactionVerifierService +import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.* @@ -518,9 +522,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun installCordaServices(myNotaryIdentity: PartyAndCertificate?) { val loadedServices = cordappLoader.cordapps.flatMap { it.services } - filterServicesToInstall(loadedServices).forEach { + loadedServices.forEach { try { - installCordaService(flowStarter, it, myNotaryIdentity) + installCordaService(flowStarter, it) } catch (e: NoSuchMethodException) { log.error("${it.name}, as a Corda service, must have a constructor with a single parameter of type " + ServiceHub::class.java.name) @@ -532,24 +536,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - private fun filterServicesToInstall(loadedServices: List>): List> { - val customNotaryServiceList = loadedServices.filter { isNotaryService(it) } - if (customNotaryServiceList.isNotEmpty()) { - if (configuration.notary?.custom == true) { - require(customNotaryServiceList.size == 1) { - "Attempting to install more than one notary service: ${customNotaryServiceList.joinToString()}" - } - } else return loadedServices - customNotaryServiceList - } - return loadedServices - } - - /** - * If the [serviceClass] is a notary service, it will only be enabled if the "custom" flag is set in - * the notary configuration. - */ - private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass) - /** * This customizes the ServiceHub for each CordaService that is initiating flows. */ @@ -590,53 +576,30 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override fun hashCode() = Objects.hash(serviceHub, flowStarter, serviceInstance) } - private fun installCordaService(flowStarter: FlowStarter, serviceClass: Class, myNotaryIdentity: PartyAndCertificate?) { + private fun installCordaService(flowStarter: FlowStarter, serviceClass: Class) { serviceClass.requireAnnotation() val service = try { - if (isNotaryService(serviceClass)) { - myNotaryIdentity ?: throw IllegalStateException("Trying to install a notary service but no notary identity specified") - try { - val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true } - constructor.newInstance(services, myNotaryIdentity.owningKey ) - } catch (ex: NoSuchMethodException) { - val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } - val serviceContext = AppServiceHubImpl(services, flowStarter) - val service = constructor.newInstance(serviceContext, myNotaryIdentity.owningKey) - serviceContext.serviceInstance = service - service - } - } else { - try { - val serviceContext = AppServiceHubImpl(services, flowStarter) - val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } - val service = extendedServiceConstructor.newInstance(serviceContext) - serviceContext.serviceInstance = service - service - } catch (ex: NoSuchMethodException) { - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true } - log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " + - "Upgrade to an AppServiceHub parameter to enable updated API features.") - constructor.newInstance(services) - } - } + val serviceContext = AppServiceHubImpl(services, flowStarter) + val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } + val service = extendedServiceConstructor.newInstance(serviceContext) + serviceContext.serviceInstance = service + service + } catch (ex: NoSuchMethodException) { + val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true } + log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " + + "Upgrade to an AppServiceHub parameter to enable updated API features.") + constructor.newInstance(services) } catch (e: InvocationTargetException) { throw ServiceInstantiationException(e.cause) } cordappServices.putInstance(serviceClass, service) - if (service is NotaryService) handleCustomNotaryService(service) service.tokenize() log.info("Installed ${serviceClass.name} Corda service") } - private fun handleCustomNotaryService(service: NotaryService) { - runOnStop += service::stop - installCoreFlow(NotaryFlow.Client::class, service::createServiceFlow) - service.start() - } - private fun registerCordappFlows() { cordappLoader.cordapps.flatMap { it.initiatedFlows } .forEach { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 28e73e4600..5da2e9724f 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -122,13 +122,12 @@ fun NodeConfiguration.shouldInitCrashShell() = shouldStartLocalShell() || should data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null, - val custom: Boolean = false, val serviceLegalName: CordaX500Name? = null, val className: String = "net.corda.node.services.transactions.SimpleNotaryService" ) { init { - require(raft == null || bftSMaRt == null || !custom) { - "raft, bftSMaRt, and custom configs cannot be specified together" + require(raft == null || bftSMaRt == null) { + "raft and bftSMaRt configs cannot be specified together" } } diff --git a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt index 71eef309ad..3dd2d51152 100644 --- a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt @@ -88,7 +88,6 @@ class TimedFlowTests { val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryIdentity, true)))) val notaryConfig = mock { - whenever(it.custom).thenReturn(true) whenever(it.isClusterConfig).thenReturn(true) whenever(it.validating).thenReturn(true) whenever(it.className).thenReturn(TestNotaryService::class.java.name) diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 7f52642b53..acf513708c 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -76,7 +76,10 @@ task deployNodesCustom(type: Cordform, dependsOn: 'jar') { address "localhost:10010" adminAddress "localhost:10110" } - notary = [validating: true, "custom": true] + notary = [ + validating: true, + className: "net.corda.notarydemo.MyCustomValidatingNotaryService" + ] } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index ad684fc489..51bc918242 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -8,8 +8,6 @@ import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.notary.NotaryInternalException import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.internal.notary.TrustedAuthorityNotaryService -import net.corda.core.node.AppServiceHub -import net.corda.core.node.services.CordaService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.core.transactions.WireTransaction @@ -19,13 +17,12 @@ import java.security.PublicKey import java.security.SignatureException /** - * A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey]. + * A custom notary service should provide a constructor that accepts two parameters of types [ServiceHubInternal] and [PublicKey]. * * Note that the support for custom notaries is still experimental – at present only a single-node notary service can be customised. * The notary-related APIs might change in the future. */ // START 1 -@CordaService class MyCustomValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory) From 0e68f26c0f60207399d9978fe8eaef56bf451689 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Wed, 10 Oct 2018 17:52:00 +0100 Subject: [PATCH 34/83] ENT-2569: Clean-up content of `registeredShutdowns`. (#4048) Please see comment for more info. --- .../net/corda/testing/node/internal/ShutdownManager.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt index f69c4e8de1..f7cd8f7823 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt @@ -48,7 +48,12 @@ class ShutdownManager(private val executorService: ExecutorService) { emptyList Unit>>() } else { isShutdown = true - registeredShutdowns + val result = ArrayList(registeredShutdowns) + // It is important to clear `registeredShutdowns` that has been actioned upon as more than 1 driver can be created per test. + // Given that `ShutdownManager` is reachable from `ApplicationShutdownHooks`, everything that was scheduled for shutdown + // during 1st driver launch will not be eligible for GC during second driver launch therefore retained in memory. + registeredShutdowns.clear() + result } } From 825c544cacb3f7e53646b2eb59be6efd4a436559 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Thu, 11 Oct 2018 13:48:32 +0100 Subject: [PATCH 35/83] ENT-1906: Modify the DJVM to wrap Java primitive types. (#4035) * WIP - sandbox classloading * Fix handling of Appendable in the sandbox. * WIP - Load wrapped Java types into SandboxClassLoader. * Add explicit toDJVM() invocation after invoking Object.toString(). * Add simple ThreadLocal to the sandbox to complete AbstractStringBuilder. * Add support for Enum types inside the sandbox. * Simplify type conversions into and out of the sandbox. * Small refactors and comments to tidy up code. * Fix Enum support to include EnumSet and EnumMap. * Fix use of "$" in whitelist regexps. * Add extra methods (i.e. bridges) to stitched interfaces. * Rename ToDJVMStringWrapper to StringReturnTypeWrapper. * Support lambdas within the sandbox. * Fix mapping of java.lang.System into the sandbox. * Don't remap exception classes that we catch into sandbox classes. * Remove unnecessary "bootstrap" classes from the DJVM jar. * Ensure that Character.UnicodeScript is available inside the sandbox. * Tweak sandboxed implementations of System and Runtime. * Ensure that Character.UnicodeScript is loaded correctly as Enum type. * Disallow invoking methods of ClassLoader inside the sandbox. * Apply updates after review. * More review fixes. --- djvm/build.gradle | 14 + .../java/sandbox/java/lang/Appendable.java | 19 + .../main/java/sandbox/java/lang/Boolean.java | 100 ++++ .../src/main/java/sandbox/java/lang/Byte.java | 129 +++++ .../java/sandbox/java/lang/CharSequence.java | 21 + .../java/sandbox/java/lang/Character.java | 481 ++++++++++++++++++ .../java/sandbox/java/lang/Comparable.java | 8 + .../main/java/sandbox/java/lang/Double.java | 163 ++++++ .../src/main/java/sandbox/java/lang/Enum.java | 27 + .../main/java/sandbox/java/lang/Float.java | 163 ++++++ .../main/java/sandbox/java/lang/Integer.java | 241 +++++++++ .../main/java/sandbox/java/lang/Iterable.java | 15 + .../src/main/java/sandbox/java/lang/Long.java | 239 +++++++++ .../main/java/sandbox/java/lang/Number.java | 21 + .../main/java/sandbox/java/lang/Object.java | 71 +++ .../main/java/sandbox/java/lang/Runtime.java | 27 + .../main/java/sandbox/java/lang/Short.java | 128 +++++ .../main/java/sandbox/java/lang/String.java | 398 +++++++++++++++ .../java/sandbox/java/lang/StringBuffer.java | 20 + .../java/sandbox/java/lang/StringBuilder.java | 20 + .../main/java/sandbox/java/lang/System.java | 28 + .../java/sandbox/java/lang/ThreadLocal.java | 59 +++ .../sandbox/java/nio/charset/Charset.java | 18 + .../java/sandbox/java/util/Comparator.java | 9 + .../java/sandbox/java/util/LinkedHashMap.java | 13 + .../main/java/sandbox/java/util/Locale.java | 9 + djvm/src/main/java/sandbox/java/util/Map.java | 7 + .../sandbox/java/util/function/Function.java | 10 + .../sandbox/java/util/function/Supplier.java | 10 + .../java/sandbox/sun/misc/JavaLangAccess.java | 10 + .../java/sandbox/sun/misc/SharedSecrets.java | 20 + .../djvm/analysis/AnalysisConfiguration.kt | 127 ++++- .../djvm/analysis/ClassAndMemberVisitor.kt | 23 +- .../net/corda/djvm/analysis/ClassResolver.kt | 11 +- .../net/corda/djvm/analysis/Whitelist.kt | 39 +- .../net/corda/djvm/code/ClassMutator.kt | 50 +- .../net/corda/djvm/code/EmitterModule.kt | 94 +++- .../main/kotlin/net/corda/djvm/code/Types.kt | 4 +- .../code/instructions/ConstantInstruction.kt | 6 + .../djvm/code/instructions/MethodEntry.kt | 9 + .../corda/djvm/execution/SandboxExecutor.kt | 32 +- .../net/corda/djvm/rewiring/ClassRewriter.kt | 54 +- .../corda/djvm/rewiring/SandboxClassLoader.kt | 115 +++-- .../djvm/rewiring/SandboxClassRemapper.kt | 52 ++ .../corda/djvm/rewiring/SandboxClassWriter.kt | 6 +- .../corda/djvm/rewiring/SandboxRemapper.kt | 32 +- .../rules/implementation/ArgumentUnwrapper.kt | 35 ++ .../DisallowNonDeterministicMethods.kt | 14 +- .../rules/implementation/ReturnTypeWrapper.kt | 27 + .../implementation/RewriteClassMethods.kt | 56 ++ .../implementation/StaticConstantRemover.kt | 30 ++ .../implementation/StringConstantWrapper.kt | 22 + .../implementation/StubOutNativeMethods.kt | 2 +- .../StubOutReflectionMethods.kt | 2 +- .../net/corda/djvm/source/ClassSource.kt | 3 + .../net/corda/djvm/utilities/Discovery.kt | 2 +- .../corda/djvm/validation/RuleValidator.kt | 1 + djvm/src/main/kotlin/sandbox/Task.kt | 24 + .../src/main/kotlin/sandbox/java/lang/DJVM.kt | 158 ++++++ .../main/kotlin/sandbox/java/lang/Object.kt | 19 - .../main/kotlin/sandbox/java/lang/System.kt | 99 ---- .../kotlin/foo/bar/sandbox/KotlinClass.kt | 11 +- .../test/kotlin/net/corda/djvm/DJVMTest.kt | 126 +++++ .../test/kotlin/net/corda/djvm/TestBase.kt | 33 +- .../test/kotlin/net/corda/djvm/Utilities.kt | 22 + .../corda/djvm/analysis/ClassResolverTest.kt | 10 +- .../net/corda/djvm/analysis/WhitelistTest.kt | 4 +- .../assertions/AssertiveClassWithByteCode.kt | 5 + .../net/corda/djvm/costing/RuntimeCostTest.kt | 9 +- .../corda/djvm/execution/SandboxEnumTest.kt | 86 ++++ .../djvm/execution/SandboxExecutorTest.kt | 320 ++++++++++-- .../corda/djvm/rewiring/ClassRewriterTest.kt | 48 +- .../djvm/source/SourceClassLoaderTest.kt | 2 +- 73 files changed, 3986 insertions(+), 336 deletions(-) create mode 100644 djvm/src/main/java/sandbox/java/lang/Appendable.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Boolean.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Byte.java create mode 100644 djvm/src/main/java/sandbox/java/lang/CharSequence.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Character.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Comparable.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Double.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Enum.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Float.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Integer.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Iterable.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Long.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Number.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Object.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Runtime.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Short.java create mode 100644 djvm/src/main/java/sandbox/java/lang/String.java create mode 100644 djvm/src/main/java/sandbox/java/lang/StringBuffer.java create mode 100644 djvm/src/main/java/sandbox/java/lang/StringBuilder.java create mode 100644 djvm/src/main/java/sandbox/java/lang/System.java create mode 100644 djvm/src/main/java/sandbox/java/lang/ThreadLocal.java create mode 100644 djvm/src/main/java/sandbox/java/nio/charset/Charset.java create mode 100644 djvm/src/main/java/sandbox/java/util/Comparator.java create mode 100644 djvm/src/main/java/sandbox/java/util/LinkedHashMap.java create mode 100644 djvm/src/main/java/sandbox/java/util/Locale.java create mode 100644 djvm/src/main/java/sandbox/java/util/Map.java create mode 100644 djvm/src/main/java/sandbox/java/util/function/Function.java create mode 100644 djvm/src/main/java/sandbox/java/util/function/Supplier.java create mode 100644 djvm/src/main/java/sandbox/sun/misc/JavaLangAccess.java create mode 100644 djvm/src/main/java/sandbox/sun/misc/SharedSecrets.java create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/ConstantInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/MethodEntry.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ArgumentUnwrapper.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ReturnTypeWrapper.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/RewriteClassMethods.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StaticConstantRemover.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StringConstantWrapper.kt create mode 100644 djvm/src/main/kotlin/sandbox/Task.kt create mode 100644 djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt delete mode 100644 djvm/src/main/kotlin/sandbox/java/lang/Object.kt delete mode 100644 djvm/src/main/kotlin/sandbox/java/lang/System.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/Utilities.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/execution/SandboxEnumTest.kt diff --git a/djvm/build.gradle b/djvm/build.gradle index db88e8c4c5..eb41df17cc 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -52,6 +52,20 @@ shadowJar { baseName 'corda-djvm' classifier '' relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm' + + // These particular classes are only needed to "bootstrap" + // the compilation of the other sandbox classes. At runtime, + // we will generate better versions from deterministic-rt.jar. + exclude 'sandbox/java/lang/Appendable.class' + exclude 'sandbox/java/lang/CharSequence.class' + exclude 'sandbox/java/lang/Character\$*.class' + exclude 'sandbox/java/lang/Comparable.class' + exclude 'sandbox/java/lang/Enum.class' + exclude 'sandbox/java/lang/Iterable.class' + exclude 'sandbox/java/lang/StringBuffer.class' + exclude 'sandbox/java/lang/StringBuilder.class' + exclude 'sandbox/java/nio/**' + exclude 'sandbox/java/util/**' } assemble.dependsOn shadowJar diff --git a/djvm/src/main/java/sandbox/java/lang/Appendable.java b/djvm/src/main/java/sandbox/java/lang/Appendable.java new file mode 100644 index 0000000000..168607c511 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Appendable.java @@ -0,0 +1,19 @@ +package sandbox.java.lang; + +import java.io.IOException; + +/** + * This is a dummy class that implements just enough of [java.lang.Appendable] + * to keep [sandbox.java.lang.StringBuilder], [sandbox.java.lang.StringBuffer] + * and [sandbox.java.lang.String] honest. + * Note that it does not extend [java.lang.Appendable]. + */ +public interface Appendable { + + Appendable append(CharSequence csq, int start, int end) throws IOException; + + Appendable append(CharSequence csq) throws IOException; + + Appendable append(char c) throws IOException; + +} diff --git a/djvm/src/main/java/sandbox/java/lang/Boolean.java b/djvm/src/main/java/sandbox/java/lang/Boolean.java new file mode 100644 index 0000000000..6d347fdd3e --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Boolean.java @@ -0,0 +1,100 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Boolean extends Object implements Comparable, Serializable { + + public static final Boolean TRUE = new Boolean(true); + public static final Boolean FALSE = new Boolean(false); + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Boolean.TYPE; + + private final boolean value; + + public Boolean(boolean value) { + this.value = value; + } + + public Boolean(String s) { + this(parseBoolean(s)); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof Boolean) && ((Boolean) other).value == value; + } + + @Override + public int hashCode() { + return hashCode(value); + } + + public static int hashCode(boolean value) { + return java.lang.Boolean.hashCode(value); + } + + public boolean booleanValue() { + return value; + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Boolean.toString(value); + } + + @Override + @NotNull + public String toDJVMString() { + return toString(value); + } + + public static String toString(boolean b) { + return String.valueOf(b); + } + + @Override + @NotNull + java.lang.Boolean fromDJVM() { + return value; + } + + @Override + public int compareTo(@NotNull Boolean other) { + return compare(value, other.value); + } + + public static int compare(boolean x, boolean y) { + return java.lang.Boolean.compare(x, y); + } + + public static boolean parseBoolean(String s) { + return java.lang.Boolean.parseBoolean(String.fromDJVM(s)); + } + + public static Boolean valueOf(boolean b) { + return b ? TRUE : FALSE; + } + + public static Boolean valueOf(String s) { + return valueOf(parseBoolean(s)); + } + + public static boolean logicalAnd(boolean a, boolean b) { + return java.lang.Boolean.logicalAnd(a, b); + } + + public static boolean logicalOr(boolean a, boolean b) { + return java.lang.Boolean.logicalOr(a, b); + } + + public static boolean logicalXor(boolean a, boolean b) { + return java.lang.Boolean.logicalXor(a, b); + } + + public static Boolean toDJVM(java.lang.Boolean b) { return (b == null) ? null : new Boolean(b); } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Byte.java b/djvm/src/main/java/sandbox/java/lang/Byte.java new file mode 100644 index 0000000000..95b329f25d --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Byte.java @@ -0,0 +1,129 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Byte extends Number implements Comparable { + public static final byte MIN_VALUE = java.lang.Byte.MIN_VALUE; + public static final byte MAX_VALUE = java.lang.Byte.MAX_VALUE; + public static final int BYTES = java.lang.Byte.BYTES; + public static final int SIZE = java.lang.Byte.SIZE; + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Byte.TYPE; + + private final byte value; + + public Byte(byte value) { + this.value = value; + } + + public Byte(String s) throws NumberFormatException { + this.value = parseByte(s); + } + + @Override + public byte byteValue() { + return value; + } + + @Override + public short shortValue() { + return (short) value; + } + + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return (long) value; + } + + @Override + public float floatValue() { + return (float) value; + } + + @Override + public double doubleValue() { + return (double) value; + } + + @Override + public int hashCode() { + return hashCode(value); + } + + public static int hashCode(byte b) { + return java.lang.Byte.hashCode(b); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof Byte) && ((Byte) other).value == value; + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Byte.toString(value); + } + + @Override + @NotNull + java.lang.Byte fromDJVM() { + return value; + } + + @Override + public int compareTo(@NotNull Byte other) { + return compare(this.value, other.value); + } + + public static int compare(byte x, byte y) { + return java.lang.Byte.compare(x, y); + } + + public static String toString(byte b) { + return Integer.toString(b); + } + + public static Byte valueOf(byte b) { + return new Byte(b); + } + + public static byte parseByte(String s, int radix) throws NumberFormatException { + return java.lang.Byte.parseByte(String.fromDJVM(s), radix); + } + + public static byte parseByte(String s) throws NumberFormatException { + return java.lang.Byte.parseByte(String.fromDJVM(s)); + } + + public static Byte valueOf(String s, int radix) throws NumberFormatException { + return toDJVM(java.lang.Byte.valueOf(String.fromDJVM(s), radix)); + } + + public static Byte valueOf(String s) throws NumberFormatException { + return toDJVM(java.lang.Byte.valueOf(String.fromDJVM(s))); + } + + public static Byte decode(String s) throws NumberFormatException { + return toDJVM(java.lang.Byte.decode(String.fromDJVM(s))); + } + + public static int toUnsignedInt(byte b) { + return java.lang.Byte.toUnsignedInt(b); + } + + public static long toUnsignedLong(byte b) { + return java.lang.Byte.toUnsignedLong(b); + } + + public static Byte toDJVM(java.lang.Byte b) { + return (b == null) ? null : valueOf(b); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/CharSequence.java b/djvm/src/main/java/sandbox/java/lang/CharSequence.java new file mode 100644 index 0000000000..1847103093 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/CharSequence.java @@ -0,0 +1,21 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +/** + * This is a dummy class that implements just enough of [java.lang.CharSequence] + * to allow us to compile [sandbox.java.lang.String]. + */ +public interface CharSequence extends java.lang.CharSequence { + + @Override + CharSequence subSequence(int start, int end); + + @NotNull + String toDJVMString(); + + @Override + @NotNull + java.lang.String toString(); + +} diff --git a/djvm/src/main/java/sandbox/java/lang/Character.java b/djvm/src/main/java/sandbox/java/lang/Character.java new file mode 100644 index 0000000000..2db6054272 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Character.java @@ -0,0 +1,481 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Character extends Object implements Comparable, Serializable { + public static final int MIN_RADIX = java.lang.Character.MIN_RADIX; + public static final int MAX_RADIX = java.lang.Character.MAX_RADIX; + public static final char MIN_VALUE = java.lang.Character.MIN_VALUE; + public static final char MAX_VALUE = java.lang.Character.MAX_VALUE; + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Character.TYPE; + + public static final byte UNASSIGNED = java.lang.Character.UNASSIGNED; + public static final byte UPPERCASE_LETTER = java.lang.Character.UPPERCASE_LETTER; + public static final byte LOWERCASE_LETTER = java.lang.Character.LOWERCASE_LETTER; + public static final byte TITLECASE_LETTER = java.lang.Character.TITLECASE_LETTER; + public static final byte MODIFIER_LETTER = java.lang.Character.MODIFIER_LETTER; + public static final byte OTHER_LETTER = java.lang.Character.OTHER_LETTER; + public static final byte NON_SPACING_MARK = java.lang.Character.NON_SPACING_MARK; + public static final byte ENCLOSING_MARK = java.lang.Character.ENCLOSING_MARK; + public static final byte COMBINING_SPACING_MARK = java.lang.Character.COMBINING_SPACING_MARK; + public static final byte DECIMAL_DIGIT_NUMBER = java.lang.Character.DECIMAL_DIGIT_NUMBER; + public static final byte LETTER_NUMBER = java.lang.Character.LETTER_NUMBER; + public static final byte OTHER_NUMBER = java.lang.Character.OTHER_NUMBER; + public static final byte SPACE_SEPARATOR = java.lang.Character.SPACE_SEPARATOR; + public static final byte LINE_SEPARATOR = java.lang.Character.LINE_SEPARATOR; + public static final byte PARAGRAPH_SEPARATOR = java.lang.Character.PARAGRAPH_SEPARATOR; + public static final byte CONTROL = java.lang.Character.CONTROL; + public static final byte FORMAT = java.lang.Character.FORMAT; + public static final byte PRIVATE_USE = java.lang.Character.PRIVATE_USE; + public static final byte SURROGATE = java.lang.Character.SURROGATE; + public static final byte DASH_PUNCTUATION = java.lang.Character.DASH_PUNCTUATION; + public static final byte START_PUNCTUATION = java.lang.Character.START_PUNCTUATION; + public static final byte END_PUNCTUATION = java.lang.Character.END_PUNCTUATION; + public static final byte CONNECTOR_PUNCTUATION = java.lang.Character.CONNECTOR_PUNCTUATION; + public static final byte OTHER_PUNCTUATION = java.lang.Character.OTHER_PUNCTUATION; + public static final byte MATH_SYMBOL = java.lang.Character.MATH_SYMBOL; + public static final byte CURRENCY_SYMBOL = java.lang.Character.CURRENCY_SYMBOL; + public static final byte MODIFIER_SYMBOL = java.lang.Character.MODIFIER_SYMBOL; + public static final byte OTHER_SYMBOL = java.lang.Character.OTHER_SYMBOL; + public static final byte INITIAL_QUOTE_PUNCTUATION = java.lang.Character.INITIAL_QUOTE_PUNCTUATION; + public static final byte FINAL_QUOTE_PUNCTUATION = java.lang.Character.FINAL_QUOTE_PUNCTUATION; + public static final byte DIRECTIONALITY_UNDEFINED = java.lang.Character.DIRECTIONALITY_UNDEFINED; + public static final byte DIRECTIONALITY_LEFT_TO_RIGHT = java.lang.Character.DIRECTIONALITY_LEFT_TO_RIGHT; + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT = java.lang.Character.DIRECTIONALITY_RIGHT_TO_LEFT; + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC = java.lang.Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC; + public static final byte DIRECTIONALITY_EUROPEAN_NUMBER = java.lang.Character.DIRECTIONALITY_EUROPEAN_NUMBER; + public static final byte DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR = java.lang.Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR; + public static final byte DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR = java.lang.Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR; + public static final byte DIRECTIONALITY_ARABIC_NUMBER = java.lang.Character.DIRECTIONALITY_ARABIC_NUMBER; + public static final byte DIRECTIONALITY_COMMON_NUMBER_SEPARATOR = java.lang.Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR; + public static final byte DIRECTIONALITY_NONSPACING_MARK = java.lang.Character.DIRECTIONALITY_NONSPACING_MARK; + public static final byte DIRECTIONALITY_BOUNDARY_NEUTRAL = java.lang.Character.DIRECTIONALITY_BOUNDARY_NEUTRAL; + public static final byte DIRECTIONALITY_PARAGRAPH_SEPARATOR = java.lang.Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR; + public static final byte DIRECTIONALITY_SEGMENT_SEPARATOR = java.lang.Character.DIRECTIONALITY_SEGMENT_SEPARATOR; + public static final byte DIRECTIONALITY_WHITESPACE = java.lang.Character.DIRECTIONALITY_WHITESPACE; + public static final byte DIRECTIONALITY_OTHER_NEUTRALS = java.lang.Character.DIRECTIONALITY_OTHER_NEUTRALS; + public static final byte DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING = java.lang.Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING; + public static final byte DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE = java.lang.Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE; + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING = java.lang.Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING; + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE = java.lang.Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE; + public static final byte DIRECTIONALITY_POP_DIRECTIONAL_FORMAT = java.lang.Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT; + public static final char MIN_HIGH_SURROGATE = java.lang.Character.MIN_HIGH_SURROGATE; + public static final char MAX_HIGH_SURROGATE = java.lang.Character.MAX_HIGH_SURROGATE; + public static final char MIN_LOW_SURROGATE = java.lang.Character.MIN_LOW_SURROGATE; + public static final char MAX_LOW_SURROGATE = java.lang.Character.MAX_LOW_SURROGATE; + public static final char MIN_SURROGATE = java.lang.Character.MIN_SURROGATE; + public static final char MAX_SURROGATE = java.lang.Character.MAX_SURROGATE; + public static final int MIN_SUPPLEMENTARY_CODE_POINT = java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT; + public static final int MIN_CODE_POINT = java.lang.Character.MIN_CODE_POINT; + public static final int MAX_CODE_POINT = java.lang.Character.MAX_CODE_POINT; + public static final int BYTES = java.lang.Character.BYTES; + public static final int SIZE = java.lang.Character.SIZE; + + private final char value; + + public Character(char c) { + this.value = c; + } + + public char charValue() { + return this.value; + } + + @Override + public int hashCode() { + return hashCode(this.value); + } + + public static int hashCode(char value) { + return java.lang.Character.hashCode(value); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof Character) && ((Character) other).value == value; + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Character.toString(value); + } + + @Override + @NotNull + public String toDJVMString() { + return toString(value); + } + + @Override + @NotNull + java.lang.Character fromDJVM() { + return value; + } + + @Override + public int compareTo(@NotNull Character var1) { + return compare(this.value, var1.value); + } + + public static int compare(char x, char y) { + return java.lang.Character.compare(x, y); + } + + public static String toString(char c) { + return String.toDJVM(java.lang.Character.toString(c)); + } + + public static Character valueOf(char c) { + return (c <= 127) ? Cache.cache[(int)c] : new Character(c); + } + + public static boolean isValidCodePoint(int codePoint) { + return java.lang.Character.isValidCodePoint(codePoint); + } + + public static boolean isBmpCodePoint(int codePoint) { + return java.lang.Character.isBmpCodePoint(codePoint); + } + + public static boolean isSupplementaryCodePoint(int codePoint) { + return java.lang.Character.isSupplementaryCodePoint(codePoint); + } + + public static boolean isHighSurrogate(char ch) { + return java.lang.Character.isHighSurrogate(ch); + } + + public static boolean isLowSurrogate(char ch) { + return java.lang.Character.isLowSurrogate(ch); + } + + public static boolean isSurrogate(char ch) { + return java.lang.Character.isSurrogate(ch); + } + + public static boolean isSurrogatePair(char high, char low) { + return java.lang.Character.isSurrogatePair(high, low); + } + + public static int charCount(int codePoint) { + return java.lang.Character.charCount(codePoint); + } + + public static int toCodePoint(char high, char low) { + return java.lang.Character.toCodePoint(high, low); + } + + public static int codePointAt(CharSequence seq, int index) { + return java.lang.Character.codePointAt(seq, index); + } + + public static int codePointAt(char[] a, int index) { + return java.lang.Character.codePointAt(a, index); + } + + public static int codePointAt(char[] a, int index, int limit) { + return java.lang.Character.codePointAt(a, index, limit); + } + + public static int codePointBefore(CharSequence seq, int index) { + return java.lang.Character.codePointBefore(seq, index); + } + + public static int codePointBefore(char[] a, int index) { + return java.lang.Character.codePointBefore(a, index); + } + + public static int codePointBefore(char[] a, int index, int limit) { + return java.lang.Character.codePointBefore(a, index, limit); + } + + public static char highSurrogate(int codePoint) { + return java.lang.Character.highSurrogate(codePoint); + } + + public static char lowSurrogate(int codePoint) { + return java.lang.Character.lowSurrogate(codePoint); + } + + public static int toChars(int codePoint, char[] dst, int dstIndex) { + return java.lang.Character.toChars(codePoint, dst, dstIndex); + } + + public static char[] toChars(int codePoint) { + return java.lang.Character.toChars(codePoint); + } + + public static int codePointCount(CharSequence seq, int beginIndex, int endIndex) { + return java.lang.Character.codePointCount(seq, beginIndex, endIndex); + } + + public static int codePointCount(char[] a, int offset, int count) { + return java.lang.Character.codePointCount(a, offset, count); + } + + public static int offsetByCodePoints(CharSequence seq, int index, int codePointOffset) { + return java.lang.Character.offsetByCodePoints(seq, index, codePointOffset); + } + + public static int offsetByCodePoints(char[] a, int start, int count, int index, int codePointOffset) { + return java.lang.Character.offsetByCodePoints(a, start, count, index, codePointOffset); + } + + public static boolean isLowerCase(char ch) { + return java.lang.Character.isLowerCase(ch); + } + + public static boolean isLowerCase(int codePoint) { + return java.lang.Character.isLowerCase(codePoint); + } + + public static boolean isUpperCase(char ch) { + return java.lang.Character.isUpperCase(ch); + } + + public static boolean isUpperCase(int codePoint) { + return java.lang.Character.isUpperCase(codePoint); + } + + public static boolean isTitleCase(char ch) { + return java.lang.Character.isTitleCase(ch); + } + + public static boolean isTitleCase(int codePoint) { + return java.lang.Character.isTitleCase(codePoint); + } + + public static boolean isDigit(char ch) { + return java.lang.Character.isDigit(ch); + } + + public static boolean isDigit(int codePoint) { + return java.lang.Character.isDigit(codePoint); + } + + public static boolean isDefined(char ch) { + return java.lang.Character.isDefined(ch); + } + + public static boolean isDefined(int codePoint) { + return java.lang.Character.isDefined(codePoint); + } + + public static boolean isLetter(char ch) { + return java.lang.Character.isLetter(ch); + } + + public static boolean isLetter(int codePoint) { + return java.lang.Character.isLetter(codePoint); + } + + public static boolean isLetterOrDigit(char ch) { + return java.lang.Character.isLetterOrDigit(ch); + } + + public static boolean isLetterOrDigit(int codePoint) { + return java.lang.Character.isLetterOrDigit(codePoint); + } + + @Deprecated + public static boolean isJavaLetter(char ch) { + return java.lang.Character.isJavaLetter(ch); + } + + @Deprecated + public static boolean isJavaLetterOrDigit(char ch) { + return java.lang.Character.isJavaLetterOrDigit(ch); + } + + public static boolean isAlphabetic(int codePoint) { + return java.lang.Character.isAlphabetic(codePoint); + } + + public static boolean isIdeographic(int codePoint) { + return java.lang.Character.isIdeographic(codePoint); + } + + public static boolean isJavaIdentifierStart(char ch) { + return java.lang.Character.isJavaIdentifierStart(ch); + } + + public static boolean isJavaIdentifierStart(int codePoint) { + return java.lang.Character.isJavaIdentifierStart(codePoint); + } + + public static boolean isJavaIdentifierPart(char ch) { + return java.lang.Character.isJavaIdentifierPart(ch); + } + + public static boolean isJavaIdentifierPart(int codePoint) { + return java.lang.Character.isJavaIdentifierPart(codePoint); + } + + public static boolean isUnicodeIdentifierStart(char ch) { + return java.lang.Character.isUnicodeIdentifierStart(ch); + } + + public static boolean isUnicodeIdentifierStart(int codePoint) { + return java.lang.Character.isUnicodeIdentifierStart(codePoint); + } + + public static boolean isUnicodeIdentifierPart(char ch) { + return java.lang.Character.isUnicodeIdentifierPart(ch); + } + + public static boolean isUnicodeIdentifierPart(int codePoint) { + return java.lang.Character.isUnicodeIdentifierPart(codePoint); + } + + public static boolean isIdentifierIgnorable(char ch) { + return java.lang.Character.isIdentifierIgnorable(ch); + } + + public static boolean isIdentifierIgnorable(int codePoint) { + return java.lang.Character.isIdentifierIgnorable(codePoint); + } + + public static char toLowerCase(char ch) { + return java.lang.Character.toLowerCase(ch); + } + + public static int toLowerCase(int codePoint) { + return java.lang.Character.toLowerCase(codePoint); + } + + public static char toUpperCase(char ch) { + return java.lang.Character.toUpperCase(ch); + } + + public static int toUpperCase(int codePoint) { + return java.lang.Character.toUpperCase(codePoint); + } + + public static char toTitleCase(char ch) { + return java.lang.Character.toTitleCase(ch); + } + + public static int toTitleCase(int codePoint) { + return java.lang.Character.toTitleCase(codePoint); + } + + public static int digit(char ch, int radix) { + return java.lang.Character.digit(ch, radix); + } + + public static int digit(int codePoint, int radix) { + return java.lang.Character.digit(codePoint, radix); + } + + public static int getNumericValue(char ch) { + return java.lang.Character.getNumericValue(ch); + } + + public static int getNumericValue(int codePoint) { + return java.lang.Character.getNumericValue(codePoint); + } + + @Deprecated + public static boolean isSpace(char ch) { + return java.lang.Character.isSpace(ch); + } + + public static boolean isSpaceChar(char ch) { + return java.lang.Character.isSpaceChar(ch); + } + + public static boolean isSpaceChar(int codePoint) { + return java.lang.Character.isSpaceChar(codePoint); + } + + public static boolean isWhitespace(char ch) { + return java.lang.Character.isWhitespace(ch); + } + + public static boolean isWhitespace(int codePoint) { + return java.lang.Character.isWhitespace(codePoint); + } + + public static boolean isISOControl(char ch) { + return java.lang.Character.isISOControl(ch); + } + + public static boolean isISOControl(int codePoint) { + return java.lang.Character.isISOControl(codePoint); + } + + public static int getType(char ch) { + return java.lang.Character.getType(ch); + } + + public static int getType(int codePoint) { + return java.lang.Character.getType(codePoint); + } + + public static char forDigit(int digit, int radix) { + return java.lang.Character.forDigit(digit, radix); + } + + public static byte getDirectionality(char ch) { + return java.lang.Character.getDirectionality(ch); + } + + public static byte getDirectionality(int codePoint) { + return java.lang.Character.getDirectionality(codePoint); + } + + public static boolean isMirrored(char ch) { + return java.lang.Character.isMirrored(ch); + } + + public static boolean isMirrored(int codePoint) { + return java.lang.Character.isMirrored(codePoint); + } + + public static String getName(int codePoint) { + return String.toDJVM(java.lang.Character.getName(codePoint)); + } + + public static Character toDJVM(java.lang.Character c) { + return (c == null) ? null : valueOf(c); + } + + // These three nested classes are placeholders to ensure that + // the Character class bytecode is generated correctly. The + // real classes will be loaded from the from the bootstrap jar + // and then mapped into the sandbox.* namespace. + public static final class UnicodeScript extends Enum { + private UnicodeScript(String name, int index) { + super(name, index); + } + + @Override + public int compareTo(@NotNull UnicodeScript other) { + throw new UnsupportedOperationException("Bootstrap implementation"); + } + } + public static final class UnicodeBlock extends Subset {} + public static class Subset extends Object {} + + /** + * Keep pre-allocated instances of the first 128 characters + * on the basis that these will be used most frequently. + */ + private static class Cache { + private static final Character[] cache = new Character[128]; + + static { + for (int c = 0; c < cache.length; ++c) { + cache[c] = new Character((char) c); + } + } + + private Cache() {} + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Comparable.java b/djvm/src/main/java/sandbox/java/lang/Comparable.java new file mode 100644 index 0000000000..686539c1b4 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Comparable.java @@ -0,0 +1,8 @@ +package sandbox.java.lang; + +/** + * This is a dummy class that implements just enough of [java.lang.Comparable] + * to allow us to compile [sandbox.java.lang.String]. + */ +public interface Comparable extends java.lang.Comparable { +} diff --git a/djvm/src/main/java/sandbox/java/lang/Double.java b/djvm/src/main/java/sandbox/java/lang/Double.java new file mode 100644 index 0000000000..d3488edde2 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Double.java @@ -0,0 +1,163 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Double extends Number implements Comparable { + public static final double POSITIVE_INFINITY = java.lang.Double.POSITIVE_INFINITY; + public static final double NEGATIVE_INFINITY = java.lang.Double.NEGATIVE_INFINITY; + public static final double NaN = java.lang.Double.NaN; + public static final double MAX_VALUE = java.lang.Double.MAX_VALUE; + public static final double MIN_NORMAL = java.lang.Double.MIN_NORMAL; + public static final double MIN_VALUE = java.lang.Double.MIN_VALUE; + public static final int MAX_EXPONENT = java.lang.Double.MAX_EXPONENT; + public static final int MIN_EXPONENT = java.lang.Double.MIN_EXPONENT; + public static final int BYTES = java.lang.Double.BYTES; + public static final int SIZE = java.lang.Double.SIZE; + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Double.TYPE; + + private final double value; + + public Double(double value) { + this.value = value; + } + + public Double(String s) throws NumberFormatException { + this.value = parseDouble(s); + } + + @Override + public double doubleValue() { + return value; + } + + @Override + public float floatValue() { + return (float)value; + } + + @Override + public long longValue() { + return (long)value; + } + + @Override + public int intValue() { + return (int)value; + } + + @Override + public short shortValue() { + return (short)value; + } + + @Override + public byte byteValue() { + return (byte)value; + } + + public boolean isNaN() { + return java.lang.Double.isNaN(value); + } + + public boolean isInfinite() { + return isInfinite(this.value); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof Double) && doubleToLongBits(((Double)other).value) == doubleToLongBits(value); + } + + @Override + public int hashCode() { + return hashCode(value); + } + + public static int hashCode(double d) { + return java.lang.Double.hashCode(d); + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Double.toString(value); + } + + @Override + @NotNull + java.lang.Double fromDJVM() { + return value; + } + + @Override + public int compareTo(@NotNull Double other) { + return compare(this.value, other.value); + } + + public static String toString(double d) { + return String.toDJVM(java.lang.Double.toString(d)); + } + + public static String toHexString(double d) { + return String.toDJVM(java.lang.Double.toHexString(d)); + } + + public static Double valueOf(String s) throws NumberFormatException { + return toDJVM(java.lang.Double.valueOf(String.fromDJVM(s))); + } + + public static Double valueOf(double d) { + return new Double(d); + } + + public static double parseDouble(String s) throws NumberFormatException { + return java.lang.Double.parseDouble(String.fromDJVM(s)); + } + + public static boolean isNaN(double d) { + return java.lang.Double.isNaN(d); + } + + public static boolean isInfinite(double d) { + return java.lang.Double.isInfinite(d); + } + + public static boolean isFinite(double d) { + return java.lang.Double.isFinite(d); + } + + public static long doubleToLongBits(double d) { + return java.lang.Double.doubleToLongBits(d); + } + + public static long doubleToRawLongBits(double d) { + return java.lang.Double.doubleToRawLongBits(d); + } + + public static double longBitsToDouble(long bits) { + return java.lang.Double.longBitsToDouble(bits); + } + + public static int compare(double d1, double d2) { + return java.lang.Double.compare(d1, d2); + } + + public static double sum(double a, double b) { + return java.lang.Double.sum(a, b); + } + + public static double max(double a, double b) { + return java.lang.Double.max(a, b); + } + + public static double min(double a, double b) { + return java.lang.Double.min(a, b); + } + + public static Double toDJVM(java.lang.Double d) { + return (d == null) ? null : valueOf(d); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Enum.java b/djvm/src/main/java/sandbox/java/lang/Enum.java new file mode 100644 index 0000000000..ffcdd8c916 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Enum.java @@ -0,0 +1,27 @@ +package sandbox.java.lang; + +import java.io.Serializable; + +/** + * This is a dummy class. We will load the actual Enum class at run-time. + */ +@SuppressWarnings("unused") +public abstract class Enum> extends Object implements Comparable, Serializable { + + private final String name; + private final int ordinal; + + protected Enum(String name, int ordinal) { + this.name = name; + this.ordinal = ordinal; + } + + public String name() { + return name; + } + + public int ordinal() { + return ordinal; + } + +} diff --git a/djvm/src/main/java/sandbox/java/lang/Float.java b/djvm/src/main/java/sandbox/java/lang/Float.java new file mode 100644 index 0000000000..bebc75f916 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Float.java @@ -0,0 +1,163 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Float extends Number implements Comparable { + public static final float POSITIVE_INFINITY = java.lang.Float.POSITIVE_INFINITY; + public static final float NEGATIVE_INFINITY = java.lang.Float.NEGATIVE_INFINITY; + public static final float NaN = java.lang.Float.NaN; + public static final float MAX_VALUE = java.lang.Float.MAX_VALUE; + public static final float MIN_NORMAL = java.lang.Float.MIN_NORMAL; + public static final float MIN_VALUE = java.lang.Float.MIN_VALUE; + public static final int MAX_EXPONENT = java.lang.Float.MAX_EXPONENT; + public static final int MIN_EXPONENT = java.lang.Float.MIN_EXPONENT; + public static final int BYTES = java.lang.Float.BYTES; + public static final int SIZE = java.lang.Float.SIZE; + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Float.TYPE; + + private final float value; + + public Float(float value) { + this.value = value; + } + + public Float(String s) throws NumberFormatException { + this.value = parseFloat(s); + } + + @Override + public int hashCode() { + return hashCode(value); + } + + public static int hashCode(float f) { + return java.lang.Float.hashCode(f); + } + + @Override + public boolean equals(java.lang.Object other) { + return other instanceof Float && floatToIntBits(((Float)other).value) == floatToIntBits(this.value); + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Float.toString(value); + } + + @Override + @NotNull + java.lang.Float fromDJVM() { + return value; + } + + @Override + public double doubleValue() { + return (double)value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public long longValue() { + return (long)value; + } + + @Override + public int intValue() { + return (int)value; + } + + @Override + public short shortValue() { + return (short)value; + } + + @Override + public byte byteValue() { + return (byte)value; + } + + @Override + public int compareTo(@NotNull Float other) { + return compare(this.value, other.value); + } + + public boolean isNaN() { + return isNaN(value); + } + + public boolean isInfinite() { + return isInfinite(value); + } + + public static String toString(float f) { + return String.valueOf(f); + } + + public static String toHexString(float f) { + return String.toDJVM(java.lang.Float.toHexString(f)); + } + + public static Float valueOf(String s) throws NumberFormatException { + return toDJVM(java.lang.Float.valueOf(String.fromDJVM(s))); + } + + public static Float valueOf(float f) { + return new Float(f); + } + + public static float parseFloat(String s) throws NumberFormatException { + return java.lang.Float.parseFloat(String.fromDJVM(s)); + } + + public static boolean isNaN(float f) { + return java.lang.Float.isNaN(f); + } + + public static boolean isInfinite(float f) { + return java.lang.Float.isInfinite(f); + } + + public static boolean isFinite(float f) { + return java.lang.Float.isFinite(f); + } + + public static int floatToIntBits(float f) { + return java.lang.Float.floatToIntBits(f); + } + + public static int floatToRawIntBits(float f) { + return java.lang.Float.floatToIntBits(f); + } + + public static float intBitsToFloat(int bits) { + return java.lang.Float.intBitsToFloat(bits); + } + + public static int compare(float f1, float f2) { + return java.lang.Float.compare(f1, f2); + } + + public static float sum(float a, float b) { + return java.lang.Float.sum(a, b); + } + + public static float max(float a, float b) { + return java.lang.Float.max(a, b); + } + + public static float min(float a, float b) { + return java.lang.Float.min(a, b); + } + + public static Float toDJVM(java.lang.Float f) { + return (f == null) ? null : valueOf(f); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Integer.java b/djvm/src/main/java/sandbox/java/lang/Integer.java new file mode 100644 index 0000000000..ae05ea0f91 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Integer.java @@ -0,0 +1,241 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Integer extends Number implements Comparable { + + public static final int MIN_VALUE = java.lang.Integer.MIN_VALUE; + public static final int MAX_VALUE = java.lang.Integer.MAX_VALUE; + public static final int BYTES = java.lang.Integer.BYTES; + public static final int SIZE = java.lang.Integer.SIZE; + + static final int[] SIZE_TABLE = new int[] { 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, MAX_VALUE }; + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Integer.TYPE; + + private final int value; + + public Integer(int value) { + this.value = value; + } + + public Integer(String s) throws NumberFormatException { + this.value = parseInt(s, 10); + } + + @Override + public int hashCode() { + return Integer.hashCode(value); + } + + public static int hashCode(int i) { + return java.lang.Integer.hashCode(i); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof Integer) && (value == ((Integer) other).value); + } + + @Override + public int intValue() { + return value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public short shortValue() { + return (short) value; + } + + @Override + public byte byteValue() { + return (byte) value; + } + + @Override + public float floatValue() { + return (float) value; + } + + @Override + public double doubleValue() { + return (double) value; + } + + @Override + public int compareTo(@NotNull Integer other) { + return compare(this.value, other.value); + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Integer.toString(value); + } + + @Override + @NotNull + java.lang.Integer fromDJVM() { + return value; + } + + public static String toString(int i, int radix) { + return String.toDJVM(java.lang.Integer.toString(i, radix)); + } + + public static String toUnsignedString(int i, int radix) { + return String.toDJVM(java.lang.Integer.toUnsignedString(i, radix)); + } + + public static String toHexString(int i) { + return String.toDJVM(java.lang.Integer.toHexString(i)); + } + + public static String toOctalString(int i) { + return String.toDJVM(java.lang.Integer.toOctalString(i)); + } + + public static String toBinaryString(int i) { + return String.toDJVM(java.lang.Integer.toBinaryString(i)); + } + + public static String toString(int i) { + return String.toDJVM(java.lang.Integer.toString(i)); + } + + public static String toUnsignedString(int i) { + return String.toDJVM(java.lang.Integer.toUnsignedString(i)); + } + + public static int parseInt(String s, int radix) throws NumberFormatException { + return java.lang.Integer.parseInt(String.fromDJVM(s), radix); + } + + public static int parseInt(String s) throws NumberFormatException { + return java.lang.Integer.parseInt(String.fromDJVM(s)); + } + + public static int parseUnsignedInt(String s, int radix) throws NumberFormatException { + return java.lang.Integer.parseUnsignedInt(String.fromDJVM(s), radix); + } + + public static int parseUnsignedInt(String s) throws NumberFormatException { + return java.lang.Integer.parseUnsignedInt(String.fromDJVM(s)); + } + + public static Integer valueOf(String s, int radix) throws NumberFormatException { + return toDJVM(java.lang.Integer.valueOf(String.fromDJVM(s), radix)); + } + + public static Integer valueOf(String s) throws NumberFormatException { + return toDJVM(java.lang.Integer.valueOf(String.fromDJVM(s))); + } + + public static Integer valueOf(int i) { + return new Integer(i); + } + + public static Integer decode(String nm) throws NumberFormatException { + return new Integer(java.lang.Integer.decode(String.fromDJVM(nm))); + } + + public static int compare(int x, int y) { + return java.lang.Integer.compare(x, y); + } + + public static int compareUnsigned(int x, int y) { + return java.lang.Integer.compareUnsigned(x, y); + } + + public static long toUnsignedLong(int x) { + return java.lang.Integer.toUnsignedLong(x); + } + + public static int divideUnsigned(int dividend, int divisor) { + return java.lang.Integer.divideUnsigned(dividend, divisor); + } + + public static int remainderUnsigned(int dividend, int divisor) { + return java.lang.Integer.remainderUnsigned(dividend, divisor); + } + + public static int highestOneBit(int i) { + return java.lang.Integer.highestOneBit(i); + } + + public static int lowestOneBit(int i) { + return java.lang.Integer.lowestOneBit(i); + } + + public static int numberOfLeadingZeros(int i) { + return java.lang.Integer.numberOfLeadingZeros(i); + } + + public static int numberOfTrailingZeros(int i) { + return java.lang.Integer.numberOfTrailingZeros(i); + } + + public static int bitCount(int i) { + return java.lang.Integer.bitCount(i); + } + + public static int rotateLeft(int i, int distance) { + return java.lang.Integer.rotateLeft(i, distance); + } + + public static int rotateRight(int i, int distance) { + return java.lang.Integer.rotateRight(i, distance); + } + + public static int reverse(int i) { + return java.lang.Integer.reverse(i); + } + + public static int signum(int i) { + return java.lang.Integer.signum(i); + } + + public static int reverseBytes(int i) { + return java.lang.Integer.reverseBytes(i); + } + + public static int sum(int a, int b) { + return java.lang.Integer.sum(a, b); + } + + public static int max(int a, int b) { + return java.lang.Integer.max(a, b); + } + + public static int min(int a, int b) { + return java.lang.Integer.min(a, b); + } + + public static Integer toDJVM(java.lang.Integer i) { + return (i == null) ? null : valueOf(i); + } + + static int stringSize(final int number) { + int i = 0; + while (number > SIZE_TABLE[i]) { + ++i; + } + return i + 1; + } + + static void getChars(final int number, int index, char[] buffer) { + java.lang.String s = java.lang.Integer.toString(number); + int length = s.length(); + + while (length > 0) { + buffer[--index] = s.charAt(--length); + } + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Iterable.java b/djvm/src/main/java/sandbox/java/lang/Iterable.java new file mode 100644 index 0000000000..6032fd97db --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Iterable.java @@ -0,0 +1,15 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +/** + * This is a dummy class that implements just enough of [java.lang.Iterable] + * to allow us to compile [sandbox.java.lang.String]. + */ +public interface Iterable extends java.lang.Iterable { + @Override + @NotNull + Iterator iterator(); +} diff --git a/djvm/src/main/java/sandbox/java/lang/Long.java b/djvm/src/main/java/sandbox/java/lang/Long.java new file mode 100644 index 0000000000..0f07158af1 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Long.java @@ -0,0 +1,239 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Long extends Number implements Comparable { + + public static final long MIN_VALUE = java.lang.Long.MIN_VALUE; + public static final long MAX_VALUE = java.lang.Long.MAX_VALUE; + public static final int BYTES = java.lang.Long.BYTES; + public static final int SIZE = java.lang.Long.SIZE; + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Long.TYPE; + + private final long value; + + public Long(long value) { + this.value = value; + } + + public Long(String s) throws NumberFormatException { + this.value = parseLong(s, 10); + } + + @Override + public int hashCode() { + return hashCode(value); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof Long) && ((Long) other).longValue() == value; + } + + public static int hashCode(long l) { + return java.lang.Long.hashCode(l); + } + + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public short shortValue() { + return (short) value; + } + + @Override + public byte byteValue() { + return (byte) value; + } + + @Override + public float floatValue() { + return (float) value; + } + + @Override + public double doubleValue() { + return (double) value; + } + + @Override + public int compareTo(@NotNull Long other) { + return compare(value, other.value); + } + + public static int compare(long x, long y) { + return java.lang.Long.compare(x, y); + } + + @Override + @NotNull + java.lang.Long fromDJVM() { + return value; + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Long.toString(value); + } + + public static String toString(long l) { + return String.toDJVM(java.lang.Long.toString(l)); + } + + public static String toString(long l, int radix) { + return String.toDJVM(java.lang.Long.toString(l, radix)); + } + + public static String toUnsignedString(long l, int radix) { + return String.toDJVM(java.lang.Long.toUnsignedString(l, radix)); + } + + public static String toUnsignedString(long l) { + return String.toDJVM(java.lang.Long.toUnsignedString(l)); + } + + public static String toHexString(long l) { + return String.toDJVM(java.lang.Long.toHexString(l)); + } + + public static String toOctalString(long l) { + return String.toDJVM(java.lang.Long.toOctalString(l)); + } + + public static String toBinaryString(long l) { + return String.toDJVM(java.lang.Long.toBinaryString(l)); + } + + public static long parseLong(String s, int radix) throws NumberFormatException { + return java.lang.Long.parseLong(String.fromDJVM(s), radix); + } + + public static long parseLong(String s) throws NumberFormatException { + return java.lang.Long.parseLong(String.fromDJVM(s)); + } + + public static long parseUnsignedLong(String s, int radix) throws NumberFormatException { + return java.lang.Long.parseUnsignedLong(String.fromDJVM(s), radix); + } + + public static long parseUnsignedLong(String s) throws NumberFormatException { + return java.lang.Long.parseUnsignedLong(String.fromDJVM(s)); + } + + public static Long valueOf(String s, int radix) throws NumberFormatException { + return toDJVM(java.lang.Long.valueOf(String.fromDJVM(s), radix)); + } + + public static Long valueOf(String s) throws NumberFormatException { + return toDJVM(java.lang.Long.valueOf(String.fromDJVM(s))); + } + + public static Long valueOf(long l) { + return new Long(l); + } + + public static Long decode(String s) throws NumberFormatException { + return toDJVM(java.lang.Long.decode(String.fromDJVM(s))); + } + + public static int compareUnsigned(long x, long y) { + return java.lang.Long.compareUnsigned(x, y); + } + + public static long divideUnsigned(long dividend, long divisor) { + return java.lang.Long.divideUnsigned(dividend, divisor); + } + + public static long remainderUnsigned(long dividend, long divisor) { + return java.lang.Long.remainderUnsigned(dividend, divisor); + } + + public static long highestOneBit(long l) { + return java.lang.Long.highestOneBit(l); + } + + public static long lowestOneBit(long l) { + return java.lang.Long.lowestOneBit(l); + } + + public static int numberOfLeadingZeros(long l) { + return java.lang.Long.numberOfLeadingZeros(l); + } + + public static int numberOfTrailingZeros(long l) { + return java.lang.Long.numberOfTrailingZeros(l); + } + + public static int bitCount(long l) { + return java.lang.Long.bitCount(l); + } + + public static long rotateLeft(long i, int distance) { + return java.lang.Long.rotateLeft(i, distance); + } + + public static long rotateRight(long i, int distance) { + return java.lang.Long.rotateRight(i, distance); + } + + public static long reverse(long l) { + return java.lang.Long.reverse(l); + } + + public static int signum(long l) { + return java.lang.Long.signum(l); + } + + public static long reverseBytes(long l) { + return java.lang.Long.reverseBytes(l); + } + + public static long sum(long a, long b) { + return java.lang.Long.sum(a, b); + } + + public static long max(long a, long b) { + return java.lang.Long.max(a, b); + } + + public static long min(long a, long b) { + return java.lang.Long.min(a, b); + } + + public static Long toDJVM(java.lang.Long l) { + return (l == null) ? null : valueOf(l); + } + + static int stringSize(final long number) { + long l = 10; + int i = 1; + + while ((i < 19) && (number >= l)) { + l *= 10; + ++i; + } + + return i; + } + + static void getChars(final long number, int index, char[] buffer) { + java.lang.String s = java.lang.Long.toString(number); + int length = s.length(); + + while (length > 0) { + buffer[--index] = s.charAt(--length); + } + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Number.java b/djvm/src/main/java/sandbox/java/lang/Number.java new file mode 100644 index 0000000000..89d0a7fd8e --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Number.java @@ -0,0 +1,21 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; +import java.io.Serializable; + +@SuppressWarnings("unused") +public abstract class Number extends Object implements Serializable { + + public abstract double doubleValue(); + public abstract float floatValue(); + public abstract long longValue(); + public abstract int intValue(); + public abstract short shortValue(); + public abstract byte byteValue(); + + @Override + @NotNull + public String toDJVMString() { + return String.toDJVM(toString()); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Object.java b/djvm/src/main/java/sandbox/java/lang/Object.java new file mode 100644 index 0000000000..4208a52a53 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Object.java @@ -0,0 +1,71 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; +import sandbox.net.corda.djvm.rules.RuleViolationError; + +public class Object { + + @Override + public int hashCode() { + return sandbox.java.lang.System.identityHashCode(this); + } + + @Override + @NotNull + public java.lang.String toString() { + return toDJVMString().toString(); + } + + @NotNull + public String toDJVMString() { + return String.toDJVM("sandbox.java.lang.Object@" + java.lang.Integer.toString(hashCode(), 16)); + } + + @NotNull + java.lang.Object fromDJVM() { + return this; + } + + public static java.lang.Object[] fromDJVM(java.lang.Object[] args) { + if (args == null) { + return null; + } + + java.lang.Object[] unwrapped = (java.lang.Object[]) java.lang.reflect.Array.newInstance( + fromDJVM(args.getClass().getComponentType()), args.length + ); + int i = 0; + for (java.lang.Object arg : args) { + unwrapped[i] = unwrap(arg); + ++i; + } + return unwrapped; + } + + private static java.lang.Object unwrap(java.lang.Object arg) { + if (arg instanceof Object) { + return ((Object) arg).fromDJVM(); + } else if (Object[].class.isAssignableFrom(arg.getClass())) { + return fromDJVM((Object[]) arg); + } else { + return arg; + } + } + + private static Class fromDJVM(Class type) { + try { + java.lang.String name = type.getName(); + return Class.forName(name.startsWith("sandbox.") ? name.substring(8) : name); + } catch (ClassNotFoundException e) { + throw new RuleViolationError(e.getMessage()); + } + } + + static java.util.Locale fromDJVM(sandbox.java.util.Locale locale) { + return java.util.Locale.forLanguageTag(locale.toLanguageTag().fromDJVM()); + } + + static java.nio.charset.Charset fromDJVM(sandbox.java.nio.charset.Charset charset) { + return java.nio.charset.Charset.forName(charset.name().fromDJVM()); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Runtime.java b/djvm/src/main/java/sandbox/java/lang/Runtime.java new file mode 100644 index 0000000000..830233072b --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Runtime.java @@ -0,0 +1,27 @@ +package sandbox.java.lang; + +@SuppressWarnings("unused") +public final class Runtime extends Object { + private static final Runtime RUNTIME = new Runtime(); + + private Runtime() {} + + public static Runtime getRuntime() { + return RUNTIME; + } + + /** + * Everything inside the sandbox is single-threaded. + * @return 1 + */ + public int availableProcessors() { + return 1; + } + + public void loadLibrary(String libraryName) {} + + public void load(String fileName) {} + + public void runFinalization() {} + public void gc() {} +} diff --git a/djvm/src/main/java/sandbox/java/lang/Short.java b/djvm/src/main/java/sandbox/java/lang/Short.java new file mode 100644 index 0000000000..a0e1cbfd39 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Short.java @@ -0,0 +1,128 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class Short extends Number implements Comparable { + public static final short MIN_VALUE = java.lang.Short.MIN_VALUE; + public static final short MAX_VALUE = java.lang.Short.MAX_VALUE; + public static final int BYTES = java.lang.Short.BYTES; + public static final int SIZE = java.lang.Short.SIZE; + + @SuppressWarnings("unchecked") + public static final Class TYPE = (Class) java.lang.Short.TYPE; + + private final short value; + + public Short(short value) { + this.value = value; + } + + public Short(String s) throws NumberFormatException { + this.value = parseShort(s); + } + + @Override + public byte byteValue() { + return (byte)value; + } + + @Override + public short shortValue() { + return value; + } + + @Override + public int intValue() { + return value; + } + + @Override + public long longValue() { + return (long)value; + } + + @Override + public float floatValue() { + return (float)value; + } + + @Override + public double doubleValue() { + return (double)value; + } + + @Override + @NotNull + public java.lang.String toString() { + return java.lang.Integer.toString(value); + } + + @Override + @NotNull + java.lang.Short fromDJVM() { + return value; + } + + @Override + public int hashCode() { + return hashCode(value); + } + + public static int hashCode(short value) { + return java.lang.Short.hashCode(value); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof Short) && ((Short) other).value == value; + } + + public int compareTo(@NotNull Short other) { + return compare(this.value, other.value); + } + + public static int compare(short x, short y) { + return java.lang.Short.compare(x, y); + } + + public static short reverseBytes(short value) { + return java.lang.Short.reverseBytes(value); + } + + public static int toUnsignedInt(short x) { + return java.lang.Short.toUnsignedInt(x); + } + + public static long toUnsignedLong(short x) { + return java.lang.Short.toUnsignedLong(x); + } + + public static short parseShort(String s, int radix) throws NumberFormatException { + return java.lang.Short.parseShort(String.fromDJVM(s), radix); + } + + public static short parseShort(String s) throws NumberFormatException { + return java.lang.Short.parseShort(String.fromDJVM(s)); + } + + public static Short valueOf(String s, int radix) throws NumberFormatException { + return toDJVM(java.lang.Short.valueOf(String.fromDJVM(s), radix)); + } + + public static Short valueOf(String s) throws NumberFormatException { + return toDJVM(java.lang.Short.valueOf(String.fromDJVM(s))); + } + + public static Short valueOf(short s) { + return new Short(s); + } + + public static Short decode(String nm) throws NumberFormatException { + return toDJVM(java.lang.Short.decode(String.fromDJVM(nm))); + } + + public static Short toDJVM(java.lang.Short i) { + return (i == null) ? null : valueOf(i); + } +} \ No newline at end of file diff --git a/djvm/src/main/java/sandbox/java/lang/String.java b/djvm/src/main/java/sandbox/java/lang/String.java new file mode 100644 index 0000000000..4cce494d30 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/String.java @@ -0,0 +1,398 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; +import sandbox.java.nio.charset.Charset; +import sandbox.java.util.Comparator; +import sandbox.java.util.Locale; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; + +@SuppressWarnings("unused") +public final class String extends Object implements Comparable, CharSequence, Serializable { + public static final Comparator CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(); + + private static class CaseInsensitiveComparator extends Object implements Comparator, Serializable { + @Override + public int compare(String s1, String s2) { + return java.lang.String.CASE_INSENSITIVE_ORDER.compare(String.fromDJVM(s1), String.fromDJVM(s2)); + } + } + + private static final String TRUE = new String("true"); + private static final String FALSE = new String("false"); + + private final java.lang.String value; + + public String() { + this.value = ""; + } + + public String(java.lang.String value) { + this.value = value; + } + + public String(char value[]) { + this.value = new java.lang.String(value); + } + + public String(char value[], int offset, int count) { + this.value = new java.lang.String(value, offset, count); + } + + public String(int[] codePoints, int offset, int count) { + this.value = new java.lang.String(codePoints, offset, count); + } + + @Deprecated + public String(byte ascii[], int hibyte, int offset, int count) { + this.value = new java.lang.String(ascii, hibyte, offset, count); + } + + @Deprecated + public String(byte ascii[], int hibyte) { + this.value = new java.lang.String(ascii, hibyte); + } + + public String(byte bytes[], int offset, int length, String charsetName) + throws UnsupportedEncodingException { + this.value = new java.lang.String(bytes, offset, length, fromDJVM(charsetName)); + } + + public String(byte bytes[], int offset, int length, Charset charset) { + this.value = new java.lang.String(bytes, offset, length, fromDJVM(charset)); + } + + public String(byte bytes[], String charsetName) + throws UnsupportedEncodingException { + this.value = new java.lang.String(bytes, fromDJVM(charsetName)); + } + + public String(byte bytes[], Charset charset) { + this.value = new java.lang.String(bytes, fromDJVM(charset)); + } + + public String(byte bytes[], int offset, int length) { + this.value = new java.lang.String(bytes, offset, length); + } + + public String(byte bytes[]) { + this.value = new java.lang.String(bytes); + } + + public String(StringBuffer buffer) { + this.value = buffer.toString(); + } + + public String(StringBuilder builder) { + this.value = builder.toString(); + } + + @Override + public char charAt(int index) { + return value.charAt(index); + } + + @Override + public int length() { + return value.length(); + } + + public boolean isEmpty() { + return value.isEmpty(); + } + + public int codePointAt(int index) { + return value.codePointAt(index); + } + + public int codePointBefore(int index) { + return value.codePointBefore(index); + } + + public int codePointCount(int beginIndex, int endIndex) { + return value.codePointCount(beginIndex, endIndex); + } + + public int offsetByCodePoints(int index, int codePointOffset) { + return value.offsetByCodePoints(index, codePointOffset); + } + + public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { + value.getChars(srcBegin, srcEnd, dst, dstBegin); + } + + @Deprecated + public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) { + value.getBytes(srcBegin, srcEnd, dst, dstBegin); + } + + public byte[] getBytes(String charsetName) throws UnsupportedEncodingException { + return value.getBytes(fromDJVM(charsetName)); + } + + public byte[] getBytes(Charset charset) { + return value.getBytes(fromDJVM(charset)); + } + + public byte[] getBytes() { + return value.getBytes(); + } + + @Override + public boolean equals(java.lang.Object other) { + return (other instanceof String) && ((String) other).value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + @NotNull + public java.lang.String toString() { + return value; + } + + @Override + @NotNull + public String toDJVMString() { + return this; + } + + @Override + @NotNull + java.lang.String fromDJVM() { + return value; + } + + public boolean contentEquals(StringBuffer sb) { + return value.contentEquals((CharSequence) sb); + } + + public boolean contentEquals(CharSequence cs) { + return value.contentEquals(cs); + } + + public boolean equalsIgnoreCase(String anotherString) { + return value.equalsIgnoreCase(fromDJVM(anotherString)); + } + + @Override + public CharSequence subSequence(int start, int end) { + return toDJVM((java.lang.String) value.subSequence(start, end)); + } + + @Override + public int compareTo(@NotNull String other) { + return value.compareTo(other.toString()); + } + + public int compareToIgnoreCase(String str) { + return value.compareToIgnoreCase(fromDJVM(str)); + } + + public boolean regionMatches(int toffset, String other, int ooffset, int len) { + return value.regionMatches(toffset, fromDJVM(other), ooffset, len); + } + + public boolean regionMatches(boolean ignoreCase, int toffset, + String other, int ooffset, int len) { + return value.regionMatches(ignoreCase, toffset, fromDJVM(other), ooffset, len); + } + + public boolean startsWith(String prefix, int toffset) { + return value.startsWith(fromDJVM(prefix), toffset); + } + + public boolean startsWith(String prefix) { + return value.startsWith(fromDJVM(prefix)); + } + + public boolean endsWith(String suffix) { + return value.endsWith(fromDJVM(suffix)); + } + + public int indexOf(int ch) { + return value.indexOf(ch); + } + + public int indexOf(int ch, int fromIndex) { + return value.indexOf(ch, fromIndex); + } + + public int lastIndexOf(int ch) { + return value.lastIndexOf(ch); + } + + public int lastIndexOf(int ch, int fromIndex) { + return value.lastIndexOf(ch, fromIndex); + } + + public int indexOf(String str) { + return value.indexOf(fromDJVM(str)); + } + + public int indexOf(String str, int fromIndex) { + return value.indexOf(fromDJVM(str), fromIndex); + } + + public int lastIndexOf(String str) { + return value.lastIndexOf(fromDJVM(str)); + } + + public int lastIndexOf(String str, int fromIndex) { + return value.lastIndexOf(fromDJVM(str), fromIndex); + } + + public String substring(int beginIndex) { + return toDJVM(value.substring(beginIndex)); + } + + public String substring(int beginIndex, int endIndex) { + return toDJVM(value.substring(beginIndex, endIndex)); + } + + public String concat(String str) { + return toDJVM(value.concat(fromDJVM(str))); + } + + public String replace(char oldChar, char newChar) { + return toDJVM(value.replace(oldChar, newChar)); + } + + public boolean matches(String regex) { + return value.matches(fromDJVM(regex)); + } + + public boolean contains(CharSequence s) { + return value.contains(s); + } + + public String replaceFirst(String regex, String replacement) { + return toDJVM(value.replaceFirst(fromDJVM(regex), fromDJVM(replacement))); + } + + public String replaceAll(String regex, String replacement) { + return toDJVM(value.replaceAll(fromDJVM(regex), fromDJVM(replacement))); + } + + public String replace(CharSequence target, CharSequence replacement) { + return toDJVM(value.replace(target, replacement)); + } + + public String[] split(String regex, int limit) { + return toDJVM(value.split(fromDJVM(regex), limit)); + } + + public String[] split(String regex) { + return toDJVM(value.split(fromDJVM(regex))); + } + + public String toLowerCase(Locale locale) { + return toDJVM(value.toLowerCase(fromDJVM(locale))); + } + + public String toLowerCase() { + return toDJVM(value.toLowerCase()); + } + + public String toUpperCase(Locale locale) { + return toDJVM(value.toUpperCase(fromDJVM(locale))); + } + + public String toUpperCase() { + return toDJVM(value.toUpperCase()); + } + + public String trim() { + return toDJVM(value.trim()); + } + + public char[] toCharArray() { + return value.toCharArray(); + } + + public static String format(String format, java.lang.Object... args) { + return toDJVM(java.lang.String.format(fromDJVM(format), fromDJVM(args))); + } + + public static String format(Locale locale, String format, java.lang.Object... args) { + return toDJVM(java.lang.String.format(fromDJVM(locale), fromDJVM(format), fromDJVM(args))); + } + + public static String join(CharSequence delimiter, CharSequence... elements) { + return toDJVM(java.lang.String.join(delimiter, elements)); + } + + public static String join(CharSequence delimiter, + Iterable elements) { + return toDJVM(java.lang.String.join(delimiter, elements)); + } + + public static String valueOf(java.lang.Object obj) { + return (obj instanceof Object) ? ((Object) obj).toDJVMString() : toDJVM(java.lang.String.valueOf(obj)); + } + + public static String valueOf(char data[]) { + return toDJVM(java.lang.String.valueOf(data)); + } + + public static String valueOf(char data[], int offset, int count) { + return toDJVM(java.lang.String.valueOf(data, offset, count)); + } + + public static String copyValueOf(char data[], int offset, int count) { + return toDJVM(java.lang.String.copyValueOf(data, offset, count)); + } + + public static String copyValueOf(char data[]) { + return toDJVM(java.lang.String.copyValueOf(data)); + } + + public static String valueOf(boolean b) { + return b ? TRUE : FALSE; + } + + public static String valueOf(char c) { + return toDJVM(java.lang.String.valueOf(c)); + } + + public static String valueOf(int i) { + return toDJVM(java.lang.String.valueOf(i)); + } + + public static String valueOf(long l) { + return toDJVM(java.lang.String.valueOf(l)); + } + + public static String valueOf(float f) { + return toDJVM(java.lang.String.valueOf(f)); + } + + public static String valueOf(double d) { + return toDJVM(java.lang.String.valueOf(d)); + } + + static String[] toDJVM(java.lang.String[] value) { + if (value == null) { + return null; + } + String[] result = new String[value.length]; + int i = 0; + for (java.lang.String v : value) { + result[i] = toDJVM(v); + ++i; + } + return result; + } + + public static String toDJVM(java.lang.String value) { + return (value == null) ? null : new String(value); + } + + public static java.lang.String fromDJVM(String value) { + return (value == null) ? null : value.fromDJVM(); + } +} \ No newline at end of file diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuffer.java b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java new file mode 100644 index 0000000000..e9cbcad328 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java @@ -0,0 +1,20 @@ +package sandbox.java.lang; + +import java.io.Serializable; + +/** + * This is a dummy class that implements just enough of [java.lang.StringBuffer] + * to allow us to compile [sandbox.java.lang.String]. + */ +public abstract class StringBuffer extends Object implements CharSequence, Appendable, Serializable { + + @Override + public abstract StringBuffer append(CharSequence seq); + + @Override + public abstract StringBuffer append(CharSequence seq, int start, int end); + + @Override + public abstract StringBuffer append(char c); + +} diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuilder.java b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java new file mode 100644 index 0000000000..ed80b2e508 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java @@ -0,0 +1,20 @@ +package sandbox.java.lang; + +import java.io.Serializable; + +/** + * This is a dummy class that implements just enough of [java.lang.StringBuilder] + * to allow us to compile [sandbox.java.lang.String]. + */ +public abstract class StringBuilder extends Object implements Appendable, CharSequence, Serializable { + + @Override + public abstract StringBuilder append(CharSequence seq); + + @Override + public abstract StringBuilder append(CharSequence seq, int start, int end); + + @Override + public abstract StringBuilder append(char c); + +} diff --git a/djvm/src/main/java/sandbox/java/lang/System.java b/djvm/src/main/java/sandbox/java/lang/System.java new file mode 100644 index 0000000000..95525d0b50 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/System.java @@ -0,0 +1,28 @@ +package sandbox.java.lang; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class System extends Object { + + private System() {} + + /* + * This class is duplicated into every sandbox, where everything is single-threaded. + */ + private static final java.util.Map objectHashCodes = new java.util.LinkedHashMap<>(); + private static int objectCounter = 0; + + public static int identityHashCode(java.lang.Object obj) { + int nativeHashCode = java.lang.System.identityHashCode(obj); + // TODO Instead of using a magic offset below, one could take in a per-context seed + return objectHashCodes.computeIfAbsent(nativeHashCode, i -> ++objectCounter + 0xfed_c0de); + } + + public static final String lineSeparator = String.toDJVM("\n"); + + public static void arraycopy(java.lang.Object src, int srcPos, java.lang.Object dest, int destPos, int length) { + java.lang.System.arraycopy(src, srcPos, dest, destPos, length); + } + + public static void runFinalization() {} + public static void gc() {} +} diff --git a/djvm/src/main/java/sandbox/java/lang/ThreadLocal.java b/djvm/src/main/java/sandbox/java/lang/ThreadLocal.java new file mode 100644 index 0000000000..f416d3db16 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/ThreadLocal.java @@ -0,0 +1,59 @@ +package sandbox.java.lang; + +import sandbox.java.util.function.Supplier; + +/** + * Everything inside the sandbox is single-threaded, so this + * implementation of ThreadLocal is sufficient. + * @param + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class ThreadLocal extends Object { + + private T value; + private boolean isSet; + + public ThreadLocal() { + } + + protected T initialValue() { + return null; + } + + public T get() { + if (!isSet) { + set(initialValue()); + } + return value; + } + + public void set(T value) { + this.value = value; + this.isSet = true; + } + + public void remove() { + value = null; + isSet = false; + } + + public static ThreadLocal withInitial(Supplier supplier) { + return new SuppliedThreadLocal<>(supplier); + } + + // Stub class for compiling ThreadLocal. The sandbox will import the + // actual SuppliedThreadLocal class at run-time. Having said that, we + // still need a working implementation here for the sake of our tests. + static final class SuppliedThreadLocal extends ThreadLocal { + private final Supplier supplier; + + SuppliedThreadLocal(Supplier supplier) { + this.supplier = supplier; + } + + @Override + protected T initialValue() { + return supplier.get(); + } + } +} diff --git a/djvm/src/main/java/sandbox/java/nio/charset/Charset.java b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java new file mode 100644 index 0000000000..371a21404a --- /dev/null +++ b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java @@ -0,0 +1,18 @@ +package sandbox.java.nio.charset; + +/** + * This is a dummy class that implements just enough of [java.nio.charset.Charset] + * to allow us to compile [sandbox.java.lang.String]. + */ +@SuppressWarnings("unused") +public abstract class Charset extends sandbox.java.lang.Object { + private final sandbox.java.lang.String canonicalName; + + protected Charset(sandbox.java.lang.String canonicalName, sandbox.java.lang.String[] aliases) { + this.canonicalName = canonicalName; + } + + public final sandbox.java.lang.String name() { + return canonicalName; + } +} diff --git a/djvm/src/main/java/sandbox/java/util/Comparator.java b/djvm/src/main/java/sandbox/java/util/Comparator.java new file mode 100644 index 0000000000..20679dee59 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/util/Comparator.java @@ -0,0 +1,9 @@ +package sandbox.java.util; + +/** + * This is a dummy class that implements just enough of [java.util.Comparator] + * to allow us to compile [sandbox.java.lang.String]. + */ +@FunctionalInterface +public interface Comparator extends java.util.Comparator { +} diff --git a/djvm/src/main/java/sandbox/java/util/LinkedHashMap.java b/djvm/src/main/java/sandbox/java/util/LinkedHashMap.java new file mode 100644 index 0000000000..37d8c56210 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/util/LinkedHashMap.java @@ -0,0 +1,13 @@ +package sandbox.java.util; + +/** + * This is a dummy class to bootstrap us into the sandbox. + */ +public class LinkedHashMap extends java.util.LinkedHashMap implements Map { + public LinkedHashMap(int initialSize) { + super(initialSize); + } + + public LinkedHashMap() { + } +} diff --git a/djvm/src/main/java/sandbox/java/util/Locale.java b/djvm/src/main/java/sandbox/java/util/Locale.java new file mode 100644 index 0000000000..3ceaea9382 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/util/Locale.java @@ -0,0 +1,9 @@ +package sandbox.java.util; + +/** + * This is a dummy class that implements just enough of [java.util.Locale] + * to allow us to compile [sandbox.java.lang.String]. + */ +public abstract class Locale extends sandbox.java.lang.Object { + public abstract sandbox.java.lang.String toLanguageTag(); +} diff --git a/djvm/src/main/java/sandbox/java/util/Map.java b/djvm/src/main/java/sandbox/java/util/Map.java new file mode 100644 index 0000000000..576e462583 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/util/Map.java @@ -0,0 +1,7 @@ +package sandbox.java.util; + +/** + * This is a dummy class to bootstrap us into the sandbox. + */ +public interface Map extends java.util.Map { +} diff --git a/djvm/src/main/java/sandbox/java/util/function/Function.java b/djvm/src/main/java/sandbox/java/util/function/Function.java new file mode 100644 index 0000000000..5cd806a01e --- /dev/null +++ b/djvm/src/main/java/sandbox/java/util/function/Function.java @@ -0,0 +1,10 @@ +package sandbox.java.util.function; + +/** + * This is a dummy class that implements just enough of [java.util.function.Function] + * to allow us to compile [sandbox.Task]. + */ +@FunctionalInterface +public interface Function { + R apply(T item); +} diff --git a/djvm/src/main/java/sandbox/java/util/function/Supplier.java b/djvm/src/main/java/sandbox/java/util/function/Supplier.java new file mode 100644 index 0000000000..31f236bae6 --- /dev/null +++ b/djvm/src/main/java/sandbox/java/util/function/Supplier.java @@ -0,0 +1,10 @@ +package sandbox.java.util.function; + +/** + * This is a dummy class that implements just enough of [java.util.function.Supplier] + * to allow us to compile [sandbox.java.lang.ThreadLocal]. + */ +@FunctionalInterface +public interface Supplier { + T get(); +} diff --git a/djvm/src/main/java/sandbox/sun/misc/JavaLangAccess.java b/djvm/src/main/java/sandbox/sun/misc/JavaLangAccess.java new file mode 100644 index 0000000000..189a7f9711 --- /dev/null +++ b/djvm/src/main/java/sandbox/sun/misc/JavaLangAccess.java @@ -0,0 +1,10 @@ +package sandbox.sun.misc; + +import sandbox.java.lang.Enum; + +@SuppressWarnings("unused") +public interface JavaLangAccess { + + > E[] getEnumConstantsShared(Class enumClass); + +} diff --git a/djvm/src/main/java/sandbox/sun/misc/SharedSecrets.java b/djvm/src/main/java/sandbox/sun/misc/SharedSecrets.java new file mode 100644 index 0000000000..a03f7689c1 --- /dev/null +++ b/djvm/src/main/java/sandbox/sun/misc/SharedSecrets.java @@ -0,0 +1,20 @@ +package sandbox.sun.misc; + +import sandbox.java.lang.Enum; + +@SuppressWarnings("unused") +public class SharedSecrets extends sandbox.java.lang.Object { + private static final JavaLangAccess javaLangAccess = new JavaLangAccessImpl(); + + private static class JavaLangAccessImpl implements JavaLangAccess { + @SuppressWarnings("unchecked") + @Override + public > E[] getEnumConstantsShared(Class enumClass) { + return (E[]) sandbox.java.lang.DJVM.getEnumConstantsShared(enumClass); + } + } + + public static JavaLangAccess getJavaLangAccess() { + return javaLangAccess; + } +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt index 2a1e7d63cf..f8d87fd1ea 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt @@ -1,12 +1,17 @@ package net.corda.djvm.analysis +import net.corda.djvm.code.EmitterModule import net.corda.djvm.code.ruleViolationError import net.corda.djvm.code.thresholdViolationError import net.corda.djvm.messages.Severity import net.corda.djvm.references.ClassModule +import net.corda.djvm.references.Member import net.corda.djvm.references.MemberModule +import net.corda.djvm.references.MethodBody import net.corda.djvm.source.BootstrapClassLoader import net.corda.djvm.source.SourceClassLoader +import org.objectweb.asm.Opcodes.* +import org.objectweb.asm.Type import sandbox.net.corda.djvm.costing.RuntimeCostAccounter import java.io.Closeable import java.io.IOException @@ -41,19 +46,22 @@ class AnalysisConfiguration( /** * Classes that have already been declared in the sandbox namespace and that should be made - * available inside the sandboxed environment. + * available inside the sandboxed environment. These classes belong to the application + * classloader and so are shared across all sandboxes. */ - val pinnedClasses: Set = setOf( - SANDBOXED_OBJECT, - RuntimeCostAccounter.TYPE_NAME, - ruleViolationError, - thresholdViolationError - ) + additionalPinnedClasses + val pinnedClasses: Set = MANDATORY_PINNED_CLASSES + additionalPinnedClasses + + /** + * These interfaces are modified as they are mapped into the sandbox by + * having their unsandboxed version "stitched in" as a super-interface. + * And in some cases, we need to add some synthetic bridge methods as well. + */ + val stitchedInterfaces: Map> get() = STITCHED_INTERFACES /** * Functionality used to resolve the qualified name and relevant information about a class. */ - val classResolver: ClassResolver = ClassResolver(pinnedClasses, whitelist, SANDBOX_PREFIX) + val classResolver: ClassResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX) private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) } val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader) @@ -65,13 +73,114 @@ class AnalysisConfiguration( } } + fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES + fun isPinnedClass(className: String): Boolean = className in pinnedClasses + companion object { /** * The package name prefix to use for classes loaded into a sandbox. */ private const val SANDBOX_PREFIX: String = "sandbox/" - private const val SANDBOXED_OBJECT = SANDBOX_PREFIX + "java/lang/Object" + /** + * These class must belong to the application class loader. + * They should already exist within the sandbox namespace. + */ + private val MANDATORY_PINNED_CLASSES: Set = setOf( + RuntimeCostAccounter.TYPE_NAME, + ruleViolationError, + thresholdViolationError + ) + + /** + * These classes will be duplicated into every sandbox's + * classloader. + */ + private val TEMPLATE_CLASSES: Set = setOf( + java.lang.Boolean::class.java, + java.lang.Byte::class.java, + java.lang.Character::class.java, + java.lang.Double::class.java, + java.lang.Float::class.java, + java.lang.Integer::class.java, + java.lang.Long::class.java, + java.lang.Number::class.java, + java.lang.Runtime::class.java, + java.lang.Short::class.java, + java.lang.String::class.java, + java.lang.String.CASE_INSENSITIVE_ORDER::class.java, + java.lang.System::class.java, + java.lang.ThreadLocal::class.java, + kotlin.Any::class.java, + sun.misc.JavaLangAccess::class.java, + sun.misc.SharedSecrets::class.java + ).sandboxed() + setOf( + "sandbox/Task", + "sandbox/java/lang/DJVM", + "sandbox/sun/misc/SharedSecrets\$1", + "sandbox/sun/misc/SharedSecrets\$JavaLangAccessImpl" + ) + + /** + * These interfaces will be modified as follows when + * added to the sandbox: + * + * interface sandbox.A extends A + */ + private val STITCHED_INTERFACES: Map> = mapOf( + sandboxed(CharSequence::class.java) to listOf( + object : MethodBuilder( + access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, + className = "sandbox/java/lang/CharSequence", + memberName = "subSequence", + descriptor = "(II)Ljava/lang/CharSequence;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + pushInteger(1) + pushInteger(2) + invokeInterface(className, memberName, "(II)L$className;") + returnObject() + } + }.withBody() + .build(), + MethodBuilder( + access = ACC_PUBLIC or ACC_ABSTRACT, + className = "sandbox/java/lang/CharSequence", + memberName = "toString", + descriptor = "()Ljava/lang/String;" + ).build() + ), + sandboxed(Comparable::class.java) to emptyList(), + sandboxed(Comparator::class.java) to emptyList(), + sandboxed(Iterable::class.java) to emptyList() + ) + + private fun sandboxed(clazz: Class<*>) = SANDBOX_PREFIX + Type.getInternalName(clazz) + private fun Set>.sandboxed(): Set = map(Companion::sandboxed).toSet() } + private open class MethodBuilder( + protected val access: Int, + protected val className: String, + protected val memberName: String, + protected val descriptor: String) { + private val bodies = mutableListOf() + + protected open fun writeBody(emitter: EmitterModule) {} + + fun withBody(): MethodBuilder { + bodies.add(::writeBody) + return this + } + + fun build() = Member( + access = access, + className = className, + memberName = memberName, + signature = descriptor, + genericsDetails = "", + body = bodies + ) + } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt index d0d9cb4e8c..8bfb997ae7 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt @@ -85,8 +85,12 @@ open class ClassAndMemberVisitor( /** * Process class after it has been fully traversed and analyzed. + * The [classVisitor] has finished visiting all of the class's + * existing elements (i.e. methods, fields, inner classes etc) + * and is about to complete. However, it can still add new + * elements to the class, if required. */ - open fun visitClassEnd(clazz: ClassRepresentation) {} + open fun visitClassEnd(classVisitor: ClassVisitor, clazz: ClassRepresentation) {} /** * Extract the meta-data indicating the source file of the traversed class (i.e., where it is compiled from). @@ -136,7 +140,7 @@ open class ClassAndMemberVisitor( */ protected fun shouldBeProcessed(className: String): Boolean { return !configuration.whitelist.inNamespace(className) && - className !in configuration.pinnedClasses + !configuration.isPinnedClass(className) } /** @@ -241,7 +245,7 @@ open class ClassAndMemberVisitor( .getClassReferencesFromClass(currentClass!!, configuration.analyzeAnnotations) .forEach(::recordTypeReference) captureExceptions { - visitClassEnd(currentClass!!) + visitClassEnd(this, currentClass!!) } super.visitEnd() } @@ -385,7 +389,9 @@ open class ClassAndMemberVisitor( */ override fun visitCode() { tryReplaceMethodBody() - super.visitCode() + visit(MethodEntry(method)) { + super.visitCode() + } } /** @@ -494,6 +500,15 @@ open class ClassAndMemberVisitor( } } + /** + * Transform values loaded from the constants pool. + */ + override fun visitLdcInsn(value: Any) { + visit(ConstantInstruction(value), defaultFirst = true) { + super.visitLdcInsn(value) + } + } + /** * Finish visiting this method, writing any new method body byte-code * if we haven't written it already. This would (presumably) only happen diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt index b1aa3ae541..a05b4ec7ec 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt @@ -26,6 +26,7 @@ import net.corda.djvm.code.asResourcePath */ class ClassResolver( private val pinnedClasses: Set, + private val templateClasses: Set, private val whitelist: Whitelist, private val sandboxPrefix: String ) { @@ -83,7 +84,7 @@ class ClassResolver( * Reverse the resolution of a class name. */ fun reverse(resolvedClassName: String): String { - if (resolvedClassName in pinnedClasses) { + if (resolvedClassName in pinnedClasses || resolvedClassName in templateClasses) { return resolvedClassName } if (resolvedClassName.startsWith(sandboxPrefix)) { @@ -103,10 +104,10 @@ class ClassResolver( } /** - * Resolve class name from a fully qualified name. + * Resolve sandboxed class name from a fully qualified name. */ private fun resolveName(name: String): String { - return if (isPinnedOrWhitelistedClass(name)) { + return if (isPinnedOrWhitelistedClass(name) || name in templateClasses) { name } else { "$sandboxPrefix$name" @@ -122,10 +123,10 @@ class ClassResolver( sandboxRegex.matches(name) } - private val sandboxRegex = "^$sandboxPrefix.*$".toRegex() + private val sandboxRegex = "^$sandboxPrefix.*\$".toRegex() companion object { - private val complexArrayTypeRegex = "^(\\[+)L(.*);$".toRegex() + private val complexArrayTypeRegex = "^(\\[+)L(.*);\$".toRegex() } } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt index 3cbbfe8223..c19cc8111e 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt @@ -89,36 +89,25 @@ open class Whitelist private constructor( * Enumerate all the entries of the whitelist. */ val items: Set - get() = textEntries + entries.map { it.pattern } + get() = textEntries + entries.map(Regex::pattern) companion object { private val everythingRegex = setOf(".*".toRegex()) private val minimumSet = setOf( - "^java/lang/Boolean(\\..*)?$".toRegex(), - "^java/lang/Byte(\\..*)?$".toRegex(), - "^java/lang/Character(\\..*)?$".toRegex(), - "^java/lang/Class(\\..*)?$".toRegex(), - "^java/lang/ClassLoader(\\..*)?$".toRegex(), - "^java/lang/Cloneable(\\..*)?$".toRegex(), - "^java/lang/Comparable(\\..*)?$".toRegex(), - "^java/lang/Double(\\..*)?$".toRegex(), - "^java/lang/Enum(\\..*)?$".toRegex(), - "^java/lang/Float(\\..*)?$".toRegex(), - "^java/lang/Integer(\\..*)?$".toRegex(), - "^java/lang/Iterable(\\..*)?$".toRegex(), - "^java/lang/Long(\\..*)?$".toRegex(), - "^java/lang/Number(\\..*)?$".toRegex(), - "^java/lang/Object(\\..*)?$".toRegex(), - "^java/lang/Override(\\..*)?$".toRegex(), - "^java/lang/Short(\\..*)?$".toRegex(), - "^java/lang/String(\\..*)?$".toRegex(), - "^java/lang/ThreadDeath(\\..*)?$".toRegex(), - "^java/lang/Throwable(\\..*)?$".toRegex(), - "^java/lang/Void(\\..*)?$".toRegex(), - "^java/lang/.*Error(\\..*)?$".toRegex(), - "^java/lang/.*Exception(\\..*)?$".toRegex(), - "^java/lang/reflect/Array(\\..*)?$".toRegex() + "^java/lang/Class(\\..*)?\$".toRegex(), + "^java/lang/ClassLoader(\\..*)?\$".toRegex(), + "^java/lang/Cloneable(\\..*)?\$".toRegex(), + "^java/lang/Object(\\..*)?\$".toRegex(), + "^java/lang/Override(\\..*)?\$".toRegex(), + // TODO: sandbox exception handling! + "^java/lang/StackTraceElement\$".toRegex(), + "^java/lang/Throwable\$".toRegex(), + "^java/lang/Void\$".toRegex(), + "^java/lang/invoke/LambdaMetafactory\$".toRegex(), + "^java/lang/invoke/MethodHandles(\\\$.*)?\$".toRegex(), + "^java/lang/reflect/Array(\\..*)?\$".toRegex(), + "^java/io/Serializable\$".toRegex() ) /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt index 777e69f9fe..3c800d9859 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt @@ -2,26 +2,48 @@ package net.corda.djvm.code import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.ClassAndMemberVisitor +import net.corda.djvm.code.instructions.MethodEntry import net.corda.djvm.references.ClassRepresentation import net.corda.djvm.references.Member +import net.corda.djvm.references.MethodBody import net.corda.djvm.utilities.Processor import net.corda.djvm.utilities.loggerFor import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Opcodes.* /** * Helper class for applying a set of definition providers and emitters to a class or set of classes. * * @param classVisitor Class visitor to use when traversing the structure of classes. + * @property configuration The configuration to use for class analysis. * @property definitionProviders A set of providers used to update the name or meta-data of classes and members. - * @property emitters A set of code emitters used to modify and instrument method bodies. + * @param emitters A set of code emitters used to modify and instrument method bodies. */ class ClassMutator( classVisitor: ClassVisitor, private val configuration: AnalysisConfiguration, private val definitionProviders: List = emptyList(), - private val emitters: List = emptyList() + emitters: List = emptyList() ) : ClassAndMemberVisitor(configuration, classVisitor) { + /** + * Internal [Emitter] to add static field initializers to + * any class constructor method. + */ + private inner class PrependClassInitializer : Emitter { + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is MethodEntry + && instruction.method.memberName == "" && instruction.method.signature == "()V" + && initializers.isNotEmpty()) { + writeByteCode(initializers) + initializers.clear() + } + } + } + + private val emitters: List = emitters + PrependClassInitializer() + private val initializers = mutableListOf() + /** * Tracks whether any modifications have been applied to any of the processed class(es) and pertinent members. */ @@ -44,6 +66,29 @@ class ClassMutator( return super.visitClass(resultingClass) } + /** + * If we have some static fields to initialise, and haven't already added them + * to an existing class initialiser block then we need to create one. + */ + override fun visitClassEnd(classVisitor: ClassVisitor, clazz: ClassRepresentation) { + tryWriteClassInitializer(classVisitor) + super.visitClassEnd(classVisitor, clazz) + } + + private fun tryWriteClassInitializer(classVisitor: ClassVisitor) { + if (initializers.isNotEmpty()) { + classVisitor.visitMethod(ACC_STATIC, "", "()V", null, null)?.also { mv -> + mv.visitCode() + EmitterModule(mv).writeByteCode(initializers) + mv.visitInsn(RETURN) + mv.visitMaxs(-1, -1) + mv.visitEnd() + } + initializers.clear() + hasBeenModified = true + } + } + /** * Apply definition providers to a method. This can be used to update the name or definition (pertinent meta-data) * of a class member. @@ -71,6 +116,7 @@ class ClassMutator( } if (field != resultingField) { logger.trace("Field has been mutated {}", field) + initializers += resultingField.body hasBeenModified = true } return super.visitField(clazz, resultingField) diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt index afe9b5165d..2e2d2fc2e4 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt @@ -1,5 +1,6 @@ package net.corda.djvm.code +import net.corda.djvm.references.MethodBody import org.objectweb.asm.Label import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* @@ -44,17 +45,9 @@ class EmitterModule( } /** - * Emit instruction for loading an integer constant onto the stack. + * Emit instruction for loading a constant onto the stack. */ - fun loadConstant(constant: Int) { - hasEmittedCustomCode = true - methodVisitor.visitLdcInsn(constant) - } - - /** - * Emit instruction for loading a string constant onto the stack. - */ - fun loadConstant(constant: String) { + fun loadConstant(constant: Any) { hasEmittedCustomCode = true methodVisitor.visitLdcInsn(constant) } @@ -67,6 +60,14 @@ class EmitterModule( methodVisitor.visitMethodInsn(INVOKESTATIC, owner, name, descriptor, isInterface) } + /** + * Emit instruction for invoking a virtual method. + */ + fun invokeVirtual(owner: String, name: String, descriptor: String, isInterface: Boolean = false) { + hasEmittedCustomCode = true + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, owner, name, descriptor, isInterface) + } + /** * Emit instruction for invoking a special method, e.g. a constructor or a method on a super-type. */ @@ -82,6 +83,19 @@ class EmitterModule( invokeSpecial(Type.getInternalName(T::class.java), name, descriptor, isInterface) } + fun invokeInterface(owner: String, name: String, descriptor: String) { + methodVisitor.visitMethodInsn(INVOKEINTERFACE, owner, name, descriptor, true) + hasEmittedCustomCode = true + } + + /** + * Emit instruction for storing a value into a static field. + */ + fun putStatic(owner: String, name: String, descriptor: String) { + methodVisitor.visitFieldInsn(PUTSTATIC, owner, name, descriptor) + hasEmittedCustomCode = true + } + /** * Emit instruction for popping one element off the stack. */ @@ -98,11 +112,52 @@ class EmitterModule( methodVisitor.visitInsn(DUP) } + /** + * Emit instruction for pushing an object reference + * from a register onto the stack. + */ + fun pushObject(regNum: Int) { + methodVisitor.visitVarInsn(ALOAD, regNum) + hasEmittedCustomCode = true + } + + /** + * Emit instruction for pushing an integer value + * from a register onto the stack. + */ + fun pushInteger(regNum: Int) { + methodVisitor.visitVarInsn(ILOAD, regNum) + hasEmittedCustomCode = true + } + + /** + * Emit instructions to rearrange the stack as follows: + * [W1] [W3] + * [W2] -> [W1] + * [w3] [W2] + */ + fun raiseThirdWordToTop() { + methodVisitor.visitInsn(DUP2_X1) + methodVisitor.visitInsn(POP2) + hasEmittedCustomCode = true + } + + /** + * Emit instructions to rearrange the stack as follows: + * [W1] [W2] + * [W2] -> [W3] + * [W3] [W1] + */ + fun sinkTopToThirdWord() { + methodVisitor.visitInsn(DUP_X2) + methodVisitor.visitInsn(POP) + hasEmittedCustomCode = true + } + /** * Emit a sequence of instructions for instantiating and throwing an exception based on the provided message. */ fun throwException(exceptionType: Class, message: String) { - hasEmittedCustomCode = true val exceptionName = Type.getInternalName(exceptionType) new(exceptionName) methodVisitor.visitInsn(DUP) @@ -121,6 +176,14 @@ class EmitterModule( hasEmittedCustomCode = true } + /** + * Emit instruction for a function that returns an object reference. + */ + fun returnObject() { + methodVisitor.visitInsn(ARETURN) + hasEmittedCustomCode = true + } + /** * Emit instructions for a new line number. */ @@ -131,6 +194,15 @@ class EmitterModule( hasEmittedCustomCode = true } + /** + * Write the bytecode from these [MethodBody] objects as provided. + */ + fun writeByteCode(bodies: Iterable) { + for (body in bodies) { + body(this) + } + } + /** * Tell the code writer not to emit the default instruction. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt index e137f196d5..93a9c5bf7d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt @@ -12,4 +12,6 @@ val thresholdViolationError: String = Type.getInternalName(ThresholdViolationErr * Local extension method for normalizing a class name. */ val String.asPackagePath: String get() = this.replace('/', '.') -val String.asResourcePath: String get() = this.replace('.', '/') \ No newline at end of file +val String.asResourcePath: String get() = this.replace('.', '/') + +val String.emptyAsNull: String? get() = if (isEmpty()) null else this \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/ConstantInstruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/ConstantInstruction.kt new file mode 100644 index 0000000000..ebd90d6f02 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/ConstantInstruction.kt @@ -0,0 +1,6 @@ +package net.corda.djvm.code.instructions + +import net.corda.djvm.code.Instruction +import org.objectweb.asm.Opcodes + +class ConstantInstruction(val value: Any) : Instruction(Opcodes.LDC) \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/MethodEntry.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/MethodEntry.kt new file mode 100644 index 0000000000..cde092c056 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/MethodEntry.kt @@ -0,0 +1,9 @@ +package net.corda.djvm.code.instructions + +import net.corda.djvm.references.Member + +/** + * Pseudo-instruction marking the beginning of a method. + * @property method [Member] describing this method. + */ +class MethodEntry(val method: Member): NoOperationInstruction() \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt index b69585538f..245e00902d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt @@ -5,6 +5,7 @@ import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.messages.Message import net.corda.djvm.references.ClassReference import net.corda.djvm.references.MemberReference +import net.corda.djvm.references.ReferenceWithLocation import net.corda.djvm.rewiring.LoadedClass import net.corda.djvm.rewiring.SandboxClassLoader import net.corda.djvm.rewiring.SandboxClassLoadingException @@ -67,12 +68,24 @@ open class SandboxExecutor( val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) val result = IsolatedTask(runnableClass.qualifiedClassName, configuration).run { validate(context, classLoader, classSources) - val loadedClass = classLoader.loadClassAndBytes(runnableClass, context) - val instance = loadedClass.type.newInstance() - val method = loadedClass.type.getMethod("apply", Any::class.java) + + // Load the "entry-point" task class into the sandbox. This task will marshall + // the input and outputs between Java types and sandbox wrapper types. + val taskClass = Class.forName("sandbox.Task", false, classLoader) + + // Create the user's task object inside the sandbox. + val runnable = classLoader.loadForSandbox(runnableClass, context).type.newInstance() + + // Fetch this sandbox's instance of Class so we can retrieve Task(Function) + // and then instantiate the Task. + val functionClass = Class.forName("sandbox.java.util.function.Function", false, classLoader) + val task = taskClass.getDeclaredConstructor(functionClass).newInstance(runnable) + + // Execute the task... + val method = taskClass.getMethod("apply", Any::class.java) try { @Suppress("UNCHECKED_CAST") - method.invoke(instance, input) as? TOutput + method.invoke(task, input) as? TOutput } catch (ex: InvocationTargetException) { throw ex.targetException } @@ -101,7 +114,7 @@ open class SandboxExecutor( fun load(classSource: ClassSource): LoadedClass { val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) val result = IsolatedTask("LoadClass", configuration).run { - classLoader.loadClassAndBytes(classSource, context) + classLoader.loadForSandbox(classSource, context) } return result.output ?: throw ClassNotFoundException(classSource.qualifiedClassName) } @@ -146,7 +159,7 @@ open class SandboxExecutor( ): ReferenceValidationSummary { processClassQueue(*classSources.toTypedArray()) { classSource, className -> val didLoad = try { - classLoader.loadClassAndBytes(classSource, context) + classLoader.loadForSandbox(classSource, context) true } catch (exception: SandboxClassLoadingException) { // Continue; all warnings and errors are captured in [context.messages] @@ -155,7 +168,7 @@ open class SandboxExecutor( if (didLoad) { context.classes[className]?.apply { context.references.referencesFromLocation(className) - .map { it.reference } + .map(ReferenceWithLocation::reference) .filterIsInstance() .filter { it.className != className } .distinct() @@ -201,6 +214,7 @@ open class SandboxExecutor( } } - private val logger = loggerFor>() - + private companion object { + private val logger = loggerFor>() + } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt index 473718512a..081bff4fa5 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt @@ -1,23 +1,26 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration +import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.AnalysisContext +import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import net.corda.djvm.code.ClassMutator +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.emptyAsNull +import net.corda.djvm.references.Member import net.corda.djvm.utilities.loggerFor import org.objectweb.asm.ClassReader -import org.objectweb.asm.commons.ClassRemapper +import org.objectweb.asm.ClassVisitor /** - * Functionality for rewrite parts of a class as it is being loaded. + * Functionality for rewriting parts of a class as it is being loaded. * * @property configuration The configuration of the sandbox. * @property classLoader The class loader used to load the classes that are to be rewritten. - * @property remapper A sandbox-aware remapper for inspecting and correcting type names and descriptors. */ open class ClassRewriter( private val configuration: SandboxConfiguration, - private val classLoader: ClassLoader, - private val remapper: SandboxRemapper = SandboxRemapper(configuration.analysisConfiguration.classResolver) + private val classLoader: ClassLoader ) { /** @@ -29,20 +32,53 @@ open class ClassRewriter( fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode { logger.debug("Rewriting class {}...", reader.className) val writer = SandboxClassWriter(reader, classLoader) - val classRemapper = ClassRemapper(writer, remapper) + val analysisConfiguration = configuration.analysisConfiguration + val classRemapper = SandboxClassRemapper(InterfaceStitcher(writer, analysisConfiguration), analysisConfiguration) val visitor = ClassMutator( classRemapper, - configuration.analysisConfiguration, + analysisConfiguration, configuration.definitionProviders, configuration.emitters ) visitor.analyze(reader, context, options = ClassReader.EXPAND_FRAMES) - val hasBeenModified = visitor.hasBeenModified - return ByteCode(writer.toByteArray(), hasBeenModified) + return ByteCode(writer.toByteArray(), visitor.hasBeenModified) } private companion object { private val logger = loggerFor() } + /** + * Extra visitor that is applied after [SandboxRemapper]. This "stitches" the original + * unmapped interface as a super-interface of the mapped version. + */ + private class InterfaceStitcher(parent: ClassVisitor, private val configuration: AnalysisConfiguration) + : ClassVisitor(API_VERSION, parent) + { + private val extraMethods = mutableListOf() + + override fun visit(version: Int, access: Int, className: String, signature: String?, superName: String?, interfaces: Array?) { + val stitchedInterfaces = configuration.stitchedInterfaces[className]?.let { methods -> + extraMethods += methods + arrayOf(*(interfaces ?: emptyArray()), configuration.classResolver.reverse(className)) + } ?: interfaces + + super.visit(version, access, className, signature, superName, stitchedInterfaces) + } + + override fun visitEnd() { + for (method in extraMethods) { + method.apply { + visitMethod(access, memberName, signature, genericsDetails.emptyAsNull, exceptions.toTypedArray())?.also { mv -> + mv.visitCode() + EmitterModule(mv).writeByteCode(body) + mv.visitMaxs(-1, -1) + mv.visitEnd() + } + } + } + extraMethods.clear() + super.visitEnd() + } + } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt index 5740534526..7f2abccb6a 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt @@ -18,7 +18,7 @@ import net.corda.djvm.validation.RuleValidator class SandboxClassLoader( configuration: SandboxConfiguration, private val context: AnalysisContext -) : ClassLoader(null) { +) : ClassLoader() { private val analysisConfiguration = configuration.analysisConfiguration @@ -36,11 +36,6 @@ class SandboxClassLoader( val analyzer: ClassAndMemberVisitor get() = ruleValidator - /** - * Set of classes that should be left untouched due to pinning. - */ - private val pinnedClasses = analysisConfiguration.pinnedClasses - /** * Set of classes that should be left untouched due to whitelisting. */ @@ -61,6 +56,17 @@ class SandboxClassLoader( */ private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader) + /** + * Given a class name, provide its corresponding [LoadedClass] for the sandbox. + */ + fun loadForSandbox(name: String, context: AnalysisContext): LoadedClass { + return loadClassAndBytes(ClassSource.fromClassName(analysisConfiguration.classResolver.resolveNormalized(name)), context) + } + + fun loadForSandbox(source: ClassSource, context: AnalysisContext): LoadedClass { + return loadForSandbox(source.qualifiedClassName, context) + } + /** * Load the class with the specified binary name. * @@ -69,69 +75,68 @@ class SandboxClassLoader( * * @return The resulting Class object. */ + @Throws(ClassNotFoundException::class) override fun loadClass(name: String, resolve: Boolean): Class<*> { - return loadClassAndBytes(ClassSource.fromClassName(name), context).type + val source = ClassSource.fromClassName(name) + return if (name.startsWith("sandbox.") && !analysisConfiguration.isPinnedClass(source.internalClassName)) { + loadClassAndBytes(source, context).type + } else { + super.loadClass(name, resolve) + } } /** * Load the class with the specified binary name. * - * @param source The class source, including the binary name of the class. + * @param request The class request, including the binary name of the class. * @param context The context in which the analysis is conducted. * * @return The resulting Class object and its byte code representation. */ - fun loadClassAndBytes(source: ClassSource, context: AnalysisContext): LoadedClass { - logger.debug("Loading class {}, origin={}...", source.qualifiedClassName, source.origin) - val name = analysisConfiguration.classResolver.reverseNormalized(source.qualifiedClassName) - val resolvedName = analysisConfiguration.classResolver.resolveNormalized(name) + private fun loadClassAndBytes(request: ClassSource, context: AnalysisContext): LoadedClass { + logger.debug("Loading class {}, origin={}...", request.qualifiedClassName, request.origin) + val requestedPath = request.internalClassName + val sourceName = analysisConfiguration.classResolver.reverseNormalized(request.qualifiedClassName) + val resolvedName = analysisConfiguration.classResolver.resolveNormalized(sourceName) // Check if the class has already been loaded. - val loadedClass = loadedClasses[name] + val loadedClass = loadedClasses[requestedPath] if (loadedClass != null) { - logger.trace("Class {} already loaded", source.qualifiedClassName) + logger.trace("Class {} already loaded", request.qualifiedClassName) return loadedClass + } else if (analysisConfiguration.isPinnedClass(requestedPath)) { + logger.debug("Class {} is loaded unmodified", request.qualifiedClassName) + return loadUnmodifiedClass(requestedPath) } - // Load the byte code for the specified class. - val reader = supportingClassLoader.classReader(name, context, source.origin) + val byteCode = if (analysisConfiguration.isTemplateClass(requestedPath)) { + loadUnmodifiedByteCode(requestedPath) + } else { + // Load the byte code for the specified class. + val reader = supportingClassLoader.classReader(sourceName, context, request.origin) - // Analyse the class if not matching the whitelist. - val readClassName = reader.className - if (!analysisConfiguration.whitelist.matches(readClassName)) { - logger.trace("Class {} does not match with the whitelist", source.qualifiedClassName) - logger.trace("Analyzing class {}...", source.qualifiedClassName) - analyzer.analyze(reader, context) - } - - // Check if the class should be left untouched. - val qualifiedName = name.asResourcePath - if (qualifiedName in pinnedClasses) { - logger.trace("Class {} is marked as pinned", source.qualifiedClassName) - val pinnedClasses = LoadedClass( - supportingClassLoader.loadClass(name), - ByteCode(ByteArray(0), false) - ) - loadedClasses[name] = pinnedClasses - if (source.origin != null) { - context.recordClassOrigin(name, ClassReference(source.origin)) + // Analyse the class if not matching the whitelist. + val readClassName = reader.className + if (!analysisConfiguration.whitelist.matches(readClassName)) { + logger.trace("Class {} does not match with the whitelist", request.qualifiedClassName) + logger.trace("Analyzing class {}...", request.qualifiedClassName) + analyzer.analyze(reader, context) } - return pinnedClasses - } - // Check if any errors were found during analysis. - if (context.messages.errorCount > 0) { - logger.trace("Errors detected after analyzing class {}", source.qualifiedClassName) - throw SandboxClassLoadingException(context) - } + // Check if any errors were found during analysis. + if (context.messages.errorCount > 0) { + logger.debug("Errors detected after analyzing class {}", request.qualifiedClassName) + throw SandboxClassLoadingException(context) + } - // Transform the class definition and byte code in accordance with provided rules. - val byteCode = rewriter.rewrite(reader, context) + // Transform the class definition and byte code in accordance with provided rules. + rewriter.rewrite(reader, context) + } // Try to define the transformed class. val clazz = try { when { - whitelistedClasses.matches(qualifiedName) -> supportingClassLoader.loadClass(name) + whitelistedClasses.matches(sourceName.asResourcePath) -> supportingClassLoader.loadClass(sourceName) else -> defineClass(resolvedName, byteCode.bytes, 0, byteCode.bytes.size) } } catch (exception: SecurityException) { @@ -140,19 +145,31 @@ class SandboxClassLoader( // Cache transformed class. val classWithByteCode = LoadedClass(clazz, byteCode) - loadedClasses[name] = classWithByteCode - if (source.origin != null) { - context.recordClassOrigin(name, ClassReference(source.origin)) + loadedClasses[requestedPath] = classWithByteCode + if (request.origin != null) { + context.recordClassOrigin(sourceName, ClassReference(request.origin)) } logger.debug("Loaded class {}, bytes={}, isModified={}", - source.qualifiedClassName, byteCode.bytes.size, byteCode.isModified) + request.qualifiedClassName, byteCode.bytes.size, byteCode.isModified) return classWithByteCode } + private fun loadUnmodifiedByteCode(internalClassName: String): ByteCode { + return ByteCode((getSystemClassLoader().getResourceAsStream("$internalClassName.class") + ?: throw ClassNotFoundException(internalClassName)).readBytes(), false) + } + + private fun loadUnmodifiedClass(className: String): LoadedClass { + return LoadedClass(supportingClassLoader.loadClass(className), UNMODIFIED).apply { + loadedClasses[className] = this + } + } + private companion object { private val logger = loggerFor() + private val UNMODIFIED = ByteCode(ByteArray(0), false) } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt new file mode 100644 index 0000000000..7412999727 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt @@ -0,0 +1,52 @@ +package net.corda.djvm.rewiring + +import net.corda.djvm.analysis.AnalysisConfiguration +import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.commons.ClassRemapper + +class SandboxClassRemapper(cv: ClassVisitor, private val configuration: AnalysisConfiguration) + : ClassRemapper(cv, SandboxRemapper(configuration.classResolver, configuration.whitelist) +) { + override fun createMethodRemapper(mv: MethodVisitor): MethodVisitor { + return MethodRemapperWithPinning(mv, super.createMethodRemapper(mv)) + } + + /** + * Do not attempt to remap references to methods and fields on pinned classes. + * For example, the methods on [RuntimeCostAccounter] really DO use [java.lang.String] + * rather than [sandbox.java.lang.String]. + */ + private inner class MethodRemapperWithPinning(private val nonmapper: MethodVisitor, remapper: MethodVisitor) + : MethodVisitor(API_VERSION, remapper) { + + private fun mapperFor(element: Element): MethodVisitor { + return if (configuration.isPinnedClass(element.owner) || configuration.isTemplateClass(element.owner) || isUnmapped(element)) { + nonmapper + } else { + mv + } + } + + override fun visitMethodInsn(opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean) { + val method = Element(owner, name, descriptor) + return mapperFor(method).visitMethodInsn(opcode, owner, name, descriptor, isInterface) + } + + override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, type: String?) { + // Don't map caught exception names - these could be thrown by the JVM itself. + nonmapper.visitTryCatchBlock(start, end, handler, type) + } + + override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { + val field = Element(owner, name, descriptor) + return mapperFor(field).visitFieldInsn(opcode, owner, name, descriptor) + } + } + + private fun isUnmapped(element: Element): Boolean = configuration.whitelist.matches(element.owner) + + private data class Element(val owner: String, val name: String, val descriptor: String) +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt index e1a051d45c..fc0ad559f6 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt @@ -8,13 +8,13 @@ import org.objectweb.asm.ClassWriter.COMPUTE_MAXS import org.objectweb.asm.Type /** - * Class writer for sandbox execution, with configurable a [classLoader] to ensure correct deduction of the used class + * Class writer for sandbox execution, with a configurable classloader to ensure correct deduction of the used class * hierarchy. * * @param classReader The [ClassReader] used to read the original class. It will be used to copy the entire constant * pool and bootstrap methods from the original class and also to copy other fragments of original byte code where * applicable. - * @property classLoader The class loader used to load the classes that are to be rewritten. + * @property cloader The class loader used to load the classes that are to be rewritten. * @param flags Option flags that can be used to modify the default behaviour of this class. Must be zero or a * combination of [COMPUTE_MAXS] and [COMPUTE_FRAMES]. These option flags do not affect methods that are copied as is * in the new class. This means that neither the maximum stack size nor the stack frames will be computed for these @@ -61,7 +61,7 @@ open class SandboxClassWriter( } } - companion object { + private companion object { private const val OBJECT_NAME = "java/lang/Object" diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxRemapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxRemapper.kt index 566b377fe6..e828fa4480 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxRemapper.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxRemapper.kt @@ -1,15 +1,19 @@ package net.corda.djvm.rewiring import net.corda.djvm.analysis.ClassResolver +import net.corda.djvm.analysis.Whitelist +import org.objectweb.asm.* import org.objectweb.asm.commons.Remapper /** * Class name and descriptor re-mapper for use in a sandbox. * * @property classResolver Functionality for resolving the class name of a sandboxed or sandboxable class. + * @property whitelist Identifies the Java APIs which are not mapped into the sandbox namespace. */ open class SandboxRemapper( - private val classResolver: ClassResolver + private val classResolver: ClassResolver, + private val whitelist: Whitelist ) : Remapper() { /** @@ -26,6 +30,32 @@ open class SandboxRemapper( return rewriteTypeName(super.map(typename)) } + /** + * Mapper for [Type] and [Handle] objects. + */ + override fun mapValue(obj: Any?): Any? { + return if (obj is Handle && whitelist.matches(obj.owner)) { + obj + } else { + super.mapValue(obj) + } + } + + /** + * All [Object.toString] methods must be transformed to [sandbox.java.lang.Object.toDJVMString], + * to allow the return type to change to [sandbox.java.lang.String]. + * + * The [sandbox.java.lang.Object] class is pinned and not mapped. + */ + override fun mapMethodName(owner: String, name: String, descriptor: String): String { + val newName = if (name == "toString" && descriptor == "()Ljava/lang/String;") { + "toDJVMString" + } else { + name + } + return super.mapMethodName(owner, newName, descriptor) + } + /** * Function for rewriting a descriptor. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ArgumentUnwrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ArgumentUnwrapper.kt new file mode 100644 index 0000000000..952efc4251 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ArgumentUnwrapper.kt @@ -0,0 +1,35 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.MemberAccessInstruction + +/** + * Some whitelisted functions have [java.lang.String] arguments, so we + * need to unwrap the [sandbox.java.lang.String] object before invoking. + * + * There are lots of rabbits in this hole because method arguments are + * theoretically arbitrary. However, in practice WE control the whitelist. + */ +class ArgumentUnwrapper : Emitter { + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is MemberAccessInstruction && context.whitelist.matches(instruction.owner)) { + fun unwrapString() = invokeStatic("sandbox/java/lang/String", "fromDJVM", "(Lsandbox/java/lang/String;)Ljava/lang/String;") + + if (hasStringArgument(instruction)) { + unwrapString() + } else if (instruction.owner == "java/lang/Class" && instruction.signature.startsWith("(Ljava/lang/String;ZLjava/lang/ClassLoader;)")) { + /** + * [kotlin.jvm.internal.Intrinsics.checkHasClass] invokes [Class.forName], so I'm + * adding support for both of this function's variants. For now. + */ + raiseThirdWordToTop() + unwrapString() + sinkTopToThirdWord() + } + } + } + + private fun hasStringArgument(method: MemberAccessInstruction) = method.signature.contains("Ljava/lang/String;)") +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt index 04ef9e3d5c..2e3b43f935 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt @@ -17,7 +17,7 @@ class DisallowNonDeterministicMethods : Emitter { override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { if (instruction is MemberAccessInstruction && isForbidden(instruction)) { when (instruction.operation) { - INVOKEVIRTUAL -> { + INVOKEVIRTUAL, INVOKESPECIAL -> { throwException("Disallowed reference to API; ${memberFormatter.format(instruction.member)}") preventDefault() } @@ -31,12 +31,20 @@ class DisallowNonDeterministicMethods : Emitter { || instruction.signature.contains("Ljava/lang/reflect/")) ) + private fun isClassLoading(instruction: MemberAccessInstruction): Boolean = + (instruction.owner == "java/lang/ClassLoader") && instruction.memberName in CLASSLOADING_METHODS + private fun isObjectMonitor(instruction: MemberAccessInstruction): Boolean = - (instruction.signature == "()V" && (instruction.memberName == "notify" || instruction.memberName == "notifyAll" || instruction.memberName == "wait")) + (instruction.signature == "()V" && instruction.memberName in MONITOR_METHODS) || (instruction.memberName == "wait" && (instruction.signature == "(J)V" || instruction.signature == "(JI)V")) private fun isForbidden(instruction: MemberAccessInstruction): Boolean - = instruction.isMethod && (isClassReflection(instruction) || isObjectMonitor(instruction)) + = instruction.isMethod && (isClassReflection(instruction) || isObjectMonitor(instruction) || isClassLoading(instruction)) private val memberFormatter = MemberFormatter() + + private companion object { + private val MONITOR_METHODS = setOf("notify", "notifyAll", "wait") + private val CLASSLOADING_METHODS = setOf("defineClass", "loadClass", "findClass") + } } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ReturnTypeWrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ReturnTypeWrapper.kt new file mode 100644 index 0000000000..7f103f346b --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ReturnTypeWrapper.kt @@ -0,0 +1,27 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.MemberAccessInstruction + +/** + * Whitelisted classes may still return [java.lang.String] from some + * functions, e.g. [java.lang.Object.toString]. So always explicitly + * invoke [sandbox.java.lang.String.toDJVM] after these. + */ +class ReturnTypeWrapper : Emitter { + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is MemberAccessInstruction && context.whitelist.matches(instruction.owner)) { + fun invokeMethod() = invokeVirtual(instruction.owner, instruction.memberName, instruction.signature) + + if (hasStringReturnType(instruction)) { + preventDefault() + invokeMethod() + invokeStatic("sandbox/java/lang/String", "toDJVM", "(Ljava/lang/String;)Lsandbox/java/lang/String;") + } + } + } + + private fun hasStringReturnType(method: MemberAccessInstruction) = method.signature.endsWith(")Ljava/lang/String;") +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/RewriteClassMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/RewriteClassMethods.kt new file mode 100644 index 0000000000..555ada7ed1 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/RewriteClassMethods.kt @@ -0,0 +1,56 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.MemberAccessInstruction +import org.objectweb.asm.Opcodes.* + +/** + * The enum-related methods on [Class] all require that enums use [java.lang.Enum] + * as their super class. So replace their all invocations with ones to equivalent + * methods on the DJVM class that require [sandbox.java.lang.Enum] instead. + */ +class RewriteClassMethods : Emitter { + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is MemberAccessInstruction && instruction.owner == "java/lang/Class") { + when (instruction.operation) { + INVOKEVIRTUAL -> if (instruction.memberName == "enumConstantDirectory" && instruction.signature == "()Ljava/util/Map;") { + invokeStatic( + owner = "sandbox/java/lang/DJVM", + name = "enumConstantDirectory", + descriptor = "(Ljava/lang/Class;)Lsandbox/java/util/Map;" + ) + preventDefault() + } else if (instruction.memberName == "isEnum" && instruction.signature == "()Z") { + invokeStatic( + owner = "sandbox/java/lang/DJVM", + name = "isEnum", + descriptor = "(Ljava/lang/Class;)Z" + ) + preventDefault() + } else if (instruction.memberName == "getEnumConstants" && instruction.signature == "()[Ljava/lang/Object;") { + invokeStatic( + owner = "sandbox/java/lang/DJVM", + name = "getEnumConstants", + descriptor = "(Ljava/lang/Class;)[Ljava/lang/Object;") + preventDefault() + } + + INVOKESTATIC -> if (isClassForName(instruction)) { + invokeStatic( + owner = "sandbox/java/lang/DJVM", + name = "classForName", + descriptor = instruction.signature + ) + preventDefault() + } + } + } + } + + private fun isClassForName(instruction: MemberAccessInstruction): Boolean + = instruction.memberName == "forName" && + (instruction.signature == "(Ljava/lang/String;)Ljava/lang/Class;" || + instruction.signature == "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;") +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StaticConstantRemover.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StaticConstantRemover.kt new file mode 100644 index 0000000000..ea6826903e --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StaticConstantRemover.kt @@ -0,0 +1,30 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.analysis.AnalysisRuntimeContext +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.MemberDefinitionProvider +import net.corda.djvm.references.Member + +/** + * Removes static constant objects that are initialised directly in the byte-code. + * Currently, the only use-case is for re-initialising [String] fields. + */ +class StaticConstantRemover : MemberDefinitionProvider { + + override fun define(context: AnalysisRuntimeContext, member: Member): Member = when { + isConstantField(member) -> member.copy(body = listOf(StringFieldInitializer(member)::writeInitializer), value = null) + else -> member + } + + private fun isConstantField(member: Member): Boolean = member.value != null && member.signature == "Ljava/lang/String;" + + class StringFieldInitializer(private val member: Member) { + fun writeInitializer(emitter: EmitterModule): Unit = with(emitter) { + member.value?.apply { + loadConstant(this) + invokeStatic("sandbox/java/lang/String", "toDJVM", "(Ljava/lang/String;)Lsandbox/java/lang/String;", false) + putStatic(member.className, member.memberName, "Lsandbox/java/lang/String;") + } + } + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StringConstantWrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StringConstantWrapper.kt new file mode 100644 index 0000000000..6223cfd0b4 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StringConstantWrapper.kt @@ -0,0 +1,22 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.ConstantInstruction + +/** + * Ensure that [String] constants loaded from the Constants + * Pool are wrapped into [sandbox.java.lang.String]. + */ +class StringConstantWrapper : Emitter { + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is ConstantInstruction) { + when (instruction.value) { + is String -> { + invokeStatic("sandbox/java/lang/String", "toDJVM", "(Ljava/lang/String;)Lsandbox/java/lang/String;", false) + } + } + } + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt index 74a58f6c7f..d1b6918fef 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt @@ -23,7 +23,7 @@ class StubOutNativeMethods : MemberDefinitionProvider { private fun writeExceptionMethodBody(emitter: EmitterModule): Unit = with(emitter) { lineNumber(0) - throwException(RuleViolationError::class.java, "Native method has been deleted") + throwException("Native method has been deleted") } private fun writeStubMethodBody(emitter: EmitterModule): Unit = with(emitter) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt index 4e486bf289..9c60f420bd 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt @@ -19,7 +19,7 @@ class StubOutReflectionMethods : MemberDefinitionProvider { private fun writeMethodBody(emitter: EmitterModule): Unit = with(emitter) { lineNumber(0) - throwException(RuleViolationError::class.java, "Disallowed reference to reflection API") + throwException("Disallowed reference to reflection API") } // The method must be public and with a Java implementation. diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt index cebe0b0b82..99ef5319fb 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt @@ -1,17 +1,20 @@ package net.corda.djvm.source +import net.corda.djvm.code.asResourcePath import java.nio.file.Path /** * The source of one or more compiled Java classes. * * @property qualifiedClassName The fully qualified class name. + * @property internalClassName The fully qualified internal class name, i.e. with '/' instead of '.'. * @property origin The origin of the class source, if any. */ class ClassSource private constructor( val qualifiedClassName: String = "", val origin: String? = null ) { + val internalClassName: String = qualifiedClassName.asResourcePath companion object { diff --git a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt index 9092e5c044..33886e76ae 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt @@ -7,7 +7,7 @@ import java.lang.reflect.Modifier * Find and instantiate types that implement a certain interface. */ object Discovery { - const val FORBIDDEN_CLASS_MASK = (Modifier.STATIC or Modifier.ABSTRACT) + const val FORBIDDEN_CLASS_MASK = (Modifier.STATIC or Modifier.ABSTRACT or Modifier.PRIVATE or Modifier.PROTECTED) /** * Get an instance of each concrete class that implements interface or class [T]. diff --git a/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt b/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt index 1f4ead8cd1..b409e83df5 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt @@ -17,6 +17,7 @@ import org.objectweb.asm.ClassVisitor * Helper class for validating a set of rules for a class or set of classes. * * @property rules A set of rules to validate for provided classes. + * @param configuration The configuration to use for class analysis. * @param classVisitor Class visitor to use when traversing the structure of classes. */ class RuleValidator( diff --git a/djvm/src/main/kotlin/sandbox/Task.kt b/djvm/src/main/kotlin/sandbox/Task.kt new file mode 100644 index 0000000000..8a2bbab78a --- /dev/null +++ b/djvm/src/main/kotlin/sandbox/Task.kt @@ -0,0 +1,24 @@ +@file:JvmName("TaskTypes") +package sandbox + +import sandbox.java.lang.sandbox +import sandbox.java.lang.unsandbox + +typealias SandboxFunction = sandbox.java.util.function.Function + +@Suppress("unused") +class Task(private val function: SandboxFunction?) : SandboxFunction { + + /** + * This function runs inside the sandbox. It marshalls the input + * object to its sandboxed equivalent, executes the user's code + * and then marshalls the result out again. + * + * The marshalling should be effective for Java primitives, + * Strings and Enums, as well as for arrays of these types. + */ + override fun apply(input: Any?): Any? { + return function?.apply(input?.sandbox())?.unsandbox() + } + +} diff --git a/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt new file mode 100644 index 0000000000..b6a3acdc77 --- /dev/null +++ b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt @@ -0,0 +1,158 @@ +@file:JvmName("DJVM") +@file:Suppress("unused") +package sandbox.java.lang + +import org.objectweb.asm.Opcodes.ACC_ENUM + +private const val SANDBOX_PREFIX = "sandbox." + +fun Any.unsandbox(): Any { + return when (this) { + is Enum<*> -> fromDJVMEnum() + is Object -> fromDJVM() + is Array<*> -> fromDJVMArray() + else -> this + } +} + +fun Any.sandbox(): Any { + return when (this) { + is kotlin.String -> String.toDJVM(this) + is kotlin.Char -> Character.toDJVM(this) + is kotlin.Long -> Long.toDJVM(this) + is kotlin.Int -> Integer.toDJVM(this) + is kotlin.Short -> Short.toDJVM(this) + is kotlin.Byte -> Byte.toDJVM(this) + is kotlin.Float -> Float.toDJVM(this) + is kotlin.Double -> Double.toDJVM(this) + is kotlin.Boolean -> Boolean.toDJVM(this) + is kotlin.Enum<*> -> toDJVMEnum() + is Array<*> -> toDJVMArray() + else -> this + } +} + +private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this) + +/** + * These functions use the "current" classloader, i.e. classloader + * that owns this DJVM class. + */ +private fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) +private fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) + +private fun kotlin.String.toSandboxPackage(): kotlin.String { + return if (startsWith(SANDBOX_PREFIX)) { + this + } else { + SANDBOX_PREFIX + this + } +} + +private fun kotlin.String.fromSandboxPackage(): kotlin.String { + return if (startsWith(SANDBOX_PREFIX)) { + drop(SANDBOX_PREFIX.length) + } else { + this + } +} + +private inline fun Array<*>.toDJVMArray(): Array { + @Suppress("unchecked_cast") + return (java.lang.reflect.Array.newInstance(javaClass.componentType.toDJVMType(), size) as Array).also { + for ((i, item) in withIndex()) { + it[i] = item?.sandbox() as T + } + } +} + +private fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> { + return javaClass.fromDJVMType().enumConstants[ordinal()] as kotlin.Enum<*> +} + +private fun kotlin.Enum<*>.toDJVMEnum(): Enum<*> { + @Suppress("unchecked_cast") + return (getEnumConstants(javaClass.toDJVMType() as Class>) as Array>)[ordinal] +} + +/** + * Replacement functions for the members of Class<*> that support Enums. + */ +fun isEnum(clazz: Class<*>): kotlin.Boolean + = (clazz.modifiers and ACC_ENUM != 0) && (clazz.superclass == sandbox.java.lang.Enum::class.java) + +fun getEnumConstants(clazz: Class>): Array<*>? { + return getEnumConstantsShared(clazz)?.clone() +} + +internal fun enumConstantDirectory(clazz: Class>): sandbox.java.util.Map>? { + // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. + return allEnumDirectories.get(clazz) ?: createEnumDirectory(clazz) +} + +@Suppress("unchecked_cast") +internal fun getEnumConstantsShared(clazz: Class>): Array>? { + return if (isEnum(clazz)) { + // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. + allEnums.get(clazz) ?: createEnum(clazz) + } else { + null + } +} + +@Suppress("unchecked_cast") +private fun createEnum(clazz: Class>): Array>? { + return clazz.getMethod("values").let { method -> + method.isAccessible = true + method.invoke(null) as? Array> + // DO NOT replace put with Kotlin's [] because Kotlin would use java.util.Map. + }?.apply { allEnums.put(clazz, this) } +} + +private fun createEnumDirectory(clazz: Class>): sandbox.java.util.Map> { + val universe = getEnumConstantsShared(clazz) ?: throw IllegalArgumentException("${clazz.name} is not an enum type") + val directory = sandbox.java.util.LinkedHashMap>(2 * universe.size) + for (entry in universe) { + // DO NOT replace put with Kotlin's [] because Kotlin would use java.util.Map. + directory.put(entry.name(), entry) + } + // DO NOT replace put with Kotlin's [] because Kotlin would use java.util.Map. + allEnumDirectories.put(clazz, directory) + return directory +} + +private val allEnums: sandbox.java.util.Map>, Array>> = sandbox.java.util.LinkedHashMap() +private val allEnumDirectories: sandbox.java.util.Map>, sandbox.java.util.Map>> = sandbox.java.util.LinkedHashMap() + +/** + * Replacement functions for Class<*>.forName(...) which protect + * against users loading classes from outside the sandbox. + */ +@Throws(ClassNotFoundException::class) +fun classForName(className: kotlin.String): Class<*> { + return Class.forName(toSandbox(className)) +} + +@Throws(ClassNotFoundException::class) +fun classForName(className: kotlin.String, initialize: kotlin.Boolean, classLoader: ClassLoader): Class<*> { + return Class.forName(toSandbox(className), initialize, classLoader) +} + +/** + * Force the qualified class name into the sandbox.* namespace. + * Throw [ClassNotFoundException] anyway if we wouldn't want to + * return the resulting sandbox class. E.g. for any of our own + * internal classes. + */ +private fun toSandbox(className: kotlin.String): kotlin.String { + if (bannedClasses.any { it.matches(className) }) { + throw ClassNotFoundException(className) + } + return SANDBOX_PREFIX + className +} + +private val bannedClasses = setOf( + "^java\\.lang\\.DJVM(.*)?\$".toRegex(), + "^net\\.corda\\.djvm\\..*\$".toRegex(), + "^Task\$".toRegex() +) diff --git a/djvm/src/main/kotlin/sandbox/java/lang/Object.kt b/djvm/src/main/kotlin/sandbox/java/lang/Object.kt deleted file mode 100644 index 14ae5df025..0000000000 --- a/djvm/src/main/kotlin/sandbox/java/lang/Object.kt +++ /dev/null @@ -1,19 +0,0 @@ -package sandbox.java.lang - -/** - * Sandboxed implementation of `java/lang/Object`. - */ -@Suppress("EqualsOrHashCode") -open class Object { - - /** - * Deterministic hash code for objects. - */ - override fun hashCode(): Int = sandbox.java.lang.System.identityHashCode(this) - - /** - * Deterministic string representation of [Object]. - */ - override fun toString(): String = "sandbox.java.lang.Object@${hashCode().toString(16)}" - -} diff --git a/djvm/src/main/kotlin/sandbox/java/lang/System.kt b/djvm/src/main/kotlin/sandbox/java/lang/System.kt deleted file mode 100644 index 0b40e0cfd7..0000000000 --- a/djvm/src/main/kotlin/sandbox/java/lang/System.kt +++ /dev/null @@ -1,99 +0,0 @@ -@file:Suppress("UNUSED_PARAMETER") - -package sandbox.java.lang - -import java.io.IOException -import java.util.* - -object System { - - private var objectCounter = object : ThreadLocal() { - override fun initialValue() = 0 - } - - private var objectHashCodes = object : ThreadLocal>() { - override fun initialValue() = mutableMapOf() - } - - @JvmField - val `in`: java.io.InputStream? = null - - @JvmField - val out: java.io.PrintStream? = null - - @JvmField - val err: java.io.PrintStream? = null - - fun setIn(stream: java.io.InputStream) {} - - fun setOut(stream: java.io.PrintStream) {} - - fun setErr(stream: java.io.PrintStream) {} - - fun console(): java.io.Console? { - throw NotImplementedError() - } - - @Throws(java.io.IOException::class) - fun inheritedChannel(): java.nio.channels.Channel? { - throw IOException() - } - - fun setSecurityManager(manager: java.lang.SecurityManager) {} - - fun getSecurityManager(): java.lang.SecurityManager? = null - - fun currentTimeMillis(): Long = 0L - - fun nanoTime(): Long = 0L - - fun arraycopy(src: Object, srcPos: Int, dest: Object, destPos: Int, length: Int) { - java.lang.System.arraycopy(src, srcPos, dest, destPos, length) - } - - fun identityHashCode(obj: Object): Int { - val nativeHashCode = java.lang.System.identityHashCode(obj) - // TODO Instead of using a magic offset below, one could take in a per-context seed - return objectHashCodes.get().getOrPut(nativeHashCode) { - val newCounter = objectCounter.get() + 1 - objectCounter.set(newCounter) - 0xfed_c0de + newCounter - } - } - - fun getProperties(): java.util.Properties { - return Properties() - } - - fun lineSeparator() = "\n" - - fun setProperties(properties: java.util.Properties) {} - - fun getProperty(property: String): String? = null - - fun getProperty(property: String, defaultValue: String): String? = defaultValue - - fun setProperty(property: String, value: String): String? = null - - fun clearProperty(property: String): String? = null - - fun getenv(variable: String): String? = null - - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - fun getenv(): java.util.Map? = null - - fun exit(exitCode: Int) {} - - fun gc() {} - - fun runFinalization() {} - - fun runFinalizersOnExit(flag: Boolean) {} - - fun load(path: String) {} - - fun loadLibrary(path: String) {} - - fun mapLibraryName(path: String): String? = null - -} diff --git a/djvm/src/test/kotlin/foo/bar/sandbox/KotlinClass.kt b/djvm/src/test/kotlin/foo/bar/sandbox/KotlinClass.kt index 44ea7e29f7..f54128a2d6 100644 --- a/djvm/src/test/kotlin/foo/bar/sandbox/KotlinClass.kt +++ b/djvm/src/test/kotlin/foo/bar/sandbox/KotlinClass.kt @@ -1,12 +1,9 @@ package foo.bar.sandbox -import java.util.* - -fun testRandom(): Int { - val random = Random() - return random.nextInt() +fun testClock(): Long { + return System.nanoTime() } -fun String.toNumber(): Int { - return this.toInt() +fun String.toNumber(): Long { + return this.toLong() } diff --git a/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt new file mode 100644 index 0000000000..d71fa1a36a --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt @@ -0,0 +1,126 @@ +package net.corda.djvm + +import org.assertj.core.api.Assertions.* +import org.junit.Assert.* +import org.junit.Test +import sandbox.java.lang.sandbox +import sandbox.java.lang.unsandbox + +class DJVMTest { + + @Test + fun testDJVMString() { + val djvmString = sandbox.java.lang.String("New Value") + assertNotEquals(djvmString, "New Value") + assertEquals(djvmString, "New Value".sandbox()) + } + + @Test + fun testSimpleIntegerFormats() { + val result = sandbox.java.lang.String.format("%d-%d-%d-%d".toDJVM(), + 10.toDJVM(), 999999L.toDJVM(), 1234.toShort().toDJVM(), 108.toByte().toDJVM()).toString() + assertEquals("10-999999-1234-108", result) + } + + @Test + fun testHexFormat() { + val result = sandbox.java.lang.String.format("%0#6x".toDJVM(), 768.toDJVM()).toString() + assertEquals("0x0300", result) + } + + @Test + fun testDoubleFormat() { + val result = sandbox.java.lang.String.format("%9.4f".toDJVM(), 1234.5678.toDJVM()).toString() + assertEquals("1234.5678", result) + } + + @Test + fun testFloatFormat() { + val result = sandbox.java.lang.String.format("%7.2f".toDJVM(), 1234.5678f.toDJVM()).toString() + assertEquals("1234.57", result) + } + + @Test + fun testCharFormat() { + val result = sandbox.java.lang.String.format("[%c]".toDJVM(), 'A'.toDJVM()).toString() + assertEquals("[A]", result) + } + + @Test + fun testObjectFormat() { + val result = sandbox.java.lang.String.format("%s".toDJVM(), object : sandbox.java.lang.Object() {}).toString() + assertThat(result).startsWith("sandbox.java.lang.Object@") + } + + @Test + fun testStringEquality() { + val number = sandbox.java.lang.String.valueOf((Double.MIN_VALUE / 2.0) * 2.0) + require(number == "0.0".sandbox()) + } + + @Test + fun testSandboxingArrays() { + val result = arrayOf(1, 10L, "Hello World", '?', false, 1234.56).sandbox() + assertThat(result) + .isEqualTo(arrayOf(1.toDJVM(), 10L.toDJVM(), "Hello World".toDJVM(), '?'.toDJVM(), false.toDJVM(), 1234.56.toDJVM())) + } + + @Test + fun testUnsandboxingObjectArray() { + val result = arrayOf(1.toDJVM(), 10L.toDJVM(), "Hello World".toDJVM(), '?'.toDJVM(), false.toDJVM(), 1234.56.toDJVM()).unsandbox() + assertThat(result) + .isEqualTo(arrayOf(1, 10L, "Hello World", '?', false, 1234.56)) + } + + @Test + fun testSandboxingPrimitiveArray() { + val result = intArrayOf(1, 2, 3, 10).sandbox() + assertThat(result).isEqualTo(intArrayOf(1, 2, 3, 10)) + } + + @Test + fun testSandboxingIntegersAsObjectArray() { + val result = arrayOf(1, 2, 3, 10).sandbox() + assertThat(result).isEqualTo(arrayOf(1.toDJVM(), 2.toDJVM(), 3.toDJVM(), 10.toDJVM())) + } + + @Test + fun testUnsandboxingArrays() { + val arr = arrayOf( + Array(1) { "Hello".toDJVM() }, + Array(1) { 1234000L.toDJVM() }, + Array(1) { 1234.toDJVM() }, + Array(1) { 923.toShort().toDJVM() }, + Array(1) { 27.toByte().toDJVM() }, + Array(1) { 'X'.toDJVM() }, + Array(1) { 987.65f.toDJVM() }, + Array(1) { 343.282.toDJVM() }, + Array(1) { true.toDJVM() }, + ByteArray(1) { 127.toByte() }, + CharArray(1) { '?'} + ) + val result = arr.unsandbox() as Array<*> + assertEquals(arr.size, result.size) + assertArrayEquals(Array(1) { "Hello" }, result[0] as Array<*>) + assertArrayEquals(Array(1) { 1234000L }, result[1] as Array<*>) + assertArrayEquals(Array(1) { 1234 }, result[2] as Array<*>) + assertArrayEquals(Array(1) { 923.toShort() }, result[3] as Array<*>) + assertArrayEquals(Array(1) { 27.toByte() }, result[4] as Array<*>) + assertArrayEquals(Array(1) { 'X' }, result[5] as Array<*>) + assertArrayEquals(Array(1) { 987.65f }, result[6] as Array<*>) + assertArrayEquals(Array(1) { 343.282 }, result[7] as Array<*>) + assertArrayEquals(Array(1) { true }, result[8] as Array<*>) + assertArrayEquals(ByteArray(1) { 127.toByte() }, result[9] as ByteArray) + assertArrayEquals(CharArray(1) { '?' }, result[10] as CharArray) + } + + private fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this) + private fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this) + private fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this) + private fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this) + private fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this) + private fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this) + private fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this) + private fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this) + private fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this) +} \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt index b54d92b16e..a771798655 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt @@ -13,6 +13,7 @@ import net.corda.djvm.messages.Severity import net.corda.djvm.references.ClassHierarchy import net.corda.djvm.rewiring.LoadedClass import net.corda.djvm.rules.Rule +import net.corda.djvm.rules.implementation.* import net.corda.djvm.source.ClassSource import net.corda.djvm.utilities.Discovery import net.corda.djvm.validation.RuleValidator @@ -35,8 +36,19 @@ abstract class TestBase { val ALL_EMITTERS = Discovery.find() + // We need at least these emitters to handle the Java API classes. + val BASIC_EMITTERS: List = listOf( + ArgumentUnwrapper(), + ReturnTypeWrapper(), + RewriteClassMethods(), + StringConstantWrapper() + ) + val ALL_DEFINITION_PROVIDERS = Discovery.find() + // We need at least these providers to handle the Java API classes. + val BASIC_DEFINITION_PROVIDERS: List = listOf(StaticConstantRemover()) + val BLANK = emptySet() val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass) @@ -86,14 +98,6 @@ abstract class TestBase { } } - /** - * Short-hand for analysing a class. - */ - inline fun analyze(block: (ClassAndMemberVisitor.(AnalysisContext) -> Unit)) { - val validator = RuleValidator(emptyList(), configuration) - block(validator, context) - } - /** * Run action on a separate thread to ensure that the code is run off a clean slate. The sandbox context is local to * the current thread, so this allows inspection of the cost summary object, etc. from within the provided delegate. @@ -106,8 +110,8 @@ abstract class TestBase { action: SandboxRuntimeContext.() -> Unit ) { val rules = mutableListOf() - val emitters = mutableListOf() - val definitionProviders = mutableListOf() + val emitters = mutableListOf().apply { addAll(BASIC_EMITTERS) } + val definitionProviders = mutableListOf().apply { addAll(BASIC_DEFINITION_PROVIDERS) } val classSources = mutableListOf() var executionProfile = ExecutionProfile.UNLIMITED var whitelist = Whitelist.MINIMAL @@ -137,7 +141,12 @@ abstract class TestBase { minimumSeverityLevel = minimumSeverityLevel ).use { analysisConfiguration -> SandboxRuntimeContext(SandboxConfiguration.of( - executionProfile, rules, emitters, definitionProviders, enableTracing, analysisConfiguration + executionProfile, + rules.distinctBy(Any::javaClass), + emitters.distinctBy(Any::javaClass), + definitionProviders.distinctBy(Any::javaClass), + enableTracing, + analysisConfiguration )).use { assertThat(runtimeCosts).areZero() action(this) @@ -163,7 +172,7 @@ abstract class TestBase { inline fun SandboxRuntimeContext.loadClass(): LoadedClass = loadClass(T::class.jvmName) fun SandboxRuntimeContext.loadClass(className: String): LoadedClass = - classLoader.loadClassAndBytes(ClassSource.fromClassName(className), context) + classLoader.loadForSandbox(className, context) /** * Run the entry-point of the loaded [Callable] class. diff --git a/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt new file mode 100644 index 0000000000..d493238723 --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt @@ -0,0 +1,22 @@ +package net.corda.djvm + +import sandbox.net.corda.djvm.costing.ThresholdViolationError +import sandbox.net.corda.djvm.rules.RuleViolationError + +object Utilities { + fun throwRuleViolationError(): Nothing = throw RuleViolationError("Can't catch this!") + + fun throwThresholdViolationError(): Nothing = throw ThresholdViolationError("Can't catch this!") + + fun throwContractConstraintViolation(): Nothing = throw IllegalArgumentException("Contract constraint violated") + + fun throwError(): Nothing = throw Error() + + fun throwThrowable(): Nothing = throw Throwable() + + fun throwThreadDeath(): Nothing = throw ThreadDeath() + + fun throwStackOverflowError(): Nothing = throw StackOverflowError("FAKE OVERFLOW!") + + fun throwOutOfMemoryError(): Nothing = throw OutOfMemoryError("FAKE OOM!") +} diff --git a/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassResolverTest.kt b/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassResolverTest.kt index 8f39f32808..d1ae149cb7 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassResolverTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassResolverTest.kt @@ -5,25 +5,25 @@ import org.junit.Test class ClassResolverTest { - private val resolver = ClassResolver(emptySet(), Whitelist.MINIMAL, "sandbox/") + private val resolver = ClassResolver(emptySet(), emptySet(), Whitelist.MINIMAL, "sandbox/") @Test fun `can resolve class name`() { assertThat(resolver.resolve("java/lang/Object")).isEqualTo("java/lang/Object") - assertThat(resolver.resolve("java/lang/String")).isEqualTo("java/lang/String") + assertThat(resolver.resolve("java/lang/String")).isEqualTo("sandbox/java/lang/String") assertThat(resolver.resolve("foo/bar/Test")).isEqualTo("sandbox/foo/bar/Test") } @Test fun `can resolve class name for arrays`() { assertThat(resolver.resolve("[Ljava/lang/Object;")).isEqualTo("[Ljava/lang/Object;") - assertThat(resolver.resolve("[Ljava/lang/String;")).isEqualTo("[Ljava/lang/String;") + assertThat(resolver.resolve("[Ljava/lang/String;")).isEqualTo("[Lsandbox/java/lang/String;") assertThat(resolver.resolve("[Lfoo/bar/Test;")).isEqualTo("[Lsandbox/foo/bar/Test;") assertThat(resolver.resolve("[[Ljava/lang/Object;")).isEqualTo("[[Ljava/lang/Object;") - assertThat(resolver.resolve("[[Ljava/lang/String;")).isEqualTo("[[Ljava/lang/String;") + assertThat(resolver.resolve("[[Ljava/lang/String;")).isEqualTo("[[Lsandbox/java/lang/String;") assertThat(resolver.resolve("[[Lfoo/bar/Test;")).isEqualTo("[[Lsandbox/foo/bar/Test;") assertThat(resolver.resolve("[[[Ljava/lang/Object;")).isEqualTo("[[[Ljava/lang/Object;") - assertThat(resolver.resolve("[[[Ljava/lang/String;")).isEqualTo("[[[Ljava/lang/String;") + assertThat(resolver.resolve("[[[Ljava/lang/String;")).isEqualTo("[[[Lsandbox/java/lang/String;") assertThat(resolver.resolve("[[[Lfoo/bar/Test;")).isEqualTo("[[[Lsandbox/foo/bar/Test;") } diff --git a/djvm/src/test/kotlin/net/corda/djvm/analysis/WhitelistTest.kt b/djvm/src/test/kotlin/net/corda/djvm/analysis/WhitelistTest.kt index a817be4108..74d223af13 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/analysis/WhitelistTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/analysis/WhitelistTest.kt @@ -11,8 +11,8 @@ class WhitelistTest : TestBase() { val whitelist = Whitelist.MINIMAL assertThat(whitelist.matches("java/lang/Object")).isTrue() assertThat(whitelist.matches("java/lang/Object.:()V")).isTrue() - assertThat(whitelist.matches("java/lang/Integer")).isTrue() - assertThat(whitelist.matches("java/lang/Integer.:(I)V")).isTrue() + assertThat(whitelist.matches("java/lang/reflect/Array")).isTrue() + assertThat(whitelist.matches("java/lang/reflect/Array.setInt(Ljava/lang/Object;II)V")).isTrue() } @Test diff --git a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt index cc122b7a8f..0957217f47 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt @@ -28,4 +28,9 @@ class AssertiveClassWithByteCode(private val loadedClass: LoadedClass) { assertThat(loadedClass.type.name).isEqualTo(className) return this } + + fun hasInterface(className: String): AssertiveClassWithByteCode { + assertThat(loadedClass.type.interfaces.map(Class<*>::getName)).contains(className) + return this + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt b/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt index 0e68806d33..0eb2d1c03e 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt @@ -4,6 +4,7 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test import sandbox.net.corda.djvm.costing.ThresholdViolationError +import kotlin.concurrent.thread class RuntimeCostTest { @@ -16,17 +17,13 @@ class RuntimeCostTest { @Test fun `cannot increment cost beyond threshold`() { - Thread { + thread(name = "Foo") { val cost = RuntimeCost(10) { "failed in ${it.name}" } assertThatExceptionOfType(ThresholdViolationError::class.java) .isThrownBy { cost.increment(11) } .withMessage("failed in Foo") assertThat(cost.value).isEqualTo(11) - }.apply { - name = "Foo" - start() - join() - } + }.join() } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxEnumTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxEnumTest.kt new file mode 100644 index 0000000000..af78c3183b --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxEnumTest.kt @@ -0,0 +1,86 @@ +package net.corda.djvm.execution + +import net.corda.djvm.TestBase +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.util.* +import java.util.function.Function + +class SandboxEnumTest : TestBase() { + @Test + fun `test enum inside sandbox`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + contractExecutor.run(0).apply { + assertThat(result).isEqualTo(arrayOf("ONE", "TWO", "THREE")) + } + } + + @Test + fun `return enum from sandbox`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run("THREE").apply { + assertThat(result).isEqualTo(ExampleEnum.THREE) + } + } + + @Test + fun `test we can identify class as Enum`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(ExampleEnum.THREE).apply { + assertThat(result).isTrue() + } + } + + @Test + fun `test we can create EnumMap`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(ExampleEnum.TWO).apply { + assertThat(result).isEqualTo(1) + } + } + + @Test + fun `test we can create EnumSet`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(ExampleEnum.ONE).apply { + assertThat(result).isTrue() + } + } +} + + +class AssertEnum : Function { + override fun apply(input: ExampleEnum): Boolean { + return input::class.java.isEnum + } +} + +class TransformEnum : Function> { + override fun apply(input: Int): Array { + return ExampleEnum.values().map(ExampleEnum::name).toTypedArray() + } +} + +class FetchEnum : Function { + override fun apply(input: String): ExampleEnum { + return ExampleEnum.valueOf(input) + } +} + +class UseEnumMap : Function { + override fun apply(input: ExampleEnum): Int { + val map = EnumMap(ExampleEnum::class.java) + map[input] = input.name + return map.size + } +} + +class UseEnumSet : Function { + override fun apply(input: ExampleEnum): Boolean { + return EnumSet.allOf(ExampleEnum::class.java).contains(input) + } +} + +enum class ExampleEnum { + ONE, TWO, THREE +} \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt index 92fe59e159..a3919c964c 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt @@ -1,10 +1,19 @@ package net.corda.djvm.execution import foo.bar.sandbox.MyObject -import foo.bar.sandbox.testRandom +import foo.bar.sandbox.testClock import foo.bar.sandbox.toNumber import net.corda.djvm.TestBase import net.corda.djvm.analysis.Whitelist +import net.corda.djvm.Utilities +import net.corda.djvm.Utilities.throwContractConstraintViolation +import net.corda.djvm.Utilities.throwError +import net.corda.djvm.Utilities.throwOutOfMemoryError +import net.corda.djvm.Utilities.throwRuleViolationError +import net.corda.djvm.Utilities.throwStackOverflowError +import net.corda.djvm.Utilities.throwThreadDeath +import net.corda.djvm.Utilities.throwThresholdViolationError +import net.corda.djvm.Utilities.throwThrowable import net.corda.djvm.assertions.AssertionExtensions.withProblem import net.corda.djvm.rewiring.SandboxClassLoadingException import org.assertj.core.api.Assertions.assertThat @@ -13,8 +22,8 @@ import org.junit.Test import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.rules.RuleViolationError import java.nio.file.Files -import java.util.* import java.util.function.Function +import java.util.stream.Collectors.* class SandboxExecutorTest : TestBase() { @@ -34,7 +43,7 @@ class SandboxExecutorTest : TestBase() { @Test fun `can load and execute contract`() = sandbox( - pinnedClasses = setOf(Transaction::class.java) + pinnedClasses = setOf(Transaction::class.java, Utilities::class.java) ) { val contractExecutor = DeterministicSandboxExecutor(configuration) val tx = Transaction(1) @@ -44,13 +53,13 @@ class SandboxExecutorTest : TestBase() { .withMessageContaining("Contract constraint violated") } - class Contract : Function { - override fun apply(input: Transaction?) { - throw IllegalArgumentException("Contract constraint violated") + class Contract : Function { + override fun apply(input: Transaction) { + throwContractConstraintViolation() } } - data class Transaction(val id: Int?) + data class Transaction(val id: Int) @Test fun `can load and execute code that overrides object hash code`() = sandbox(DEFAULT) { @@ -65,7 +74,11 @@ class SandboxExecutorTest : TestBase() { val obj = Object() val hash1 = obj.hashCode() val hash2 = obj.hashCode() - require(hash1 == hash2) + //require(hash1 == hash2) + // TODO: Replace require() once we have working exception support. + if (hash1 != hash2) { + throwError() + } return Object().hashCode() } } @@ -123,37 +136,37 @@ class SandboxExecutorTest : TestBase() { @Test fun `can detect illegal references in Kotlin meta-classes`() = sandbox(DEFAULT, ExecutionProfile.DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(0) } - .withCauseInstanceOf(RuleViolationError::class.java) - .withMessageContaining("Disallowed reference to reflection API") + .withCauseInstanceOf(NoSuchMethodError::class.java) + .withProblem("sandbox.java.lang.System.nanoTime()J") } - class TestKotlinMetaClasses : Function { - override fun apply(input: Int): Int { - val someNumber = testRandom() + class TestKotlinMetaClasses : Function { + override fun apply(input: Int): Long { + val someNumber = testClock() return "12345".toNumber() * someNumber } } @Test fun `cannot execute runnable that references non-deterministic code`() = sandbox(DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(0) } - .withCauseInstanceOf(RuleViolationError::class.java) - .withProblem("Disallowed reference to reflection API") + .withCauseInstanceOf(NoSuchMethodError::class.java) + .withProblem("sandbox.java.lang.System.currentTimeMillis()J") } - class TestNonDeterministicCode : Function { - override fun apply(input: Int): Int { - return Random().nextInt() + class TestNonDeterministicCode : Function { + override fun apply(input: Int): Long { + return System.currentTimeMillis() } } @Test - fun `cannot execute runnable that catches ThreadDeath`() = sandbox(DEFAULT) { + fun `cannot execute runnable that catches ThreadDeath`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { TestCatchThreadDeath().apply { assertThat(apply(0)).isEqualTo(1) } @@ -167,7 +180,7 @@ class SandboxExecutorTest : TestBase() { class TestCatchThreadDeath : Function { override fun apply(input: Int): Int { return try { - throw ThreadDeath() + throwThreadDeath() } catch (exception: ThreadDeath) { 1 } @@ -175,7 +188,7 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `cannot execute runnable that catches ThresholdViolationError`() = sandbox(DEFAULT) { + fun `cannot execute runnable that catches ThresholdViolationError`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { TestCatchThresholdViolationError().apply { assertThat(apply(0)).isEqualTo(1) } @@ -190,7 +203,7 @@ class SandboxExecutorTest : TestBase() { class TestCatchThresholdViolationError : Function { override fun apply(input: Int): Int { return try { - throw ThresholdViolationError("Can't catch this!") + throwThresholdViolationError() } catch (exception: ThresholdViolationError) { 1 } @@ -198,7 +211,7 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `cannot execute runnable that catches RuleViolationError`() = sandbox(DEFAULT) { + fun `cannot execute runnable that catches RuleViolationError`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { TestCatchRuleViolationError().apply { assertThat(apply(0)).isEqualTo(1) } @@ -213,7 +226,7 @@ class SandboxExecutorTest : TestBase() { class TestCatchRuleViolationError : Function { override fun apply(input: Int): Int { return try { - throw RuleViolationError("Can't catch this!") + throwRuleViolationError() } catch (exception: RuleViolationError) { 1 } @@ -221,7 +234,7 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `can catch Throwable`() = sandbox(DEFAULT) { + fun `can catch Throwable`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { val contractExecutor = DeterministicSandboxExecutor(configuration) contractExecutor.run(1).apply { assertThat(result).isEqualTo(1) @@ -229,7 +242,7 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `can catch Error`() = sandbox(DEFAULT) { + fun `can catch Error`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { val contractExecutor = DeterministicSandboxExecutor(configuration) contractExecutor.run(2).apply { assertThat(result).isEqualTo(2) @@ -237,7 +250,7 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `cannot catch ThreadDeath`() = sandbox(DEFAULT) { + fun `cannot catch ThreadDeath`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(3) } @@ -248,8 +261,8 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throw Throwable() - 2 -> throw Error() + 1 -> throwThrowable() + 2 -> throwError() else -> 0 } } catch (exception: Error) { @@ -264,20 +277,20 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throw Throwable() - 2 -> throw Error() + 1 -> throwThrowable() + 2 -> throwError() 3 -> try { - throw ThreadDeath() + throwThreadDeath() } catch (ex: ThreadDeath) { 3 } 4 -> try { - throw StackOverflowError("FAKE OVERFLOW!") + throwStackOverflowError() } catch (ex: StackOverflowError) { 4 } 5 -> try { - throw OutOfMemoryError("FAKE OOM!") + throwOutOfMemoryError() } catch (ex: OutOfMemoryError) { 5 } @@ -292,7 +305,7 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `cannot catch stack-overflow error`() = sandbox(DEFAULT) { + fun `cannot catch stack-overflow error`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(4) } @@ -301,7 +314,7 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `cannot catch out-of-memory error`() = sandbox(DEFAULT) { + fun `cannot catch out-of-memory error`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) { val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(5) } @@ -371,7 +384,7 @@ class SandboxExecutorTest : TestBase() { @Test fun `can load and execute code that uses notify()`() = sandbox(DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(1) } .withCauseInstanceOf(RuleViolationError::class.java) @@ -381,7 +394,7 @@ class SandboxExecutorTest : TestBase() { @Test fun `can load and execute code that uses notifyAll()`() = sandbox(DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(2) } .withCauseInstanceOf(RuleViolationError::class.java) @@ -391,7 +404,7 @@ class SandboxExecutorTest : TestBase() { @Test fun `can load and execute code that uses wait()`() = sandbox(DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(3) } .withCauseInstanceOf(RuleViolationError::class.java) @@ -401,7 +414,7 @@ class SandboxExecutorTest : TestBase() { @Test fun `can load and execute code that uses wait(long)`() = sandbox(DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(4) } .withCauseInstanceOf(RuleViolationError::class.java) @@ -411,7 +424,7 @@ class SandboxExecutorTest : TestBase() { @Test fun `can load and execute code that uses wait(long,int)`() = sandbox(DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(5) } .withCauseInstanceOf(RuleViolationError::class.java) @@ -421,13 +434,13 @@ class SandboxExecutorTest : TestBase() { @Test fun `code after forbidden APIs is intact`() = sandbox(DEFAULT) { - val contractExecutor = DeterministicSandboxExecutor(configuration) + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThat(contractExecutor.run(0).result) .isEqualTo("unknown") } - class TestMonitors : Function { - override fun apply(input: Int): String { + class TestMonitors : Function { + override fun apply(input: Int): String? { return synchronized(this) { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") val javaObject = this as java.lang.Object @@ -493,6 +506,73 @@ class SandboxExecutorTest : TestBase() { } } + @Test + fun `check building a string`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run("Hello Sandbox!").apply { + assertThat(result) + .isEqualTo("SANDBOX: Boolean=true, Char='X', Integer=1234, Long=99999, Short=3200, Byte=101, String='Hello Sandbox!', Float=123.456, Double=987.6543") + } + } + + class TestStringBuilding : Function { + override fun apply(input: String?): String? { + return StringBuilder("SANDBOX") + .append(": Boolean=").append(true) + .append(", Char='").append('X') + .append("', Integer=").append(1234) + .append(", Long=").append(99999L) + .append(", Short=").append(3200.toShort()) + .append(", Byte=").append(101.toByte()) + .append(", String='").append(input) + .append("', Float=").append(123.456f) + .append(", Double=").append(987.6543) + .toString() + } + } + + @Test + fun `check System-arraycopy still works with Objects`() = sandbox(DEFAULT) { + val source = arrayOf("one", "two", "three") + assertThat(TestArrayCopy().apply(source)) + .isEqualTo(source) + .isNotSameAs(source) + + val contractExecutor = DeterministicSandboxExecutor, Array>(configuration) + contractExecutor.run(source).apply { + assertThat(result) + .isEqualTo(source) + .isNotSameAs(source) + } + } + + class TestArrayCopy : Function, Array> { + override fun apply(input: Array): Array { + val newArray = Array(input.size) { "" } + System.arraycopy(input, 0, newArray, 0, newArray.size) + return newArray + } + } + + @Test + fun `test System-arraycopy still works with CharArray`() = sandbox(DEFAULT) { + val source = CharArray(10) { '?' } + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(source).apply { + assertThat(result) + .isEqualTo(source) + .isNotSameAs(source) + } + } + + class TestCharArrayCopy : Function { + override fun apply(input: CharArray): CharArray { + val newArray = CharArray(input.size) { 'X' } + System.arraycopy(input, 0, newArray, 0, newArray.size) + return newArray + } + } + @Test fun `can load and execute class that has finalize`() = sandbox(DEFAULT) { assertThatExceptionOfType(UnsupportedOperationException::class.java) @@ -515,4 +595,152 @@ class SandboxExecutorTest : TestBase() { throw UnsupportedOperationException("Very Bad Thing") } } + + @Test + fun `can execute parallel stream`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run("Pebble").apply { + assertThat(result).isEqualTo("Five,Four,One,Pebble,Three,Two") + } + } + + class TestParallelStream : Function { + override fun apply(input: String): String { + return listOf(input, "One", input, "Two", input, "Three", input, "Four", input, "Five") + .stream() + .distinct() + .sorted() + .collect(joining(",")) + } + } + + @Test + fun `users cannot load our sandboxed classes`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run("java.lang.DJVM") } + .withCauseInstanceOf(ClassNotFoundException::class.java) + .withMessageContaining("java.lang.DJVM") + } + + @Test + fun `users can load sandboxed classes`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + contractExecutor.run("java.util.List").apply { + assertThat(result?.name).isEqualTo("sandbox.java.util.List") + } + } + + class TestClassForName : Function> { + override fun apply(input: String): Class<*> { + return Class.forName(input) + } + } + + @Test + fun `test case-insensitive string sorting`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor, Array>(configuration) + contractExecutor.run(arrayOf("Zelda", "angela", "BOB", "betsy", "ALBERT")).apply { + assertThat(result).isEqualTo(arrayOf("ALBERT", "angela", "betsy", "BOB", "Zelda")) + } + } + + class CaseInsensitiveSort : Function, Array> { + override fun apply(input: Array): Array { + return listOf(*input).sortedWith(String.CASE_INSENSITIVE_ORDER).toTypedArray() + } + } + + @Test + fun `test unicode characters`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(0x01f600).apply { + assertThat(result).isEqualTo("EMOTICONS") + } + } + + class ExamineUnicodeBlock : Function { + override fun apply(codePoint: Int): String { + return Character.UnicodeBlock.of(codePoint).toString() + } + } + + @Test + fun `test unicode scripts`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run("COMMON").apply { + assertThat(result).isEqualTo(Character.UnicodeScript.COMMON) + } + } + + class ExamineUnicodeScript : Function { + override fun apply(scriptName: String): Character.UnicodeScript? { + val script = Character.UnicodeScript.valueOf(scriptName) + return if (script::class.java.isEnum) script else null + } + } + + @Test + fun `test users cannot define new classes`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run("sandbox.java.lang.DJVM") } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.ClassLoader.defineClass") + } + + class DefineNewClass : Function> { + override fun apply(input: String): Class<*> { + val data = ByteArray(0) + val cl = object : ClassLoader(this::class.java.classLoader) { + fun define(): Class<*> { + return super.defineClass(input, data, 0, data.size) + } + } + return cl.define() + } + } + + @Test + fun `test users cannot load new classes`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run("sandbox.java.lang.DJVM") } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.ClassLoader.loadClass") + } + + class LoadNewClass : Function> { + override fun apply(input: String): Class<*> { + val cl = object : ClassLoader(this::class.java.classLoader) { + fun load(): Class<*> { + return super.loadClass(input) + } + } + return cl.load() + } + } + + @Test + fun `test users cannot lookup classes`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run("sandbox.java.lang.DJVM") } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.ClassLoader.findClass") + } + + class FindClass : Function> { + override fun apply(input: String): Class<*> { + val cl = object : ClassLoader(this::class.java.classLoader) { + fun find(): Class<*> { + return super.findClass(input) + } + } + return cl.find() + } + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt index bd7e86dac0..68b60da1cd 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt @@ -1,16 +1,14 @@ package net.corda.djvm.rewiring -import foo.bar.sandbox.A -import foo.bar.sandbox.B -import foo.bar.sandbox.Empty -import foo.bar.sandbox.StrictFloat +import foo.bar.sandbox.* import net.corda.djvm.TestBase import net.corda.djvm.assertions.AssertionExtensions.assertThat import net.corda.djvm.execution.ExecutionProfile -import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.* import org.junit.Test import sandbox.net.corda.djvm.costing.ThresholdViolationError import java.nio.file.Paths +import java.util.* class ClassRewriterTest : TestBase() { @@ -102,4 +100,44 @@ class ClassRewriterTest : TestBase() { return input } } + + @Test + fun `can load class with constant fields`() = sandbox(DEFAULT) { + assertThat(loadClass()) + .hasClassName("sandbox.net.corda.djvm.rewiring.ObjectWithConstants") + .hasBeenModified() + } + + @Test + fun `test rewrite static method`() = sandbox(DEFAULT) { + assertThat(loadClass()) + .hasClassName("sandbox.java.util.Arrays") + .hasBeenModified() + } + + @Test + fun `test stitch new super-interface`() = sandbox(DEFAULT) { + assertThat(loadClass()) + .hasClassName("sandbox.java.lang.CharSequence") + .hasInterface("java.lang.CharSequence") + .hasBeenModified() + } + + @Test + fun `test class with stitched interface`() = sandbox(DEFAULT) { + assertThat(loadClass()) + .hasClassName("sandbox.java.lang.StringBuilder") + .hasInterface("sandbox.java.lang.CharSequence") + .hasBeenModified() + } } + +@Suppress("unused") +private object ObjectWithConstants { + const val MESSAGE = "Hello Sandbox!" + const val BIG_NUMBER = 99999L + const val NUMBER = 100 + const val CHAR = '?' + const val BYTE = 7f.toByte() + val DATA = Array(0) { "" } +} \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt b/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt index 85f4596f1f..401e0b5c0a 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt @@ -10,7 +10,7 @@ import java.nio.file.Path class SourceClassLoaderTest { - private val classResolver = ClassResolver(emptySet(), Whitelist.MINIMAL, "") + private val classResolver = ClassResolver(emptySet(), emptySet(), Whitelist.MINIMAL, "") @Test fun `can load class from Java's lang package when no files are provided to the class loader`() { From e3685f5e8119f3947e63b1151defa0e627f8ae97 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Thu, 11 Oct 2018 18:01:54 +0200 Subject: [PATCH 36/83] Make blobinspector not log to console by default (#4059) --- .../main/kotlin/net/corda/blobinspector/BlobInspector.kt | 8 -------- tools/blobinspector/src/main/resources/log4j2.xml | 5 +++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index 8887247bf6..8670989fd1 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -52,14 +52,6 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised override fun runProgram() = run(System.out) - override fun initLogging() { - if (verbose) { - loggingLevel = Level.TRACE - } - val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) - System.setProperty("logLevel", loggingLevel) // This property is referenced from the XML config file. - } - fun run(out: PrintStream): Int { val inputBytes = source!!.readBytes() val bytes = parseToBinaryRelaxed(inputFormatType, inputBytes) diff --git a/tools/blobinspector/src/main/resources/log4j2.xml b/tools/blobinspector/src/main/resources/log4j2.xml index 98b3648e6b..b7a8bfcd2f 100644 --- a/tools/blobinspector/src/main/resources/log4j2.xml +++ b/tools/blobinspector/src/main/resources/log4j2.xml @@ -1,7 +1,8 @@ - off + ${sys:consoleLogLevel:-error} + ${sys:defaultLogLevel:-info} @@ -9,7 +10,7 @@ - + From 8c41ae208da787fd59e084ae549659d504afa9c3 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Thu, 11 Oct 2018 10:45:43 +0100 Subject: [PATCH 37/83] CORDA-535: Remove BFT-Smart related migration parts --- .../migration/node-notary.changelog-init.xml | 11 ----------- .../migration/node-notary.changelog-pkey.xml | 5 ----- .../resources/migration/node-notary.changelog-v1.xml | 1 - 3 files changed, 17 deletions(-) diff --git a/node/src/main/resources/migration/node-notary.changelog-init.xml b/node/src/main/resources/migration/node-notary.changelog-init.xml index 8d0f1bcb6f..7bb5a20c52 100644 --- a/node/src/main/resources/migration/node-notary.changelog-init.xml +++ b/node/src/main/resources/migration/node-notary.changelog-init.xml @@ -5,17 +5,6 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" logicalFilePath="migration/node-services.changelog-init.xml"> - - - - - - - - - - - diff --git a/node/src/main/resources/migration/node-notary.changelog-pkey.xml b/node/src/main/resources/migration/node-notary.changelog-pkey.xml index c4d7c59376..8130c3b156 100644 --- a/node/src/main/resources/migration/node-notary.changelog-pkey.xml +++ b/node/src/main/resources/migration/node-notary.changelog-pkey.xml @@ -8,9 +8,4 @@ - - - - \ No newline at end of file diff --git a/node/src/main/resources/migration/node-notary.changelog-v1.xml b/node/src/main/resources/migration/node-notary.changelog-v1.xml index 3002133bad..4a7fc0e723 100644 --- a/node/src/main/resources/migration/node-notary.changelog-v1.xml +++ b/node/src/main/resources/migration/node-notary.changelog-v1.xml @@ -6,7 +6,6 @@ logicalFilePath="migration/node-services.changelog-init.xml"> - \ No newline at end of file From aced03df5400e05f0bb50833cae0dbe9865c1b5c Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 11 Oct 2018 19:50:26 +0100 Subject: [PATCH 38/83] CORDA-1274: Migrated usage of FastClasspathScanner to ClassGraph (#4060) FastClasspathScanner was renamed to ClassGraph for the version 4 release --- build.gradle | 2 +- djvm/build.gradle | 4 +- .../net/corda/djvm/tools/cli/Utilities.kt | 18 ++++----- .../net/corda/djvm/utilities/Discovery.kt | 26 ++++++------- experimental/behave/build.gradle | 4 +- .../corda/behave/scenarios/StepsContainer.kt | 16 ++++---- node-api/build.gradle | 4 +- .../nodeapi/internal/ContractsScanning.kt | 16 ++++---- .../cordapp/JarScanningCordappLoader.kt | 38 ++++++++++++------- serialization/build.gradle | 4 +- .../internal/amqp/AMQPSerializationScheme.kt | 18 ++++++--- .../node/internal/TestCordappsUtils.kt | 11 ++++-- 12 files changed, 89 insertions(+), 72 deletions(-) diff --git a/build.gradle b/build.gradle index f3cc3c97ca..500dc25229 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ buildscript { ext.commons_cli_version = '1.4' ext.protonj_version = '0.27.1' // This is now aligned with the Artemis version, but retaining in case we ever need to diverge again for a bug fix. ext.snappy_version = '0.4' - ext.fast_classpath_scanner_version = '2.12.3' + ext.class_graph_version = '4.2.12' ext.jcabi_manifests_version = '1.1' ext.picocli_version = '3.5.2' diff --git a/djvm/build.gradle b/djvm/build.gradle index eb41df17cc..d40a4d5523 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -36,8 +36,8 @@ dependencies { compile "org.ow2.asm:asm-tree:$asm_version" compile "org.ow2.asm:asm-commons:$asm_version" - // Classpath scanner - shadow "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + // ClassGraph: classpath scanning + shadow "io.github.classgraph:classgraph:$class_graph_version" // Test utilities testCompile "junit:junit:$junit_version" diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt index 66d5d4d918..adc88a1423 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt @@ -1,8 +1,9 @@ @file:JvmName("Utilities") package net.corda.djvm.tools.cli -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner -import java.lang.reflect.Modifier +import io.github.classgraph.ClassGraph +import java.lang.reflect.Modifier.isAbstract +import java.lang.reflect.Modifier.isStatic import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -92,13 +93,10 @@ val userClassPath: String = System.getProperty("java.class.path") * Get a reference of each concrete class that implements interface or class [T]. */ inline fun find(scanSpec: String = "net/corda/djvm"): List> { - val references = mutableListOf>() - FastClasspathScanner(scanSpec) - .matchClassesImplementing(T::class.java) { clazz -> - if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { - references.add(clazz) - } - } + return ClassGraph() + .whitelistPaths(scanSpec) + .enableAllInfo() .scan() - return references + .use { it.getClassesImplementing(T::class.java.name).loadClasses(T::class.java) } + .filter { !isAbstract(it.modifiers) && !isStatic(it.modifiers) } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt index 33886e76ae..a08261bb02 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt @@ -1,6 +1,6 @@ package net.corda.djvm.utilities -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import io.github.classgraph.ClassGraph import java.lang.reflect.Modifier /** @@ -13,19 +13,19 @@ object Discovery { * Get an instance of each concrete class that implements interface or class [T]. */ inline fun find(): List { - val instances = mutableListOf() - FastClasspathScanner("net/corda/djvm") - .matchClassesImplementing(T::class.java) { clazz -> - if (clazz.modifiers and FORBIDDEN_CLASS_MASK == 0) { - try { - instances.add(clazz.newInstance()) - } catch (exception: Throwable) { - throw Exception("Unable to instantiate ${clazz.name}", exception) - } + return ClassGraph() + .whitelistPaths("net/corda/djvm") + .enableAllInfo() + .scan() + .use { it.getClassesImplementing(T::class.java.name).loadClasses(T::class.java) } + .filter { it.modifiers and FORBIDDEN_CLASS_MASK == 0 } + .map { + try { + it.newInstance() + } catch (exception: Throwable) { + throw Exception("Unable to instantiate ${it.name}", exception) } } - .scan() - return instances - } + } } diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle index c5cba5eb8a..8cf7b06b80 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -51,8 +51,8 @@ dependencies { // JOptSimple: command line option parsing compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version" - // FastClasspathScanner: classpath scanning - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + // ClassGraph: classpath scanning + compile "io.github.classgraph:classgraph:$class_graph_version" compile "commons-io:commons-io:$commonsio_version" compile "com.spotify:docker-client:$docker_client_version" diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt index fbc06d59a9..dc5a0762e3 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt @@ -1,27 +1,29 @@ package net.corda.behave.scenarios import cucumber.api.java8.En -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import io.github.classgraph.ClassGraph import net.corda.behave.scenarios.api.StepsBlock import net.corda.behave.scenarios.api.StepsProvider import net.corda.behave.scenarios.steps.* import net.corda.core.internal.objectOrNewInstance -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.contextLogger @Suppress("KDocMissingDocumentation") class StepsContainer(val state: ScenarioState) : En { companion object { + private val log = contextLogger() + val stepsProviders: List by lazy { - FastClasspathScanner().addClassLoader(this::class.java.classLoader).scan() - .getNamesOfClassesImplementing(StepsProvider::class.java) - .mapNotNull { this::class.java.classLoader.loadClass(it).asSubclass(StepsProvider::class.java) } + ClassGraph() + .addClassLoader(this::class.java.classLoader) + .enableAllInfo() + .scan() + .use { it.getClassesImplementing(StepsProvider::class.java.name).loadClasses(StepsProvider::class.java) } .map { it.kotlin.objectOrNewInstance() } } } - private val log = loggerFor() - private val stepDefinitions: List = listOf( CashSteps(), ConfigurationSteps(), diff --git a/node-api/build.gradle b/node-api/build.gradle index d70b5af596..0f2c1a8c3e 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -27,8 +27,8 @@ dependencies { compile "org.apache.qpid:proton-j:$protonj_version" - // FastClasspathScanner: classpath scanning - needed for the NetworkBootstrapper. - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + // ClassGraph: classpath scanning + compile "io.github.classgraph:classgraph:$class_graph_version" // For caches rather than guava compile "com.github.ben-manes.caffeine:caffeine:$caffeine_version" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt index 674868e909..3ec56d525b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt @@ -1,6 +1,6 @@ package net.corda.nodeapi.internal -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import io.github.classgraph.ClassGraph import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.UpgradedContract @@ -28,15 +28,13 @@ class ContractsJarFile(private val file: Path) : ContractsJar { override val hash: SecureHash by lazy(LazyThreadSafetyMode.NONE, file::hash) override fun scan(): List { - val scanResult = FastClasspathScanner() - // A set of a single element may look odd, but if this is removed "Path" which itself is an `Iterable` - // is getting broken into pieces to scan individually, which doesn't yield desired effect. - .overrideClasspath(singleton(file)) - .scan() + val scanResult = ClassGraph().overrideClasspath(singleton(file)).enableAllInfo().scan() - val contractClassNames = coreContractClasses - .flatMap { scanResult.getNamesOfClassesImplementing(it.qualifiedName) } - .toSet() + val contractClassNames = scanResult.use { + coreContractClasses + .flatMap { scanResult.getClassesImplementing(it.qualifiedName).names } + .toSet() + } return URLClassLoader(arrayOf(file.toUri().toURL()), Contract::class.java.classLoader).use { cl -> contractClassNames.mapNotNull { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index e47badd63e..4981ae7977 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -1,7 +1,7 @@ package net.corda.node.internal.cordapp -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner -import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult +import io.github.classgraph.ClassGraph +import io.github.classgraph.ScanResult import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 @@ -95,7 +95,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } private fun loadCordapps(): List { val cordapps = cordappJarPaths - .map { scanCordapp(it).toCordapp(it) } + .map { url -> scanCordapp(url).use { it.toCordapp(url) } } .filter { if (it.info.minimumPlatformVersion > versionInfo.platformVersion) { logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum " + @@ -202,7 +202,8 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult { logger.info("Scanning CorDapp in ${cordappJarPath.url}") return cachedScanResult.computeIfAbsent(cordappJarPath) { - RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix) + val scanResult = ClassGraph().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).enableAllInfo().scan() + RestrictedScanResult(scanResult, cordappJarPath.qualifiedNamePrefix) } } @@ -239,40 +240,49 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: return map { it.kotlin.objectOrNewInstance() } } - private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) { + private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) : AutoCloseable { fun getNamesOfClassesImplementing(type: KClass<*>): List { - return scanResult.getNamesOfClassesImplementing(type.java) - .filter { it.startsWith(qualifiedNamePrefix) } + return scanResult.getClassesImplementing(type.java.name).names.filter { it.startsWith(qualifiedNamePrefix) } } fun getClassesWithSuperclass(type: KClass): List> { - return scanResult.getNamesOfSubclassesOf(type.java) + return scanResult + .getSubclasses(type.java.name) + .names .filter { it.startsWith(qualifiedNamePrefix) } .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } + .filterNot { it.isAbstractClass } } fun getClassesImplementing(type: KClass): List { - return scanResult.getNamesOfClassesImplementing(type.java) + return scanResult + .getClassesImplementing(type.java.name) + .names .filter { it.startsWith(qualifiedNamePrefix) } .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } + .filterNot { it.isAbstractClass } .map { it.kotlin.objectOrNewInstance() } } fun getClassesWithAnnotation(type: KClass, annotation: KClass): List> { - return scanResult.getNamesOfClassesWithAnnotation(annotation.java) + return scanResult + .getClassesWithAnnotation(annotation.java.name) + .names .filter { it.startsWith(qualifiedNamePrefix) } .mapNotNull { loadClass(it, type) } .filterNot { Modifier.isAbstract(it.modifiers) } } fun getConcreteClassesOfType(type: KClass): List> { - return scanResult.getNamesOfSubclassesOf(type.java) + return scanResult + .getSubclasses(type.java.name) + .names .filter { it.startsWith(qualifiedNamePrefix) } .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } + .filterNot { it.isAbstractClass } } + + override fun close() = scanResult.close() } } diff --git a/serialization/build.gradle b/serialization/build.gradle index eafc8cd155..fc1275c382 100644 --- a/serialization/build.gradle +++ b/serialization/build.gradle @@ -19,8 +19,8 @@ dependencies { // For AMQP serialisation. compile "org.apache.qpid:proton-j:$protonj_version" - // FastClasspathScanner: classpath scanning - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + // ClassGraph: classpath scanning + compile "io.github.classgraph:classgraph:$class_graph_version" // Pure-Java Snappy compression compile "org.iq80.snappy:snappy:$snappy_version" diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt index 94c0d2223f..f6b5556f82 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt @@ -2,11 +2,12 @@ package net.corda.serialization.internal.amqp -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import io.github.classgraph.ClassGraph import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM import net.corda.core.StubOutForDJVM import net.corda.core.cordapp.Cordapp +import net.corda.core.internal.isAbstractClass import net.corda.core.internal.objectOrNewInstance import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* @@ -16,7 +17,6 @@ import net.corda.serialization.internal.CordaSerializationMagic import net.corda.serialization.internal.DefaultWhitelist import net.corda.serialization.internal.MutableClassWhitelist import net.corda.serialization.internal.SerializationScheme -import java.lang.reflect.Modifier import java.util.* val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == amqpMagic @@ -72,10 +72,16 @@ abstract class AbstractAMQPSerializationScheme( @StubOutForDJVM private fun scanClasspathForSerializers(scanSpec: String): List> = this::class.java.classLoader.let { cl -> - FastClasspathScanner(scanSpec).addClassLoader(cl).scan() - .getNamesOfClassesImplementing(SerializationCustomSerializer::class.java) - .map { cl.loadClass(it).asSubclass(SerializationCustomSerializer::class.java) } - .filterNot { Modifier.isAbstract(it.modifiers) } + ClassGraph() + .whitelistPackages(scanSpec) + .addClassLoader(cl) + .enableAllInfo() + .scan() + .use { + val serializerClass = SerializationCustomSerializer::class.java + it.getClassesImplementing(serializerClass.name).loadClasses(serializerClass) + } + .filterNot { it.isAbstractClass } .map { it.kotlin.objectOrNewInstance() } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt index 6f755db697..6980e8eaca 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt @@ -1,6 +1,6 @@ package net.corda.testing.node.internal -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import io.github.classgraph.ClassGraph import net.corda.core.internal.createDirectories import net.corda.core.internal.deleteIfExists import net.corda.core.internal.outputStream @@ -69,9 +69,12 @@ fun Iterable.packageInDirectory(directory: Path) { * Returns all classes within the [targetPackage]. */ fun allClassesForPackage(targetPackage: String): Set> { - - val scanResult = FastClasspathScanner(targetPackage).strictWhitelist().scan() - return scanResult.namesOfAllClasses.filter { className -> className.startsWith(targetPackage) }.map(scanResult::classNameToClassRef).toSet() + return ClassGraph() + .whitelistPackages(targetPackage) + .enableAllInfo() + .scan() + .use { it.allClasses.loadClasses() } + .toSet() } /** From b769ad80bd9b86cf795d3dda7364fa945ae39d8f Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Fri, 12 Oct 2018 16:54:39 +0100 Subject: [PATCH 39/83] CORDA-195 When collecting JAR Signatures allow META-INF/*.EC block signature to follow jarsinger tool capabilities (#4065) jarsigner can produce META-INF/*.EC block signature for EC algorithm (https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html) even if this is contrary to JAR File spec (https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html). Allow block signature be also in *.EC file. --- .../core/internal/JarSignatureCollector.kt | 7 +++++-- .../internal/JarSignatureCollectorTest.kt | 20 +++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 70d8c84873..78d5a15517 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -11,8 +11,11 @@ import java.util.jar.JarInputStream */ object JarSignatureCollector { - /** @see */ - private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA)|SIG-.*)".toRegex() + /** + * @see + * also accepting *.EC as this can be created and accepted by jarsigner tool @see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html + * and Java Security Manager. */ + private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA|EC)|SIG-.*)".toRegex() /** * Returns an ordered list of every [Party] which has signed every signable item in the given [JarInputStream]. diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt index b8620082d8..fed904c1fa 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt @@ -4,6 +4,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.AfterClass @@ -38,15 +39,18 @@ class JarSignatureCollectorTest { private const val ALICE_PASS = "alicepass" private const val BOB = "bob" private const val BOB_PASS = "bobpass" + private const val CHARLIE = "Charlie" + private const val CHARLIE_PASS = "charliepass" - private fun generateKey(alias: String, password: String, name: CordaX500Name) = - execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name.toString()) + private fun generateKey(alias: String, password: String, name: CordaX500Name, keyalg: String = "RSA") = + execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name.toString()) @BeforeClass @JvmStatic fun beforeClass() { generateKey(ALICE, ALICE_PASS, ALICE_NAME) generateKey(BOB, BOB_PASS, BOB_NAME) + generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME, "EC") (dir / "_signable1").writeLines(listOf("signable1")) (dir / "_signable2").writeLines(listOf("signable2")) @@ -141,6 +145,18 @@ class JarSignatureCollectorTest { assertFailsWith { getJarSigners() } } + // Signing using EC algorithm produced JAR File spec incompatible signature block (META-INF/*.EC) which is anyway accepted by jarsiner, see [JarSignatureCollector] + @Test + fun `one signer with EC sign algorithm`() { + createJar("_signable1", "_signable2") + signJar(CHARLIE, CHARLIE_PASS) + assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // We only reused CHARLIE's distinguished name, so the keys will be different. + + (dir / "my-dir").createDirectory() + updateJar("my-dir") + assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // Unsigned directory is irrelevant. + } + //region Helper functions private fun createJar(vararg contents: String) = execute(*(arrayOf("jar", "cvf", FILENAME) + contents)) From 2c9a942e1a15d4d7c6f403915229b187f1d93de2 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 15 Oct 2018 10:11:18 +0100 Subject: [PATCH 40/83] CORDA-2088: Simplified the TestCordapp public API (#4064) The entry point to the API has been simplified to just requireing a list of packages to scan, with sensible defaults provided for the metadata. Because of the wither methods, having parameters for the metadata (with default values) seems unnecessary. Also the ability to scan just individual classes has been made internal, as it seems unlikely app developers would need that level of control when testing their apps. TestCordappImpl is a data class and thus acts as a natural key for the Jar caching, where previously the key was the package names. This fixes an issue where it was not possible to create two CorDapp Jars of the same package but different metadata. --- .ci/api-current.txt | 89 +------- ...tachmentsClassLoaderStaticContractTests.kt | 29 +-- .../node/flows/AsymmetricCorDappsTests.kt | 29 +-- ...owCheckpointVersionNodeStartupCheckTest.kt | 201 ++++++++--------- .../node/services/AttachmentLoadingTests.kt | 20 +- .../cordapp/JarScanningCordappLoader.kt | 10 +- .../node/internal/cordapp/ManifestUtils.kt | 3 +- .../net/corda/node/flows/cordapp.properties | 1 - .../cordapp/JarScanningCordappLoaderTest.kt | 43 +--- .../node/services/FinalityHandlerTest.kt | 9 +- .../kotlin/net/corda/testing/driver/Driver.kt | 29 +-- .../net/corda/testing/driver/DriverDSL.kt | 7 +- .../net/corda/testing/driver/TestCorDapp.kt | 85 ------- .../net/corda/testing/node/MockNetwork.kt | 44 ++-- .../net/corda/testing/node/MockServices.kt | 8 +- .../net/corda/testing/node/TestCordapp.kt | 69 ++++++ .../testing/node/internal/DriverDSLImpl.kt | 59 +++-- .../node/internal/InternalMockNetwork.kt | 14 +- .../node/internal/MutableTestCorDapp.kt | 73 ------ .../testing/node/internal/NodeBasedTest.kt | 5 +- .../corda/testing/node/internal/RPCDriver.kt | 4 +- .../node/internal/TestCordappDirectories.kt | 70 ++---- .../testing/node/internal/TestCordappImpl.kt | 26 +++ .../node/internal/TestCordappsUtils.kt | 208 +++--------------- .../node/internal/TestCordappsUtilsTest.kt | 93 ++++++++ .../corda/testing/node/internal/resource.txt | 0 26 files changed, 475 insertions(+), 753 deletions(-) delete mode 100644 node/src/main/resources/net/corda/node/flows/cordapp.properties delete mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/driver/TestCorDapp.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt delete mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt create mode 100644 testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestCordappsUtilsTest.kt create mode 100644 testing/node-driver/src/test/resources/net/corda/testing/node/internal/resource.txt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 13ae84905c..8d3efd6638 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -5822,8 +5822,6 @@ public interface net.corda.testing.driver.DriverDSL @NotNull public abstract net.corda.core.concurrent.CordaFuture startNode(net.corda.testing.driver.NodeParameters, net.corda.core.identity.CordaX500Name, java.util.List, net.corda.testing.driver.VerifierType, java.util.Map, Boolean, String) @NotNull - public abstract net.corda.core.concurrent.CordaFuture startNode(net.corda.testing.driver.NodeParameters, net.corda.core.identity.CordaX500Name, java.util.List, net.corda.testing.driver.VerifierType, java.util.Map, Boolean, String, java.util.Set, boolean) - @NotNull public abstract net.corda.core.concurrent.CordaFuture startWebserver(net.corda.testing.driver.NodeHandle) @NotNull public abstract net.corda.core.concurrent.CordaFuture startWebserver(net.corda.testing.driver.NodeHandle, String) @@ -5832,10 +5830,7 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public () public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, boolean) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, boolean, java.util.Set) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Set) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, boolean, boolean) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, boolean, boolean, java.util.Set) public final boolean component1() @NotNull public final java.util.List component10() @@ -6028,75 +6023,6 @@ public static final class net.corda.testing.driver.PortAllocation$Incremental ex public final java.util.concurrent.atomic.AtomicInteger getPortCounter() public int nextPort() ## -@DoNotImplement -public interface net.corda.testing.driver.TestCorDapp - @NotNull - public abstract java.util.Set> getClasses() - @NotNull - public abstract String getName() - @NotNull - public abstract java.util.Set getResources() - @NotNull - public abstract String getTitle() - @NotNull - public abstract String getVendor() - @NotNull - public abstract String getVersion() - @NotNull - public abstract java.nio.file.Path packageAsJarInDirectory(java.nio.file.Path) - public abstract void packageAsJarWithPath(java.nio.file.Path) -## -public static final class net.corda.testing.driver.TestCorDapp$Factory extends java.lang.Object - public () - @NotNull - public static final net.corda.testing.driver.TestCorDapp$Mutable create(String, String, String, String, java.util.Set>, kotlin.jvm.functions.Function2) - public static final net.corda.testing.driver.TestCorDapp$Factory$Companion Companion -## -public static final class net.corda.testing.driver.TestCorDapp$Factory$Companion extends java.lang.Object - @NotNull - public final net.corda.testing.driver.TestCorDapp$Mutable create(String, String, String, String, java.util.Set>, kotlin.jvm.functions.Function2) -## -@DoNotImplement -public static interface net.corda.testing.driver.TestCorDapp$Mutable extends net.corda.testing.driver.TestCorDapp - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable minus(Class) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable minusPackage(Package) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable minusPackage(String) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable minusPackages(Package, Package...) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable minusPackages(String, String...) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable minusPackages(java.util.Set) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable minusResource(String, java.net.URL) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable plus(Class) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable plusPackage(Package) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable plusPackage(String) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable plusPackages(Package, Package...) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable plusPackages(String, String...) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable plusPackages(java.util.Set) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable plusResource(String, java.net.URL) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable withClasses(java.util.Set>) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable withName(String) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable withTitle(String) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable withVendor(String) - @NotNull - public abstract net.corda.testing.driver.TestCorDapp$Mutable withVersion(String) -## public final class net.corda.testing.driver.VerifierType extends java.lang.Enum protected () public static net.corda.testing.driver.VerifierType valueOf(String) @@ -6238,9 +6164,9 @@ public class net.corda.testing.node.MockNetwork extends java.lang.Object @NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1) @NotNull - public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.List) + public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Collection) @NotNull - public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Set) + public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.List) @NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.testing.node.MockNodeParameters) @NotNull @@ -6256,9 +6182,9 @@ public class net.corda.testing.node.MockNetwork extends java.lang.Object @NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1) @NotNull - public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.List) + public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Collection) @NotNull - public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Set) + public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.List) @NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.testing.node.MockNodeParameters) @NotNull @@ -6339,8 +6265,7 @@ public final class net.corda.testing.node.MockNetworkParameters extends java.lan public final class net.corda.testing.node.MockNodeParameters extends java.lang.Object public () public (Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1) - public (Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.List) - public (Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Set) + public (Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Collection) @Nullable public final Integer component1() @Nullable @@ -6352,9 +6277,7 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O @NotNull public final net.corda.testing.node.MockNodeParameters copy(Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1) @NotNull - public final net.corda.testing.node.MockNodeParameters copy(Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.List) - @NotNull - public final net.corda.testing.node.MockNodeParameters copy(Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Set) + public final net.corda.testing.node.MockNodeParameters copy(Integer, net.corda.core.identity.CordaX500Name, java.math.BigInteger, kotlin.jvm.functions.Function1, java.util.Collection) public boolean equals(Object) @NotNull public final kotlin.jvm.functions.Function1 getConfigOverrides() diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index c699439e19..f7b7f49cfe 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -15,7 +15,6 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.JarScanningCordappLoader @@ -25,16 +24,13 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.internal.TestCordappDirectories import net.corda.testing.node.internal.cordappsForPackages -import net.corda.testing.node.internal.getTimestampAsDirectoryName -import net.corda.testing.node.internal.packageInDirectory import net.corda.testing.services.MockAttachmentStorage +import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Rule import org.junit.Test -import java.nio.file.Path -import java.nio.file.Paths class AttachmentsClassLoaderStaticContractTests { private companion object { @@ -101,22 +97,11 @@ class AttachmentsClassLoaderStaticContractTests { @Test fun `verify that contract DummyContract is in classPath`() { val contractClass = Class.forName("net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract") - val contract = contractClass.newInstance() as Contract - - assertNotNull(contract) + assertThat(contractClass.newInstance()).isInstanceOf(Contract::class.java) } - private fun cordappLoaderForPackages(packages: Iterable): CordappLoader { - - val cordapps = cordappsForPackages(packages) - return testDirectory().let { directory -> - cordapps.packageInDirectory(directory) - JarScanningCordappLoader.fromDirectories(listOf(directory), VersionInfo.UNKNOWN) - } + private fun cordappLoaderForPackages(packages: Collection): CordappLoader { + val dirs = cordappsForPackages(packages).map { TestCordappDirectories.getJarDirectory(it) } + return JarScanningCordappLoader.fromDirectories(dirs) } - - private fun testDirectory(): Path { - - return Paths.get("build", getTimestampAsDirectoryName()) - } -} \ No newline at end of file +} diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt b/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt index 2875c21527..6e1e07d8ee 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt @@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose -import net.corda.core.internal.packageName import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap @@ -12,8 +11,8 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.TestCorDapp import net.corda.testing.driver.driver +import net.corda.testing.node.internal.cordappForClasses import org.junit.Test import kotlin.test.assertEquals @@ -46,23 +45,18 @@ class AsymmetricCorDappsTests { } @Test - fun noSharedCorDappsWithAsymmetricSpecificClasses() { - + fun `no shared cordapps with asymmetric specific classes`() { driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = emptySet())) { - - val nodeA = startNode(providedName = ALICE_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java)))).getOrThrow() - val nodeB = startNode(providedName = BOB_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java, Pong::class.java)))).getOrThrow() + val nodeA = startNode(providedName = ALICE_NAME, additionalCordapps = setOf(cordappForClasses(Ping::class.java))).getOrThrow() + val nodeB = startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForClasses(Ping::class.java, Pong::class.java))).getOrThrow() nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow() } } @Test - fun sharedCorDappsWithAsymmetricSpecificClasses() { - - val resourceName = "cordapp.properties" - val cordappPropertiesResource = this::class.java.getResource(resourceName) - val sharedCordapp = TestCorDapp.Factory.create("shared", "1.0", classes = setOf(Ping::class.java)).plusResource("${AsymmetricCorDappsTests::class.java.packageName}.$resourceName", cordappPropertiesResource) - val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java)) + fun `shared cordapps with asymmetric specific classes`() { + val sharedCordapp = cordappForClasses(Ping::class.java) + val cordappForNodeB = cordappForClasses(Pong::class.java) driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = setOf(sharedCordapp))) { val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow() @@ -71,12 +65,9 @@ class AsymmetricCorDappsTests { } @Test - fun sharedCorDappsWithAsymmetricSpecificClassesInProcess() { - - val resourceName = "cordapp.properties" - val cordappPropertiesResource = this::class.java.getResource(resourceName) - val sharedCordapp = TestCorDapp.Factory.create("shared", "1.0", classes = setOf(Ping::class.java)).plusResource("${AsymmetricCorDappsTests::class.java.packageName}.$resourceName", cordappPropertiesResource) - val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java)) + fun `shared cordapps with asymmetric specific classes in process`() { + val sharedCordapp = cordappForClasses(Ping::class.java) + val cordappForNodeB = cordappForClasses(Pong::class.java) driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = setOf(sharedCordapp))) { val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow() diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt index ab2d706403..169ba207ce 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt @@ -1,27 +1,30 @@ package net.corda.node.flows -import net.corda.client.rpc.CordaRPCClient +import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.div import net.corda.core.internal.list +import net.corda.core.internal.moveTo import net.corda.core.internal.readLines import net.corda.core.messaging.startTrackedFlow import net.corda.core.utilities.getOrThrow import net.corda.node.internal.CheckpointIncompatibleException import net.corda.node.internal.NodeStartup -import net.corda.node.services.Permissions.Companion.invokeRpc -import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.testMessage.Message -import net.corda.testMessage.MessageState +import net.corda.testMessage.* import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.TestCorDapp import net.corda.testing.driver.driver -import net.corda.testing.node.User +import net.corda.testing.node.TestCordapp import net.corda.testing.node.internal.ListenProcessDeathException +import net.corda.testing.node.internal.TestCordappDirectories +import net.corda.testing.node.internal.cordappForClasses +import net.test.cordapp.v1.Record import net.test.cordapp.v1.SendMessageFlow import org.junit.Test +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.util.* import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -30,125 +33,111 @@ import kotlin.test.assertNotNull class FlowCheckpointVersionNodeStartupCheckTest { companion object { val message = Message("Hello world!") - val classes = setOf(net.corda.testMessage.MessageState::class.java, - net.corda.testMessage.MessageContract::class.java, - net.test.cordapp.v1.SendMessageFlow::class.java, - net.corda.testMessage.MessageSchema::class.java, - net.corda.testMessage.MessageSchemaV1::class.java, - net.test.cordapp.v1.Record::class.java) - val user = User("mark", "dadada", setOf(startFlow(), invokeRpc("vaultQuery"), invokeRpc("vaultTrack"))) + val defaultCordapp = cordappForClasses( + MessageState::class.java, + MessageContract::class.java, + SendMessageFlow::class.java, + MessageSchema::class.java, + MessageSchemaV1::class.java, + Record::class.java + ) } @Test fun `restart node successfully with suspended flow`() { - - val cordapps = setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)) - - return driver(DriverParameters(isDebug = true, startNodesInProcess = false, inMemoryDB = false, cordappsForAllNodes = cordapps)) { - { - val alice = startNode(rpcUsers = listOf(user), providedName = ALICE_NAME).getOrThrow() - val bob = startNode(rpcUsers = listOf(user), providedName = BOB_NAME).getOrThrow() - alice.stop() - CordaRPCClient(bob.rpcAddress).start(user.username, user.password).use { - val flowTracker = it.proxy.startTrackedFlow(::SendMessageFlow, message, defaultNotaryIdentity, alice.nodeInfo.singleIdentity()).progress - //wait until Bob progresses as far as possible because alice node is off - flowTracker.takeFirst { it == SendMessageFlow.Companion.FINALISING_TRANSACTION.label }.toBlocking().single() - } - bob.stop() - }() - val result = { - //Bob will resume the flow - val alice = startNode(rpcUsers = listOf(user), providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow() - startNode(providedName = BOB_NAME, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to false)).getOrThrow() - CordaRPCClient(alice.rpcAddress).start(user.username, user.password).use { - val page = it.proxy.vaultTrack(MessageState::class.java) - if (page.snapshot.states.isNotEmpty()) { - page.snapshot.states.first() - } else { - val r = page.updates.timeout(5, TimeUnit.SECONDS).take(1).toBlocking().single() - if (r.consumed.isNotEmpty()) r.consumed.first() else r.produced.first() - } - } - }() + return driver(parametersForRestartingNodes(listOf(defaultCordapp))) { + createSuspendedFlowInBob(cordapps = emptySet()) + // Bob will resume the flow + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + startNode(providedName = BOB_NAME).getOrThrow() + val page = alice.rpc.vaultTrack(MessageState::class.java) + val result = if (page.snapshot.states.isNotEmpty()) { + page.snapshot.states.first() + } else { + val r = page.updates.timeout(5, TimeUnit.SECONDS).take(1).toBlocking().single() + if (r.consumed.isNotEmpty()) r.consumed.first() else r.produced.first() + } assertNotNull(result) assertEquals(message, result.state.data.message) } } - private fun assertNodeRestartFailure( - cordapps: Set?, - cordappsVersionAtStartup: Set, - cordappsVersionAtRestart: Set, - reuseAdditionalCordappsAtRestart: Boolean, - assertNodeLogs: String - ) { + @Test + fun `restart node with incompatible version of suspended flow due to different jar name`() { + driver(parametersForRestartingNodes()) { + val cordapp = defaultCordapp.withName("different-jar-name-test-${UUID.randomUUID()}") + // Create the CorDapp jar file manually first to get hold of the directory that will contain it so that we can + // rename the filename later. The cordappDir, which acts as pointer to the jar file, does not get renamed. + val cordappDir = TestCordappDirectories.getJarDirectory(cordapp) + val cordappJar = cordappDir.list().single() - return driver(DriverParameters( - startNodesInProcess = false, // start nodes in separate processes to ensure CordappLoader is not shared between restarts - inMemoryDB = false, // ensure database is persisted between node restarts so we can keep suspended flow in Bob's node - cordappsForAllNodes = cordapps) - ) { - val bobLogFolder = { - val alice = startNode(rpcUsers = listOf(user), providedName = ALICE_NAME, additionalCordapps = cordappsVersionAtStartup).getOrThrow() - val bob = startNode(rpcUsers = listOf(user), providedName = BOB_NAME, additionalCordapps = cordappsVersionAtStartup).getOrThrow() - alice.stop() - CordaRPCClient(bob.rpcAddress).start(user.username, user.password).use { - val flowTracker = it.proxy.startTrackedFlow(::SendMessageFlow, message, defaultNotaryIdentity, alice.nodeInfo.singleIdentity()).progress - // wait until Bob progresses as far as possible because Alice node is offline - flowTracker.takeFirst { it == SendMessageFlow.Companion.FINALISING_TRANSACTION.label }.toBlocking().single() - } - val logFolder = bob.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME - // SendMessageFlow suspends in Bob node - bob.stop() - logFolder - }() + createSuspendedFlowInBob(setOf(cordapp)) - startNode(rpcUsers = listOf(user), providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false), - additionalCordapps = cordappsVersionAtRestart, regenerateCordappsOnStart = !reuseAdditionalCordappsAtRestart).getOrThrow() + // Rename the jar file. TestCordappDirectories caches the location of the jar file but the use of the random + // UUID in the name means there's zero chance of contaminating another test. + cordappJar.moveTo(cordappDir / "renamed-${cordappJar.fileName}") - assertFailsWith(ListenProcessDeathException::class) { - startNode(providedName = BOB_NAME, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to false), - additionalCordapps = cordappsVersionAtRestart, regenerateCordappsOnStart = !reuseAdditionalCordappsAtRestart).getOrThrow() - } - - val logFile = bobLogFolder.list { it.filter { it.fileName.toString().endsWith(".log") }.findAny().get() } - val numberOfNodesThatLogged = logFile.readLines { it.filter { assertNodeLogs in it }.count() } - assertEquals(1, numberOfNodesThatLogged) + assertBobFailsToStartWithLogMessage( + setOf(cordapp), + CheckpointIncompatibleException.FlowNotInstalledException(SendMessageFlow::class.java).message + ) } } @Test - fun `restart nodes with incompatible version of suspended flow due to different jar name`() { + fun `restart node with incompatible version of suspended flow due to different jar hash`() { + driver(parametersForRestartingNodes()) { + val originalCordapp = defaultCordapp.withName("different-jar-hash-test-${UUID.randomUUID()}") + val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single() - assertNodeRestartFailure( - emptySet(), - setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)), - setOf(TestCorDapp.Factory.create("testJar2", "1.0", classes = classes)), - false, - CheckpointIncompatibleException.FlowNotInstalledException(SendMessageFlow::class.java).message) + createSuspendedFlowInBob(setOf(originalCordapp)) + + // The vendor is part of the MANIFEST so changing it is sufficient to change the jar hash + val modifiedCordapp = originalCordapp.withVendor("${originalCordapp.vendor}-modified") + val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single() + modifiedCordappJar.moveTo(originalCordappJar, REPLACE_EXISTING) + + assertBobFailsToStartWithLogMessage( + setOf(originalCordapp), + // The part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException + "that is incompatible with the current installed version of" + ) + } } - @Test - fun `restart nodes with incompatible version of suspended flow`() { - - assertNodeRestartFailure( - emptySet(), - setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)), - setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes + net.test.cordapp.v1.SendMessageFlow::class.java)), - false, - // the part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException - "that is incompatible with the current installed version of") + private fun DriverDSL.createSuspendedFlowInBob(cordapps: Set) { + val (alice, bob) = listOf(ALICE_NAME, BOB_NAME) + .map { startNode(providedName = it, additionalCordapps = cordapps) } + .transpose() + .getOrThrow() + alice.stop() + val flowTracker = bob.rpc.startTrackedFlow(::SendMessageFlow, message, defaultNotaryIdentity, alice.nodeInfo.singleIdentity()).progress + // Wait until Bob progresses as far as possible because Alice node is offline + flowTracker.takeFirst { it == SendMessageFlow.Companion.FINALISING_TRANSACTION.label }.toBlocking().single() + bob.stop() } - @Test - fun `restart nodes with incompatible version of suspended flow due to different timestamps only`() { + private fun DriverDSL.assertBobFailsToStartWithLogMessage(cordapps: Collection, logMessage: String) { + assertFailsWith(ListenProcessDeathException::class) { + startNode( + providedName = BOB_NAME, + customOverrides = mapOf("devMode" to false), + additionalCordapps = cordapps, + regenerateCordappsOnStart = true + ).getOrThrow() + } - assertNodeRestartFailure( - emptySet(), - setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)), - setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)), - false, - // the part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException - "that is incompatible with the current installed version of") + val logDir = baseDirectory(BOB_NAME) / NodeStartup.LOGS_DIRECTORY_NAME + val logFile = logDir.list { it.filter { it.fileName.toString().endsWith(".log") }.findAny().get() } + val matchingLineCount = logFile.readLines { it.filter { line -> logMessage in line }.count() } + assertEquals(1, matchingLineCount) } -} \ No newline at end of file + + private fun parametersForRestartingNodes(cordappsForAllNodes: List = emptyList()): DriverParameters { + return DriverParameters( + startNodesInProcess = false, // Start nodes in separate processes to ensure CordappLoader is not shared between restarts + inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows + cordappsForAllNodes = cordappsForAllNodes + ) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index b9e5029fc9..5f77275446 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -3,17 +3,11 @@ package net.corda.node.services import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.CordaRuntimeException -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.PartyAndReference -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.toLedgerTransaction import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution @@ -21,19 +15,16 @@ import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo -import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity -import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.internal.rigorousMock @@ -59,7 +50,6 @@ class AttachmentLoadingTests { private val appContext get() = provider.getAppContext(cordapp) private companion object { - private val logger = contextLogger() val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!! const val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract" @@ -70,12 +60,6 @@ class AttachmentLoadingTests { .asSubclass(FlowLogic::class.java) val DUMMY_BANK_A = TestIdentity(DUMMY_BANK_A_NAME, 40).party val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party - private fun DriverDSL.createTwoNodes(): List { - return listOf( - startNode(providedName = bankAName), - startNode(providedName = bankBName) - ).transpose().getOrThrow() - } } private val services = object : ServicesForResolution { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 4981ae7977..ac1badeafe 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -61,11 +61,13 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: /** * Creates a CordappLoader from multiple directories. * - * @param corDappDirectories Directories used to scan for CorDapp JARs. + * @param cordappDirs Directories used to scan for CorDapp JARs. */ - fun fromDirectories(corDappDirectories: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList()): JarScanningCordappLoader { - logger.info("Looking for CorDapps in ${corDappDirectories.distinct().joinToString(", ", "[", "]")}") - val paths = corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } + fun fromDirectories(cordappDirs: Collection, + versionInfo: VersionInfo = VersionInfo.UNKNOWN, + extraCordapps: List = emptyList()): JarScanningCordappLoader { + logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") + val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } return JarScanningCordappLoader(paths, versionInfo, extraCordapps) } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt index 6e10661c86..fbca1b7eec 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt @@ -5,7 +5,7 @@ import net.corda.core.internal.cordapp.CordappImpl.Info.Companion.UNKNOWN_VALUE import java.util.jar.Attributes import java.util.jar.Manifest -fun createTestManifest(name: String, title: String, version: String, vendor: String): Manifest { +fun createTestManifest(name: String, title: String, version: String, vendor: String, targetVersion: Int): Manifest { val manifest = Manifest() // Mandatory manifest attribute. If not present, all other entries are silently skipped. @@ -20,6 +20,7 @@ fun createTestManifest(name: String, title: String, version: String, vendor: Str manifest["Implementation-Title"] = title manifest["Implementation-Version"] = version manifest["Implementation-Vendor"] = vendor + manifest["Target-Platform-Version"] = targetVersion.toString() return manifest } diff --git a/node/src/main/resources/net/corda/node/flows/cordapp.properties b/node/src/main/resources/net/corda/node/flows/cordapp.properties deleted file mode 100644 index 4b10332984..0000000000 --- a/node/src/main/resources/net/corda/node/flows/cordapp.properties +++ /dev/null @@ -1 +0,0 @@ -key=value \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 4dd255f414..176bbfb5d8 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -2,14 +2,12 @@ package net.corda.node.internal.cordapp import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.* +import net.corda.core.internal.packageName import net.corda.node.VersionInfo -import net.corda.node.cordapp.CordappLoader -import net.corda.testing.node.internal.cordappsForPackages -import net.corda.testing.node.internal.getTimestampAsDirectoryName -import net.corda.testing.node.internal.packageInDirectory +import net.corda.testing.node.internal.TestCordappDirectories +import net.corda.testing.node.internal.cordappForPackages import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.nio.file.Path import java.nio.file.Paths @InitiatingFlow @@ -38,7 +36,6 @@ class DummyRPCFlow : FlowLogic() { class JarScanningCordappLoaderTest { private companion object { - const val testScanPackage = "net.corda.node.internal.cordapp" const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract" const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator" } @@ -70,32 +67,18 @@ class JarScanningCordappLoaderTest { @Test fun `flows are loaded by loader`() { - val loader = cordappLoaderForPackages(listOf(testScanPackage)) + val dir = TestCordappDirectories.getJarDirectory(cordappForPackages(javaClass.packageName)) + val loader = JarScanningCordappLoader.fromDirectories(listOf(dir)) - val actual = loader.cordapps.toTypedArray() // One cordapp from this source tree. In gradle it will also pick up the node jar. - assertThat(actual.size == 0 || actual.size == 1).isTrue() + assertThat(loader.cordapps).isNotEmpty - val actualCordapp = actual.single { !it.initiatedFlows.isEmpty() } + val actualCordapp = loader.cordapps.single { !it.initiatedFlows.isEmpty() } assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java) assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java) assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java) } - @Test - fun `duplicate packages are ignored`() { - val loader = cordappLoaderForPackages(listOf(testScanPackage, testScanPackage)) - val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows } - assertThat(cordapps).hasSize(1) - } - - @Test - fun `sub-packages are ignored`() { - val loader = cordappLoaderForPackages(listOf("net.corda.core", testScanPackage)) - val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows } - assertThat(cordapps).hasSize(1) - } - // This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader // being used internally. Later iterations will use a classloader per cordapp and this test can be retired. @Test @@ -159,16 +142,4 @@ class JarScanningCordappLoaderTest { val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) assertThat(loader.cordapps).hasSize(1) } - - private fun cordappLoaderForPackages(packages: Iterable): CordappLoader { - val cordapps = cordappsForPackages(packages) - return testDirectory().let { directory -> - cordapps.packageInDirectory(directory) - JarScanningCordappLoader.fromDirectories(listOf(directory)) - } - } - - private fun testDirectory(): Path { - return Paths.get("build", getTimestampAsDirectoryName()) - } } diff --git a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt index 0f1e21be0e..f055b468fb 100644 --- a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt @@ -15,11 +15,7 @@ import net.corda.node.services.statemachine.StaffedFlowHospital.MedicalRecord.Ke import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity -import net.corda.testing.driver.TestCorDapp -import net.corda.testing.node.internal.InternalMockNetwork -import net.corda.testing.node.internal.InternalMockNodeParameters -import net.corda.testing.node.internal.TestStartedNode -import net.corda.testing.node.internal.startFlow +import net.corda.testing.node.internal.* import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test @@ -38,8 +34,7 @@ class FinalityHandlerTest { // CorDapp. Bob's FinalityHandler will error when validating the tx. mockNet = InternalMockNetwork() - val assertCordapp = TestCorDapp.Factory.create("net.corda.finance.contracts.asset", "1.0").plusPackage("net.corda.finance.contracts.asset") - val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = setOf(assertCordapp))) + val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = setOf(FINANCE_CORDAPP))) var bob = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index e413f5d90c..793c6fd8bb 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -19,6 +19,7 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.driver.PortAllocation.Incremental import net.corda.testing.driver.internal.internalServices import net.corda.testing.node.NotarySpec +import net.corda.testing.node.TestCordapp import net.corda.testing.node.User import net.corda.testing.node.internal.* import rx.Observable @@ -134,8 +135,8 @@ abstract class PortAllocation { * in. If null the Driver-level value will be used. * @property maximumHeapSize The maximum JVM heap size to use for the node. * @property logLevel Logging level threshold. - * @property additionalCordapps Additional [TestCorDapp]s that this node will have available, in addition to the ones common to all nodes managed by the [DriverDSL]. - * @property regenerateCordappsOnStart Whether existing [TestCorDapp]s unique to this node will be re-generated on start. Useful when stopping and restarting the same node. + * @property additionalCordapps Additional [TestCordapp]s that this node will have available, in addition to the ones common to all nodes managed by the [DriverDSL]. + * @property regenerateCordappsOnStart Whether existing [TestCordapp]s unique to this node will be re-generated on start. Useful when stopping and restarting the same node. */ @Suppress("unused") data class NodeParameters( @@ -146,7 +147,7 @@ data class NodeParameters( val startInSameProcess: Boolean? = null, val maximumHeapSize: String = "512m", val logLevel: String? = null, - val additionalCordapps: Set = emptySet(), + val additionalCordapps: Collection = emptySet(), val regenerateCordappsOnStart: Boolean = false ) { /** @@ -226,8 +227,8 @@ data class NodeParameters( * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running * in. If null the Driver-level value will be used. * @param maximumHeapSize The maximum JVM heap size to use for the node. - * @param additionalCordapps Additional [TestCorDapp]s that this node will have available, in addition to the ones common to all nodes managed by the [DriverDSL]. - * @param regenerateCordappsOnStart Whether existing [TestCorDapp]s unique to this node will be re-generated on start. Useful when stopping and restarting the same node. + * @param additionalCordapps Additional [TestCordapp]s that this node will have available, in addition to the ones common to all nodes managed by the [DriverDSL]. + * @param regenerateCordappsOnStart Whether existing [TestCordapp]s unique to this node will be re-generated on start. Useful when stopping and restarting the same node. */ constructor( providedName: CordaX500Name?, @@ -236,7 +237,7 @@ data class NodeParameters( customOverrides: Map, startInSameProcess: Boolean?, maximumHeapSize: String, - additionalCordapps: Set = emptySet(), + additionalCordapps: Set = emptySet(), regenerateCordappsOnStart: Boolean = false ) : this( providedName, @@ -291,7 +292,7 @@ data class NodeParameters( fun withStartInSameProcess(startInSameProcess: Boolean?): NodeParameters = copy(startInSameProcess = startInSameProcess) fun withMaximumHeapSize(maximumHeapSize: String): NodeParameters = copy(maximumHeapSize = maximumHeapSize) fun withLogLevel(logLevel: String?): NodeParameters = copy(logLevel = logLevel) - fun withAdditionalCordapps(additionalCordapps: Set): NodeParameters = copy(additionalCordapps = additionalCordapps) + fun withAdditionalCordapps(additionalCordapps: Set): NodeParameters = copy(additionalCordapps = additionalCordapps) fun withDeleteExistingCordappsDirectory(regenerateCordappsOnStart: Boolean): NodeParameters = copy(regenerateCordappsOnStart = regenerateCordappsOnStart) } @@ -379,7 +380,7 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr * @property inMemoryDB Whether to use in-memory H2 for new nodes rather then on-disk (the node starts quicker, however * the data is not persisted between node restarts). Has no effect if node is configured * in any way to use database other than H2. - * @property cordappsForAllNodes [TestCorDapp]s that will be added to each node started by the [DriverDSL]. + * @property cordappsForAllNodes [TestCordapp]s that will be added to each node started by the [DriverDSL]. */ @Suppress("unused") data class DriverParameters( @@ -398,7 +399,7 @@ data class DriverParameters( val notaryCustomOverrides: Map = emptyMap(), val initialiseSerialization: Boolean = true, val inMemoryDB: Boolean = true, - val cordappsForAllNodes: Set? = null + val cordappsForAllNodes: Collection? = null ) { constructor( isDebug: Boolean = false, @@ -480,7 +481,7 @@ data class DriverParameters( extraCordappPackagesToScan: List, jmxPolicy: JmxPolicy, networkParameters: NetworkParameters, - cordappsForAllNodes: Set? = null + cordappsForAllNodes: Collection? = null ) : this( isDebug, driverDirectory, @@ -549,7 +550,7 @@ data class DriverParameters( networkParameters: NetworkParameters, initialiseSerialization: Boolean, inMemoryDB: Boolean, - cordappsForAllNodes: Set? = null + cordappsForAllNodes: Set? = null ) : this( isDebug, driverDirectory, @@ -584,7 +585,7 @@ data class DriverParameters( fun withNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters) fun withNotaryCustomOverrides(notaryCustomOverrides: Map): DriverParameters = copy(notaryCustomOverrides = notaryCustomOverrides) fun withInMemoryDB(inMemoryDB: Boolean): DriverParameters = copy(inMemoryDB = inMemoryDB) - fun withCordappsForAllNodes(cordappsForAllNodes: Set?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes) + fun withCordappsForAllNodes(cordappsForAllNodes: Set?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes) fun copy( isDebug: Boolean, @@ -660,7 +661,7 @@ data class DriverParameters( extraCordappPackagesToScan: List, jmxPolicy: JmxPolicy, networkParameters: NetworkParameters, - cordappsForAllNodes: Set? + cordappsForAllNodes: Set? ) = this.copy( isDebug = isDebug, driverDirectory = driverDirectory, @@ -693,7 +694,7 @@ data class DriverParameters( jmxPolicy: JmxPolicy, networkParameters: NetworkParameters, initialiseSerialization: Boolean, - cordappsForAllNodes: Set? + cordappsForAllNodes: Set? ) = this.copy( isDebug = isDebug, driverDirectory = driverDirectory, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt index 1c4c01c3c0..bd1bcb464a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt @@ -6,6 +6,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.map import net.corda.node.internal.Node +import net.corda.testing.node.TestCordapp import net.corda.testing.node.User import net.corda.testing.node.NotarySpec import java.nio.file.Path @@ -95,8 +96,8 @@ interface DriverDSL { * @param maximumHeapSize The maximum JVM heap size to use for the node as a [String]. By default a number is interpreted * as being in bytes. Append the letter 'k' or 'K' to the value to indicate Kilobytes, 'm' or 'M' to indicate * megabytes, and 'g' or 'G' to indicate gigabytes. The default value is "512m" = 512 megabytes. - * @param additionalCordapps Additional [TestCorDapp]s that this node will have available, in addition to the ones common to all nodes managed by the [DriverDSL]. - * @param regenerateCordappsOnStart Whether existing [TestCorDapp]s unique to this node will be re-generated on start. Useful when stopping and restarting the same node. + * @param additionalCordapps Additional [TestCordapp]s that this node will have available, in addition to the ones common to all nodes managed by the [DriverDSL]. + * @param regenerateCordappsOnStart Whether existing [TestCordapp]s unique to this node will be re-generated on start. Useful when stopping and restarting the same node. * @return A [CordaFuture] on the [NodeHandle] to the node. The future will complete when the node is available and * it sees all previously started nodes, including the notaries. */ @@ -108,7 +109,7 @@ interface DriverDSL { customOverrides: Map = defaultParameters.customOverrides, startInSameProcess: Boolean? = defaultParameters.startInSameProcess, maximumHeapSize: String = defaultParameters.maximumHeapSize, - additionalCordapps: Set = defaultParameters.additionalCordapps, + additionalCordapps: Collection = defaultParameters.additionalCordapps, regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart ): CordaFuture diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/TestCorDapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/TestCorDapp.kt deleted file mode 100644 index 8dd209c34f..0000000000 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/TestCorDapp.kt +++ /dev/null @@ -1,85 +0,0 @@ -package net.corda.testing.driver - -import net.corda.core.DoNotImplement -import net.corda.testing.node.internal.MutableTestCorDapp -import java.net.URL -import java.nio.file.Path - -/** - * Represents information about a CorDapp. Used to generate CorDapp JARs in tests. - */ -@DoNotImplement -interface TestCorDapp { - - val name: String - val title: String - val version: String - val vendor: String - - val classes: Set> - - val resources: Set - - fun packageAsJarInDirectory(parentDirectory: Path): Path - - fun packageAsJarWithPath(jarFilePath: Path) - - /** - * Responsible of creating [TestCorDapp]s. - */ - class Factory { - companion object { - - /** - * Returns a builder-style [TestCorDapp] to easily generate different [TestCorDapp]s that have something in common. - */ - @JvmStatic - fun create(name: String, version: String, vendor: String = "R3", title: String = name, classes: Set> = emptySet(), willResourceBeAddedBeToCorDapp: (fullyQualifiedName: String, url: URL) -> Boolean = MutableTestCorDapp.Companion::filterTestCorDappClass): TestCorDapp.Mutable { - - return MutableTestCorDapp(name, version, vendor, title, classes, willResourceBeAddedBeToCorDapp) - } - } - } - - @DoNotImplement - interface Mutable : TestCorDapp { - - fun withName(name: String): TestCorDapp.Mutable - - fun withTitle(title: String): TestCorDapp.Mutable - - fun withVersion(version: String): TestCorDapp.Mutable - - fun withVendor(vendor: String): TestCorDapp.Mutable - - fun withClasses(classes: Set>): TestCorDapp.Mutable - - fun plusPackages(pckgs: Set): TestCorDapp.Mutable - - fun minusPackages(pckgs: Set): TestCorDapp.Mutable - - fun plusPackage(pckg: String): TestCorDapp.Mutable = plusPackages(setOf(pckg)) - - fun minusPackage(pckg: String): TestCorDapp.Mutable = minusPackages(setOf(pckg)) - - fun plusPackage(pckg: Package): TestCorDapp.Mutable = plusPackages(pckg.name) - - fun minusPackage(pckg: Package): TestCorDapp.Mutable = minusPackages(pckg.name) - - operator fun plus(clazz: Class<*>): TestCorDapp.Mutable = withClasses(classes + clazz) - - operator fun minus(clazz: Class<*>): TestCorDapp.Mutable = withClasses(classes - clazz) - - fun plusPackages(pckg: String, vararg pckgs: String): TestCorDapp.Mutable = plusPackages(setOf(pckg, *pckgs)) - - fun plusPackages(pckg: Package, vararg pckgs: Package): TestCorDapp.Mutable = minusPackages(setOf(pckg, *pckgs).map { it.name }.toSet()) - - fun minusPackages(pckg: String, vararg pckgs: String): TestCorDapp.Mutable = minusPackages(setOf(pckg, *pckgs)) - - fun minusPackages(pckg: Package, vararg pckgs: Package): TestCorDapp.Mutable = minusPackages(setOf(pckg, *pckgs).map { it.name }.toSet()) - - fun plusResource(fullyQualifiedName: String, url: URL): TestCorDapp.Mutable - - fun minusResource(fullyQualifiedName: String, url: URL): TestCorDapp.Mutable - } -} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 61b0198c2a..3c3ba57aa5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -16,7 +16,6 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.config.NodeConfiguration import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.TestCorDapp import net.corda.testing.node.internal.* import rx.Observable import java.math.BigInteger @@ -34,36 +33,29 @@ import java.util.concurrent.Future * @property entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overridden to cause nodes to have stable or colliding identity/service keys. * @property configOverrides Add/override behaviour of the [NodeConfiguration] mock object. - * @property additionalCordapps [TestCorDapp]s that will be added to this node in addition to the ones shared by all nodes, which get specified at [MockNetwork] level. + * @property additionalCordapps [TestCordapp]s that will be added to this node in addition to the ones shared by all nodes, which get specified at [MockNetwork] level. */ @Suppress("unused") -data class MockNodeParameters constructor( +data class MockNodeParameters( val forcedID: Int? = null, val legalName: CordaX500Name? = null, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), val configOverrides: (NodeConfiguration) -> Any? = {}, - val additionalCordapps: Set) { + val additionalCordapps: Collection = emptyList()) { - @JvmOverloads - constructor( - forcedID: Int? = null, - legalName: CordaX500Name? = null, - entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - configOverrides: (NodeConfiguration) -> Any? = {}, - extraCordappPackages: List = emptyList() - ) : this(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps = cordappsForPackages(extraCordappPackages)) + constructor(forcedID: Int? = null, + legalName: CordaX500Name? = null, + entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), + configOverrides: (NodeConfiguration) -> Any? = {} + ) : this(forcedID, legalName, entropyRoot, configOverrides, emptyList()) fun withForcedID(forcedID: Int?): MockNodeParameters = copy(forcedID = forcedID) fun withLegalName(legalName: CordaX500Name?): MockNodeParameters = copy(legalName = legalName) fun withEntropyRoot(entropyRoot: BigInteger): MockNodeParameters = copy(entropyRoot = entropyRoot) fun withConfigOverrides(configOverrides: (NodeConfiguration) -> Any?): MockNodeParameters = copy(configOverrides = configOverrides) - fun withExtraCordappPackages(extraCordappPackages: List): MockNodeParameters = copy(forcedID = forcedID, legalName = legalName, entropyRoot = entropyRoot, configOverrides = configOverrides, extraCordappPackages = extraCordappPackages) - fun withAdditionalCordapps(additionalCordapps: Set): MockNodeParameters = copy(additionalCordapps = additionalCordapps) + fun withAdditionalCordapps(additionalCordapps: Collection): MockNodeParameters = copy(additionalCordapps = additionalCordapps) fun copy(forcedID: Int?, legalName: CordaX500Name?, entropyRoot: BigInteger, configOverrides: (NodeConfiguration) -> Any?): MockNodeParameters { - return MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps = emptySet()) - } - fun copy(forcedID: Int?, legalName: CordaX500Name?, entropyRoot: BigInteger, configOverrides: (NodeConfiguration) -> Any?, extraCordappPackages: List = emptyList()): MockNodeParameters { - return MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, extraCordappPackages) + return MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides) } } @@ -293,7 +285,7 @@ inline fun > StartedMockNode.registerResponderFlow( * @property notarySpecs The notaries to use in the mock network. By default you get one mock notary and that is usually sufficient. * @property networkParameters The network parameters to be used by all the nodes. [NetworkParameters.notaries] must be * empty as notaries are defined by [notarySpecs]. - * @property cordappsForAllNodes [TestCorDapp]s that will be added to each node started by the [MockNetwork]. + * @property cordappsForAllNodes [TestCordapp]s that will be added to each node started by the [MockNetwork]. */ @Suppress("MemberVisibilityCanBePrivate", "CanBeParameter") open class MockNetwork( @@ -304,7 +296,7 @@ open class MockNetwork( val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, val notarySpecs: List = defaultParameters.notarySpecs, val networkParameters: NetworkParameters = defaultParameters.networkParameters, - val cordappsForAllNodes: Set = cordappsForPackages(cordappPackages)) { + val cordappsForAllNodes: Collection = cordappsForPackages(cordappPackages)) { @JvmOverloads constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) @@ -345,7 +337,9 @@ open class MockNetwork( fun createPartyNode(legalName: CordaX500Name? = null): StartedMockNode = StartedMockNode.create(internalMockNetwork.createPartyNode(legalName)) /** Create a started node with the given parameters. **/ - fun createNode(parameters: MockNodeParameters = MockNodeParameters()): StartedMockNode = StartedMockNode.create(internalMockNetwork.createNode(InternalMockNodeParameters(parameters))) + fun createNode(parameters: MockNodeParameters = MockNodeParameters()): StartedMockNode { + return StartedMockNode.create(internalMockNetwork.createNode(InternalMockNodeParameters(parameters))) + } /** * Create a started node with the given parameters. @@ -375,13 +369,13 @@ open class MockNetwork( * @param entropyRoot The initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overridden to cause nodes to have stable or colliding identity/service keys. * @param configOverrides Add/override behaviour of the [NodeConfiguration] mock object. - * @param additionalCordapps Additional [TestCorDapp]s that this node will have available, in addition to the ones common to all nodes managed by the [MockNetwork]. + * @param additionalCordapps Additional [TestCordapp]s that this node will have available, in addition to the ones common to all nodes managed by the [MockNetwork]. */ fun createNode(legalName: CordaX500Name? = null, forcedID: Int? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), configOverrides: (NodeConfiguration) -> Any? = {}, - additionalCordapps: Set): StartedMockNode { + additionalCordapps: Collection): StartedMockNode { val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps) return StartedMockNode.create(internalMockNetwork.createNode(InternalMockNodeParameters(parameters))) } @@ -417,13 +411,13 @@ open class MockNetwork( * @param entropyRoot The initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overridden to cause nodes to have stable or colliding identity/service keys. * @param configOverrides Add/override behaviour of the [NodeConfiguration] mock object. - * @param additionalCordapps Additional [TestCorDapp]s that this node will have available, in addition to the ones common to all nodes managed by the [MockNetwork]. + * @param additionalCordapps Additional [TestCordapp]s that this node will have available, in addition to the ones common to all nodes managed by the [MockNetwork]. */ fun createUnstartedNode(legalName: CordaX500Name? = null, forcedID: Int? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), configOverrides: (NodeConfiguration) -> Any? = {}, - additionalCordapps: Set): UnstartedMockNode { + additionalCordapps: Collection): UnstartedMockNode { val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps) return UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(InternalMockNodeParameters(parameters))) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 112cf3f19f..e78d0c12a3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -34,10 +34,7 @@ import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.MockCordappProvider -import net.corda.testing.node.internal.MockKeyManagementService -import net.corda.testing.node.internal.MockTransactionStorage -import net.corda.testing.node.internal.TestCordappDirectories -import net.corda.testing.node.internal.getCallerPackage +import net.corda.testing.node.internal.* import net.corda.testing.services.MockAttachmentStorage import java.security.KeyPair import java.sql.Connection @@ -70,8 +67,7 @@ open class MockServices private constructor( companion object { private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { - - val cordappPaths = TestCordappDirectories.forPackages(packages) + val cordappPaths = cordappsForPackages(packages).map { TestCordappDirectories.getJarDirectory(it) } return JarScanningCordappLoader.fromDirectories(cordappPaths, versionInfo) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt new file mode 100644 index 0000000000..6f881660c1 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt @@ -0,0 +1,69 @@ +package net.corda.testing.node + +import net.corda.core.DoNotImplement +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.testing.node.internal.TestCordappImpl +import net.corda.testing.node.internal.simplifyScanPackages + +/** + * Represents information about a CorDapp. Used to generate CorDapp JARs in tests. + */ +@DoNotImplement +interface TestCordapp { + /** Returns the name, defaults to "test-cordapp" if not specified. */ + val name: String + + /** Returns the title, defaults to "test-title" if not specified. */ + val title: String + + /** Returns the version string, defaults to "1.0" if not specified. */ + val version: String + + /** Returns the vendor string, defaults to "Corda" if not specified. */ + val vendor: String + + /** Returns the target platform version, defaults to the current platform version if not specified. */ + val targetVersion: Int + + /** Returns the set of package names scanned for this test CorDapp. */ + val packages: Set + + /** Return a copy of this [TestCordapp] but with the specified name. */ + fun withName(name: String): TestCordapp + + /** Return a copy of this [TestCordapp] but with the specified title. */ + fun withTitle(title: String): TestCordapp + + /** Return a copy of this [TestCordapp] but with the specified version string. */ + fun withVersion(version: String): TestCordapp + + /** Return a copy of this [TestCordapp] but with the specified vendor string. */ + fun withVendor(vendor: String): TestCordapp + + /** Return a copy of this [TestCordapp] but with the specified target platform version. */ + fun withTargetVersion(targetVersion: Int): TestCordapp + + class Factory { + companion object { + @JvmStatic + fun fromPackages(vararg packageNames: String): TestCordapp = fromPackages(packageNames.asList()) + + /** + * Create a [TestCordapp] object by scanning the given packages. The meta data on the CorDapp will be the + * default values, which can be specified with the wither methods. + */ + @JvmStatic + fun fromPackages(packageNames: Collection): TestCordapp { + return TestCordappImpl( + name = "test-cordapp", + version = "1.0", + vendor = "Corda", + title = "test-title", + targetVersion = PLATFORM_VERSION, + packages = simplifyScanPackages(packageNames), + classes = emptySet() + ) + } + } + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index bd562e41f5..d952d07414 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -28,7 +28,6 @@ import net.corda.node.services.Permissions import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NodeRegistrationHelper -import net.corda.core.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook @@ -38,6 +37,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.testing.node.TestCordapp import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME @@ -91,7 +91,7 @@ class DriverDSLImpl( val networkParameters: NetworkParameters, val notaryCustomOverrides: Map, val inMemoryDB: Boolean, - val cordappsForAllNodes: Set + val cordappsForAllNodes: Collection ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null @@ -184,7 +184,25 @@ class DriverDSLImpl( } } - override fun startNode(defaultParameters: NodeParameters, providedName: CordaX500Name?, rpcUsers: List, verifierType: VerifierType, customOverrides: Map, startInSameProcess: Boolean?, maximumHeapSize: String) = startNode(defaultParameters, providedName, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, defaultParameters.additionalCordapps, defaultParameters.regenerateCordappsOnStart) + override fun startNode(defaultParameters: NodeParameters, + providedName: CordaX500Name?, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + startInSameProcess: Boolean?, + maximumHeapSize: String): CordaFuture { + return startNode( + defaultParameters, + providedName, + rpcUsers, + verifierType, + customOverrides, + startInSameProcess, + maximumHeapSize, + defaultParameters.additionalCordapps, + defaultParameters.regenerateCordappsOnStart + ) + } override fun startNode( defaultParameters: NodeParameters, @@ -194,7 +212,7 @@ class DriverDSLImpl( customOverrides: Map, startInSameProcess: Boolean?, maximumHeapSize: String, - additionalCordapps: Set, + additionalCordapps: Collection, regenerateCordappsOnStart: Boolean ): CordaFuture { val p2pAddress = portAllocation.nextHostAndPort() @@ -225,7 +243,7 @@ class DriverDSLImpl( startInSameProcess: Boolean? = null, maximumHeapSize: String = "512m", p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort(), - additionalCordapps: Set = emptySet(), + additionalCordapps: Collection = emptySet(), regenerateCordappsOnStart: Boolean = false): CordaFuture { val rpcAddress = portAllocation.nextHostAndPort() val rpcAdminAddress = portAllocation.nextHostAndPort() @@ -535,16 +553,12 @@ class DriverDSLImpl( } } - private val sharedCordappsDirectories: Iterable by lazy { - TestCordappDirectories.cached(cordappsForAllNodes) - } - private fun startNodeInternal(specifiedConfig: NodeConfig, webAddress: NetworkHostAndPort, startInProcess: Boolean?, maximumHeapSize: String, localNetworkMap: LocalNetworkMap?, - additionalCordapps: Set, + additionalCordapps: Collection, regenerateCordappsOnStart: Boolean = false): CordaFuture { val visibilityHandle = networkVisibilityController.register(specifiedConfig.corda.myLegalName) val baseDirectory = specifiedConfig.corda.baseDirectory.createDirectories() @@ -558,11 +572,17 @@ class DriverDSLImpl( val useHTTPS = specifiedConfig.typesafe.run { hasPath("useHTTPS") && getBoolean("useHTTPS") } - val existingCorDappDirectoriesOption = if (regenerateCordappsOnStart) emptyList() else if (specifiedConfig.typesafe.hasPath(NodeConfiguration.cordappDirectoriesKey)) specifiedConfig.typesafe.getStringList(NodeConfiguration.cordappDirectoriesKey) else emptyList() + val existingCorDappDirectoriesOption = if (regenerateCordappsOnStart) { + emptyList() + } else if (specifiedConfig.typesafe.hasPath(NodeConfiguration.cordappDirectoriesKey)) { + specifiedConfig.typesafe.getStringList(NodeConfiguration.cordappDirectoriesKey) + } else { + emptyList() + } - val cordappDirectories = existingCorDappDirectoriesOption + sharedCordappsDirectories.map { it.toString() } + TestCordappDirectories.cached(additionalCordapps, regenerateCordappsOnStart).map { it.toString() } + val cordappDirectories = existingCorDappDirectoriesOption + (cordappsForAllNodes + additionalCordapps).map { TestCordappDirectories.getJarDirectory(it).toString() } - val config = NodeConfig(specifiedConfig.typesafe.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories))) + val config = NodeConfig(specifiedConfig.typesafe.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories.toSet()))) if (startInProcess ?: startNodesInProcess) { val nodeAndThreadFuture = startInProcessNode(executorService, config) @@ -687,8 +707,13 @@ class DriverDSLImpl( private fun oneOf(array: Array) = array[Random().nextInt(array.size)] - fun cordappsInCurrentAndAdditionalPackages(packagesToScan: Iterable = emptySet()): Set = cordappsForPackages(getCallerPackage() + packagesToScan) - fun cordappsInCurrentAndAdditionalPackages(firstPackage: String, vararg otherPackages: String): Set = cordappsInCurrentAndAdditionalPackages(otherPackages.toList() + firstPackage) + fun cordappsInCurrentAndAdditionalPackages(packagesToScan: Collection = emptySet()): List { + return cordappsForPackages(getCallerPackage() + packagesToScan) + } + + fun cordappsInCurrentAndAdditionalPackages(firstPackage: String, vararg otherPackages: String): List { + return cordappsInCurrentAndAdditionalPackages(otherPackages.asList() + firstPackage) + } private fun startInProcessNode( executorService: ScheduledExecutorService, @@ -1085,7 +1110,7 @@ fun internalDriver( compatibilityZone: CompatibilityZoneParams? = null, notaryCustomOverrides: Map = DriverParameters().notaryCustomOverrides, inMemoryDB: Boolean = DriverParameters().inMemoryDB, - cordappsForAllNodes: Set = DriverParameters().cordappsForAllNodes(), + cordappsForAllNodes: Collection = DriverParameters().cordappsForAllNodes(), dsl: DriverDSLImpl.() -> A ): A { return genericDriver( @@ -1125,7 +1150,7 @@ private fun Config.toNodeOnly(): Config { return if (hasPath("webAddress")) withoutPath("webAddress").withoutPath("useHTTPS") else this } -internal fun DriverParameters.cordappsForAllNodes(): Set = cordappsForAllNodes +internal fun DriverParameters.cordappsForAllNodes(): Collection = cordappsForAllNodes ?: cordappsInCurrentAndAdditionalPackages(extraCordappPackagesToScan) fun DriverDSL.startNode(providedName: CordaX500Name, devMode: Boolean, parameters: NodeParameters = NodeParameters()): CordaFuture { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 05ebe55eae..d60f9f6ee1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -51,8 +51,8 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.node.TestCordapp import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.driver.TestCorDapp import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.internal.stubs.CertificateStoreStubs @@ -89,7 +89,7 @@ data class InternalMockNodeParameters( val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), val configOverrides: (NodeConfiguration) -> Any? = {}, val version: VersionInfo = MOCK_VERSION_INFO, - val additionalCordapps: Set? = null) { + val additionalCordapps: Collection? = null) { constructor(mockNodeParameters: MockNodeParameters) : this( mockNodeParameters.forcedID, mockNodeParameters.legalName, @@ -148,7 +148,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe val testDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), val networkParameters: NetworkParameters = testNetworkParameters(), val defaultFactory: (MockNodeArgs) -> MockNode = { args -> MockNode(args) }, - val cordappsForAllNodes: Set = emptySet(), + val cordappsForAllNodes: Collection = emptySet(), val autoVisibleNodes: Boolean = true) : AutoCloseable { init { // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. @@ -174,10 +174,6 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } private val sharedUserCount = AtomicInteger(0) - private val sharedCorDappsDirectories: Iterable by lazy { - TestCordappDirectories.cached(cordappsForAllNodes) - } - /** A read only view of the current set of nodes. */ val nodes: List get() = _nodes @@ -453,8 +449,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe parameters.configOverrides(it) } - val cordapps: Set = parameters.additionalCordapps ?: emptySet() - val cordappDirectories = sharedCorDappsDirectories + TestCordappDirectories.cached(cordapps) + val cordapps = (parameters.additionalCordapps ?: emptySet()) + cordappsForAllNodes + val cordappDirectories = cordapps.map { TestCordappDirectories.getJarDirectory(it) }.distinct() doReturn(cordappDirectories).whenever(config).cordappDirectories val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version)) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt deleted file mode 100644 index d06270c2cd..0000000000 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt +++ /dev/null @@ -1,73 +0,0 @@ -package net.corda.testing.node.internal - -import net.corda.core.internal.div -import net.corda.testing.driver.TestCorDapp -import java.io.File -import java.net.URL -import java.nio.file.Path - -internal class MutableTestCorDapp private constructor(override val name: String, override val version: String, override val vendor: String, override val title: String, private val willResourceBeAddedToCorDapp: (String, URL) -> Boolean, private val jarEntries: Set) : TestCorDapp.Mutable { - - constructor(name: String, version: String, vendor: String, title: String, classes: Set>, willResourceBeAddedToCorDapp: (String, URL) -> Boolean) : this(name, version, vendor, title, willResourceBeAddedToCorDapp, jarEntriesFromClasses(classes)) - - companion object { - - private const val jarExtension = ".jar" - private const val whitespace = " " - private const val whitespaceReplacement = "_" - private val productionPathSegments = setOf<(String) -> String>({ "out${File.separator}production${File.separator}classes" }, { fullyQualifiedName -> "main${File.separator}${fullyQualifiedName.packageToJarPath()}" }) - private val excludedCordaPackages = setOf("net.corda.core", "net.corda.node") - - fun filterTestCorDappClass(fullyQualifiedName: String, url: URL): Boolean { - - return isTestResource(fullyQualifiedName, url) || !isInExcludedCordaPackage(fullyQualifiedName) - } - - private fun isTestResource(fullyQualifiedName: String, url: URL): Boolean { - - return productionPathSegments.map { it.invoke(fullyQualifiedName) }.none { url.toString().contains(it) } - } - - private fun isInExcludedCordaPackage(packageName: String): Boolean { - - return excludedCordaPackages.any { packageName.startsWith(it) } - } - - private fun jarEntriesFromClasses(classes: Set>): Set { - - val illegal = classes.filter { it.protectionDomain?.codeSource?.location == null } - if (illegal.isNotEmpty()) { - throw IllegalArgumentException("Some classes do not have a location, typically because they are part of Java or Kotlin. Offending types were: ${illegal.joinToString(", ", "[", "]") { it.simpleName }}") - } - return classes.map(Class<*>::jarEntryInfo).toSet() - } - } - - override val classes: Set> = jarEntries.filterIsInstance(JarEntryInfo.ClassJarEntryInfo::class.java).map(JarEntryInfo.ClassJarEntryInfo::clazz).toSet() - - override val resources: Set = jarEntries.map(JarEntryInfo::url).toSet() - - override fun withName(name: String) = MutableTestCorDapp(name, version, vendor, title, classes, willResourceBeAddedToCorDapp) - - override fun withTitle(title: String) = MutableTestCorDapp(name, version, vendor, title, classes, willResourceBeAddedToCorDapp) - - override fun withVersion(version: String) = MutableTestCorDapp(name, version, vendor, title, classes, willResourceBeAddedToCorDapp) - - override fun withVendor(vendor: String) = MutableTestCorDapp(name, version, vendor, title, classes, willResourceBeAddedToCorDapp) - - override fun withClasses(classes: Set>) = MutableTestCorDapp(name, version, vendor, title, classes, willResourceBeAddedToCorDapp) - - override fun plusPackages(pckgs: Set) = withClasses(pckgs.map { allClassesForPackage(it) }.fold(classes) { all, packageClasses -> all + packageClasses }) - - override fun minusPackages(pckgs: Set) = withClasses(pckgs.map { allClassesForPackage(it) }.fold(classes) { all, packageClasses -> all - packageClasses }) - - override fun plusResource(fullyQualifiedName: String, url: URL): TestCorDapp.Mutable = MutableTestCorDapp(name, version, vendor, title, willResourceBeAddedToCorDapp, jarEntries + JarEntryInfo.ResourceJarEntryInfo(fullyQualifiedName, url)) - - override fun minusResource(fullyQualifiedName: String, url: URL): TestCorDapp.Mutable = MutableTestCorDapp(name, version, vendor, title, willResourceBeAddedToCorDapp, jarEntries - JarEntryInfo.ResourceJarEntryInfo(fullyQualifiedName, url)) - - override fun packageAsJarWithPath(jarFilePath: Path) = jarEntries.packageToCorDapp(jarFilePath, name, version, vendor, title, willResourceBeAddedToCorDapp) - - override fun packageAsJarInDirectory(parentDirectory: Path): Path = (parentDirectory / defaultJarName()).also { packageAsJarWithPath(it) } - - private fun defaultJarName(): String = "${name}_$version$jarExtension".replace(whitespace, whitespaceReplacement) -} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index 66a4f2408d..dae33f8c4b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -9,7 +9,6 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor import net.corda.node.VersionInfo import net.corda.node.internal.Node import net.corda.node.internal.NodeWithInfo @@ -108,9 +107,9 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi val existingCorDappDirectoriesOption = if (config.hasPath(NodeConfiguration.cordappDirectoriesKey)) config.getStringList(NodeConfiguration.cordappDirectoriesKey) else emptyList() - val cordappDirectories = existingCorDappDirectoriesOption + TestCordappDirectories.cached(cordapps).map { it.toString() } + val cordappDirectories = existingCorDappDirectoriesOption + cordapps.map { TestCordappDirectories.getJarDirectory(it).toString() } - val specificConfig = config.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories)) + val specificConfig = config.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories.toSet())) val parsedConfig = specificConfig.parseAsNodeConfiguration().also { nodeConfiguration -> val errors = nodeConfiguration.validate() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 05b5662a83..38109bf114 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -24,11 +24,11 @@ import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT +import net.corda.testing.node.TestCordapp import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.PortAllocation -import net.corda.testing.driver.TestCorDapp import net.corda.testing.node.NotarySpec import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages @@ -118,7 +118,7 @@ fun rpcDriver( networkParameters: NetworkParameters = testNetworkParameters(), notaryCustomOverrides: Map = emptyMap(), inMemoryDB: Boolean = true, - cordappsForAllNodes: Set = cordappsInCurrentAndAdditionalPackages(), + cordappsForAllNodes: Collection = cordappsInCurrentAndAdditionalPackages(), dsl: RPCDriverDSL.() -> A ): A { return genericDriver( diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt index 6f261817d2..3cd553de39 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt @@ -1,70 +1,46 @@ package net.corda.testing.node.internal +import net.corda.core.crypto.sha256 import net.corda.core.internal.createDirectories import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div -import net.corda.core.internal.exists +import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor -import net.corda.testing.driver.TestCorDapp +import net.corda.testing.node.TestCordapp import java.nio.file.Path import java.nio.file.Paths +import java.util.* import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap - -internal object TestCordappDirectories { +object TestCordappDirectories { private val logger = loggerFor() - private const val whitespace = " " - private const val whitespaceReplacement = "_" + private val whitespace = "\\s".toRegex() - private val cordappsCache: ConcurrentMap, Path> = ConcurrentHashMap, Path>() + private val testCordappsCache = ConcurrentHashMap() - internal fun cached(cordapps: Iterable, replaceExistingOnes: Boolean = false, cordappsDirectory: Path = defaultCordappsDirectory): Iterable { - - cordappsDirectory.toFile().deleteOnExit() - return cordapps.map { cached(it, replaceExistingOnes, cordappsDirectory) } - } - - internal fun cached(cordapp: TestCorDapp, replaceExistingOnes: Boolean = false, cordappsDirectory: Path = defaultCordappsDirectory): Path { - - cordappsDirectory.toFile().deleteOnExit() - val cacheKey = cordapp.resources.map { it.toExternalForm() }.sorted() - return if (replaceExistingOnes) { - cordappsCache.remove(cacheKey) - cordappsCache.getOrPut(cacheKey) { - - val cordappDirectory = (cordappsDirectory / "${cordapp.name}_${cordapp.version}".replace(whitespace, whitespaceReplacement)).toAbsolutePath() - cordappDirectory.createDirectories() - cordapp.packageAsJarInDirectory(cordappDirectory) - cordappDirectory - } - } else { - cordappsCache.getOrPut(cacheKey) { - - val cordappDirectory = (cordappsDirectory / "${cordapp.name}_${cordapp.version}".replace(whitespace, whitespaceReplacement)).toAbsolutePath() - cordappDirectory.createDirectories() - cordapp.packageAsJarInDirectory(cordappDirectory) - cordappDirectory + fun getJarDirectory(cordapp: TestCordapp, cordappsDirectory: Path = defaultCordappsDirectory): Path { + cordapp as TestCordappImpl + return testCordappsCache.computeIfAbsent(cordapp) { + val cordappDir = (cordappsDirectory / UUID.randomUUID().toString()).createDirectories() + val uniqueScanString = if (cordapp.packages.size == 1 && cordapp.classes.isEmpty()) { + cordapp.packages.first() + } else { + "${cordapp.packages}${cordapp.classes.joinToString { it.name }}".toByteArray().sha256().toString() } + val jarFileName = cordapp.run { "${name}_${vendor}_${title}_${version}_${targetVersion}_$uniqueScanString.jar".replace(whitespace, "-") } + val jarFile = cordappDir / jarFileName + cordapp.packageAsJar(jarFile) + logger.debug { "$cordapp packaged into $jarFile" } + cordappDir } } - internal fun forPackages(packages: Iterable, replaceExistingOnes: Boolean = false, cordappsDirectory: Path = defaultCordappsDirectory): Iterable { - - cordappsDirectory.toFile().deleteOnExit() - val cordapps = simplifyScanPackages(packages).distinct().fold(emptySet()) { all, packageName -> all + testCorDapp(packageName) } - return cached(cordapps, replaceExistingOnes, cordappsDirectory) - } - private val defaultCordappsDirectory: Path by lazy { - val cordappsDirectory = (Paths.get("build") / "tmp" / getTimestampAsDirectoryName() / "generated-test-cordapps").toAbsolutePath() logger.info("Initialising generated test CorDapps directory in $cordappsDirectory") - if (cordappsDirectory.exists()) { - cordappsDirectory.deleteRecursively() - } + cordappsDirectory.toFile().deleteOnExit() + cordappsDirectory.deleteRecursively() cordappsDirectory.createDirectories() - cordappsDirectory } -} \ No newline at end of file +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt new file mode 100644 index 0000000000..831198320c --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt @@ -0,0 +1,26 @@ +package net.corda.testing.node.internal + +import net.corda.testing.node.TestCordapp + +data class TestCordappImpl(override val name: String, + override val version: String, + override val vendor: String, + override val title: String, + override val targetVersion: Int, + override val packages: Set, + val classes: Set>) : TestCordapp { + + override fun withName(name: String): TestCordappImpl = copy(name = name) + + override fun withVersion(version: String): TestCordappImpl = copy(version = version) + + override fun withVendor(vendor: String): TestCordappImpl = copy(vendor = vendor) + + override fun withTitle(title: String): TestCordappImpl = copy(title = title) + + override fun withTargetVersion(targetVersion: Int): TestCordappImpl = copy(targetVersion = targetVersion) + + fun withClasses(vararg classes: Class<*>): TestCordappImpl { + return copy(classes = classes.filter { clazz -> packages.none { clazz.name.startsWith("$it.") } }.toSet()) + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt index 6980e8eaca..5ab15cb556 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt @@ -1,100 +1,34 @@ package net.corda.testing.node.internal import io.github.classgraph.ClassGraph -import net.corda.core.internal.createDirectories -import net.corda.core.internal.deleteIfExists import net.corda.core.internal.outputStream import net.corda.node.internal.cordapp.createTestManifest -import net.corda.testing.driver.TestCorDapp -import org.apache.commons.io.IOUtils -import java.io.OutputStream -import java.net.URI -import java.net.URL +import net.corda.testing.node.TestCordapp import java.nio.file.Path import java.nio.file.attribute.FileTime import java.time.Instant -import java.util.* import java.util.jar.JarOutputStream import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream import kotlin.reflect.KClass -/** - * Packages some [JarEntryInfo] into a CorDapp JAR with specified [path]. - * @param path The path of the JAR. - * @param willResourceBeAddedBeToCorDapp A filter for the inclusion of [JarEntryInfo] in the JAR. - */ -internal fun Iterable.packageToCorDapp(path: Path, name: String, version: String, vendor: String, title: String = name, willResourceBeAddedBeToCorDapp: (String, URL) -> Boolean = { _, _ -> true }) { +@JvmField +val FINANCE_CORDAPP: TestCordappImpl = cordappForPackages("net.corda.finance") - var hasContent = false - try { - hasContent = packageToCorDapp(path.outputStream(), name, version, vendor, title, willResourceBeAddedBeToCorDapp) - } finally { - if (!hasContent) { - path.deleteIfExists() - } - } +/** Creates a [TestCordappImpl] for each package. */ +fun cordappsForPackages(vararg packageNames: String): List = cordappsForPackages(packageNames.asList()) + +fun cordappsForPackages(packageNames: Iterable): List { + return simplifyScanPackages(packageNames).map { cordappForPackages(it) } } -/** - * Packages some [JarEntryInfo] into a CorDapp JAR using specified [outputStream]. - * @param outputStream The [OutputStream] for the JAR. - * @param willResourceBeAddedBeToCorDapp A filter for the inclusion of [JarEntryInfo] in the JAR. - */ -internal fun Iterable.packageToCorDapp(outputStream: OutputStream, name: String, version: String, vendor: String, title: String = name, willResourceBeAddedBeToCorDapp: (String, URL) -> Boolean = { _, _ -> true }): Boolean { - - val manifest = createTestManifest(name, title, version, vendor) - return JarOutputStream(outputStream, manifest).use { jos -> zip(jos, willResourceBeAddedBeToCorDapp) } +/** Creates a single [TestCordappImpl] containing all the given packges. */ +fun cordappForPackages(vararg packageNames: String): TestCordappImpl { + return TestCordapp.Factory.fromPackages(*packageNames) as TestCordappImpl } -/** - * Transforms a [Class] into a [JarEntryInfo]. - */ -internal fun Class<*>.jarEntryInfo(): JarEntryInfo { - - return JarEntryInfo.ClassJarEntryInfo(this) -} - -/** - * Packages some [TestCorDapp]s under a root [directory], each with it's own JAR. - * @param directory The parent directory in which CorDapp JAR will be created. - */ -fun Iterable.packageInDirectory(directory: Path) { - - directory.createDirectories() - forEach { cordapp -> cordapp.packageAsJarInDirectory(directory) } -} - -/** - * Returns all classes within the [targetPackage]. - */ -fun allClassesForPackage(targetPackage: String): Set> { - return ClassGraph() - .whitelistPackages(targetPackage) - .enableAllInfo() - .scan() - .use { it.allClasses.loadClasses() } - .toSet() -} - -/** - * Maps each package to a [TestCorDapp] with resources found in that package. - */ -fun cordappsForPackages(packages: Iterable): Set { - - return simplifyScanPackages(packages).toSet().fold(emptySet()) { all, packageName -> all + testCorDapp(packageName) } -} - -/** - * Maps each package to a [TestCorDapp] with resources found in that package. - */ -fun cordappsForPackages(firstPackage: String, vararg otherPackages: String): Set { - - return cordappsForPackages(setOf(*otherPackages) + firstPackage) -} +fun cordappForClasses(vararg classes: Class<*>): TestCordappImpl = cordappForPackages().withClasses(*classes) fun getCallerClass(directCallerClass: KClass<*>): Class<*>? { - val stackTrace = Throwable().stackTrace val index = stackTrace.indexOfLast { it.className == directCallerClass.java.name } if (index == -1) return null @@ -105,112 +39,42 @@ fun getCallerClass(directCallerClass: KClass<*>): Class<*>? { } } -fun getCallerPackage(directCallerClass: KClass<*>): String? { - - return getCallerClass(directCallerClass)?.`package`?.name -} - -/** - * Returns a [TestCorDapp] containing resources found in [packageName]. - */ -internal fun testCorDapp(packageName: String): TestCorDapp { - - val uuid = UUID.randomUUID() - val name = "$packageName-$uuid" - val version = "$uuid" - return TestCorDapp.Factory.create(name, version).plusPackage(packageName) -} +fun getCallerPackage(directCallerClass: KClass<*>): String? = getCallerClass(directCallerClass)?.`package`?.name /** * Squashes child packages if the parent is present. Example: ["com.foo", "com.foo.bar"] into just ["com.foo"]. */ -fun simplifyScanPackages(scanPackages: Iterable): List { - - return scanPackages.sorted().fold(emptyList()) { listSoFar, packageName -> +fun simplifyScanPackages(scanPackages: Iterable): Set { + return scanPackages.sorted().fold(emptySet()) { soFar, packageName -> when { - listSoFar.isEmpty() -> listOf(packageName) - packageName.startsWith(listSoFar.last()) -> listSoFar - else -> listSoFar + packageName + soFar.isEmpty() -> setOf(packageName) + packageName.startsWith("${soFar.last()}.") -> soFar + else -> soFar + packageName } } } -/** - * Transforms a class or package name into a path segment. - */ -internal fun String.packageToJarPath() = replace(".", "/") +fun TestCordappImpl.packageAsJar(file: Path) { + // Don't mention "classes" in the error message as that feature is only available internally + require(packages.isNotEmpty() || classes.isNotEmpty()) { "At least one package must be specified" } -private fun Iterable.zip(outputStream: ZipOutputStream, willResourceBeAddedBeToCorDapp: (String, URL) -> Boolean): Boolean { + val scanResult = ClassGraph() + .whitelistPackages(*packages.toTypedArray()) + .whitelistClasses(*classes.map { it.name }.toTypedArray()) + .scan() - val entries = filter { (fullyQualifiedName, url) -> willResourceBeAddedBeToCorDapp(fullyQualifiedName, url) } - if (entries.isNotEmpty()) { - zip(outputStream, entries) - } - return entries.isNotEmpty() -} - -private fun zip(outputStream: ZipOutputStream, allInfo: Iterable) { - - val time = FileTime.from(Instant.EPOCH) - val classLoader = Thread.currentThread().contextClassLoader - allInfo.distinctBy { it.url }.sortedBy { it.url.toExternalForm() }.forEach { info -> - - try { - val entry = ZipEntry(info.entryName).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time) - outputStream.putNextEntry(entry) - classLoader.getResourceAsStream(info.entryName).use { - IOUtils.copy(it, outputStream) + scanResult.use { + val manifest = createTestManifest(name, title, version, vendor, targetVersion) + JarOutputStream(file.outputStream(), manifest).use { jos -> + val time = FileTime.from(Instant.now()) + // The same resource may be found in different locations (this will happen when running from gradle) so just + // pick the first one found. + scanResult.allResources.asMap().forEach { path, resourceList -> + val entry = ZipEntry(path).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time) + jos.putNextEntry(entry) + resourceList[0].open().use { it.copyTo(jos) } + jos.closeEntry() } - } finally { - outputStream.closeEntry() } } } - -/** - * Represents a single resource to be added to a CorDapp JAR. - */ -internal sealed class JarEntryInfo(val fullyQualifiedName: String, val url: URL) { - - abstract val entryName: String - - /** - * Represents a class to be added to a CorDapp JAR. - */ - class ClassJarEntryInfo(val clazz: Class<*>) : JarEntryInfo(clazz.name, clazz.classFileURL()) { - - override val entryName = "${fullyQualifiedName.packageToJarPath()}$fileExtensionSeparator$classFileExtension" - } - - /** - * Represents a resource file to be added to a CorDapp JAR. - */ - class ResourceJarEntryInfo(fullyQualifiedName: String, url: URL) : JarEntryInfo(fullyQualifiedName, url) { - - override val entryName: String - get() { - val extensionIndex = fullyQualifiedName.lastIndexOf(fileExtensionSeparator) - return "${fullyQualifiedName.substring(0 until extensionIndex).packageToJarPath()}${fullyQualifiedName.substring(extensionIndex)}" - } - } - - operator fun component1(): String = fullyQualifiedName - - operator fun component2(): URL = url - - private companion object { - - private const val classFileExtension = "class" - private const val fileExtensionSeparator = "." - private const val whitespace = " " - private const val whitespaceReplacement = "%20" - - private fun Class<*>.classFileURL(): URL { - - require(protectionDomain?.codeSource?.location != null) { "Invalid class $name for test CorDapp. Classes without protection domain cannot be referenced. This typically happens for Java / Kotlin types." } - return URI.create("${protectionDomain.codeSource.location}/${name.packageToJarPath()}$fileExtensionSeparator$classFileExtension".escaped()).toURL() - } - - private fun String.escaped(): String = this.replace(whitespace, whitespaceReplacement) - } -} \ No newline at end of file diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestCordappsUtilsTest.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestCordappsUtilsTest.kt new file mode 100644 index 0000000000..581195dfc8 --- /dev/null +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/TestCordappsUtilsTest.kt @@ -0,0 +1,93 @@ +package net.corda.testing.node.internal + +import net.corda.core.internal.inputStream +import net.corda.node.internal.cordapp.get +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.nio.file.Path +import java.util.jar.JarInputStream + +class TestCordappsUtilsTest { + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + @Test + fun `test simplifyScanPackages`() { + assertThat(simplifyScanPackages(emptyList())).isEmpty() + assertThat(simplifyScanPackages(listOf("com.foo.bar"))).containsExactlyInAnyOrder("com.foo.bar") + assertThat(simplifyScanPackages(listOf("com.foo", "com.foo"))).containsExactlyInAnyOrder("com.foo") + assertThat(simplifyScanPackages(listOf("com.foo", "com.bar"))).containsExactlyInAnyOrder("com.foo", "com.bar") + assertThat(simplifyScanPackages(listOf("com.foo", "com.foo.bar"))).containsExactlyInAnyOrder("com.foo") + assertThat(simplifyScanPackages(listOf("com.foo.bar", "com.foo"))).containsExactlyInAnyOrder("com.foo") + assertThat(simplifyScanPackages(listOf("com.foobar", "com.foo.bar"))).containsExactlyInAnyOrder("com.foobar", "com.foo.bar") + assertThat(simplifyScanPackages(listOf("com.foobar", "com.foo"))).containsExactlyInAnyOrder("com.foobar", "com.foo") + } + + @Test + fun `packageAsJar writes out the CorDapp info into the manifest`() { + val cordapp = cordappForPackages("net.corda.testing.node.internal") + .withTargetVersion(123) + .withName("TestCordappsUtilsTest") + + val jarFile = packageAsJar(cordapp) + JarInputStream(jarFile.inputStream()).use { + assertThat(it.manifest["Target-Platform-Version"]).isEqualTo("123") + assertThat(it.manifest["Name"]).isEqualTo("TestCordappsUtilsTest") + } + } + + @Test + fun `packageAsJar on leaf package`() { + val entries = packageAsJarThenReadBack(cordappForPackages("net.corda.testing.node.internal")) + + assertThat(entries).contains( + "net/corda/testing/node/internal/TestCordappsUtilsTest.class", + "net/corda/testing/node/internal/resource.txt" // Make sure non-class resource files are also picked up + ).doesNotContain( + "net/corda/testing/node/MockNetworkTest.class" + ) + + // Make sure the MockNetworkTest class does actually exist to ensure the above is not a false-positive + assertThat(javaClass.classLoader.getResource("net/corda/testing/node/MockNetworkTest.class")).isNotNull() + } + + @Test + fun `packageAsJar on package with sub-packages`() { + val entries = packageAsJarThenReadBack(cordappForPackages("net.corda.testing.node")) + + assertThat(entries).contains( + "net/corda/testing/node/internal/TestCordappsUtilsTest.class", + "net/corda/testing/node/internal/resource.txt", + "net/corda/testing/node/MockNetworkTest.class" + ) + } + + @Test + fun `packageAsJar on single class`() { + val entries = packageAsJarThenReadBack(cordappForClasses(InternalMockNetwork::class.java)) + + assertThat(entries).containsOnly("${InternalMockNetwork::class.java.name.replace('.', '/')}.class") + } + + private fun packageAsJar(cordapp: TestCordappImpl): Path { + val jarFile = tempFolder.newFile().toPath() + cordapp.packageAsJar(jarFile) + return jarFile + } + + private fun packageAsJarThenReadBack(cordapp: TestCordappImpl): List { + val jarFile = packageAsJar(cordapp) + val entries = ArrayList() + JarInputStream(jarFile.inputStream()).use { + while (true) { + val e = it.nextJarEntry ?: break + entries += e.name + it.closeEntry() + } + } + return entries + } +} diff --git a/testing/node-driver/src/test/resources/net/corda/testing/node/internal/resource.txt b/testing/node-driver/src/test/resources/net/corda/testing/node/internal/resource.txt new file mode 100644 index 0000000000..e69de29bb2 From acd3490cde130568b6e4e486bd8b4be80b06af21 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 15 Oct 2018 11:40:10 +0200 Subject: [PATCH 41/83] Updates docs structure. (#4072) --- docs/source/building-a-cordapp-index.rst | 12 +++++++----- docs/source/getting-set-up.rst | 4 ++-- docs/source/quickstart-index.rst | 9 --------- docs/source/tutorial-cordapp.rst | 4 ++-- docs/source/writing-a-cordapp.rst | 4 ++-- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index ad7b5457e5..1586941e93 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -5,16 +5,18 @@ CorDapps :maxdepth: 1 cordapp-overview + getting-set-up + tutorial-cordapp + building-a-cordapp-samples writing-a-cordapp + cordapp-build-systems + building-against-master debugging-a-cordapp upgrade-notes upgrading-cordapps - cordapp-build-systems - building-against-master - corda-api secure-coding-guidelines + corda-api flow-cookbook + cheat-sheet vault soft-locking - cheat-sheet - building-a-cordapp-samples diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index fe0f0f62e8..e6c2c27d52 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -1,5 +1,5 @@ -Getting set up -============== +Getting set up for CorDapp development +====================================== Software requirements --------------------- diff --git a/docs/source/quickstart-index.rst b/docs/source/quickstart-index.rst index 40a2798807..c20b4ed10b 100644 --- a/docs/source/quickstart-index.rst +++ b/docs/source/quickstart-index.rst @@ -1,15 +1,6 @@ Quickstart ========== -.. only:: pdfmode - - .. toctree:: - :caption: Other docs - :maxdepth: 1 - - getting-set-up.rst - tutorial-cordapp.rst - Welcome to the Corda Quickstart Guide. Follow the links below to help get going quickly with Corda. I want to: diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 06789ea9a0..805808b761 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -4,8 +4,8 @@ -The example CorDapp -=================== +Running the example CorDapp +=========================== .. contents:: diff --git a/docs/source/writing-a-cordapp.rst b/docs/source/writing-a-cordapp.rst index 02bca02c2b..9678e0f9e6 100644 --- a/docs/source/writing-a-cordapp.rst +++ b/docs/source/writing-a-cordapp.rst @@ -1,5 +1,5 @@ -CorDapp structure -================= +Structuring a CorDapp +===================== .. contents:: From af48612d46ca61254f081d795cb2f9d81784787c Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 15 Oct 2018 10:40:58 +0100 Subject: [PATCH 42/83] ENT-1906: Fix JavaDoc error for DJVM. (#4063) --- djvm/src/main/java/sandbox/java/lang/ThreadLocal.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djvm/src/main/java/sandbox/java/lang/ThreadLocal.java b/djvm/src/main/java/sandbox/java/lang/ThreadLocal.java index f416d3db16..8a2853bf82 100644 --- a/djvm/src/main/java/sandbox/java/lang/ThreadLocal.java +++ b/djvm/src/main/java/sandbox/java/lang/ThreadLocal.java @@ -4,8 +4,8 @@ import sandbox.java.util.function.Supplier; /** * Everything inside the sandbox is single-threaded, so this - * implementation of ThreadLocal is sufficient. - * @param + * implementation of ThreadLocal is sufficient. + * @param Underlying type of this thread-local variable. */ @SuppressWarnings({"unused", "WeakerAccess"}) public class ThreadLocal extends Object { From 194969477714286a46f0d13cdf5989fa0da24f12 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 20 Aug 2018 17:58:36 +0200 Subject: [PATCH 43/83] Add design doc on package namespace ownership --- .../package-namespace-ownership.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/source/design/data-model-upgrades/package-namespace-ownership.md diff --git a/docs/source/design/data-model-upgrades/package-namespace-ownership.md b/docs/source/design/data-model-upgrades/package-namespace-ownership.md new file mode 100644 index 0000000000..aaffcf7473 --- /dev/null +++ b/docs/source/design/data-model-upgrades/package-namespace-ownership.md @@ -0,0 +1,92 @@ +# Package namespace ownership + +This design document outlines a new Corda feature that allows a compatibility zone to give ownership of parts of the Java package namespace to certain users. + +"*There are only two hard problems in computer science: 1. Cache invalidation, 2. Naming things, 3. Off by one errors*" + + + +## Background + +Corda implements a decentralised database that can be unilaterally extended with new data types and logic by its users, without any involvement by the closest equivalent we have to administrators (the "zone operator"). Even informing them is not required. + +This design minimises the power zone operators have and ensures deploying new apps can be fast and cheap - it's limited only by the speed with which the users themselves can move. But it introduces problematic levels of namespace complexity which can make programming securely harder than in regular non-decentralised programming. + +#### Java namespaces + +A typical Java application, seen from the JVM level, has a flat namespace in which a single string name binds to a single class. In object oriented programming a class defines both a data structure and the code used to enforce various invariants like "a person's age may not be negative", so this allows a developer to reason about what the identifier `com.example.Person` really means throughout the lifetime of his program. + +More complex Java applications may have a nested namespace using classloaders, thus inside a JVM a class is actually a pair of (classloader pointer, class name) and this can be used to support tricks like having two different versions of the same class in use simultaneously. The downside is more complexity for the developer to deal with. When things get mixed up this can surface (in Java 8) as nonsensical error messages like "com.example.Person cannot be casted to com.example.Person". In Java 9 classloaders were finally given names so these errors make more sense. + +#### Corda namespaces + +Corda has an even more complex namespace. States are identified using ordinary Java class names because programmers have to be able to write source code that works with them. But because there's no centralised coordination point and Java package namespacing is just a convention, the true name of a class when loaded and run includes the hash of the JAR that defines it. These "attached JARs" are a property of a *transaction* not a state, thus a state only has specific meaning within the context of a transaction. In this way the code that defines a state/contract can evolve over time without needing to constantly edit the ledger when new versions are released. We call this type of just-in-time selection of final logic when building a transaction as "implicit upgrades". The assumption is that data may live for much longer than code. + +The node maps attachments to class paths to classloaders, which ensures that the JVM's namespace is synced with the ledger during verification and it will interpret names relative to the transaction being processed. States outside of a transaction are bound to these true "decentralised names" using an indirection intended to allow for smooth migration between versions called *contract constraints*. + +A constraint specifies what attachments can be used to implement the state's class name, with differing levels of ambiguity. Therefore a transaction's attachments must satisfy the included state's constraints. This scheme is somewhat complex, but gives developers freedom to combine multiple applications together in an agile environment where software is constantly changing, might be malicious and where trust relationships can be complex. + +The problem is that working with these sorts of sophisticated namespaces is hard, including for platform developers. + +As the Java 8 error message example shows, even implementors of complex namespaces often don't get every detail right. Corda expects developers to understand that a `com.megacorp.token.MegaToken` class they find in a transaction or deserialise out of the vault might *not* have been the same as the `com.megacorp.token.MegaToken` class they had in mind when writing a program. It might be a legitimate later version, but it may also be a totally different class in the worst case written by an adversary e.g. one that gives the adversary the right to spend the token. + +## Goals + +* Provide a way to reduce the complexity of naming and working with names in Corda by allowing for a small amount of centralisation, balanced by a reduction in developer mental load. +* Keep it optional for both zones and developers. +* Allow most developers to work just with ordinary Java class names, without needing to consider the complexities of a decentralised namespace. + +## Non-goals + +* Directly make it easier to work with "decentralised names". This might come later. + +## Design + +To make it harder to accidentally write insecure code, we would like to support a compromise configuration in which a compatibility zone can publish a map of Java package namespaces to public keys. An app/attachment JAR may only define a class in that namespace if it is signed by the given public key. Using this feature would make a zone slightly less decentralised, in order to obtain a significant reduction in mental overhead for developers. + +Example of how the network parameters would be extended, in pseudo-code: + +```kotlin +data class JavaPackageName(name: String) { + init { /* verify 'name' is a valid Java package name */ } +} + +data class NetworkParameters( + ... + val packageOwnership: Map +) +``` + +Where the `PublicKey` object can be any of the algorithms supported by signature constraints. The map defines a set of dotted package names like `com.foo.bar` where any class in that package or any sub-package of that package is considered to match (so `com.foo.bar.baz.boz.Bish` is a match but `com.foo.barrier` does not). + +When a class is loaded from an attachment or application JAR signature checking is enabled. If the package of the class matches one of the owned namespaces, the JAR must be have enough signatures to satisfy the PublicKey (there may need to be more than one if the PublicKey is composite). + +Please note the following: + +* It's OK to have unsigned JARs. +* It's OK to have JARs that are signed, but for which there are no claims in the network parameters. +* It's OK if entries in the map are removed (system becomes more open). If entries in the map are added, this could cause consensus failures if people are still using old unsigned versions of the app. +* The map specifies keys not certificate chains, therefore, the keys do not have to chain off the identity key of a zone member. App developers do not need to be members of a zone for their app to be used there. + +From a privacy and decentralisation perspective, the zone operator *may* learn who is developing apps in their zone or (in cases where a vendor makes a single app and thus it's obvious) which apps are being used. This is not ideal, but there are mitigations: + +* The privacy leak is optional. +* The zone operator still doesn't learn who is using which apps. +* There is no obligation for Java package namespaces to correlate obviously to real world identities or products. For example you could register a trivial "front" domain and claim ownership of that, then use it for your apps. The zone operator would see only a codename. + +#### Claiming a namespace + +The exact mechanism used to claim a namespace is up to the zone operator. A typical approach would be to accept an SSL certificate with the domain in it as proof of domain ownership, or to accept an email from that domain as long as the domain is using DKIM to prevent from header spoofing. + +#### The vault API + +The vault query API is an example of how tricky it can be to manage truly decentralised namespaces. The `Vault.Page` class does not include constraint information for a state. Therefore, if a generic app were to be storing states of many different types to the vault without having the specific apps installed, it might be possible for someone to create a confusing name e.g. an app created by MiniCorp could export a class named `com.megacorp.example.Token` and this would be mapped by the RPC deserialisation logic to the actual MegaCorp app - the RPC client would have no way to know this had happened, even if the user was correctly checking, which it's unlikely they would. + +The `StateMetadata` class can be easily extended to include constraint information, to make safely programming against a decentralised namespace possible. As part of this work this extension will be made. + +But the new field would still need to be used - a subtle detail that would be easy to overlook. Package namespace ownership ensures that if you have an app installed locally on the client side that implements `com.megacorp.example` , then that code is likely to match closely enough with the version that was verified by the node. + + + + + From 8e590cfc55b7d1d1839edf5dc3b8de29c5dfa058 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 12 Oct 2018 15:56:13 +0200 Subject: [PATCH 44/83] Attempt to improve the explanation at the start. --- .../package-namespace-ownership.md | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/source/design/data-model-upgrades/package-namespace-ownership.md b/docs/source/design/data-model-upgrades/package-namespace-ownership.md index aaffcf7473..2878222be1 100644 --- a/docs/source/design/data-model-upgrades/package-namespace-ownership.md +++ b/docs/source/design/data-model-upgrades/package-namespace-ownership.md @@ -18,17 +18,35 @@ A typical Java application, seen from the JVM level, has a flat namespace in whi More complex Java applications may have a nested namespace using classloaders, thus inside a JVM a class is actually a pair of (classloader pointer, class name) and this can be used to support tricks like having two different versions of the same class in use simultaneously. The downside is more complexity for the developer to deal with. When things get mixed up this can surface (in Java 8) as nonsensical error messages like "com.example.Person cannot be casted to com.example.Person". In Java 9 classloaders were finally given names so these errors make more sense. + + #### Corda namespaces -Corda has an even more complex namespace. States are identified using ordinary Java class names because programmers have to be able to write source code that works with them. But because there's no centralised coordination point and Java package namespacing is just a convention, the true name of a class when loaded and run includes the hash of the JAR that defines it. These "attached JARs" are a property of a *transaction* not a state, thus a state only has specific meaning within the context of a transaction. In this way the code that defines a state/contract can evolve over time without needing to constantly edit the ledger when new versions are released. We call this type of just-in-time selection of final logic when building a transaction as "implicit upgrades". The assumption is that data may live for much longer than code. +Corda faces an extension of the Java namespace problem - we have a global namespace in which malicious adversaries might be choosing names to be deliberately confusing. Nothing forces an app developer to follow the standard conventions for Java package or class names - someone could make an app that uses the same class name as one of your own apps. Corda needs to keep these two different classes, from different origins, separated. -The node maps attachments to class paths to classloaders, which ensures that the JVM's namespace is synced with the ledger during verification and it will interpret names relative to the transaction being processed. States outside of a transaction are bound to these true "decentralised names" using an indirection intended to allow for smooth migration between versions called *contract constraints*. +On the core ledger this is done by associating each state with an _attachment_. The attachment is the JAR file that contains the class files used by states. To load a state, a classloader is defined that uses the attachments on a transaction, and then the state class is loaded via that classloader. -A constraint specifies what attachments can be used to implement the state's class name, with differing levels of ambiguity. Therefore a transaction's attachments must satisfy the included state's constraints. This scheme is somewhat complex, but gives developers freedom to combine multiple applications together in an agile environment where software is constantly changing, might be malicious and where trust relationships can be complex. +With this infrastructure in place, the Corda node and JVM can internally keep two classes that share the same name separated. The name of the state is, in effect, a list of attachments (hashes of JAR files) combined with a regular class name. -The problem is that working with these sorts of sophisticated namespaces is hard, including for platform developers. -As the Java 8 error message example shows, even implementors of complex namespaces often don't get every detail right. Corda expects developers to understand that a `com.megacorp.token.MegaToken` class they find in a transaction or deserialise out of the vault might *not* have been the same as the `com.megacorp.token.MegaToken` class they had in mind when writing a program. It might be a legitimate later version, but it may also be a totally different class in the worst case written by an adversary e.g. one that gives the adversary the right to spend the token. + +#### Namespaces and versioning + +Names and namespaces are a critical part of how platforms of any kind handle software evolution. If component A is verifying the precise content of component B, e.g. by hashing it, then there can be no agility - component B can never be upgraded. Sometimes this is what's wanted. But usually you want the indirection of a name or set of names that stands in for some behaviour. Exactly how that behaviour is provided is abstracted away behind the mapping of the namespace to concrete artifacts. + +Versioning and resistance to malicious attack are likewise heavily interrelated, because given two different codebases that export the same names, it's possible that one is a legitimate upgrade which changes the logic behind the names in beneficial ways, and the other is an imposter that changes the logic in malicious ways. It's important to keep the differences straight, which can be hard because by their very nature, two versions of the same app tend to be nearly identical. + + + +#### Namespace complexity + +Reasoning about namespaces is hard and has historically led to security flaws in many platforms. + +Although the Corda namespace system _can_ keep overlapping but distinct apps separated, that unfortunately doesn't mean that everywhere it actually does. In a few places Corda does not currently provide all the data needed to work with full state names, although we are adding this data to RPC in Corda 4. + +Even if Corda was sure to get every detail of this right in every area, a full ecosystem consists of many programs written by app developers - not just contracts and flows, but also RPC clients, bridges from internal systems and so on. It is unreasonable to expect developers to fully keep track of Corda compound names everywhere throughout the entire pipeline of tools and processes that may surround the node: some of them will lose track of the attachments list and end up with only a class name, and others will do things like serialise to JSON in which even type names go missing. + +Although we can work on improving our support and APIs for working with sophisticated compound names, we should also allow people to work with simpler namespaces again - like just Java class names. This involves a small sacrifice of decentralisation but the increase in security is probably worth it for most developers. ## Goals @@ -38,7 +56,7 @@ As the Java 8 error message example shows, even implementors of complex namespac ## Non-goals -* Directly make it easier to work with "decentralised names". This might come later. +* Directly make it easier to work with "decentralised names". This can be a project that comes later. ## Design From 6d4bdb84b94398448505e5eee494e1734517407c Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 15 Oct 2018 12:01:15 +0100 Subject: [PATCH 45/83] Code cleanup, mostly shortening long lines (#4070) --- .../net/corda/core/flows/FinalityFlowTests.kt | 16 ++--- .../flows/WithReferencedStatesFlowTests.kt | 5 +- docs/source/changelog.rst | 63 ++++++++++++------- .../net/corda/docs/java/FlowCookbook.java | 8 +-- .../java/tutorial/helloworld/IOUFlow.java | 10 +-- .../docs/java/tutorial/twoparty/IOUFlow.java | 6 +- .../kotlin/tutorial/helloworld/IOUFlow.kt | 2 +- .../docs/kotlin/tutorial/twoparty/IOUFlow.kt | 1 - docs/source/tutorial-attachments.rst | 4 ++ .../FlowsDrainingModeContentionTest.kt | 30 ++++----- .../events/ScheduledFlowIntegrationTests.kt | 25 ++++++-- .../test/cordapp/v1/FlowCheckpointCordapp.kt | 9 ++- .../ScheduledFlowsDrainingModeTest.kt | 35 ++++++----- .../node/services/FinalityHandlerTest.kt | 8 +-- .../services/ServiceHubConcurrentUsageTest.kt | 12 ++-- .../services/events/ScheduledFlowTests.kt | 6 +- .../vault/VaultSoftLockManagerTest.kt | 18 ++++-- .../net/corda/verification/TestCommsFlow.kt | 13 ++-- .../net/corda/verification/TestNotaryFlow.kt | 13 ++-- 19 files changed, 157 insertions(+), 127 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index 08d6741580..fd8ceab510 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -2,8 +2,6 @@ package net.corda.core.flows import com.natpryce.hamkrest.and import com.natpryce.hamkrest.assertion.assert -import net.corda.testing.internal.matchers.flow.willReturn -import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithFinality import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction @@ -12,6 +10,8 @@ import net.corda.finance.POUNDS import net.corda.finance.contracts.asset.Cash import net.corda.finance.issuedBy import net.corda.testing.core.* +import net.corda.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.cordappsForPackages @@ -21,7 +21,10 @@ import org.junit.Test class FinalityFlowTests : WithFinality { companion object { private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party - private val classMockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts.asset","net.corda.finance.schemas")) + private val classMockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages( + "net.corda.finance.contracts.asset", + "net.corda.finance.schemas" + )) @JvmStatic @AfterClass @@ -33,7 +36,6 @@ class FinalityFlowTests : WithFinality { private val aliceNode = makeNode(ALICE_NAME) private val bobNode = makeNode(BOB_NAME) - private val alice = aliceNode.info.singleIdentity() private val bob = bobNode.info.singleIdentity() private val notary = mockNet.defaultNotaryIdentity @@ -59,11 +61,9 @@ class FinalityFlowTests : WithFinality { } private fun TestStartedNode.signCashTransactionWith(other: Party): SignedTransaction { - val amount = 1000.POUNDS.issuedBy(alice.ref(0)) + val amount = 1000.POUNDS.issuedBy(info.singleIdentity().ref(0)) val builder = TransactionBuilder(notary) Cash().generateIssue(builder, amount, other, notary) - return services.signInitialTransaction(builder) } - -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt index a6d007b549..16be871aa0 100644 --- a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -55,13 +55,13 @@ internal class CreateRefState : FlowLogic() { } // A flow to update a specific reference state. -internal class UpdateRefState(private val stateAndRef: StateAndRef) : FlowLogic() { +internal class UpdateRefState(private val stateAndRef: StateAndRef) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryIdentities.first() val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { addInputState(stateAndRef) - addOutputState((stateAndRef.state.data as RefState.State).update(), RefState.CONTRACT_ID) + addOutputState(stateAndRef.state.data.update(), RefState.CONTRACT_ID) addCommand(RefState.Update(), listOf(ourIdentity.owningKey)) }) return subFlow(FinalityFlow(stx)) @@ -160,5 +160,4 @@ class WithReferencedStatesFlowTests { val result = useRefTx.getOrThrow() assertEquals(updatedRefState.ref, result.tx.references.single()) } - } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 9b3e3bb571..f5fd3d3ae3 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -15,7 +15,8 @@ Unreleased * New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call. -* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive. +* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default + queries will be case sensitive. * Getter added to ``CordaRPCOps`` for the node's network parameters. @@ -32,7 +33,8 @@ Unreleased * "app", "rpc", "p2p" and "unknown" are no longer allowed as uploader values when importing attachments. These are used internally in security sensitive code. -* Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``. +* Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` + and ``MockServices``. * Change type of the ``checkpoint_value`` column. Please check the upgrade-notes on how to update your database. @@ -46,7 +48,8 @@ Unreleased rather than IllegalStateException. * The Corda JPA entities no longer implement java.io.Serializable, as this was causing persistence errors in obscure cases. - Java serialization is disabled globally in the node, but in the unlikely event you were relying on these types being Java serializable please contact us. + Java serialization is disabled globally in the node, but in the unlikely event you were relying on these types being Java + serializable please contact us. * Remove all references to the out-of-process transaction verification. @@ -104,7 +107,8 @@ Unreleased * The node's configuration is only printed on startup if ``devMode`` is ``true``, avoiding the risk of printing passwords in a production setup. -* ``NodeStartup`` will now only print node's configuration if ``devMode`` is ``true``, avoiding the risk of printing passwords in a production setup. +* ``NodeStartup`` will now only print node's configuration if ``devMode`` is ``true``, avoiding the risk of printing passwords + in a production setup. * SLF4J's MDC will now only be printed to the console if not empty. No more log lines ending with "{}". @@ -169,13 +173,15 @@ Unreleased * Added public support for creating ``CordaRPCClient`` using SSL. For this to work the node needs to provide client applications a certificate to be added to a truststore. See :doc:`tutorial-clientrpc-api` -* The node RPC broker opens 2 endpoints that are configured with ``address`` and ``adminAddress``. RPC Clients would connect to the address, while the node will connect - to the adminAddress. Previously if ssl was enabled for RPC the ``adminAddress`` was equal to ``address``. +*The node RPC broker opens 2 endpoints that are configured with ``address`` and ``adminAddress``. RPC Clients would connect + to the address, while the node will connect to the adminAddress. Previously if ssl was enabled for RPC the ``adminAddress`` + was equal to ``address``. * Upgraded H2 to v1.4.197 -* Shell (embedded available only in dev mode or via SSH) connects to the node via RPC instead of using the ``CordaRPCOps`` object directly. - To enable RPC connectivity ensure node’s ``rpcSettings.address`` and ``rpcSettings.adminAddress`` settings are present. +* Shell (embedded available only in dev mode or via SSH) connects to the node via RPC instead of using the ``CordaRPCOps`` + object directly. To enable RPC connectivity ensure node’s ``rpcSettings.address`` and ``rpcSettings.adminAddress`` settings + are present. * Changes to the network bootstrapper: @@ -183,7 +189,8 @@ Unreleased whitelist. * The CorDapp jars are also copied to each nodes' ``cordapps`` directory. -* Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation of internal data. +* Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation + of internal data. * Serializing an inner class (non-static nested class in Java, inner class in Kotlin) will be rejected explicitly by the serialization framework. Prior to this change it didn't work, but the error thrown was opaque (complaining about too few arguments @@ -191,13 +198,15 @@ Unreleased reference to the outer class) as per the Java documentation `here `_ we are disallowing this as the paradigm in general makes little sense for contract states. -* Node can be shut down abruptly by ``shutdown`` function in ``CordaRPCOps`` or gracefully (draining flows first) through ``gracefulShutdown`` command from shell. +* Node can be shut down abruptly by ``shutdown`` function in ``CordaRPCOps`` or gracefully (draining flows first) through + ``gracefulShutdown`` command from shell. * API change: ``net.corda.core.schemas.PersistentStateRef`` fields (index and txId) are now non-nullable. The fields were always effectively non-nullable - values were set from non-nullable fields of other objects. The class is used as database Primary Key columns of other entities and databases already impose those columns as non-nullable (even if JPA annotation nullable=false was absent). - In case your Cordapps use this entity class to persist data in own custom tables as non Primary Key columns refer to :doc:`upgrade-notes` for upgrade instructions. + In case your Cordapps use this entity class to persist data in own custom tables as non Primary Key columns refer to + :doc:`upgrade-notes` for upgrade instructions. * Adding a public method to check if a public key satisfies Corda recommended algorithm specs, `Crypto.validatePublicKey(java.security.PublicKey)`. For instance, this method will check if an ECC key lies on a valid curve or if an RSA key is >= 2048bits. This might @@ -248,9 +257,10 @@ Version 3.0 * Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated at CorDapp context creation time from a file source during runtime. -* Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts. - In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed. - This will ensure that the number of checkpoints will strictly diminish with time, allowing for a clean shutdown. +* Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to + support graceful node shutdown/restarts. In particular, when this mode is on, new flows through RPC will be rejected, + scheduled flows will be ignored, and initial session messages will not be consumed. This will ensure that the number of + checkpoints will strictly diminish with time, allowing for a clean shutdown. * Make the serialisation finger-printer a pluggable entity rather than hard wiring into the factory @@ -261,17 +271,19 @@ Version 3.0 * Refactored ``NodeConfiguration`` to expose ``NodeRpcOptions`` (using top-level "rpcAddress" property still works with warning). * Modified ``CordaRPCClient`` constructor to take a ``SSLConfiguration?`` additional parameter, defaulted to ``null``. -* Introduced ``CertificateChainCheckPolicy.UsernameMustMatchCommonName`` sub-type, allowing customers to optionally enforce username == CN condition on RPC SSL certificates. +* Introduced ``CertificateChainCheckPolicy.UsernameMustMatchCommonName`` sub-type, allowing customers to optionally enforce + username == CN condition on RPC SSL certificates. * Modified ``DriverDSL`` and sub-types to allow specifying RPC settings for the Node. -* Modified the ``DriverDSL`` to start Cordformation nodes allowing automatic generation of "rpcSettings.adminAddress" in case "rcpSettings.useSsl" is ``false`` (the default). +* Modified the ``DriverDSL`` to start Cordformation nodes allowing automatic generation of "rpcSettings.adminAddress" in case + "rcpSettings.useSsl" is ``false`` (the default). * Introduced ``UnsafeCertificatesFactory`` allowing programmatic generation of X509 certificates for test purposes. * JPA Mapping annotations for States extending ``CommonSchemaV1.LinearState`` and ``CommonSchemaV1.FungibleState`` on the - `participants` collection need to be moved to the actual class. This allows to properly specify the unique table name per a collection. - See: DummyDealStateSchemaV1.PersistentDummyDealState + `participants` collection need to be moved to the actual class. This allows to properly specify the unique table name per + a collection. See: DummyDealStateSchemaV1.PersistentDummyDealState * X.509 certificates now have an extension that specifies the Corda role the certificate is used for, and the role hierarchy is now enforced in the validation code. See ``net.corda.core.internal.CertRole`` for the current implementation @@ -556,7 +568,9 @@ Release 1.0 * Vault query soft locking enhancements and deprecations * removed original ``VaultService`` ``softLockedStates`` query mechanism. - * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified by set of lock ids) + * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification of + different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified by set + of lock ids) * Trader demo now issues cash and commercial paper directly from the bank node, rather than the seller node self-issuing commercial paper but labelling it as if issued by the bank. @@ -586,7 +600,8 @@ Release 1.0 This may require adjusting imports of Cash flow references and also of ``StartFlow`` permission in ``gradle.build`` files. * Removed the concept of relevancy from ``LinearState``. The ``ContractState``'s relevancy to the vault can be determined - by the flow context, the vault will process any transaction from a flow which is not derived from transaction resolution verification. + by the flow context, the vault will process any transaction from a flow which is not derived from transaction resolution + verification. * Removed the tolerance attribute from ``TimeWindowChecker`` and thus, there is no extra tolerance on the notary side anymore. @@ -769,9 +784,11 @@ Milestone 14 * Pagination simplification. Pagination continues to be optional, with following changes: - - If no PageSpecification provided then a maximum of MAX_PAGE_SIZE (200) results will be returned, otherwise we fail-fast with a ``VaultQueryException`` to alert the API user to the need to specify a PageSpecification. - Internally, we no longer need to calculate a results count (thus eliminating an expensive SQL query) unless a PageSpecification is supplied (note: that a value of -1 is returned for total_results in this scenario). - Internally, we now use the AggregateFunction capability to perform the count. + - If no PageSpecification provided then a maximum of MAX_PAGE_SIZE (200) results will be returned, otherwise we fail-fast + with a ``VaultQueryException`` to alert the API user to the need to specify a PageSpecification. Internally, we no + longer need to calculate a results count (thus eliminating an expensive SQL query) unless a PageSpecification is + supplied (note: that a value of -1 is returned for total_results in this scenario). Internally, we now use the + AggregateFunction capability to perform the count. - Paging now starts from 1 (was previously 0). * Additional Sort criteria: by StateRef (or constituents: txId, index) diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java b/docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java index b8cb3a439b..db7ea8818d 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java @@ -28,11 +28,11 @@ import java.security.GeneralSecurityException; import java.security.PublicKey; import java.time.Duration; import java.time.Instant; -import java.util.Collections; import java.util.List; import java.util.Set; import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Collections.*; import static net.corda.core.contracts.ContractsDSL.requireThat; import static net.corda.core.crypto.Crypto.generateKeyPair; @@ -528,7 +528,7 @@ public class FlowCookbook { // other required signers using ``CollectSignaturesFlow``. // The responder flow will need to call ``SignTransactionFlow``. // DOCSTART 15 - SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, Collections.emptySet(), SIGS_GATHERING.childProgressTracker())); + SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, emptySet(), SIGS_GATHERING.childProgressTracker())); // DOCEND 15 /*------------------------ @@ -557,7 +557,7 @@ public class FlowCookbook { // ``Arrays.asList(counterpartyPubKey)`` instead of // ``Collections.singletonList(counterpartyPubKey)``. // DOCSTART 54 - onceSignedTx.verifySignaturesExcept(Collections.singletonList(counterpartyPubKey)); + onceSignedTx.verifySignaturesExcept(singletonList(counterpartyPubKey)); // DOCEND 54 // We can also choose to only check the signatures that are @@ -583,7 +583,7 @@ public class FlowCookbook { // We can also choose to send it to additional parties who aren't one // of the state's participants. // DOCSTART 10 - Set additionalParties = Collections.singleton(regulator); + Set additionalParties = singleton(regulator); SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())); // DOCEND 10 diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java index fd35ab75ac..c752acd59d 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java @@ -15,7 +15,7 @@ import net.corda.core.utilities.ProgressTracker; import static com.template.TemplateContract.TEMPLATE_CONTRACT_ID; -// Replace TemplateFlow's definition with: +// Replace Initiator's definition with: @InitiatingFlow @StartableByRPC public class IOUFlow extends FlowLogic { @@ -44,7 +44,7 @@ public class IOUFlow extends FlowLogic { @Override public Void call() throws FlowException { // We retrieve the notary identity from the network map. - final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); // We create the transaction components. IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); @@ -52,12 +52,12 @@ public class IOUFlow extends FlowLogic { Command cmd = new Command<>(cmdType, getOurIdentity().getOwningKey()); // We create a transaction builder and add the components. - final TransactionBuilder txBuilder = new TransactionBuilder(notary) + TransactionBuilder txBuilder = new TransactionBuilder(notary) .addOutputState(outputState, TEMPLATE_CONTRACT_ID) .addCommand(cmd); // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Finalising the transaction. subFlow(new FinalityFlow(signedTx)); @@ -65,4 +65,4 @@ public class IOUFlow extends FlowLogic { return null; } } -// DOCEND 01 \ No newline at end of file +// DOCEND 01 diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java index a7dd8c6c22..afc88a12fb 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java @@ -43,11 +43,11 @@ public class IOUFlow extends FlowLogic { @Override public Void call() throws FlowException { // We retrieve the notary identity from the network map. - final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); // DOCSTART 02 // We create a transaction builder. - final TransactionBuilder txBuilder = new TransactionBuilder(); + TransactionBuilder txBuilder = new TransactionBuilder(); txBuilder.setNotary(notary); // We create the transaction components. @@ -63,7 +63,7 @@ public class IOUFlow extends FlowLogic { txBuilder.verify(getServiceHub()); // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Creating a session with the other party. FlowSession otherPartySession = initiateFlow(otherParty); diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt index 9c92571ce3..d560b5553d 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt @@ -18,7 +18,7 @@ import net.corda.core.utilities.ProgressTracker import com.template.TemplateContract.TEMPLATE_CONTRACT_ID -// Replace TemplateFlow's definition with: +// Replace Initiator's definition with: @InitiatingFlow @StartableByRPC class IOUFlow(val iouValue: Int, diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt index bffd19472d..f438107c88 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt @@ -14,7 +14,6 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker - // DOCEND 01 @InitiatingFlow diff --git a/docs/source/tutorial-attachments.rst b/docs/source/tutorial-attachments.rst index cbd3ad8013..4c01efb226 100644 --- a/docs/source/tutorial-attachments.rst +++ b/docs/source/tutorial-attachments.rst @@ -1,4 +1,8 @@ .. highlight:: kotlin +.. raw:: html + + + Using attachments ================= diff --git a/node/src/integration-test/kotlin/net/corda/node/modes/draining/FlowsDrainingModeContentionTest.kt b/node/src/integration-test/kotlin/net/corda/node/modes/draining/FlowsDrainingModeContentionTest.kt index 72bd435160..24b4c83340 100644 --- a/node/src/integration-test/kotlin/net/corda/node/modes/draining/FlowsDrainingModeContentionTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/modes/draining/FlowsDrainingModeContentionTest.kt @@ -1,10 +1,8 @@ package net.corda.node.modes.draining import co.paralleluniverse.fibers.Suspendable -import net.corda.testMessage.MESSAGE_CONTRACT_PROGRAM_ID -import net.corda.testMessage.Message -import net.corda.testMessage.MessageContract -import net.corda.testMessage.MessageState +import net.corda.RpcInfo +import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Command import net.corda.core.contracts.StateAndContract import net.corda.core.flows.* @@ -15,9 +13,11 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.RpcInfo -import net.corda.client.rpc.CordaRPCClient import net.corda.node.services.Permissions.Companion.all +import net.corda.testMessage.MESSAGE_CONTRACT_PROGRAM_ID +import net.corda.testMessage.Message +import net.corda.testMessage.MessageContract +import net.corda.testMessage.MessageState import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity @@ -53,7 +53,11 @@ class FlowsDrainingModeContentionTest { @Test fun `draining mode does not deadlock with acks between 2 nodes`() { val message = "Ground control to Major Tom" - driver(DriverParameters(startNodesInProcess = true, portAllocation = portAllocation, extraCordappPackagesToScan = listOf(MessageState::class.packageName))) { + driver(DriverParameters( + startNodesInProcess = true, + portAllocation = portAllocation, + extraCordappPackagesToScan = listOf(MessageState::class.packageName) + )) { val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow() val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow() @@ -70,11 +74,12 @@ class FlowsDrainingModeContentionTest { @StartableByRPC @InitiatingFlow -class ProposeTransactionAndWaitForCommit(private val data: String, private val myRpcInfo: RpcInfo, private val counterParty: Party, private val notary: Party) : FlowLogic() { - +class ProposeTransactionAndWaitForCommit(private val data: String, + private val myRpcInfo: RpcInfo, + private val counterParty: Party, + private val notary: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { - val session = initiateFlow(counterParty) val messageState = MessageState(message = Message(data), by = ourIdentity) val command = Command(MessageContract.Commands.Send(), messageState.participants.map { it.owningKey }) @@ -91,10 +96,8 @@ class ProposeTransactionAndWaitForCommit(private val data: String, private val m @InitiatedBy(ProposeTransactionAndWaitForCommit::class) class SignTransactionTriggerDrainingModeAndFinality(private val session: FlowSession) : FlowLogic() { - @Suspendable override fun call() { - val tx = subFlow(ReceiveTransactionFlow(session)) val signedTx = serviceHub.addSignature(tx) val initiatingRpcInfo = session.receive().unwrap { it } @@ -105,9 +108,8 @@ class SignTransactionTriggerDrainingModeAndFinality(private val session: FlowSes } private fun triggerDrainingModeForInitiatingNode(initiatingRpcInfo: RpcInfo) { - CordaRPCClient(initiatingRpcInfo.address).start(initiatingRpcInfo.username, initiatingRpcInfo.password).use { it.proxy.setFlowsDrainingModeEnabled(true) } } -} \ No newline at end of file +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt index f858e6189c..c7442a3cc7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt @@ -32,11 +32,14 @@ import kotlin.test.assertEquals class ScheduledFlowIntegrationTests { @StartableByRPC - class InsertInitialStateFlow(private val destination: Party, private val notary: Party, private val identity: Int = 1, private val scheduledFor: Instant? = null) : FlowLogic() { + class InsertInitialStateFlow(private val destination: Party, + private val notary: Party, + private val identity: Int = 1, + private val scheduledFor: Instant? = null) : FlowLogic() { @Suspendable override fun call() { - val scheduledState = ScheduledState(scheduledFor - ?: serviceHub.clock.instant(), ourIdentity, destination, identity.toString()) + val creationTime = scheduledFor ?: serviceHub.clock.instant() + val scheduledState = ScheduledState(creationTime, ourIdentity, destination, identity.toString()) val builder = TransactionBuilder(notary) .addOutputState(scheduledState, DummyContract.PROGRAM_ID) .addCommand(dummyCommand(ourIdentity.owningKey)) @@ -90,8 +93,20 @@ class ScheduledFlowIntegrationTests { val scheduledFor = Instant.now().plusSeconds(10) val initialiseFutures = mutableListOf>() for (i in 0 until N) { - initialiseFutures.add(aliceClient.proxy.startFlow(::InsertInitialStateFlow, bob.nodeInfo.legalIdentities.first(), defaultNotaryIdentity, i, scheduledFor).returnValue) - initialiseFutures.add(bobClient.proxy.startFlow(::InsertInitialStateFlow, alice.nodeInfo.legalIdentities.first(), defaultNotaryIdentity, i + 100, scheduledFor).returnValue) + initialiseFutures.add(aliceClient.proxy.startFlow( + ::InsertInitialStateFlow, + bob.nodeInfo.legalIdentities.first(), + defaultNotaryIdentity, + i, + scheduledFor + ).returnValue) + initialiseFutures.add(bobClient.proxy.startFlow( + ::InsertInitialStateFlow, + alice.nodeInfo.legalIdentities.first(), + defaultNotaryIdentity, + i + 100, + scheduledFor + ).returnValue) } initialiseFutures.getOrThrowAll() diff --git a/node/src/integration-test/kotlin/net/test/cordapp/v1/FlowCheckpointCordapp.kt b/node/src/integration-test/kotlin/net/test/cordapp/v1/FlowCheckpointCordapp.kt index 75ca920b44..e32f106338 100644 --- a/node/src/integration-test/kotlin/net/test/cordapp/v1/FlowCheckpointCordapp.kt +++ b/node/src/integration-test/kotlin/net/test/cordapp/v1/FlowCheckpointCordapp.kt @@ -46,12 +46,12 @@ class SendMessageFlow(private val message: Message, private val notary: Party, p progressTracker.currentStep = FINALISING_TRANSACTION - if (reciepent != null) { + return if (reciepent != null) { val session = initiateFlow(reciepent) subFlow(SendTransactionFlow(session, signedTx)) - return subFlow(FinalityFlow(signedTx, setOf(reciepent), FINALISING_TRANSACTION.childProgressTracker())) + subFlow(FinalityFlow(signedTx, setOf(reciepent), FINALISING_TRANSACTION.childProgressTracker())) } else { - return subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker())) + subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker())) } } } @@ -59,10 +59,9 @@ class SendMessageFlow(private val message: Message, private val notary: Party, p @InitiatedBy(SendMessageFlow::class) class Record(private val session: FlowSession) : FlowLogic() { - @Suspendable override fun call() { val tx = subFlow(ReceiveTransactionFlow(session, statesToRecord = StatesToRecord.ALL_VISIBLE)) serviceHub.addSignature(tx) } -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt b/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt index 9382566ab5..ba34d75539 100644 --- a/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt +++ b/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt @@ -8,8 +8,8 @@ import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.flows.SchedulableFlow import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -28,6 +28,9 @@ import kotlin.reflect.jvm.jvmName import kotlin.test.fail class ScheduledFlowsDrainingModeTest { + companion object { + private val logger = contextLogger() + } private lateinit var mockNet: InternalMockNetwork private lateinit var aliceNode: TestStartedNode @@ -38,10 +41,6 @@ class ScheduledFlowsDrainingModeTest { private var executor: ScheduledExecutorService? = null - companion object { - private val logger = loggerFor() - } - @Before fun setup() { mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"), threadPerNode = true) @@ -61,7 +60,6 @@ class ScheduledFlowsDrainingModeTest { @Test fun `flows draining mode ignores scheduled flows until unset`() { - val latch = CountDownLatch(1) var shouldFail = true @@ -73,7 +71,8 @@ class ScheduledFlowsDrainingModeTest { .map { update -> update.produced.single().state.data as ScheduledState } scheduledStates.filter { state -> !state.processed }.doOnNext { _ -> - // this is needed because there is a delay between the moment a SchedulableState gets in the Vault and the first time nextScheduledActivity is called + // This is needed because there is a delay between the moment a SchedulableState gets in the Vault and the + // first time nextScheduledActivity is called executor!!.schedule({ logger.info("Disabling flows draining mode") shouldFail = false @@ -96,8 +95,11 @@ class ScheduledFlowsDrainingModeTest { latch.await() } - data class ScheduledState(private val creationTime: Instant, val source: Party, val destination: Party, val processed: Boolean = false, override val linearId: UniqueIdentifier = UniqueIdentifier()) : SchedulableState, LinearState { - + data class ScheduledState(private val creationTime: Instant, + val source: Party, + val destination: Party, + val processed: Boolean = false, + override val linearId: UniqueIdentifier = UniqueIdentifier()) : SchedulableState, LinearState { override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { return if (!processed) { val logicRef = flowLogicRefFactory.create(ScheduledFlow::class.jvmName, thisStateRef) @@ -111,12 +113,12 @@ class ScheduledFlowsDrainingModeTest { } class InsertInitialStateFlow(private val destination: Party, private val notary: Party) : FlowLogic() { - @Suspendable override fun call() { - val scheduledState = ScheduledState(serviceHub.clock.instant(), ourIdentity, destination) - val builder = TransactionBuilder(notary).addOutputState(scheduledState, DummyContract.PROGRAM_ID).addCommand(dummyCommand(ourIdentity.owningKey)) + val builder = TransactionBuilder(notary) + .addOutputState(scheduledState, DummyContract.PROGRAM_ID) + .addCommand(dummyCommand(ourIdentity.owningKey)) val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx)) } @@ -124,10 +126,8 @@ class ScheduledFlowsDrainingModeTest { @SchedulableFlow class ScheduledFlow(private val stateRef: StateRef) : FlowLogic() { - @Suspendable override fun call() { - val state = serviceHub.toStateAndRef(stateRef) val scheduledState = state.state.data // Only run flow over states originating on this node @@ -137,9 +137,12 @@ class ScheduledFlowsDrainingModeTest { require(!scheduledState.processed) { "State should not have been previously processed" } val notary = state.state.notary val newStateOutput = scheduledState.copy(processed = true) - val builder = TransactionBuilder(notary).addInputState(state).addOutputState(newStateOutput, DummyContract.PROGRAM_ID).addCommand(dummyCommand(ourIdentity.owningKey)) + val builder = TransactionBuilder(notary) + .addInputState(state) + .addOutputState(newStateOutput, DummyContract.PROGRAM_ID) + .addCommand(dummyCommand(ourIdentity.owningKey)) val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx, setOf(scheduledState.destination))) } } -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt index f055b468fb..9a09548b82 100644 --- a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt @@ -21,7 +21,7 @@ import org.junit.After import org.junit.Test class FinalityHandlerTest { - private lateinit var mockNet: InternalMockNetwork + private val mockNet = InternalMockNetwork() @After fun cleanUp() { @@ -32,8 +32,6 @@ class FinalityHandlerTest { fun `sent to flow hospital on error and attempted retry on node restart`() { // Setup a network where only Alice has the finance CorDapp and it sends a cash tx to Bob who doesn't have the // CorDapp. Bob's FinalityHandler will error when validating the tx. - mockNet = InternalMockNetwork() - val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = setOf(FINANCE_CORDAPP))) var bob = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) @@ -82,8 +80,6 @@ class FinalityHandlerTest { } private fun TestStartedNode.getTransaction(id: SecureHash): SignedTransaction? { - return database.transaction { - services.validatedTransactions.getTransaction(id) - } + return services.validatedTransactions.getTransaction(id) } } diff --git a/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt b/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt index 4d2b8147b7..df679fcbb5 100644 --- a/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt @@ -24,8 +24,11 @@ import rx.schedulers.Schedulers import java.util.concurrent.CountDownLatch class ServiceHubConcurrentUsageTest { - - private val mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.finance.schemas", "net.corda.node.services.vault.VaultQueryExceptionsTests", Cash::class.packageName)) + private val mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages( + "net.corda.finance.schemas", + "net.corda.node.services.vault.VaultQueryExceptionsTests", + Cash::class.packageName + )) @After fun stopNodes() { @@ -34,7 +37,6 @@ class ServiceHubConcurrentUsageTest { @Test fun `operations requiring a transaction work from another thread`() { - val latch = CountDownLatch(1) var successful = false val initiatingFlow = TestFlow(mockNet.defaultNotaryIdentity) @@ -57,10 +59,8 @@ class ServiceHubConcurrentUsageTest { } class TestFlow(private val notary: Party) : FlowLogic() { - @Suspendable override fun call(): SignedTransaction { - val builder = TransactionBuilder(notary) val issuer = ourIdentity.ref(OpaqueBytes.of(0)) Cash().generateIssue(builder, 10.DOLLARS.issuedBy(issuer), ourIdentity, notary) @@ -68,4 +68,4 @@ class ServiceHubConcurrentUsageTest { return subFlow(FinalityFlow(stx)) } } -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index c9515977ba..eeb2f0582e 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -33,7 +33,6 @@ import kotlin.test.assertEquals class ScheduledFlowTests { companion object { - const val PAGE_SIZE = 20 val SORTING = Sort(listOf(Sort.SortColumn(SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID), Sort.Direction.DESC))) } @@ -168,6 +167,7 @@ class ScheduledFlowTests { assertTrue("Expect all states have run the scheduled task", statesFromB.all { it.state.data.processed }) } - private fun queryStates(vaultService: VaultService): List> = - vaultService.queryBy(VaultQueryCriteria(), sorting = SORTING).states + private fun queryStates(vaultService: VaultService): List> { + return vaultService.queryBy(VaultQueryCriteria(), sorting = SORTING).states + } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 073800fd0b..5972fcf4df 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -52,7 +52,7 @@ class NodePair(private val mockNet: InternalMockNetwork) { @InitiatingFlow abstract class AbstractClientLogic(nodePair: NodePair) : FlowLogic() { - protected val server = nodePair.server.info.singleIdentity() + private val server = nodePair.server.info.singleIdentity() protected abstract fun callImpl(): T @Suspendable override fun call() = callImpl().also { @@ -82,9 +82,12 @@ class VaultSoftLockManagerTest { private val mockVault = rigorousMock().also { doNothing().whenever(it).softLockRelease(any(), anyOrNull()) } + private val mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(ContractImpl::class.packageName), defaultFactory = { args -> object : InternalMockNetwork.MockNode(args) { - override fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, database: CordaPersistence): VaultServiceInternal { + override fun makeVaultService(keyManagementService: KeyManagementService, + services: ServicesForResolution, + database: CordaPersistence): VaultServiceInternal { val node = this val realVault = super.makeVaultService(keyManagementService, services, database) return object : VaultServiceInternal by realVault { @@ -97,13 +100,11 @@ class VaultSoftLockManagerTest { } } }) + private val nodePair = NodePair(mockNet) - @After - fun tearDown() { - mockNet.stopNodes() - } object CommandDataImpl : CommandData + class ClientLogic(nodePair: NodePair, val state: ContractState) : NodePair.AbstractClientLogic>(nodePair) { override fun callImpl() = run { subFlow(FinalityFlow(serviceHub.signInitialTransaction(TransactionBuilder(notary = ourIdentity).apply { @@ -151,6 +152,11 @@ class VaultSoftLockManagerTest { verifyNoMoreInteractions(mockVault) } + @After + fun tearDown() { + mockNet.stopNodes() + } + @Test fun `plain old state is not soft locked`() = run(false, PlainOldState(nodePair), false) diff --git a/samples/network-verifier/src/main/kotlin/net/corda/verification/TestCommsFlow.kt b/samples/network-verifier/src/main/kotlin/net/corda/verification/TestCommsFlow.kt index 3437cce254..55a3f66109 100644 --- a/samples/network-verifier/src/main/kotlin/net/corda/verification/TestCommsFlow.kt +++ b/samples/network-verifier/src/main/kotlin/net/corda/verification/TestCommsFlow.kt @@ -14,10 +14,9 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap - @StartableByRPC @InitiatingFlow -class TestCommsFlowInitiator(val x500Name: CordaX500Name? = null) : FlowLogic>() { +class TestCommsFlowInitiator(private val x500Name: CordaX500Name? = null) : FlowLogic>() { object SENDING : ProgressTracker.Step("SENDING") object RECIEVED_ALL : ProgressTracker.Step("RECIEVED_ALL") @@ -42,7 +41,7 @@ class TestCommsFlowInitiator(val x500Name: CordaX500Name? = null) : FlowLogic
  • () { +class TestCommsFlowResponder(private val otherSideSession: FlowSession) : FlowLogic() { @Suspendable override fun call() { otherSideSession.send("Hello from: " + serviceHub.myInfo.legalIdentities.first().name.toString()) } - } @CordaSerializable data class CommsTestState(val responses: List, val issuer: AbstractParty) : ContractState { override val participants: List get() = listOf(issuer) - } - @CordaSerializable object CommsTestCommand : CommandData - class CommsTestContract : Contract { override fun verify(tx: LedgerTransaction) { } -} \ No newline at end of file +} diff --git a/samples/network-verifier/src/main/kotlin/net/corda/verification/TestNotaryFlow.kt b/samples/network-verifier/src/main/kotlin/net/corda/verification/TestNotaryFlow.kt index 1dd423ff40..febc4fcfce 100644 --- a/samples/network-verifier/src/main/kotlin/net/corda/verification/TestNotaryFlow.kt +++ b/samples/network-verifier/src/main/kotlin/net/corda/verification/TestNotaryFlow.kt @@ -13,7 +13,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import co.paralleluniverse.fibers.Suspendable - @StartableByRPC class TestNotaryFlow : FlowLogic() { @@ -28,38 +27,34 @@ class TestNotaryFlow : FlowLogic() { override fun call(): String { val issueBuilder = TransactionBuilder() val notary = serviceHub.networkMapCache.notaryIdentities.first() - issueBuilder.notary = notary; + issueBuilder.notary = notary val myIdentity = serviceHub.myInfo.legalIdentities.first() - issueBuilder.addOutputState(NotaryTestState(notary.name.toString(), myIdentity), NotaryTestContract::class.qualifiedName!!) + issueBuilder.addOutputState(NotaryTestState(notary.name.toString(), myIdentity), NotaryTestContract::class.java.name) issueBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey) val signedTx = serviceHub.signInitialTransaction(issueBuilder) val issueResult = subFlow(FinalityFlow(signedTx)) progressTracker.currentStep = ISSUED val destroyBuilder = TransactionBuilder() - destroyBuilder.notary = notary; + destroyBuilder.notary = notary destroyBuilder.addInputState(issueResult.tx.outRefsOfType().first()) destroyBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey) val signedDestroyT = serviceHub.signInitialTransaction(destroyBuilder) val result = subFlow(FinalityFlow(signedDestroyT)) progressTracker.currentStep = DESTROYING progressTracker.currentStep = FINALIZED - return "notarised: " + result.notary.toString() + "::" + result.tx.id + return "notarised: ${result.notary}::${result.tx.id}" } } - @CordaSerializable data class NotaryTestState(val id: String, val issuer: AbstractParty) : ContractState { override val participants: List get() = listOf(issuer) - } - @CordaSerializable object NotaryTestCommand : CommandData - class NotaryTestContract : Contract { override fun verify(tx: LedgerTransaction) { } From 380a942954f0b632bad4aa20e933cf468f56f6e5 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 15 Oct 2018 13:44:02 +0100 Subject: [PATCH 46/83] CORDA-2030: Prevent kotlin-stdlib-jre8 from becoming an accidental transitive dependency. (#4073) --- node/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/build.gradle b/node/build.gradle index b100797903..c0a64db26b 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -79,7 +79,7 @@ dependencies { compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - runtime "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + runtimeOnly "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" From 1f835a349619408cd4f237a53511501bee5b1679 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 15 Oct 2018 15:01:51 +0100 Subject: [PATCH 47/83] Add package namespace ownership design doc to toc tree --- docs/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 9aaaeaaf73..8e191c699e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -83,6 +83,7 @@ We look forward to seeing what you can do with Corda! design/sgx-infrastructure/design.md design/threat-model/corda-threat-model.md design/data-model-upgrades/signature-constraints.md + design/data-model-upgrades/package-namespace-ownership.md .. conditional-toctree:: :caption: Participate From 2248f54f9f94b0bd29f03b1a16c7544b78cafeaf Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 15 Oct 2018 19:51:28 +0100 Subject: [PATCH 48/83] CORDA-2096: Migrate finance test classes into .test sub-packages. (#4079) --- .../finance/compat/CashExceptionSerialisationTest.kt | 10 +--------- .../flows/{ => test}/CashConfigDataFlowTest.kt | 7 +++++-- .../asset/{ => test}/DummyFungibleContract.kt | 11 ++++++----- .../finance/flows/test/CashExceptionThrowingFlow.kt | 12 ++++++++++++ .../finance/schemas/{ => test}/SampleCashSchemaV1.kt | 2 +- .../finance/schemas/{ => test}/SampleCashSchemaV2.kt | 2 +- .../finance/schemas/{ => test}/SampleCashSchemaV3.kt | 2 +- .../{ => test}/SampleCommercialPaperSchemaV1.kt | 2 +- .../{ => test}/SampleCommercialPaperSchemaV2.kt | 2 +- .../node/services/vault/VaultQueryJavaTests.java | 2 +- .../persistence/HibernateConfigurationTest.kt | 8 ++++---- .../node/services/vault/VaultQueryExceptionsTests.kt | 3 +-- .../net/corda/node/services/vault/VaultQueryTests.kt | 4 ++-- 13 files changed, 37 insertions(+), 30 deletions(-) rename finance/src/integration-test/kotlin/net/corda/finance/flows/{ => test}/CashConfigDataFlowTest.kt (74%) rename finance/src/test/kotlin/net/corda/finance/contracts/asset/{ => test}/DummyFungibleContract.kt (95%) create mode 100644 finance/src/test/kotlin/net/corda/finance/flows/test/CashExceptionThrowingFlow.kt rename finance/src/test/kotlin/net/corda/finance/schemas/{ => test}/SampleCashSchemaV1.kt (97%) rename finance/src/test/kotlin/net/corda/finance/schemas/{ => test}/SampleCashSchemaV2.kt (97%) rename finance/src/test/kotlin/net/corda/finance/schemas/{ => test}/SampleCashSchemaV3.kt (97%) rename finance/src/test/kotlin/net/corda/finance/schemas/{ => test}/SampleCommercialPaperSchemaV1.kt (98%) rename finance/src/test/kotlin/net/corda/finance/schemas/{ => test}/SampleCommercialPaperSchemaV2.kt (98%) diff --git a/finance/src/integration-test/kotlin/net/corda/finance/compat/CashExceptionSerialisationTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/compat/CashExceptionSerialisationTest.kt index ad7ac19fc8..cd6efc7f2d 100644 --- a/finance/src/integration-test/kotlin/net/corda/finance/compat/CashExceptionSerialisationTest.kt +++ b/finance/src/integration-test/kotlin/net/corda/finance/compat/CashExceptionSerialisationTest.kt @@ -1,10 +1,9 @@ package net.corda.finance.compat -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.finance.flows.CashException +import net.corda.finance.flows.test.CashExceptionThrowingFlow import net.corda.node.services.Permissions.Companion.all import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver @@ -26,10 +25,3 @@ class CashExceptionSerialisationTest { } } } - -@StartableByRPC -class CashExceptionThrowingFlow : FlowLogic() { - override fun call() { - throw CashException("BOOM!", IllegalStateException("Nope dude!")) - } -} \ No newline at end of file diff --git a/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt similarity index 74% rename from finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt rename to finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt index 4c9374075c..77316c148e 100644 --- a/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt +++ b/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt @@ -1,9 +1,10 @@ -package net.corda.finance.flows +package net.corda.finance.flows.test import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.finance.EUR import net.corda.finance.USD +import net.corda.finance.flows.CashConfigDataFlow import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThat @@ -12,7 +13,9 @@ import org.junit.Test class CashConfigDataFlowTest { @Test fun `issuable currencies are read in from node config`() { - driver(DriverParameters(notarySpecs = emptyList())) { + driver(DriverParameters( + extraCordappPackagesToScan = listOf("net.corda.finance.flows"), + notarySpecs = emptyList())) { val node = startNode(customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("EUR", "USD")))).getOrThrow() val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow() assertThat(config.issuableCurrencies).containsExactly(EUR, USD) diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/test/DummyFungibleContract.kt similarity index 95% rename from finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt rename to finance/src/test/kotlin/net/corda/finance/contracts/asset/test/DummyFungibleContract.kt index 2b6b79d9a6..aec5b1f088 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/test/DummyFungibleContract.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.asset +package net.corda.finance.contracts.asset.test import net.corda.core.contracts.* import net.corda.core.crypto.toStringShort @@ -8,9 +8,10 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction -import net.corda.finance.schemas.SampleCashSchemaV1 -import net.corda.finance.schemas.SampleCashSchemaV2 -import net.corda.finance.schemas.SampleCashSchemaV3 +import net.corda.finance.contracts.asset.OnLedgerAsset +import net.corda.finance.schemas.test.SampleCashSchemaV1 +import net.corda.finance.schemas.test.SampleCashSchemaV2 +import net.corda.finance.schemas.test.SampleCashSchemaV3 import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero @@ -18,7 +19,7 @@ import java.security.PublicKey import java.util.* class DummyFungibleContract : OnLedgerAsset() { - override fun extractCommands(commands: Collection>): List> + override fun extractCommands(commands: Collection>): List> = commands.select() data class State( diff --git a/finance/src/test/kotlin/net/corda/finance/flows/test/CashExceptionThrowingFlow.kt b/finance/src/test/kotlin/net/corda/finance/flows/test/CashExceptionThrowingFlow.kt new file mode 100644 index 0000000000..a8e03cd67f --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/flows/test/CashExceptionThrowingFlow.kt @@ -0,0 +1,12 @@ +package net.corda.finance.flows.test + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.finance.flows.CashException + +@StartableByRPC +class CashExceptionThrowingFlow : FlowLogic() { + override fun call() { + throw CashException("BOOM!", IllegalStateException("Nope dude!")) + } +} diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV1.kt similarity index 97% rename from finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV1.kt index 86efb2c5c5..b3b1262803 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.finance.schemas +package net.corda.finance.schemas.test import net.corda.core.contracts.MAX_ISSUER_REF_SIZE import net.corda.core.schemas.MappedSchema diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV2.kt similarity index 97% rename from finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV2.kt index a17f863841..ba6473f4b8 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV2.kt @@ -1,4 +1,4 @@ -package net.corda.finance.schemas +package net.corda.finance.schemas.test import net.corda.core.identity.AbstractParty import net.corda.core.schemas.CommonSchemaV1 diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV3.kt similarity index 97% rename from finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV3.kt index 987210cd84..eded7dfbdf 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCashSchemaV3.kt @@ -1,4 +1,4 @@ -package net.corda.finance.schemas +package net.corda.finance.schemas.test import net.corda.core.contracts.MAX_ISSUER_REF_SIZE import net.corda.core.identity.AbstractParty diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCommercialPaperSchemaV1.kt similarity index 98% rename from finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCommercialPaperSchemaV1.kt index 236d96fdd5..e7f5fa0def 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCommercialPaperSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.finance.schemas +package net.corda.finance.schemas.test import net.corda.core.contracts.MAX_ISSUER_REF_SIZE import net.corda.core.schemas.MappedSchema diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCommercialPaperSchemaV2.kt similarity index 98% rename from finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCommercialPaperSchemaV2.kt index 9eeac47cc0..eacd846df5 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/test/SampleCommercialPaperSchemaV2.kt @@ -1,4 +1,4 @@ -package net.corda.finance.schemas +package net.corda.finance.schemas.test import net.corda.core.contracts.MAX_ISSUER_REF_SIZE import net.corda.core.identity.AbstractParty 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 6ccb168d58..9472c5b330 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 @@ -19,7 +19,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; import net.corda.finance.contracts.DealState; import net.corda.finance.contracts.asset.Cash; import net.corda.finance.schemas.CashSchemaV1; -import net.corda.finance.schemas.SampleCashSchemaV2; +import net.corda.finance.schemas.test.SampleCashSchemaV2; import net.corda.node.services.api.IdentityServiceInternal; import net.corda.nodeapi.internal.persistence.CordaPersistence; import net.corda.nodeapi.internal.persistence.DatabaseTransaction; 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 dc1867bf19..72071192cd 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 @@ -23,11 +23,11 @@ import net.corda.finance.DOLLARS import net.corda.finance.POUNDS import net.corda.finance.SWISS_FRANCS import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.DummyFungibleContract +import net.corda.finance.contracts.asset.test.DummyFungibleContract import net.corda.finance.schemas.CashSchemaV1 -import net.corda.finance.schemas.SampleCashSchemaV1 -import net.corda.finance.schemas.SampleCashSchemaV2 -import net.corda.finance.schemas.SampleCashSchemaV3 +import net.corda.finance.schemas.test.SampleCashSchemaV1 +import net.corda.finance.schemas.test.SampleCashSchemaV2 +import net.corda.finance.schemas.test.SampleCashSchemaV3 import net.corda.finance.utils.sumCash import net.corda.node.internal.configureDatabase import net.corda.node.services.api.IdentityServiceInternal diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt index 9d3cb8a5f8..137de148c0 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt @@ -6,8 +6,7 @@ import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* import net.corda.finance.* import net.corda.finance.contracts.asset.Cash -import net.corda.finance.schemas.SampleCashSchemaV3 -import net.corda.finance.schemas.CashSchemaV1 +import net.corda.finance.schemas.test.SampleCashSchemaV3 import net.corda.testing.core.* import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 import org.assertj.core.api.Assertions.assertThatThrownBy 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 99010acca7..d7f890a7ab 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 @@ -22,8 +22,8 @@ import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.CashSchemaV1.PersistentCashState import net.corda.finance.schemas.CommercialPaperSchemaV1 -import net.corda.finance.schemas.SampleCashSchemaV2 -import net.corda.finance.schemas.SampleCashSchemaV3 +import net.corda.finance.schemas.test.SampleCashSchemaV2 +import net.corda.finance.schemas.test.SampleCashSchemaV3 import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig From 47068e6b7ad0afb3ed8df61a429470f19331b7b5 Mon Sep 17 00:00:00 2001 From: Florian Friemel Date: Mon, 15 Oct 2018 21:11:52 +0100 Subject: [PATCH 49/83] [CORDA-2077] Use latest gradle plugin version (4.0.32), set target version in core and sample CorDapps (#4038) * Upgrade gradle plugin; add target version attribute to finance and sample cordapps. * Remove '-SNAPSHOT' from gradlePluginsVersion. * Fix naming. * Update docs. * Respond to feedback. * Fix irs demo * Fix more samples * Fix more samples * Fix deployNodes * Fix deployNodes * more fixes * fix simm valuation * more fixes * more fixes * more fixes * more fixes * Publication should have *nothing* to do with cordformation and deployNodes. Remove it! And if this exposes a bug then "so be it". * Disable CorDapp signing for Cordapp Configuration and Network Verifier. * Disable CorDapp signing for SIMM Valuation Demo. * Remove remaining publishing nonsense from samples. * Workarounds fpr cordapp-configuration, network-verifier and simm-valuation-demo: JarSigner rejects jars with duplicates inside, so remove them. * Upgrade to Gradle plugin 4.0.32 and reenable CorDapp signing for samples. --- constants.properties | 2 +- docs/source/cordapp-build-systems.rst | 18 ++++++++-- experimental/notary-bft-smart/build.gradle | 10 ++++++ experimental/notary-raft/build.gradle | 9 +++++ finance/build.gradle | 2 ++ samples/attachment-demo/build.gradle | 32 ++++++++--------- samples/bank-of-corda-demo/build.gradle | 31 ++++++++--------- samples/cordapp-configuration/build.gradle | 27 ++++++++++----- .../corda/configsample/GetStringConfigFlow.kt | 25 ++++++++++++++ samples/irs-demo/build.gradle | 1 - samples/irs-demo/cordapp/build.gradle | 24 ++++++++----- samples/network-verifier/build.gradle | 23 +++++++++---- samples/notary-demo/build.gradle | 24 +++++++++---- samples/simm-valuation-demo/build.gradle | 17 ++++++++-- .../contracts-states/build.gradle | 6 ++++ .../simm-valuation-demo/flows/build.gradle | 4 +++ samples/trader-demo/build.gradle | 34 ++++++++----------- 17 files changed, 199 insertions(+), 90 deletions(-) diff --git a/constants.properties b/constants.properties index d472994d01..5bc1a09cd5 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=4.0.29 +gradlePluginsVersion=4.0.32 kotlinVersion=1.2.51 # ***************************************************************# # When incrementing platformVersion make sure to update # diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index af785ae777..7ca31b2b1a 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -198,11 +198,23 @@ CorDapps can advertise their minimum and target platform version. The minimum pl .. sourcecode:: groovy - 'Min-Platform-Version': 3 + 'Min-Platform-Version': 4 'Target-Platform-Version': 4 +Using the `cordapp` Gradle plugin, this can be achieved by putting this in your CorDapp's `build.gradle`: -In gradle, this can be achieved by modifying the jar task as shown in this example: +.. container:: codeset + + .. sourcecode:: groovy + + cordapp { + info { + targetPlatformVersion 4 + minimumPlatformVersion 4 + } + } + +Without using the `cordapp` plugin, you can achieve the same by modifying the jar task as shown in this example: .. container:: codeset @@ -211,7 +223,7 @@ In gradle, this can be achieved by modifying the jar task as shown in this examp jar { manifest { attributes( - 'Min-Platform-Version': 3 + 'Min-Platform-Version': 4 'Target-Platform-Version': 4 ) } diff --git a/experimental/notary-bft-smart/build.gradle b/experimental/notary-bft-smart/build.gradle index b4ff48b444..bcd0614686 100644 --- a/experimental/notary-bft-smart/build.gradle +++ b/experimental/notary-bft-smart/build.gradle @@ -33,3 +33,13 @@ idea { publish { name 'corda-notary-bft-smart' } + + +cordapp { + info { + name "net/corda/experimental/notary-bft-smart" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/experimental/notary-raft/build.gradle b/experimental/notary-raft/build.gradle index b07659ef2f..fc6a1894da 100644 --- a/experimental/notary-raft/build.gradle +++ b/experimental/notary-raft/build.gradle @@ -33,3 +33,12 @@ idea { publish { name 'corda-notary-raft' } + +cordapp { + info { + name "net/corda/experimental/notary-raft" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/finance/build.gradle b/finance/build.gradle index b55819f201..be028830eb 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -87,6 +87,8 @@ cordapp { info { name "net/corda/finance" vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 } } diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 71fbfe48d7..7629c833fa 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -1,11 +1,8 @@ -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' -apply plugin: 'maven-publish' sourceSets { integrationTest { @@ -27,14 +24,16 @@ dependencies { testCompile "junit:junit:$junit_version" // 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') cordaCompile project(':node-driver') } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { +def nodeTask = tasks.getByPath(':node:capsule:assemble') +def webTask = tasks.getByPath(':webserver:webcapsule:assemble') +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow", "InvokeRpc.wellKnownPartyFromX500Name", "InvokeRpc.attachmentExists", @@ -92,18 +91,6 @@ idea { } } -publishing { - publications { - jarAndSources(MavenPublication) { - from components.java - artifactId 'attachmentdemo' - - artifact sourceJar - artifact javadocJar - } - } -} - task runSender(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'net.corda.attachmentdemo.AttachmentDemoKt' @@ -125,3 +112,12 @@ jar { ) } } + +cordapp { + info { + name "net/corda/samples/attachment-demo" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index c05f0172e3..0990cc5987 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -2,10 +2,8 @@ apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' -apply plugin: 'maven-publish' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" @@ -14,8 +12,8 @@ dependencies { cordapp project(':finance') // 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(':client:jfx') cordaCompile project(':client:rpc') @@ -32,7 +30,9 @@ dependencies { testCompile "junit:junit:$junit_version" } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { +def nodeTask = tasks.getByPath(':node:capsule:assemble') +def webTask = tasks.getByPath(':webserver:webcapsule:assemble') +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) { nodeDefaults { cordapp project(':finance') } @@ -82,18 +82,6 @@ idea { } } -publishing { - publications { - jarAndSources(MavenPublication) { - from components.java - artifactId 'bankofcorda' - - artifact sourceJar - artifact javadocJar - } - } -} - task runRPCCashIssue(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'net.corda.bank.IssueCash' @@ -123,3 +111,12 @@ jar { ) } } + +cordapp { + info { + name "net/corda/samples/bank-of-corda-demo" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle index b1f121b9d1..2203482080 100644 --- a/samples/cordapp-configuration/build.gradle +++ b/samples/cordapp-configuration/build.gradle @@ -1,17 +1,20 @@ apply plugin: 'kotlin' -apply plugin: 'java' +apply plugin: 'idea' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' dependencies { - cordaCompile project(":core") - cordaCompile project(":node-api") - cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - cordaRuntime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + cordaCompile project(':core') + cordaCompile project(':node-api') runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + + // Corda integration dependencies + cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts') } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { +def nodeTask = tasks.getByPath(':node:capsule:assemble') +def webTask = tasks.getByPath(':webserver:webcapsule:assemble') +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) { directory file("$buildDir/nodes") nodeDefaults { cordapps = [] @@ -25,7 +28,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10003 adminPort 10004 } - rpcUsers = [] extraConfig = ['h2Settings.address' : 'localhost:10005'] } node { @@ -54,4 +56,13 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } extraConfig = ['h2Settings.address' : 'localhost:10013'] } -} \ No newline at end of file +} + +cordapp { + info { + name "net/corda/samples/cordapp-configuration" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/GetStringConfigFlow.kt b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/GetStringConfigFlow.kt index 7fbbaeff77..87cafce820 100644 --- a/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/GetStringConfigFlow.kt +++ b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/GetStringConfigFlow.kt @@ -1,8 +1,14 @@ package net.corda.configsample import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.ProgressTracker @StartableByRPC @@ -17,3 +23,22 @@ class GetStringConfigFlow(private val configKey: String) : FlowLogic() { return config.getString(configKey) } } + + + +@CordaSerializable +data class GetStringTestState(val responses: List, val issuer: AbstractParty) : ContractState { + override val participants: List + get() = listOf(issuer) + +} + + +@CordaSerializable +object GetStringTestCommand : CommandData + + +class GetStringTestContract : Contract { + override fun verify(tx: LedgerTransaction) { + } +} \ No newline at end of file diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index f1fb1ef397..74bb3241a3 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -21,7 +21,6 @@ ext['jackson.version'] = "$jackson_version" ext['dropwizard-metrics.version'] = "$metrics_version" ext['mockito.version'] = "$mockito_version" -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'org.springframework.boot' diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index fba2b8f523..0359cc52d1 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -1,11 +1,8 @@ -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'net.corda.plugins.cordapp' -apply plugin: 'maven-publish' apply plugin: 'application' mainClassName = 'net.corda.irs.IRSDemo' @@ -31,7 +28,7 @@ dependencies { cordapp project(':finance') // Corda integration dependencies - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') // Cordapp dependencies @@ -57,7 +54,8 @@ def rpcUsersList = [ ]] ] -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { +def nodeTask = tasks.getByPath(':node:capsule:assemble') +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) { node { name "O=Notary Service,L=Zurich,C=CH" @@ -112,7 +110,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } -task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar']) { +task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar', nodeTask]) { node { name "O=Notary Service,L=Zurich,C=CH" @@ -159,15 +157,25 @@ tasks.withType(CreateStartScripts).each { task -> idea { module { - downloadJavadoc = true // defaults to false + downloadJavadoc = true downloadSources = true } } jar { from sourceSets.test.output + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } artifacts { demoArtifacts jar -} \ No newline at end of file +} + +cordapp { + info { + name "net/corda/irs-demo" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/samples/network-verifier/build.gradle b/samples/network-verifier/build.gradle index 0245d6027f..eea17b8ae2 100644 --- a/samples/network-verifier/build.gradle +++ b/samples/network-verifier/build.gradle @@ -1,18 +1,20 @@ apply plugin: 'kotlin' -apply plugin: 'java' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' dependencies { - cordaCompile project(":core") - cordaCompile project(":node-api") - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaCompile project(':core') + cordaCompile project(':node-api') testCompile project(":test-utils") testCompile "junit:junit:$junit_version" + + // Corda integration dependencies + cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts') } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { +def nodeTask = tasks.getByPath(':node:capsule:assemble') +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) { ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] directory "./build/nodes" @@ -48,4 +50,13 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } extraConfig = ['h2Settings.address' : 'localhost:0'] } -} \ No newline at end of file +} + +cordapp { + info { + name "net/corda/samples/network-verifier" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index acf513708c..ad7f9110e1 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -16,8 +16,8 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // 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(':client:jfx') cordaCompile project(':client:rpc') @@ -28,9 +28,12 @@ dependencies { cordapp project(':experimental:notary-bft-smart') } +def nodeTask = tasks.getByPath(':node:capsule:assemble') +def webTask = tasks.getByPath(':webserver:webcapsule:assemble') + task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom']) -task deployNodesSingle(type: Cordform, dependsOn: 'jar') { +task deployNodesSingle(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { directory file("$buildDir/nodes/nodesSingle") nodeDefaults { extraConfig = [h2Settings: [address: "localhost:0"]] @@ -55,7 +58,7 @@ task deployNodesSingle(type: Cordform, dependsOn: 'jar') { } } -task deployNodesCustom(type: Cordform, dependsOn: 'jar') { +task deployNodesCustom(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { directory file("$buildDir/nodes/nodesCustom") nodeDefaults { extraConfig = [h2Settings: [address: "localhost:0"]] @@ -83,7 +86,7 @@ task deployNodesCustom(type: Cordform, dependsOn: 'jar') { } } -task deployNodesRaft(type: Cordform, dependsOn: 'jar') { +task deployNodesRaft(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { def className = "net.corda.notary.raft.RaftNotaryService" directory file("$buildDir/nodes/nodesRaft") nodeDefaults { @@ -151,7 +154,7 @@ task deployNodesRaft(type: Cordform, dependsOn: 'jar') { } } -task deployNodesBFT(type: Cordform, dependsOn: 'jar') { +task deployNodesBFT(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { def clusterAddresses = ["localhost:11000", "localhost:11010", "localhost:11020", "localhost:11030"] def className = "net.corda.notary.bftsmart.BftSmartNotaryService" directory file("$buildDir/nodes/nodesBFT") @@ -250,3 +253,12 @@ jar { ) } } + +cordapp { + info { + name "net/corda/samples/notary-demo" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 8fc1e65c05..27a7b83e1b 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -4,7 +4,6 @@ allprojects { } } -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' @@ -27,7 +26,7 @@ configurations { } dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + cordaCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // The SIMM demo CorDapp depends upon Cash CorDapp features cordapp project(':finance') @@ -62,7 +61,9 @@ dependencies { testCompile "org.assertj:assertj-core:$assertj_version" } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { +def nodeTask = tasks.getByPath(':node:capsule:assemble') +def webTask = tasks.getByPath(':webserver:webcapsule:assemble') +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) { directory file("$buildDir/nodes") nodeDefaults { cordapp project(':finance') @@ -70,6 +71,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapp project(':samples:simm-valuation-demo:flows') rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] } + signing { + enabled false + } node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] @@ -137,3 +141,10 @@ task integrationTest(type: Test, dependsOn: []) { testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } + +cordapp { + info { + vendor = 'R3' + targetPlatformVersion = corda_platform_version.toInteger() + } +} diff --git a/samples/simm-valuation-demo/contracts-states/build.gradle b/samples/simm-valuation-demo/contracts-states/build.gradle index 146966bbee..2849ad30ff 100644 --- a/samples/simm-valuation-demo/contracts-states/build.gradle +++ b/samples/simm-valuation-demo/contracts-states/build.gradle @@ -6,6 +6,12 @@ def shrinkJar = file("$buildDir/libs/${project.name}-${project.version}-tiny.jar cordapp { info { vendor = 'R3' + targetPlatformVersion = corda_platform_version.toInteger() + } + signing { + // We need to sign the output of the "shrink" task, + // but the jar signer doesn't support that yet. + enabled false } } diff --git a/samples/simm-valuation-demo/flows/build.gradle b/samples/simm-valuation-demo/flows/build.gradle index fd3b512fab..4520f321dd 100644 --- a/samples/simm-valuation-demo/flows/build.gradle +++ b/samples/simm-valuation-demo/flows/build.gradle @@ -4,6 +4,10 @@ apply plugin: 'net.corda.plugins.cordapp' cordapp { info { vendor = 'R3' + targetPlatformVersion = corda_platform_version.toInteger() + } + signing { + enabled false } } diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index afd027fdbc..f8ffd4edf3 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -1,11 +1,8 @@ -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' -apply plugin: 'maven-publish' sourceSets { integrationTest { @@ -29,8 +26,8 @@ dependencies { cordapp project(':finance') // 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') // Corda Plugins: dependent flows and services @@ -41,7 +38,9 @@ dependencies { testCompile "org.assertj:assertj-core:${assertj_version}" } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { +def nodeTask = tasks.getByPath(':node:capsule:assemble') +def webTask = tasks.getByPath(':webserver:webcapsule:assemble') +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["ALL"]]] directory "./build/nodes" @@ -104,18 +103,6 @@ idea { } } -publishing { - publications { - jarAndSources(MavenPublication) { - from components.java - artifactId 'traderdemo' - - artifact sourceJar - artifact javadocJar - } - } -} - task runBank(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'net.corda.traderdemo.TraderDemoKt' @@ -136,4 +123,13 @@ jar { 'Automatic-Module-Name': 'net.corda.samples.demos.trader' ) } -} \ No newline at end of file +} + +cordapp { + info { + name "net/corda/samples/trader-demo" + vendor "Corda Open Source" + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + } +} From 38517af8f3cb14ddc8581d545e8ee29d854c46c1 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Tue, 16 Oct 2018 10:00:32 +0100 Subject: [PATCH 50/83] CORDA-1707 Tests to prove bug doesn't exist. (#4075) --- .../net/corda/node/flows/FlowRetryTest.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt index cc5361ee4c..92334a7548 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt @@ -2,8 +2,10 @@ package net.corda.node.flows import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient +import net.corda.core.CordaRuntimeException import net.corda.core.flows.* import net.corda.core.identity.Party +import net.corda.core.internal.IdempotentFlow import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.ProgressTracker @@ -16,6 +18,8 @@ import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.User +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.hibernate.exception.ConstraintViolationException import org.junit.Before import org.junit.Test import java.lang.management.ManagementFactory @@ -51,6 +55,42 @@ class FlowRetryTest { assertNotNull(result) assertEquals("$numSessions:$numIterations", result) } + + @Test + fun `flow gives up after number of exceptions, even if this is the first line of the flow`() { + val user = User("mark", "dadada", setOf(Permissions.startFlow())) + assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy { + driver(DriverParameters( + startNodesInProcess = isQuasarAgentSpecified(), + notarySpecs = emptyList() + )) { + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + + val result = CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use { + it.proxy.startFlow(::RetryFlow).returnValue.getOrThrow() + } + result + } + } + } + + @Test + fun `flow that throws in constructor throw for the RPC client that attempted to start them`() { + val user = User("mark", "dadada", setOf(Permissions.startFlow())) + assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy { + driver(DriverParameters( + startNodesInProcess = isQuasarAgentSpecified(), + notarySpecs = emptyList() + )) { + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + + val result = CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use { + it.proxy.startFlow(::ThrowingFlow).returnValue.getOrThrow() + } + result + } + } + } } fun isQuasarAgentSpecified(): Boolean { @@ -60,6 +100,8 @@ fun isQuasarAgentSpecified(): Boolean { class ExceptionToCauseRetry : SQLException("deadlock") +class ExceptionToCauseFiniteRetry : ConstraintViolationException("Faked violation", SQLException("Fake"), "Fake name") + @StartableByRPC @InitiatingFlow class InitiatorFlow(private val sessionsCount: Int, private val iterationsCount: Int, private val other: Party) : FlowLogic() { @@ -157,3 +199,42 @@ data class SessionInfo(val sessionNum: Int, val iterationsCount: Int) enum class Step { First, BeforeInitiate, AfterInitiate, AfterInitiateSendReceive, BeforeSend, AfterSend, BeforeReceive, AfterReceive } data class Visited(val sessionNum: Int, val iterationNum: Int, val step: Step) + +@StartableByRPC +class RetryFlow() : FlowLogic(), IdempotentFlow { + companion object { + object FIRST_STEP : ProgressTracker.Step("Step one") + + fun tracker() = ProgressTracker(FIRST_STEP) + } + + override val progressTracker = tracker() + + @Suspendable + override fun call(): String { + progressTracker.currentStep = FIRST_STEP + throw ExceptionToCauseFiniteRetry() + return "Result" + } +} + +@StartableByRPC +class ThrowingFlow() : FlowLogic(), IdempotentFlow { + companion object { + object FIRST_STEP : ProgressTracker.Step("Step one") + + fun tracker() = ProgressTracker(FIRST_STEP) + } + + override val progressTracker = tracker() + + init { + throw IllegalStateException("This flow can never be ") + } + + @Suspendable + override fun call(): String { + progressTracker.currentStep = FIRST_STEP + return "Result" + } +} \ No newline at end of file From 87a65855738256aa8fa178223f1e85bcea99c453 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 16 Oct 2018 10:51:57 +0100 Subject: [PATCH 51/83] Documents default node tables. (#4077) * Documents default node tables. * Addresses review comment. --- docs/source/node-database.rst | 89 +++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/docs/source/node-database.rst b/docs/source/node-database.rst index 924f52ac4c..f9c7e3a06c 100644 --- a/docs/source/node-database.rst +++ b/docs/source/node-database.rst @@ -1,20 +1,22 @@ Node database ============= -Default in-memory database --------------------------- +.. contents:: + +Configuring the node database +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +H2 +-- By default, nodes store their data in an H2 database. See :doc:`node-database-access-h2`. +Nodes can also be configured to use PostgreSQL and SQL Server. However, these are experimental community contributions. +The Corda continuous integration pipeline does not run unit tests or integration tests of these databases. + PostgreSQL ---------- -Nodes can also be configured to use PostgreSQL 9.6, using PostgreSQL JDBC Driver 42.1.4. - -.. warning:: This is an experimental community contribution. The Corda continuous integration pipeline does not run unit - tests or integration tests of this feature. - -Configuration -~~~~~~~~~~~~~ -Here is an example node configuration for PostgreSQL: +Nodes can also be configured to use PostgreSQL 9.6, using PostgreSQL JDBC Driver 42.1.4. Here is an example node +configuration for PostgreSQL: .. sourcecode:: groovy @@ -40,15 +42,9 @@ Note that: the schema search path must be set explicitly for the user. SQLServer ----------- -Nodes also have untested support for Microsoft SQL Server 2017, using Microsoft JDBC Driver 6.2 for SQL Server. - -.. warning:: This is an experimental community contribution, and is currently untested. We welcome pull requests to add - tests and additional support for this feature. - -Configuration -~~~~~~~~~~~~~ -Here is an example node configuration for SQLServer: +--------- +Nodes also have untested support for Microsoft SQL Server 2017, using Microsoft JDBC Driver 6.2 for SQL Server. Here is +an example node configuration for SQLServer: .. sourcecode:: groovy @@ -69,5 +65,58 @@ Note that: * The ``database.schema`` property is optional and is ignored as of release 3.1. * Ensure the directory referenced by jarDirs contains only one JDBC driver JAR file; by the default, sqljdbc_6.2/enu/contains two JDBC JAR file for different Java versions. -======= +Node database tables +^^^^^^^^^^^^^^^^^^^^ + +By default, the node database has the following tables: + ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Table name | Columns | ++=============================+==========================================================================================================================================================================================================+ +| DATABASECHANGELOG | ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, EXECTYPE, MD5SUM, DESCRIPTION, COMMENTS, TAG, LIQUIBASE, CONTEXTS, LABELS, DEPLOYMENT_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| DATABASECHANGELOGLOCK | ID, LOCKED, LOCKGRANTED, LOCKEDBY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_ATTACHMENTS | ATT_ID, CONTENT, FILENAME, INSERTION_DATE, UPLOADER | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_ATTACHMENTS_CONTRACTS | ATT_ID, CONTRACT_CLASS_NAME | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_CHECKPOINTS | CHECKPOINT_ID, CHECKPOINT_VALUE | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_CONTRACT_UPGRADES | STATE_REF, CONTRACT_CLASS_NAME | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_IDENTITIES | PK_HASH, IDENTITY_VALUE | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_INFOS | NODE_INFO_ID, NODE_INFO_HASH, PLATFORM_VERSION, SERIAL | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_INFO_HOSTS | HOST_NAME, PORT, NODE_INFO_ID, HOSTS_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_INFO_PARTY_CERT | PARTY_NAME, ISMAIN, OWNING_KEY_HASH, PARTY_CERT_BINARY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_LINK_NODEINFO_PARTY | NODE_INFO_ID, PARTY_NAME | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_MESSAGE_IDS | MESSAGE_ID, INSERTION_TIME, SENDER, SEQUENCE_NUMBER | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_NAMES_IDENTITIES | NAME, PK_HASH | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_OUR_KEY_PAIRS | PUBLIC_KEY_HASH, PRIVATE_KEY, PUBLIC_KEY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_PROPERTIES | PROPERTY_KEY, PROPERTY_VALUE | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_SCHEDULED_STATES | OUTPUT_INDEXTRANSACTION_IDSCHEDULED_AT | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_TRANSACTIONS | TX_ID, TRANSACTION_VALUE, STATE_MACHINE_RUN_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_FUNGIBLE_STATES | OUTPUT_INDEX, TRANSACTION_ID, ISSUER_NAME, ISSUER_REF, OWNER_NAME, QUANTITY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_FUNGIBLE_STATES_PARTS | OUTPUT_INDEX, TRANSACTION_ID, PARTICIPANTS | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_LINEAR_STATES | OUTPUT_INDEX, TRANSACTION_ID, EXTERNAL_ID, UUID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_LINEAR_STATES_PARTS | OUTPUT_INDEX, TRANSACTION_ID, PARTICIPANTS | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_STATES | OUTPUT_INDEX, TRANSACTION_ID, CONSUMED_TIMESTAMP, CONTRACT_STATE_CLASS_NAME, LOCK_ID, LOCK_TIMESTAMP, NOTARY_NAME, RECORDED_TIMESTAMP, STATE_STATUS, RELEVANCY_STATUS, CONSTRAINT_TYPE, CONSTRAINT_DATA | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_TRANSACTION_NOTES | SEQ_NO, NOTE, TRANSACTION_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ From 68d736dd8116636d8f7b95b3ad5660b3f6b93095 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Tue, 16 Oct 2018 11:16:28 +0100 Subject: [PATCH 52/83] Doorman can sign TLS certs directly. (#4078) --- .../net/corda/core/internal/CertRole.kt | 17 ++-- .../net/corda/core/internal/CertRoleTests.kt | 79 ++++++++++++++++++- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt index b53f8977e8..f2946d3c72 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt @@ -26,20 +26,25 @@ import java.security.cert.X509Certificate enum class CertRole(val validParents: NonEmptySet, val isIdentity: Boolean, val isWellKnown: Boolean) : ASN1Encodable { /** Signing certificate for the Doorman CA. */ DOORMAN_CA(NonEmptySet.of(null), false, false), + /** Signing certificate for the network map. */ NETWORK_MAP(NonEmptySet.of(null), false, false), + /** Well known (publicly visible) identity of a service (such as notary). */ SERVICE_IDENTITY(NonEmptySet.of(DOORMAN_CA), true, true), + /** Node level CA from which the TLS and well known identity certificates are issued. */ NODE_CA(NonEmptySet.of(DOORMAN_CA), false, false), + + // [DOORMAN_CA] is also added as a valid parent of [TLS] and [LEGAL_IDENTITY] for backwards compatibility purposes + // (eg. if we decide [TLS] has its own [ROOT_CA] and [DOORMAN_CA] directly issues [TLS] and [LEGAL_IDENTITY]; thus, + // there won't be a requirement for [NODE_CA]). /** Transport layer security certificate for a node. */ - TLS(NonEmptySet.of(NODE_CA), false, false), + TLS(NonEmptySet.of(DOORMAN_CA, NODE_CA), false, false), + /** Well known (publicly visible) identity of a legal entity. */ - // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [DOORMAN_CA] is also added - // as a valid parent of [LEGAL_IDENTITY] for backwards compatibility purposes (eg. if we decide TLS has its - // own Root CA and Doorman CA directly issues Legal Identities; thus, there won't be a requirement for - // Node CA). Consider removing [DOORMAN_CA] from [validParents] when the model is finalised. LEGAL_IDENTITY(NonEmptySet.of(DOORMAN_CA, NODE_CA), true, true), + /** Confidential (limited visibility) identity of a legal entity. */ CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false); @@ -88,4 +93,4 @@ enum class CertRole(val validParents: NonEmptySet, val isIdentity: Bo fun isValidParent(parent: CertRole?): Boolean = parent in validParents override fun toASN1Primitive(): ASN1Primitive = ASN1Integer(this.ordinal + 1L) -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt index 60f81927c8..49653648de 100644 --- a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt +++ b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt @@ -1,9 +1,12 @@ package net.corda.core.internal +import net.corda.core.crypto.Crypto +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities import org.bouncycastle.asn1.ASN1Integer import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import javax.security.auth.x500.X500Principal +import kotlin.test.* class CertRoleTests { @Test @@ -22,4 +25,74 @@ class CertRoleTests { // Outside of the range of integers assertFailsWith { CertRole.getInstance(ASN1Integer(Integer.MAX_VALUE + 1L)) } } -} \ No newline at end of file + + @Test + fun `check cert roles verify for various cert hierarchies`(){ + + // Testing for various certificate hierarchies (with or without NodeCA). + // ROOT -> Intermediate Root -> Doorman -> NodeCA -> Legal Identity cert -> Confidential key cert + // -> NodeCA -> TLS + // -> Legal Identity cert -> Confidential key cert + // -> TLS + val rootSubject = X500Principal("CN=Root,O=R3 Ltd,L=London,C=GB") + val intermediateRootSubject = X500Principal("CN=Intermediate Root,O=R3 Ltd,L=London,C=GB") + val doormanSubject = X500Principal("CN=Doorman,O=R3 Ltd,L=London,C=GB") + val nodeSubject = X500Principal("CN=Node,O=R3 Ltd,L=London,C=GB") + + val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(rootSubject, rootKeyPair) + val rootCertRole = CertRole.extract(rootCert) + + val intermediateRootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Note that [CertificateType.ROOT_CA] is used for both root and intermediate root. + val intermediateRootCert = X509Utilities.createCertificate(CertificateType.ROOT_CA, rootCert, rootKeyPair, intermediateRootSubject, intermediateRootKeyPair.public) + val intermediateRootCertRole = CertRole.extract(intermediateRootCert) + + val doormanKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Note that [CertificateType.INTERMEDIATE_CA] has actually role = CertRole.DOORMAN_CA, see [CertificateType] in [X509Utilities]. + val doormanCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateRootCert, intermediateRootKeyPair, doormanSubject, doormanKeyPair.public) + val doormanCertRole = CertRole.extract(doormanCert)!! + + val nodeCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val nodeCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, doormanCert, doormanKeyPair, nodeSubject, nodeCAKeyPair.public) + val nodeCACertRole = CertRole.extract(nodeCACert)!! + + val tlsKeyPairFromNodeCA = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCertFromNodeCA = X509Utilities.createCertificate(CertificateType.TLS, nodeCACert, nodeCAKeyPair, nodeSubject, tlsKeyPairFromNodeCA.public) + val tlsCertFromNodeCARole = CertRole.extract(tlsCertFromNodeCA)!! + + val tlsKeyPairFromDoorman = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCertFromDoorman = X509Utilities.createCertificate(CertificateType.TLS, doormanCert, doormanKeyPair, nodeSubject, tlsKeyPairFromDoorman.public) + val tlsCertFromDoormanRole = CertRole.extract(tlsCertFromDoorman)!! + + val legalIDKeyPairFromNodeCA = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val legalIDCertFromNodeCA = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCACert, nodeCAKeyPair, nodeSubject, legalIDKeyPairFromNodeCA.public) + val legalIDCertFromNodeCARole = CertRole.extract(legalIDCertFromNodeCA)!! + + val legalIDKeyPairFromDoorman = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val legalIDCertFromDoorman = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, doormanCert, doormanKeyPair, nodeSubject, legalIDKeyPairFromDoorman.public) + val legalIDCertFromDoormanRole = CertRole.extract(legalIDCertFromDoorman)!! + + val confidentialKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val confidentialCert = X509Utilities.createCertificate(CertificateType.CONFIDENTIAL_LEGAL_IDENTITY, legalIDCertFromNodeCA, legalIDKeyPairFromNodeCA, nodeSubject, confidentialKeyPair.public) + val confidentialCertRole = CertRole.extract(confidentialCert)!! + + assertNull(rootCertRole) + assertNull(intermediateRootCertRole) + assertEquals(tlsCertFromNodeCARole, tlsCertFromDoormanRole) + assertEquals(legalIDCertFromNodeCARole, legalIDCertFromDoormanRole) + + assertTrue { doormanCertRole.isValidParent(intermediateRootCertRole) } // Doorman is signed by Intermediate Root. + assertTrue { nodeCACertRole.isValidParent(doormanCertRole) } // NodeCA is signed by Doorman. + assertTrue { tlsCertFromNodeCARole.isValidParent(nodeCACertRole) } // TLS is signed by NodeCA. + assertTrue { tlsCertFromDoormanRole.isValidParent(doormanCertRole) } // TLS can also be signed by Doorman. + assertTrue { legalIDCertFromNodeCARole.isValidParent(nodeCACertRole) } // Legal Identity is signed by NodeCA. + assertTrue { legalIDCertFromDoormanRole.isValidParent(doormanCertRole) } // Legal Identity can also be signed by Doorman. + assertTrue { confidentialCertRole.isValidParent(legalIDCertFromNodeCARole) } // Confidential key cert is signed by Legal Identity. + + assertFalse { legalIDCertFromDoormanRole.isValidParent(tlsCertFromDoormanRole) } // Legal Identity cannot be signed by TLS. + assertFalse { tlsCertFromNodeCARole.isValidParent(legalIDCertFromNodeCARole) } // TLS cannot be signed by Legal Identity. + assertFalse { confidentialCertRole.isValidParent(nodeCACertRole) } // Confidential key cert cannot be signed by NodeCA. + assertFalse { confidentialCertRole.isValidParent(doormanCertRole) } // Confidential key cert cannot be signed by Doorman. + } +} From 715c38766d1bc6520afee2a8d2441cd4ba1ccc71 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 16 Oct 2018 16:33:04 +0100 Subject: [PATCH 53/83] CORDA-2109: Fix a bug that prevents consecutive multiparty contract upgrades The contract upgrade handler assumes that the state to be upgraded is created by a WireTransaction. This breaks the upgrade process if it was in fact issued by a ContractUpgradeWireTransactions or a NotaryChangeWireTransaction. --- .../core/flows/ContractUpgradeFlowTest.kt | 96 ++++++++++--------- .../corda/core/flows/mixins/WithContracts.kt | 10 +- .../corda/node/services/CoreFlowHandlers.kt | 2 +- .../testing/contracts/DummyContractV3.kt | 36 +++++++ 4 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV3.kt diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 8d30b83b62..889b8ceeee 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -3,15 +3,12 @@ package net.corda.core.flows import com.natpryce.hamkrest.* import com.natpryce.hamkrest.assertion.assert import net.corda.core.contracts.* -import net.corda.testing.internal.matchers.flow.willReturn -import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithFinality import net.corda.core.identity.AbstractParty import net.corda.core.internal.Emoji import net.corda.core.transactions.ContractUpgradeLedgerTransaction import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.USD @@ -20,9 +17,12 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 +import net.corda.testing.contracts.DummyContractV3 import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity +import net.corda.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.cordappsForPackages @@ -57,54 +57,67 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { aliceNode.finalise(stx, bob) - val atx = aliceNode.getValidatedTransaction(stx) - val btx = bobNode.getValidatedTransaction(stx) + val aliceTx = aliceNode.getValidatedTransaction(stx) + val bobTx = bobNode.getValidatedTransaction(stx) // The request is expected to be rejected because party B hasn't authorised the upgrade yet. assert.that( - aliceNode.initiateDummyContractUpgrade(atx), + aliceNode.initiateContractUpgrade(aliceTx, DummyContractV2::class), willThrow()) - // Party B authorise the contract state upgrade, and immediately deauthorise the same. - assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn()) - assert.that(bobNode.deauthoriseContractUpgrade(btx), willReturn()) + // Party B authorises the contract state upgrade, and immediately de-authorises the same. + assert.that(bobNode.authoriseContractUpgrade(bobTx, DummyContractV2::class), willReturn()) + assert.that(bobNode.deauthoriseContractUpgrade(bobTx), willReturn()) - // The request is expected to be rejected because party B has subsequently deauthorised a previously authorised upgrade. + // The request is expected to be rejected because party B has subsequently de-authorised a previously authorised upgrade. assert.that( - aliceNode.initiateDummyContractUpgrade(atx), + aliceNode.initiateContractUpgrade(aliceTx, DummyContractV2::class), willThrow()) - // Party B authorise the contract state upgrade - assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn()) + // Party B authorises the contract state upgrade. + assert.that(bobNode.authoriseContractUpgrade(bobTx, DummyContractV2::class), willReturn()) // Party A initiates contract upgrade flow, expected to succeed this time. assert.that( - aliceNode.initiateDummyContractUpgrade(atx), + aliceNode.initiateContractUpgrade(aliceTx, DummyContractV2::class), willReturn( - aliceNode.hasDummyContractUpgradeTransaction() - and bobNode.hasDummyContractUpgradeTransaction())) + aliceNode.hasContractUpgradeTransaction() + and bobNode.hasContractUpgradeTransaction())) + + val upgradedState = aliceNode.getStateFromVault(DummyContractV2.State::class) + + // We now test that the upgraded state can be upgraded further, to V3. + // Party B authorises the contract state upgrade. + assert.that(bobNode.authoriseContractUpgrade(upgradedState, DummyContractV3::class), willReturn()) + + // Party A initiates contract upgrade flow which is expected to succeed. + assert.that( + aliceNode.initiateContractUpgrade(upgradedState, DummyContractV3::class), + willReturn( + aliceNode.hasContractUpgradeTransaction() + and bobNode.hasContractUpgradeTransaction())) } private fun TestStartedNode.issueCash(amount: Amount = Amount(1000, USD)) = - services.startFlow(CashIssueFlow(amount, OpaqueBytes.of(1), notary)) - .andRunNetwork() - .resultFuture.getOrThrow() + services.startFlow(CashIssueFlow(amount, OpaqueBytes.of(1), notary)) + .andRunNetwork() + .resultFuture.getOrThrow() private fun TestStartedNode.getBaseStateFromVault() = getStateFromVault(ContractState::class) private fun TestStartedNode.getCashStateFromVault() = getStateFromVault(CashV2.State::class) private fun hasIssuedAmount(expected: Amount>) = - hasContractState(has(CashV2.State::amount, equalTo(expected))) + hasContractState(has(CashV2.State::amount, equalTo(expected))) private fun belongsTo(vararg recipients: AbstractParty) = - hasContractState(has(CashV2.State::owners, equalTo(recipients.toList()))) + hasContractState(has(CashV2.State::owners, equalTo(recipients.toList()))) private fun hasContractState(expectation: Matcher) = - has, T>( - "contract state", - { it.state.data }, - expectation) + has, T>( + "contract state", + { it.state.data }, + expectation) @Test fun `upgrade Cash to v2`() { @@ -123,14 +136,14 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { val upgradedState = aliceNode.getCashStateFromVault() assert.that(upgradedState, hasIssuedAmount(Amount(1000000, USD) `issued by` (alice.ref(1))) - and belongsTo(anonymisedRecipient)) + and belongsTo(anonymisedRecipient)) // Make sure the upgraded state can be spent val movedState = upgradedState.state.data.copy(amount = upgradedState.state.data.amount.times(2)) val spendUpgradedTx = aliceNode.signInitialTransaction { addInputState(upgradedState) addOutputState( - upgradedState.state.copy(data = movedState) + upgradedState.state.copy(data = movedState) ) addCommand(CashV2.Move(), alice.owningKey) } @@ -161,35 +174,24 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { override fun verify(tx: LedgerTransaction) {} } - //region Operations - private fun TestStartedNode.initiateDummyContractUpgrade(tx: SignedTransaction) = - initiateContractUpgrade(tx, DummyContractV2::class) - - private fun TestStartedNode.authoriseDummyContractUpgrade(tx: SignedTransaction) = - authoriseContractUpgrade(tx, DummyContractV2::class) - //endregion - //region Matchers - private fun TestStartedNode.hasDummyContractUpgradeTransaction() = - hasContractUpgradeTransaction() - - private inline fun TestStartedNode.hasContractUpgradeTransaction() = - has, ContractUpgradeLedgerTransaction>( - "a contract upgrade transaction", - { getContractUpgradeTransaction(it) }, - isUpgrade()) + private inline fun TestStartedNode.hasContractUpgradeTransaction() = + has, ContractUpgradeLedgerTransaction>( + "a contract upgrade transaction", + { getContractUpgradeTransaction(it) }, + isUpgrade()) private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef) = - services.validatedTransactions.getTransaction(state.ref.txhash)!! - .resolveContractUpgradeTransaction(services) + services.validatedTransactions.getTransaction(state.ref.txhash)!! + .resolveContractUpgradeTransaction(services) private inline fun isUpgrade() = isUpgradeFrom() and isUpgradeTo() - private inline fun isUpgradeFrom() = + private inline fun isUpgradeFrom() = has("input data", { it.inputs.single().state.data }, isA(anything)) - private inline fun isUpgradeTo() = + private inline fun isUpgradeTo() = has("output data", { it.outputs.single().data }, isA(anything)) //endregion } diff --git a/core/src/test/kotlin/net/corda/core/flows/mixins/WithContracts.kt b/core/src/test/kotlin/net/corda/core/flows/mixins/WithContracts.kt index c5794007fe..7206b1ce76 100644 --- a/core/src/test/kotlin/net/corda/core/flows/mixins/WithContracts.kt +++ b/core/src/test/kotlin/net/corda/core/flows/mixins/WithContracts.kt @@ -51,9 +51,13 @@ interface WithContracts : WithMockNet { fun > TestStartedNode.authoriseContractUpgrade( tx: SignedTransaction, toClass: KClass) = - startFlow( - ContractUpgradeFlow.Authorise(tx.tx.outRef(0), toClass.java) - ) + authoriseContractUpgrade(tx.tx.outRef(0), toClass) + + fun > TestStartedNode.authoriseContractUpgrade( + stateAndRef: StateAndRef, toClass: KClass) = + startFlow( + ContractUpgradeFlow.Authorise(stateAndRef, toClass.java) + ) fun TestStartedNode.deauthoriseContractUpgrade(tx: SignedTransaction) = startFlow( ContractUpgradeFlow.Deauthorise(tx.tx.outRef(0).ref) diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index df2a1ec019..7b6bbb75a6 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -56,7 +56,7 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF // verify outputs matches the proposed upgrade. val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash) requireNotNull(ourSTX) { "We don't have a copy of the referenced state" } - val oldStateAndRef = ourSTX!!.tx.outRef(proposal.stateRef.index) + val oldStateAndRef = ourSTX!!.resolveBaseTransaction(serviceHub).outRef(proposal.stateRef.index) val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}") val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV3.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV3.kt new file mode 100644 index 0000000000..eb8b9a1e97 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV3.kt @@ -0,0 +1,36 @@ +package net.corda.testing.contracts + +import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty +import net.corda.core.transactions.LedgerTransaction + +// The dummy contract doesn't do anything useful. It exists for testing purposes. + +/** + * Dummy contract state for testing of the upgrade process. + */ +class DummyContractV3 : UpgradedContractWithLegacyConstraint { + companion object { + const val PROGRAM_ID: ContractClassName = "net.corda.testing.contracts.DummyContractV3" + } + + override val legacyContract: String = DummyContractV2.PROGRAM_ID + override val legacyContractConstraint: AttachmentConstraint = AlwaysAcceptAttachmentConstraint + + data class State(val magicNumber: Int = 0, val owners: List) : ContractState { + override val participants: List = owners + } + + interface Commands : CommandData { + class Create : TypeOnlyCommandData(), Commands + class Move : TypeOnlyCommandData(), Commands + } + + override fun upgrade(state: DummyContractV2.State): State { + return State(state.magicNumber, state.participants) + } + + override fun verify(tx: LedgerTransaction) { + // Other verifications. + } +} From 456c9a85e1d13aaebd6fb7fa9cbdee9494a6204f Mon Sep 17 00:00:00 2001 From: Stefano Franz Date: Wed, 17 Oct 2018 11:27:14 +0100 Subject: [PATCH 54/83] =?UTF-8?q?remove=20requirement=20to=20override=20de?= =?UTF-8?q?fault=20progress=20tracker=20for=20interacti=E2=80=A6=20(#3985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove requirement to override default progress tracker for interactive shell - this is no longer needed * fix failing tests --- .../kotlin/net/corda/core/flows/FlowLogic.kt | 21 +++-- .../corda/core/utilities/ProgressTracker.kt | 84 +++++++++++-------- .../core/utilities/ProgressTrackerTest.kt | 42 +++++----- .../statemachine/FlowStateMachineImpl.kt | 6 +- .../statemachine/FlowFrameworkTests.kt | 6 +- .../corda/attachmentdemo/AttachmentDemo.kt | 13 ++- .../net/corda/tools/shell/InteractiveShell.kt | 10 +-- 7 files changed, 110 insertions(+), 72 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index e85439a601..10f101bdc7 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -64,8 +64,10 @@ abstract class FlowLogic { /** * Return the outermost [FlowLogic] instance, or null if not in a flow. */ - @Suppress("unused") @JvmStatic - val currentTopLevel: FlowLogic<*>? get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic + @Suppress("unused") + @JvmStatic + val currentTopLevel: FlowLogic<*>? + get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic /** * If on a flow, suspends the flow and only wakes it up after at least [duration] time has passed. Otherwise, @@ -123,10 +125,11 @@ abstract class FlowLogic { * Note: The current implementation returns the single identity of the node. This will change once multiple identities * is implemented. */ - val ourIdentityAndCert: PartyAndCertificate get() { - return serviceHub.myInfo.legalIdentitiesAndCerts.find { it.party == stateMachine.ourIdentity } - ?: throw IllegalStateException("Identity specified by ${stateMachine.id} (${stateMachine.ourIdentity}) is not one of ours!") - } + val ourIdentityAndCert: PartyAndCertificate + get() { + return serviceHub.myInfo.legalIdentitiesAndCerts.find { it.party == stateMachine.ourIdentity } + ?: throw IllegalStateException("Identity specified by ${stateMachine.id} (${stateMachine.ourIdentity}) is not one of ours!") + } /** * Specifies the identity to use for this flow. This will be one of the multiple identities that belong to this node. @@ -141,9 +144,11 @@ abstract class FlowLogic { // Used to implement the deprecated send/receive functions using Party. When such a deprecated function is used we // create a fresh session for the Party, put it here and use it in subsequent deprecated calls. private val deprecatedPartySessionMap = HashMap() + private fun getDeprecatedSessionForParty(party: Party): FlowSession { return deprecatedPartySessionMap.getOrPut(party) { initiateFlow(party) } } + /** * Returns a [FlowInfo] object describing the flow [otherParty] is using. With [FlowInfo.flowVersion] it * provides the necessary information needed for the evolution of flows and enabling backwards compatibility. @@ -342,7 +347,7 @@ abstract class FlowLogic { * Note that this has to return a tracker before the flow is invoked. You can't change your mind half way * through. */ - open val progressTracker: ProgressTracker? = null + open val progressTracker: ProgressTracker? = ProgressTracker.DEFAULT_TRACKER() /** * This is where you fill out your business logic. @@ -383,7 +388,7 @@ abstract class FlowLogic { * * @return Returns null if this flow has no progress tracker. */ - fun trackStepsTree(): DataFeed>, List>>? { + fun trackStepsTree(): DataFeed>, List>>? { // TODO this is not threadsafe, needs an atomic get-step-and-subscribe return progressTracker?.let { DataFeed(it.allStepsLabels, it.stepsTreeChanges) diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt index f646f0569a..7741500c31 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt @@ -30,10 +30,11 @@ import java.util.* * using the [Observable] subscribeOn call. */ @CordaSerializable -class ProgressTracker(vararg steps: Step) { +class ProgressTracker(vararg inputSteps: Step) { + @CordaSerializable sealed class Change(val progressTracker: ProgressTracker) { - data class Position(val tracker: ProgressTracker, val newStep: Step) : Change(tracker) { + data class Position(val tracker: ProgressTracker, val newStep: Step) : Change(tracker) { override fun toString() = newStep.label } @@ -64,6 +65,10 @@ class ProgressTracker(vararg steps: Step) { override fun equals(other: Any?) = other === UNSTARTED } + object STARTING : Step("Starting") { + override fun equals(other: Any?) = other === STARTING + } + object DONE : Step("Done") { override fun equals(other: Any?) = other === DONE } @@ -74,7 +79,7 @@ class ProgressTracker(vararg steps: Step) { private val childProgressTrackers = mutableMapOf() /** The steps in this tracker, same as the steps passed to the constructor but with UNSTARTED and DONE inserted. */ - val steps = arrayOf(UNSTARTED, *steps, DONE) + val steps = arrayOf(UNSTARTED, STARTING, *inputSteps, DONE) private var _allStepsCache: List> = _allSteps() @@ -83,42 +88,16 @@ class ProgressTracker(vararg steps: Step) { private val _stepsTreeChanges by transient { PublishSubject.create>>() } private val _stepsTreeIndexChanges by transient { PublishSubject.create() } - - - init { - steps.forEach { - val childTracker = it.childProgressTracker() - if (childTracker != null) { - setChildProgressTracker(it, childTracker) - } - } - } - - /** The zero-based index of the current step in the [steps] array (i.e. with UNSTARTED and DONE) */ - var stepIndex: Int = 0 - private set(value) { - field = value - } - - /** The zero-bases index of the current step in a [allStepsLabels] list */ - var stepsTreeIndex: Int = -1 - private set(value) { - field = value - _stepsTreeIndexChanges.onNext(value) - } - - /** - * Reading returns the value of steps[stepIndex], writing moves the position of the current tracker. Once moved to - * the [DONE] state, this tracker is finished and the current step cannot be moved again. - */ var currentStep: Step get() = steps[stepIndex] set(value) { - check(!hasEnded) { "Cannot rewind a progress tracker once it has ended" } + check((value === DONE && hasEnded) || !hasEnded) { + "Cannot rewind a progress tracker once it has ended" + } if (currentStep == value) return val index = steps.indexOf(value) - require(index != -1, { "Step ${value.label} not found in progress tracker." }) + require(index != -1) { "Step ${value.label} not found in progress tracker." } if (index < stepIndex) { // We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back @@ -144,6 +123,39 @@ class ProgressTracker(vararg steps: Step) { } } + + init { + steps.forEach { + configureChildTrackerForStep(it) + } + this.currentStep = UNSTARTED + } + + private fun configureChildTrackerForStep(it: Step) { + val childTracker = it.childProgressTracker() + if (childTracker != null) { + setChildProgressTracker(it, childTracker) + } + } + + /** The zero-based index of the current step in the [steps] array (i.e. with UNSTARTED and DONE) */ + var stepIndex: Int = 0 + private set(value) { + field = value + } + + /** The zero-bases index of the current step in a [allStepsLabels] list */ + var stepsTreeIndex: Int = -1 + private set(value) { + field = value + _stepsTreeIndexChanges.onNext(value) + } + + /** + * Reading returns the value of steps[stepIndex], writing moves the position of the current tracker. Once moved to + * the [DONE] state, this tracker is finished and the current step cannot be moved again. + */ + /** Returns the current step, descending into children to find the deepest step we are up to. */ val currentStepRecursive: Step get() = getChildProgressTracker(currentStep)?.currentStepRecursive ?: currentStep @@ -263,7 +275,7 @@ class ProgressTracker(vararg steps: Step) { /** * An observable stream of changes to the [allStepsLabels] */ - val stepsTreeChanges: Observable>> get() = _stepsTreeChanges + val stepsTreeChanges: Observable>> get() = _stepsTreeChanges /** * An observable stream of changes to the [stepsTreeIndex] @@ -272,6 +284,10 @@ class ProgressTracker(vararg steps: Step) { /** Returns true if the progress tracker has ended, either by reaching the [DONE] step or prematurely with an error */ val hasEnded: Boolean get() = _changes.hasCompleted() || _changes.hasThrowable() + + companion object { + val DEFAULT_TRACKER = { ProgressTracker() } + } } // TODO: Expose the concept of errors. // TODO: It'd be helpful if this class was at least partly thread safe. diff --git a/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt b/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt index 6a682faff6..e476df1a8b 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt @@ -50,11 +50,11 @@ class ProgressTrackerTest { assertEquals(0, pt.stepIndex) var stepNotification: ProgressTracker.Step? = null pt.changes.subscribe { stepNotification = (it as? ProgressTracker.Change.Position)?.newStep } - + assertEquals(ProgressTracker.UNSTARTED, pt.currentStep) + assertEquals(ProgressTracker.STARTING, pt.nextStep()) assertEquals(SimpleSteps.ONE, pt.nextStep()) - assertEquals(1, pt.stepIndex) + assertEquals(2, pt.stepIndex) assertEquals(SimpleSteps.ONE, stepNotification) - assertEquals(SimpleSteps.TWO, pt.nextStep()) assertEquals(SimpleSteps.THREE, pt.nextStep()) assertEquals(SimpleSteps.FOUR, pt.nextStep()) @@ -87,8 +87,10 @@ class ProgressTrackerTest { assertEquals(SimpleSteps.TWO, (stepNotification.pollFirst() as ProgressTracker.Change.Structural).parent) assertNextStep(SimpleSteps.TWO) + assertEquals(pt2.currentStep, ProgressTracker.UNSTARTED) + assertEquals(ProgressTracker.STARTING, pt2.nextStep()) assertEquals(ChildSteps.AYY, pt2.nextStep()) - assertNextStep(ChildSteps.AYY) + assertEquals((stepNotification.last as ProgressTracker.Change.Position).newStep, ChildSteps.AYY) assertEquals(ChildSteps.BEE, pt2.nextStep()) } @@ -115,19 +117,19 @@ class ProgressTrackerTest { // Travel tree. pt.currentStep = SimpleSteps.ONE - assertCurrentStepsTree(0, SimpleSteps.ONE) + assertCurrentStepsTree(1, SimpleSteps.ONE) pt.currentStep = SimpleSteps.TWO - assertCurrentStepsTree(1, SimpleSteps.TWO) + assertCurrentStepsTree(2, SimpleSteps.TWO) pt2.currentStep = ChildSteps.BEE - assertCurrentStepsTree(3, ChildSteps.BEE) + assertCurrentStepsTree(5, ChildSteps.BEE) pt.currentStep = SimpleSteps.THREE - assertCurrentStepsTree(5, SimpleSteps.THREE) + assertCurrentStepsTree(7, SimpleSteps.THREE) // Assert no structure changes and proper steps propagation. - assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(0, 1, 3, 5)) + assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(1, 2, 5, 7)) assertThat(stepsTreeNotification).isEmpty() } @@ -153,16 +155,16 @@ class ProgressTrackerTest { } pt.currentStep = SimpleSteps.ONE - assertCurrentStepsTree(0, SimpleSteps.ONE) + assertCurrentStepsTree(1, SimpleSteps.ONE) pt.currentStep = SimpleSteps.FOUR - assertCurrentStepsTree(3, SimpleSteps.FOUR) + assertCurrentStepsTree(4, SimpleSteps.FOUR) pt2.currentStep = ChildSteps.SEA - assertCurrentStepsTree(6, ChildSteps.SEA) + assertCurrentStepsTree(8, ChildSteps.SEA) // Assert no structure changes and proper steps propagation. - assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(0, 3, 6)) + assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(1, 4, 8)) assertThat(stepsTreeNotification).isEmpty() } @@ -189,18 +191,18 @@ class ProgressTrackerTest { } pt.currentStep = SimpleSteps.TWO - assertCurrentStepsTree(1, SimpleSteps.TWO) + assertCurrentStepsTree(2, SimpleSteps.TWO) pt.currentStep = SimpleSteps.FOUR - assertCurrentStepsTree(6, SimpleSteps.FOUR) + assertCurrentStepsTree(8, SimpleSteps.FOUR) pt.setChildProgressTracker(SimpleSteps.THREE, pt3) - assertCurrentStepsTree(9, SimpleSteps.FOUR) + assertCurrentStepsTree(12, SimpleSteps.FOUR) // Assert no structure changes and proper steps propagation. - assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(1, 6, 9)) + assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(2, 8, 12)) assertThat(stepsTreeNotification).hasSize(2) // 1 change + 1 our initial state } @@ -228,14 +230,14 @@ class ProgressTrackerTest { pt.currentStep = SimpleSteps.TWO pt2.currentStep = ChildSteps.SEA pt3.currentStep = BabySteps.UNOS - assertCurrentStepsTree(4, ChildSteps.SEA) + assertCurrentStepsTree(6, ChildSteps.SEA) pt.setChildProgressTracker(SimpleSteps.TWO, pt3) - assertCurrentStepsTree(2, BabySteps.UNOS) + assertCurrentStepsTree(4, BabySteps.UNOS) // Assert no structure changes and proper steps propagation. - assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(1, 4, 2)) + assertThat(stepsIndexNotifications).containsExactlyElementsOf(listOf(2, 6, 4)) assertThat(stepsTreeNotification).hasSize(2) // 1 change + 1 our initial state. } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 4fcd24e7d6..34b81ff9df 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -14,6 +14,7 @@ import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.internal.checkpointSerialize +import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.trace @@ -205,6 +206,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, @Suspendable override fun run() { + logic.progressTracker?.currentStep = ProgressTracker.STARTING logic.stateMachine = this setLoggingContext() @@ -263,7 +265,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, processEventImmediately( Event.EnterSubFlow(subFlow.javaClass, createSubFlowVersion( - serviceHub.cordappProvider.getCordappForFlow(subFlow), serviceHub.myInfo.platformVersion + serviceHub.cordappProvider.getCordappForFlow(subFlow), serviceHub.myInfo.platformVersion ) ), isDbTransactionOpenOnEntry = true, @@ -435,7 +437,7 @@ val Class>.flowVersionAndInitiatingClass: Pair() { + @Suspendable + override fun call(): String { + return "You Called me!" + } +} + + @Suppress("DEPRECATION") // DOCSTART 1 fun recipient(rpc: CordaRPCOps, webPort: Int) { 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 6729ab0ea2..55109ff233 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 @@ -17,7 +17,10 @@ import net.corda.core.flows.FlowLogic import net.corda.core.internal.* import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.messaging.* +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.messaging.StateMachineUpdate import net.corda.nodeapi.internal.pendingFlowsCount import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer @@ -359,11 +362,6 @@ object InteractiveShell { errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.genericParameterTypes.size} needed)") continue } - val flow = ctor.newInstance(*args) as FlowLogic<*> - if (flow.progressTracker == null) { - errors.add("A flow must override the progress tracker in order to be run from the shell") - continue - } return invoke(clazz, args) } catch (e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { errors.add("${getPrototype()}: missing parameter ${e.paramName}") From cc75a65f9233b5ac0bcdc5e03c2af032c43fbba5 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 17 Oct 2018 15:09:48 +0100 Subject: [PATCH 55/83] RELEASE - Merge 3.3 upgrade / notes / changelog backto master (#4085) --- docs/source/changelog.rst | 116 ++++++++++++++++++++++---- docs/source/release-notes.rst | 150 +++++++++++++++++++++++++++++++++- docs/source/upgrade-notes.rst | 36 +++++--- 3 files changed, 273 insertions(+), 29 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index f5fd3d3ae3..827dee6db5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -26,8 +26,6 @@ Unreleased * Removed experimental feature ``CordformDefinition`` -* Vault query fix: support query by parent classes of Contract State classes (see https://github.com/corda/corda/issues/3714) - * Added ``registerResponderFlow`` method to ``StartedMockNode``, to support isolated testing of responder flow behaviour. * "app", "rpc", "p2p" and "unknown" are no longer allowed as uploader values when importing attachments. These are used @@ -57,9 +55,6 @@ Unreleased interfaces that will have unimplemented methods. This is useful, for example, for object viewers. This can be turned on with ``SerializationContext.withLenientCarpenter``. -* Introduced a grace period before the initial node registration fails if the node cannot connect to the Doorman. - It retries 10 times with a 1 minute interval in between each try. At the moment this is not configurable. - * Added a ``FlowMonitor`` to log information about flows that have been waiting for IO more than a configurable threshold. * H2 database changes: @@ -173,7 +168,7 @@ Unreleased * Added public support for creating ``CordaRPCClient`` using SSL. For this to work the node needs to provide client applications a certificate to be added to a truststore. See :doc:`tutorial-clientrpc-api` -*The node RPC broker opens 2 endpoints that are configured with ``address`` and ``adminAddress``. RPC Clients would connect +* The node RPC broker opens 2 endpoints that are configured with ``address`` and ``adminAddress``. RPC Clients would connect to the address, while the node will connect to the adminAddress. Previously if ssl was enabled for RPC the ``adminAddress`` was equal to ``address``. @@ -227,6 +222,73 @@ Unreleased normal state when it occurs in an input or output position. *This feature is only available on Corda networks running with a minimum platform version of 4.* +Version 3.3 +----------- + +* Vault query fix: support query by parent classes of Contract State classes (see https://github.com/corda/corda/issues/3714) + +* Fixed an issue preventing Shell from returning control to the user when CTRL+C is pressed in the terminal. + +* Fixed a problem that sometimes prevented nodes from starting in presence of custom state types in the database without a corresponding type from installed CorDapps. + +* Introduced a grace period before the initial node registration fails if the node cannot connect to the Doorman. + It retries 10 times with a 1 minute interval in between each try. At the moment this is not configurable. + +* Fixed an error thrown by NodeVaultService upon recording a transaction with a number of inputs greater than the default page size. + +* Changes to the JSON/YAML serialisation format from ``JacksonSupport``, which also applies to the node shell: + + * ``Instant`` and ``Date`` objects are serialised as ISO-8601 formatted strings rather than timestamps + * ``PublicKey`` objects are serialised and looked up according to their Base58 encoded string + * ``Party`` objects can be deserialised by looking up their public key, in addition to their name + * ``NodeInfo`` objects are serialised as an object and can be looked up using the same mechanism as ``Party`` + * ``NetworkHostAndPort`` serialised according to its ``toString()`` + * ``PartyAndCertificate`` is serialised as the name + * ``SerializedBytes`` is serialised by materialising the bytes into the object it represents, and then serialising that + object into YAML/JSON + * ``X509Certificate`` is serialised as an object with key fields such as ``issuer``, ``publicKey``, ``serialNumber``, etc. + The encoded bytes are also serialised into the ``encoded`` field. This can be used to deserialise an ``X509Certificate`` + back. + * ``CertPath`` objects are serialised as a list of ``X509Certificate`` objects. + +* ``fullParties`` boolean parameter added to ``JacksonSupport.createDefaultMapper`` and ``createNonRpcMapper``. If ``true`` + then ``Party`` objects are serialised as JSON objects with the ``name`` and ``owningKey`` fields. For ``PartyAndCertificate`` + the ``certPath`` is serialised. + +* Several members of ``JacksonSupport`` have been deprecated to highlight that they are internal and not to be used + +* ``ServiceHub`` and ``CordaRPCOps`` can now safely be used from multiple threads without incurring in database transaction problems. + +* Fixed an issue preventing out of process nodes started by the ``Driver`` from logging to file. + +* The Vault Criteria API has been extended to take a more precise specification of which class contains a field. This primarily impacts Java users; Kotlin users need take no action. The old methods have been deprecated but still work - the new methods avoid bugs that can occur when JPA schemas inherit from each other. + +* Removed -xmx VM argument from Explorer's Capsule setup. This helps avoiding out of memory errors. + +* Node will now gracefully fail to start if one of the required ports is already in use. + +* Fixed incorrect exception handling in ``NodeVaultService._query()``. + +* Avoided a memory leak deriving from incorrect MappedSchema caching strategy. + +* Fix CORDA-1403 where a property of a class that implemented a generic interface could not be deserialised in + a factory without a serialiser as the subtype check for the class instance failed. Fix is to compare the raw + type. + +* Fix CORDA-1229. Setter-based serialization was broken with generic types when the property was stored + as the raw type, List for example. + +.. _changelog_v3.2: + +Version 3.2 +----------- + +* Doorman and NetworkMap URLs can now be configured individually rather than being assumed to be + the same server. Current ``compatibilityZoneURL`` configurations remain valid. See both :doc:`corda-configuration-file` + and :doc:`permissioning` for details. + +* Table name with a typo changed from ``NODE_ATTCHMENTS_CONTRACTS`` to ``NODE_ATTACHMENTS_CONTRACTS``. + .. _changelog_v3.1: Version 3.1 @@ -235,7 +297,7 @@ Version 3.1 * Update the fast-classpath-scanner dependent library version from 2.0.21 to 2.12.3 .. note:: Whilst this is not the latest version of this library, that being 2.18.1 at time of writing, versions -later than 2.12.3 (including 2.12.4) exhibit a different issue. + later than 2.12.3 (including 2.12.4) exhibit a different issue. * Updated the api scanner gradle plugin to work the same way as the version in master. These changes make the api scanner more accurate and fix a couple of bugs, and change the format of the api-current.txt file slightly. Backporting these changes @@ -254,21 +316,25 @@ later than 2.12.3 (including 2.12.4) exhibit a different issue. Version 3.0 ----------- -* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated - at CorDapp context creation time from a file source during runtime. +* Due to a security risk, the `conflict` property has been removed from `NotaryError.Conflict` error object. It has been replaced + with `consumedStates` instead. The new property no longer specifies the original requesting party and transaction id for + a consumed state. Instead, only the hash of the transaction id is revealed. For more details why this change had to be + made please refer to the release notes. -* Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to - support graceful node shutdown/restarts. In particular, when this mode is on, new flows through RPC will be rejected, - scheduled flows will be ignored, and initial session messages will not be consumed. This will ensure that the number of - checkpoints will strictly diminish with time, allowing for a clean shutdown. +* Added ``NetworkMapCache.getNodesByLegalName`` for querying nodes belonging to a distributed service such as a notary cluster + where they all share a common identity. ``NetworkMapCache.getNodeByLegalName`` has been tightened to throw if more than + one node with the legal name is found. -* Make the serialisation finger-printer a pluggable entity rather than hard wiring into the factory +* Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts. + In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed. + This will ensure that the number of checkpoints will strictly diminish with time, allowing for a clean shutdown. * Removed blacklisted word checks in Corda X.500 name to allow "Server" or "Node" to be use as part of the legal name. * Separated our pre-existing Artemis broker into an RPC broker and a P2P broker. * Refactored ``NodeConfiguration`` to expose ``NodeRpcOptions`` (using top-level "rpcAddress" property still works with warning). + * Modified ``CordaRPCClient`` constructor to take a ``SSLConfiguration?`` additional parameter, defaulted to ``null``. * Introduced ``CertificateChainCheckPolicy.UsernameMustMatchCommonName`` sub-type, allowing customers to optionally enforce @@ -284,6 +350,28 @@ Version 3.0 * JPA Mapping annotations for States extending ``CommonSchemaV1.LinearState`` and ``CommonSchemaV1.FungibleState`` on the `participants` collection need to be moved to the actual class. This allows to properly specify the unique table name per a collection. See: DummyDealStateSchemaV1.PersistentDummyDealState +* Database schema changes - an H2 database instance of Corda 1.0 and 2.0 cannot be reused for Corda 3.0, listed changes for Vault and Finance module: + + * ``NODE_TRANSACTIONS``: + column ``"TRANSACTION”`` renamed to ``TRANSACTION_VALUE``, serialization format of BLOB stored in the column has changed to AMQP + * ``VAULT_STATES``: + column ``CONTRACT_STATE`` removed + * ``VAULT_FUNGIBLE_STATES``: + column ``ISSUER_REFERENCE`` renamed to ``ISSUER_REF`` and the field size increased + * ``"VAULTSCHEMAV1$VAULTFUNGIBLESTATES_PARTICIPANTS"``: + table renamed to ``VAULT_FUNGIBLE_STATES_PARTS``, + column ``"VAULTSCHEMAV1$VAULTFUNGIBLESTATES_OUTPUT_INDEX"`` renamed to ``OUTPUT_INDEX``, + column ``"VAULTSCHEMAV1$VAULTFUNGIBLESTATES_TRANSACTION_ID"`` renamed to ``TRANSACTION_ID`` + * ``VAULT_LINEAR_STATES``: + type of column ``"UUID"`` changed from ``VARBINARY`` to ``VARCHAR(255)`` - select varbinary column as ``CAST("UUID" AS UUID)`` to get UUID in varchar format + * ``"VAULTSCHEMAV1$VAULTLINEARSTATES_PARTICIPANTS"``: + table renamed to ``VAULT_LINEAR_STATES_PARTS``, + column ``"VAULTSCHEMAV1$VAULTLINEARSTATES_OUTPUT_INDEX"`` renamed to ``OUTPUT_INDEX``, + column ``"VAULTSCHEMAV1$VAULTLINEARSTATES_TRANSACTION_ID"`` renamed to ``TRANSACTION_ID`` + * ``contract_cash_states``: + columns storing Base58 representation of the serialised public key (e.g. ``issuer_key``) were changed to store Base58 representation of SHA-256 of public key prefixed with `DL` + * ``contract_cp_states``: + table renamed to ``cp_states``, column changes as for ``contract_cash_states`` * X.509 certificates now have an extension that specifies the Corda role the certificate is used for, and the role hierarchy is now enforced in the validation code. See ``net.corda.core.internal.CertRole`` for the current implementation diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 3f3e0e9d60..b1db04faf5 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -1,11 +1,14 @@ Release notes ============= -Unreleased ----------- +.. _release_notes_v4_0: + +Release 4.0 (Unreleased) +------------------------ Significant Changes in 4.0 ~~~~~~~~~~~~~~~~~~~~~~~~~~ + * **Retirement of non-elliptic Diffie-Hellman for TLS** The TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 family of ciphers is retired from the list of allowed ciphers for TLS as it is a legacy cipher family not supported by all native SSL/TLS implementations. @@ -18,9 +21,150 @@ Significant Changes in 4.0 for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal state when it occurs in an input or output position. +<< MORE TO COME >> + +.. _release_notes_v3_3: + +Release 3.3 +----------- + +Corda 3.3 brings together many small improvements, fixes, and community contributions to deliver a stable and polished release +of Corda. Where both the 3.1 and 3.2 releases delivered a smaller number of critical bug fixes addressing immediate and impactful error conditions, 3.3 +addresses a much greater number of issues, both small and large, that have been found and fixed since the release of 3.0 back in March. Rolling up a great +many improvements and polish to truly make the Corda experience just that much better. + +In addition to work undertaken by the main Corda development team, we've taken the opportunity in 3.3 to bring back many of the contributions made +by community members from master onto the currently released stable branch. It has been said many times before, but the community and its members +are the real life-blood of Corda and anyone who takes the time to contribute is a star in our eyes. Bringing that code into the current version we hope +gives people the opportunity to see their work in action, and to help their fellow community members by having these contributions available in a +supported release. + +Changes of Note +~~~~~~~~~~~~~~~ + +* **Serialization fixes** + + Things "in the lab" always work so much better than they do in the wild, where everything you didn't think of is thrown at your code and a mockery + is made of some dearly held assumptions. A great example of this is the serialization framework which delivers Corda's wire stability guarantee + that was introduced in 3.0 and has subsequently been put to a rigorous test by our users. Corda 3.3 consolidates a great many fixes in that framework, + both programmatically in terms of fixing bugs, but also in the documentation, hopefully making things clearer and easier to work with. + +* **Certificate Hierarchy** + + After consultation, collaboration, and discussion with industry experts, we have decided to alter the default Certificate Hierarchy (PKI) utilized by + Corda and the Corda Network. To facilitate this, the nodes have had their certificate path verification logic made much more flexible. All existing + certificate hierarchy, certificates, and networks will remain valid. The possibility now exists for nodes to recognize a deeper certificate chain and + thus Compatibility Zone operators can deploy and adhere to the PKI standards they expect and are comfortable with. + + Practically speaking, the old code assumed a 3-level hierarchy of Root -> Intermediate CA (Doorman) -> Node, and this was hard coded. From 3.3 onward an + arbitrary depth of certificate chain is supported. For the Corda Network, this means the introduction of an intermediate layer between the root and the + signing certificates (Network Map and Doorman). This has the effect of allowing the root certificate to *always* be kept offline and never retrieved or + used. Those new intermediate certificates can be used to generate, if ever needed, new signing certs without risking compromise of the root key. + +Special Thanks +~~~~~~~~~~~~~~ + +The Corda community is a vibrant and exciting ecosystem that spreads far outside the virtual walls of the +R3 organisation. Without that community, and the most welcome contributions of its members, the Corda project +would be a much poorer place. + +We're therefore happy to extend thanks to the following members of that community for their contributions + + * `Dan Newton `_ for a fix to cleanup node registration in the test framework. The changes can be found `here `_. + * `Tushar Singh Bora `_ for a number of `documentation tweaks `_. In addition, some updates to the tutorial documentation `here `_. + * `Jiachuan Li `_ for a number of corrections to our documentation. Those contributions can be found `here `_ and `here `_. + * `Yogesh `_ for a documentation tweak that can be see `here `_. + * `Roman Plášil `_ for speeding up node shutdown when connecting to an http network map. This fix can be found `here `_. + * `renlulu `_ for a small `PR `_ to optimize some of the imports. + * `cxyzhang0 `_ for making the ``IdentitySyncFlow`` more useful. See `here `_. + * `Venelin Stoykov `_ with updates to the `documentation `_ around the progress tracker. + * `Mohamed Amine Legheraba `_ for updates to the Azure documentation that can be seen `here `_. + * `Stanly Johnson `_ with a `fix `_ to the network bootstrapper. + * `Tittu Varghese `_ for adding a favicon to the docsite. This commit can be found `here `_ + Issues Fixed ~~~~~~~~~~~~ -* Cordform Gradle task (`deployNodes`) doesn't work when `configFile` element was used. + +* Refactoring ``DigitalSignatureWithCertPath`` for more performant storing of the certificate chain. [`CORDA-1995 `_] +* The serializers class carpenter fails when superclass has double-size primitive field. [`Corda-1945 `_] +* If a second identity is mistakenly created the node will not start. [`CORDA-1811 `_] +* Demobench profile load fails with stack dump. [`CORDA-1948 `_] +* Deletes of NodeInfo can fail to propagate leading to infinite retries. [`CORDA-2029 `_] +* Copy all the certificates from the network-trust-store.jks file to the node's trust store. [`CORDA-2012 `_] +* Add SNI (Server Name Indication) header to TLS connections. [`CORDA-2001 `_] +* Fix duplicate index declaration in the Cash schema. [`CORDA-1952 `_] +* Hello World Tutorial Page mismatch between code sample and explanatory text. [`CORDA-1950 `_] +* Java Instructions to Invoke Hello World CorDapp are incorrect. [`CORDA-1949 `_] +* Add ``VersionInfo`` to the ``NodeInfo`` submission request to the network map element of the Compatibility Zone. [`CORDA-1938 `_] +* Rename current INTERMEDIATE_CA certificate role to DOORMAN_CA certificate role. [`CORDA-1934 `_] +* Make node-side network map verification agnostic to the certificate hierarchy. [`CORDA-1932 `_] +* Corda Shell incorrectly deserializes generic types as raw types. [`CORDA-1907 `_] +* The Corda web server does not support asynchronous servlets. [`CORDA-1906 `_] +* Amount is deserialized from JSON and YAML as Amount, for all values of T. [`CORDA-1905 `_] +* ``NodeVaultService.loadStates`` queries without a ``PageSpecification`` property set. This leads to issues with large transactions. [`CORDA-1895 `_] +* If a node has two flows, where one's name is a longer version of the other's, they cannot be started [`CORDA-1892 `_] +* Vault Queries across ``LinearStates`` and ``FungibleState`` tables return incorrect results. [`CORDA-1888 `_] +* Checking the version of the Corda jar file by executing the jar with the ``--version`` flag without specifying a valid node configuration file causes an exception to be thrown. [`CORDA-1884 `_] +* RPC deadlocks after a node restart. [`CORDA-1875 `_] +* Vault query fails to find a state if it extends some class (``ContractState``) and it is that base class that is used as the predicate (``vaultService.queryBy()``). [`CORDA-1858 `_] +* Missing unconsumed states from linear id when querying vault caused by a the previous transaction failing with an SQL exception. [`CORDA-1847 `_] +* Inconsistency in how a web path is written. [`CORDA-1841 `_] +* Cannot use ``TestIdentities`` with same organization name in ``net.corda.testing.driver.Driver``. [`CORDA-1837 `_] +* Docs page typos. [`CORDA-1834 `_] +* Adding flexibility to the serialization frameworks unit tests support and utility code. [`CORDA-1808 `_] +* Cannot use ``--initial-registration`` with the ``networkServices`` configuration option in place of the older ``compatibilityzone`` option within ``node.conf``. [`CORDA-1789 `_] +* Document more clearly the supported version of both IntelliJ and the IntelliJ Kotlin Plugins. [`CORDA-1727 `_] +* DemoBench's "Launch Explorer" button is not re-enabled when you close Node Explorer. [`CORDA-1686 `_] +* It is not possible to run ``stateMachinesSnapshot`` from the shell. [`CORDA-1681 `_] +* Node won't start if CorDapps generate states prior to deletion [`CORDA-1663 `_] +* Serializer Evolution breaks with Java classes adding nullable properties. [`CORDA-1662 `_] +* Add Java examples for the creation of proxy serializers to complement the existing kotlin ones. [`CORDA-1641 `_] +* Proxy serializer documentation isn't clear on how to write a proxy serializer. [`CORDA-1640 `_] +* Node crashes in ``--initial-registration`` polling mode if doorman returns a transient HTTP error. [`CORDA-1638 `_] +* Nodes started by gradle task are not stopped when the gradle task exits. [`CORDA-1634 `_] +* Notarizations time out if notary doesn't have up-to-date network map. [`CORDA-1628 `_] +* Node explorer: Improve error handling when connection to nodes cannot be established. [`CORDA-1617 `_] +* Validating notary fails to resolve an attachment. [`CORDA-1588 `_] +* Out of process nodes started by the driver do not log to file. [`CORDA-1575 `_] +* Once ``--initial-registration`` has been passed to a node, further restarts should assume that mode until a cert is collected. [`CORDA-1572 `_] +* An array of primitive byte arrays (an array of arrays) won't deserialize in a virgin factory (i.e. one that didn't build the serializer for serialization). [`CORDA-1545 `_] +* Ctrl-C in the shell fails to aborts the flow. [`CORDA-1542 `_] +* One transaction with two identical cash outputs cannot be save in the vault. [`CORDA-1535 `_] +* The unit tests for the enum evolver functionality cannot be regenerated. This is because verification logic added after their initial creation has a bug that incorrectly identifies a cycle in the graph. [`CORDA-1498 `_] +* Add in a safety check that catches flow checkpoints from older versions. [`CORDA-1477 `_] +* Buggy ``CommodityContract`` issuance logic. [`CORDA-1459 `_] +* Error in the process-id deletion process allows multiple instances of the same node to be run. [`CORDA-1455 `_] +* Node crashes if network map returns HTTP 50X error. [`CORDA-1414 `_] +* Delegate Property doesn't serialize, throws an erroneous type mismatch error. [`CORDA-1403 `_] +* If a vault query throws an exception, the stack trace is swallowed. [`CORDA-1397 `_] +* Node can fail to fully start when a port conflict occurs, no useful error message is generated when this occurs. [`CORDA-1394 `_] +* Running the ``deployNodes`` gradle task back to back without a clean doesn't work. [`CORDA-1389 `_] +* Stripping issuer from Amount> does not preserve ``displayTokenSize``. [`CORDA-1386 `_] +* ``CordaServices`` are instantiated multiple times per Party when using ``NodeDriver``. [`CORDA-1385 `_] +* Out of memory errors can be seen when using Demobench + Explorer. [`CORDA-1356 `_] +* Reduce the amount of classpath scanning during integration tests execution. [`CORDA-1355 `_] +* SIMM demo throws "attachment too big" errors. [`CORDA-1346 `_] +* Fix vault query paging example in ``ScheduledFlowTests``. [`CORDA-1344 `_] +* The shell doesn't print the return value of a started flow. [`CORDA-1342 `_] +* Provide access to database transactions for CorDapp developers. [`CORDA-1341 `_] +* Error with ``VaultQuery`` for entity inheriting from ``CommonSchemaV1.FungibleState``. [`CORDA-1338 `_] +* The ``--network-root-truststore`` command line option not defaulted. [`CORDA-1317 `_] +* Java example in "Upgrading CorDapps" documentation is wrong. [`CORDA-1315 `_] +* Remove references to ``registerInitiatedFlow`` in testing documentation as it is not needed. [`CORDA-1304 `_] +* Regression: Recording a duplicate transaction attempts second insert to vault. [`CORDA-1303 `_] +* Columns in the Corda database schema should have correct NULL/NOT NULL constraints. [`CORDA-1297 `_] +* MockNetwork/Node API needs a way to register ``@CordaService`` objects. [`CORDA-1292 `_] +* Deleting a ``NodeInfo`` from the additional-node-infos directory should remove it from cache. [`CORDA-1093 `_] +* ``FailNodeOnNotMigratedAttachmentContractsTableNameTests`` is sometimes failing with database constraint "Notary" is null. [`CORDA-1976 `_] +* Revert keys for DEV certificates. [`CORDA-1661 `_] +* Node Info file watcher should block and load ``NodeInfo`` when node startup. [`CORDA-1604 `_] +* Improved logging of the network parameters update process. [`CORDA-1405 `_] +* Ensure all conditions in cash selection query are tested. [`CORDA-1266 `_] +* ``NodeVaultService`` bug. Start node, issue cash, stop node, start node, ``getCashBalances()`` will not show any cash +* A Corda node doesn't re-select cluster from HA Notary. +* Event Horizon is not wire compatible with older network parameters objects. +* Notary unable to resolve Party after processing a flow from same Party. +* Misleading error message shown when a node is restarted after a flag day event. .. _release_notes_v3_2: diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst index f2c51349b9..09ccfb1c9f 100644 --- a/docs/source/upgrade-notes.rst +++ b/docs/source/upgrade-notes.rst @@ -9,33 +9,31 @@ first public Beta (:ref:`Milestone 12 `), to :ref:`V1.0 >> - * Database upgrade - Change the type of the ``checkpoint_value``. This will address the issue that the `vacuum` function is unable to clean up deleted checkpoints as they are still referenced from the ``pg_shdepend`` table. @@ -86,6 +84,17 @@ For H2: No action is needed for default node tables as ``PersistentStateRef`` is used as Primary Key only and the backing columns are automatically not nullable or custom Cordapp entities using ``PersistentStateRef`` as Primary Key. +V3.2 to v3.3 +------------ + +* Update the Corda Release version + + The ``corda_release_version`` identifier in your projects gradle file will need changing as follows: + + .. sourcecode:: shell + + ext.corda_release_version = '3.3-corda' + v3.1 to v3.2 ------------ @@ -118,8 +127,11 @@ Database schema changes ALTER TABLE node_checkpoints ALTER COLUMN checkpoint_value set data type bytea using null; - .. important:: - The Corda node will fail on startup if the database was not updated with the above commands. + .. note:: + This change will also need to be run when migrating from version 3.0. + +.. important:: + The Corda node will fail on startup if the database was not updated with the above commands. v3.0 to v3.1 ------------ From 55731ef81606e93d2f7d413a08f600b6c3536091 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Thu, 18 Oct 2018 10:38:43 +0100 Subject: [PATCH 56/83] ENT-2431 Tidy up buildNamed and CacheFactory --- .../client/jfx/model/NetworkIdentityModel.kt | 3 +- .../client/rpc/internal/ClientCacheFactory.kt | 19 +++++ .../rpc/internal/RPCClientProxyHandler.kt | 8 +-- .../net/corda/core/internal/NamedCache.kt | 37 ++++------ .../net/corda/core/internal/NamedCacheTest.kt | 14 +++- .../notary/raft/RaftUniquenessProvider.kt | 2 +- .../raft/RaftTransactionCommitLogTests.kt | 2 +- .../nodeapi/internal/DeduplicationChecker.kt | 9 ++- .../internal/persistence/CordaPersistence.kt | 4 +- .../persistence/HibernateConfiguration.kt | 5 +- .../messaging/ArtemisMessagingTest.kt | 4 +- .../network/PersistentNetworkMapCacheTest.kt | 4 +- .../node/services/rpc/ArtemisRpcTests.kt | 4 +- .../net/corda/node/internal/AbstractNode.kt | 28 +++----- .../kotlin/net/corda/node/internal/Node.kt | 6 +- .../security/RPCSecurityManagerImpl.kt | 33 ++++----- .../node/services/api/ServiceHubInternal.kt | 2 +- .../identity/PersistentIdentityService.kt | 2 +- .../keys/PersistentKeyManagementService.kt | 2 +- .../messaging/InternalRPCMessagingClient.kt | 5 +- .../messaging/P2PMessageDeduplicator.kt | 2 +- .../services/messaging/P2PMessagingClient.kt | 2 +- .../node/services/messaging/RPCServer.kt | 23 ++---- .../network/PersistentNetworkMapCache.kt | 2 +- .../persistence/DBTransactionStorage.kt | 2 +- .../persistence/NodeAttachmentService.kt | 1 - .../NodePropertiesPersistentStore.kt | 10 +-- .../PersistentUniquenessProvider.kt | 2 +- .../upgrade/ContractUpgradeServiceImpl.kt | 10 +-- .../node/utilities/AppendOnlyPersistentMap.kt | 2 +- .../corda/node/utilities/NodeNamedCache.kt | 71 +++++++++++++------ .../node/utilities/NonInvalidatingCache.kt | 1 + .../utilities/NonInvalidatingUnboundCache.kt | 10 +-- .../net/corda/node/utilities/PersistentMap.kt | 7 +- .../net/corda/node/CordaRPCOpsImplTest.kt | 3 +- .../corda/node/internal/AbstractNodeTests.kt | 2 +- .../net/corda/node/internal/NodeTest.kt | 1 + .../node/services/RPCSecurityManagerTest.kt | 4 +- .../events/NodeSchedulerServiceTest.kt | 2 +- .../PersistentScheduledFlowRepositoryTest.kt | 3 +- .../PersistentIdentityServiceTests.kt | 2 +- .../AppendOnlyPersistentMapTest.kt | 2 +- .../persistence/DBCheckpointStorageTests.kt | 6 +- .../persistence/DBTransactionStorageTests.kt | 2 +- .../persistence/HibernateConfigurationTest.kt | 2 +- .../persistence/NodeAttachmentServiceTest.kt | 2 +- .../persistence/TransactionCallbackTest.kt | 2 +- .../schema/PersistentStateServiceTests.kt | 9 +-- .../PersistentUniquenessProviderTests.kt | 2 +- .../node/services/vault/VaultQueryTests.kt | 2 +- .../corda/node/utilities/ObservablesTests.kt | 2 +- .../node/utilities/PersistentMapTests.kt | 6 +- .../corda/irs/api/NodeInterestRatesTest.kt | 5 +- .../internal/SerializationScheme.kt | 3 +- .../net/corda/testing/node/MockServices.kt | 2 +- .../corda/testing/node/internal/RPCDriver.kt | 7 +- .../testing/internal/InternalTestUtils.kt | 41 ++++++++++- .../internal/TestingNamedCacheFactory.kt | 13 ++-- .../explorer/identicon/IdenticonRenderer.kt | 3 +- 59 files changed, 269 insertions(+), 197 deletions(-) create mode 100644 client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ClientCacheFactory.kt diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt index aa7f7e50f2..af2acc1aac 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt @@ -7,7 +7,6 @@ import javafx.collections.FXCollections import javafx.collections.ObservableList import net.corda.client.jfx.utils.* import net.corda.core.identity.AnonymousParty -import net.corda.core.internal.buildNamed import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache.MapChange import java.security.PublicKey @@ -32,7 +31,7 @@ class NetworkIdentityModel { private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable) private val identityCache = Caffeine.newBuilder() - .buildNamed>("NetworkIdentityModel_identity", CacheLoader { publicKey: PublicKey -> + .build>(CacheLoader { publicKey: PublicKey -> publicKey.let { rpcProxy.map { it?.cordaRPCOps?.nodeInfoFromParty(AnonymousParty(publicKey)) } } }) val notaries = ChosenList(rpcProxy.map { FXCollections.observableList(it?.cordaRPCOps?.notaryIdentities() ?: emptyList()) }, "notaries") diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ClientCacheFactory.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ClientCacheFactory.kt new file mode 100644 index 0000000000..bad74ecd2b --- /dev/null +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ClientCacheFactory.kt @@ -0,0 +1,19 @@ +package net.corda.client.rpc.internal + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.CacheLoader +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.LoadingCache +import net.corda.core.internal.NamedCacheFactory + +class ClientCacheFactory : NamedCacheFactory { + override fun buildNamed(caffeine: Caffeine, name: String): Cache { + checkCacheName(name) + return caffeine.build() + } + + override fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache { + checkCacheName(name) + return caffeine.build(loader) + } +} \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index be74d7b316..6ceff2b9d0 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -83,7 +83,8 @@ class RPCClientProxyHandler( private val sessionId: Trace.SessionId, private val externalTrace: Trace?, private val impersonatedActor: Actor?, - private val targetLegalIdentity: CordaX500Name? + private val targetLegalIdentity: CordaX500Name?, + private val cacheFactory: NamedCacheFactory = ClientCacheFactory() ) : InvocationHandler { private enum class State { @@ -169,8 +170,7 @@ class RPCClientProxyHandler( } observablesToReap.locked { observables.add(observableId) } } - return Caffeine.newBuilder(). - weakValues().removalListener(onObservableRemove).executor(SameThreadExecutor.getExecutor()).buildNamed("RpcClientProxyHandler_rpcObservable") + return cacheFactory.buildNamed(Caffeine.newBuilder().weakValues().removalListener(onObservableRemove).executor(SameThreadExecutor.getExecutor()), "RpcClientProxyHandler_rpcObservable") } private var sessionFactory: ClientSessionFactory? = null @@ -179,7 +179,7 @@ class RPCClientProxyHandler( private var rpcProducer: ClientProducer? = null private var rpcConsumer: ClientConsumer? = null - private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry) + private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry, cacheFactory = cacheFactory) private val deduplicationSequenceNumber = AtomicLong(0) private val sendingEnabled = AtomicBoolean(true) diff --git a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt index 2a96de4835..632b0c3060 100644 --- a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt +++ b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt @@ -6,30 +6,21 @@ import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.LoadingCache /** - * Restrict the allowed characters of a cache name - this ensures that each cache has a name, and that - * the name can be used to create a file name or a metric name. + * Allow extra functionality to be injected to our caches. */ -internal fun checkCacheName(name: String) { - require(!name.isBlank()) - require(allowedChars.matches(name)) +interface NamedCacheFactory { + /** + * Restrict the allowed characters of a cache name - this ensures that each cache has a name, and that + * the name can be used to create a file name or a metric name. + */ + fun checkCacheName(name: String) { + require(!name.isBlank()) + require(allowedChars.matches(name)) + } + + fun buildNamed(caffeine: Caffeine, name: String): Cache + + fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache } private val allowedChars = Regex("^[0-9A-Za-z_.]*\$") - -/* buildNamed is the central helper method to build caffeine caches in Corda. - * This allows to easily add tweaks to all caches built in Corda, and also forces - * cache users to give their cache a (meaningful) name that can be used e.g. for - * capturing cache traces etc. - * - * Currently it is not used in this version of CORDA, but there are plans to do so. - */ - -fun Caffeine.buildNamed(name: String): Cache { - checkCacheName(name) - return this.build() -} - -fun Caffeine.buildNamed(name: String, loader: CacheLoader): LoadingCache { - checkCacheName(name) - return this.build(loader) -} diff --git a/core/src/test/kotlin/net/corda/core/internal/NamedCacheTest.kt b/core/src/test/kotlin/net/corda/core/internal/NamedCacheTest.kt index 3358696bce..4d9f9297ab 100644 --- a/core/src/test/kotlin/net/corda/core/internal/NamedCacheTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/NamedCacheTest.kt @@ -1,9 +1,21 @@ package net.corda.core.internal +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.CacheLoader +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.LoadingCache import org.junit.Test import kotlin.test.assertEquals -class NamedCacheTest { +class NamedCacheTest : NamedCacheFactory { + override fun buildNamed(caffeine: Caffeine, name: String): Cache { + throw IllegalStateException("Should not be called") + } + + override fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache { + throw IllegalStateException("Should not be called") + } + fun checkNameHelper(name: String, throws: Boolean) { var exceptionThrown = false try { diff --git a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt index c7670d0c18..8146178559 100644 --- a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt @@ -18,6 +18,7 @@ import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.SecureHash import net.corda.core.flows.NotarisationRequestSignature import net.corda.core.identity.Party +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.notary.NotaryInternalException import net.corda.core.internal.notary.UniquenessProvider import net.corda.core.schemas.PersistentStateRef @@ -27,7 +28,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.config.RaftConfig import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX diff --git a/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftTransactionCommitLogTests.kt b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftTransactionCommitLogTests.kt index 8b0f31f12d..f8848a4746 100644 --- a/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftTransactionCommitLogTests.kt +++ b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftTransactionCommitLogTests.kt @@ -14,7 +14,6 @@ import net.corda.core.internal.concurrent.asCordaFuture import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow -import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -23,6 +22,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.hamcrest.Matchers.instanceOf import org.junit.After diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt index 71d99e0fec..8b913c6ea2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt @@ -2,7 +2,7 @@ package net.corda.nodeapi.internal import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine -import net.corda.core.internal.buildNamed +import net.corda.core.internal.NamedCacheFactory import java.time.Duration import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong @@ -10,11 +10,10 @@ import java.util.concurrent.atomic.AtomicLong /** * A class allowing the deduplication of a strictly incrementing sequence number. */ -class DeduplicationChecker(cacheExpiry: Duration, name: String = "DeduplicationChecker") { +class DeduplicationChecker(cacheExpiry: Duration, name: String = "DeduplicationChecker", cacheFactory: NamedCacheFactory) { // dedupe identity -> watermark cache - private val watermarkCache = Caffeine.newBuilder() - .expireAfterAccess(cacheExpiry.toNanos(), TimeUnit.NANOSECONDS) - .buildNamed("${name}_watermark", WatermarkCacheLoader) + private val watermarkCache = cacheFactory.buildNamed(Caffeine.newBuilder() + .expireAfterAccess(cacheExpiry.toNanos(), TimeUnit.NANOSECONDS), "${name}_watermark", WatermarkCacheLoader) private object WatermarkCacheLoader : CacheLoader { override fun load(key: Any) = AtomicLong(-1) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index e1558971eb..d3320d46ee 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal.persistence import co.paralleluniverse.strands.Strand +import net.corda.core.internal.NamedCacheFactory import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger import rx.Observable @@ -52,6 +53,7 @@ class CordaPersistence( databaseConfig: DatabaseConfig, schemas: Set, val jdbcUrl: String, + cacheFactory: NamedCacheFactory, attributeConverters: Collection> = emptySet() ) : Closeable { companion object { @@ -61,7 +63,7 @@ class CordaPersistence( private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel val hibernateConfig: HibernateConfiguration by lazy { transaction { - HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl) + HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt index 5afeb3fe8d..ae371ca88e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt @@ -1,7 +1,7 @@ package net.corda.nodeapi.internal.persistence import com.github.benmanes.caffeine.cache.Caffeine -import net.corda.core.internal.buildNamed +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.castIfPossible import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger @@ -31,6 +31,7 @@ class HibernateConfiguration( private val databaseConfig: DatabaseConfig, private val attributeConverters: Collection>, private val jdbcUrl: String, + cacheFactory: NamedCacheFactory, val cordappClassLoader: ClassLoader? = null ) { companion object { @@ -58,7 +59,7 @@ class HibernateConfiguration( } } - private val sessionFactories = Caffeine.newBuilder().maximumSize(databaseConfig.mappedSchemaCacheSize).buildNamed, SessionFactory>("HibernateConfiguration_sessionFactories") + private val sessionFactories = cacheFactory.buildNamed, SessionFactory>(Caffeine.newBuilder(), "HibernateConfiguration_sessionFactories") val sessionFactoryForRegisteredSchemas = schemas.let { logger.info("Init HibernateConfiguration for schemas: $it") diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 4a427940cf..04b5e2e973 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -7,7 +7,6 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds -import net.corda.node.internal.configureDatabase import net.corda.node.services.config.FlowTimeoutConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate @@ -21,11 +20,12 @@ import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.internal.MOCK_VERSION_INFO -import net.corda.testing.internal.TestingNamedCacheFactory import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 8f35b7a543..175b7530dd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -2,14 +2,14 @@ package net.corda.node.services.network import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.internal.configureDatabase import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.identity.InMemoryIdentityService import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* -import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.junit.After diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt index c1e3cc935b..75a2838286 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt @@ -23,6 +23,8 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.fromUserList import net.corda.testing.internal.p2pSslOptions import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -128,7 +130,7 @@ class ArtemisRpcTests { private fun InternalRPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) { apply { - init(ops, securityManager) + init(ops, securityManager, TestingNamedCacheFactory()) start(brokerControl) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 141c92778b..950e2df204 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -17,6 +17,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture @@ -115,7 +116,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair // TODO Log warning if this node is a notary but not one of the ones specified in the network parameters, both for core and custom abstract class AbstractNode(val configuration: NodeConfiguration, val platformClock: CordaClock, - cacheFactoryPrototype: NamedCacheFactory, + cacheFactoryPrototype: BindableNamedCacheFactory, protected val versionInfo: VersionInfo, protected val serverThread: AffinityExecutor.ServiceAffinityExecutor, private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { @@ -150,8 +151,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, - configuration.dataSourceProperties - ) + configuration.dataSourceProperties, + cacheFactory) init { // TODO Break cyclic dependency identityService.database = database @@ -169,7 +170,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage) @Suppress("LeakingThis") val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize() - val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) + val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory) val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) val networkMapUpdater = NetworkMapUpdater( networkMapCache, @@ -185,7 +186,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ).closeOnStop() @Suppress("LeakingThis") val transactionVerifierService = InMemoryTransactionVerifierService(transactionVerifierWorkerCount).tokenize() - val contractUpgradeService = ContractUpgradeServiceImpl().tokenize() + val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val auditService = DummyAuditService().tokenize() @Suppress("LeakingThis") protected val network: MessagingService = makeMessagingService().tokenize() @@ -999,23 +1000,12 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi class ConfigurationException(message: String) : CordaException(message) -// TODO This is no longer used by AbstractNode and can be moved elsewhere -fun configureDatabase(hikariProperties: Properties, - databaseConfig: DatabaseConfig, - wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, - wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, - schemaService: SchemaService = NodeSchemaService(), - internalSchemas: Set = NodeSchemaService().internalSchemas()): CordaPersistence { - val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties) - persistence.startHikariPool(hikariProperties, databaseConfig, internalSchemas) - return persistence -} - fun createCordaPersistence(databaseConfig: DatabaseConfig, wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, schemaService: SchemaService, - hikariProperties: Properties): CordaPersistence { + hikariProperties: Properties, + cacheFactory: NamedCacheFactory): CordaPersistence { // Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately // Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default // so we end up providing both descriptor and converter. We should re-examine this in later versions to see if @@ -1023,7 +1013,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) val attributeConverters = listOf(AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") - return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters) + return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, cacheFactory, attributeConverters) } fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set, metricRegistry: MetricRegistry? = null) { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 0af843dd5b..c6cad69913 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -85,7 +85,7 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) { open class Node(configuration: NodeConfiguration, versionInfo: VersionInfo, private val initialiseSerialization: Boolean = true, - cacheFactoryPrototype: NamedCacheFactory = DefaultNamedCacheFactory() + cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory() ) : AbstractNode( configuration, createClock(configuration), @@ -223,7 +223,7 @@ open class Node(configuration: NodeConfiguration, val securityManagerConfig = configuration.security?.authService ?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers) - val securityManager = with(RPCSecurityManagerImpl(securityManagerConfig)) { + val securityManager = with(RPCSecurityManagerImpl(securityManagerConfig, cacheFactory)) { if (configuration.shouldStartLocalShell()) RPCSecurityManagerWithAdditionalUser(this, User(INTERNAL_SHELL_USER, INTERNAL_SHELL_USER, setOf(Permissions.all()))) else this } @@ -267,7 +267,7 @@ open class Node(configuration: NodeConfiguration, // Start up the MQ clients. internalRpcMessagingClient?.run { closeOnStop() - init(rpcOps, securityManager) + init(rpcOps, securityManager, cacheFactory) } network.closeOnStop() network.start( diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt index bd7fcb9f72..5186e234b7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt @@ -4,14 +4,13 @@ package net.corda.node.internal.security import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import com.google.common.primitives.Ints -import net.corda.core.context.AuthServiceId -import net.corda.core.internal.buildNamed +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.loggerFor import net.corda.node.internal.DataSourceFactory +import net.corda.node.services.config.AuthDataSourceType import net.corda.node.services.config.PasswordEncryption import net.corda.node.services.config.SecurityConfiguration -import net.corda.node.services.config.AuthDataSourceType import net.corda.nodeapi.internal.config.User import org.apache.shiro.authc.* import org.apache.shiro.authc.credential.PasswordMatcher @@ -28,22 +27,22 @@ import org.apache.shiro.realm.jdbc.JdbcRealm import org.apache.shiro.subject.PrincipalCollection import org.apache.shiro.subject.SimplePrincipalCollection import java.io.Closeable -import javax.security.auth.login.FailedLoginException import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit +import javax.security.auth.login.FailedLoginException + private typealias AuthServiceConfig = SecurityConfiguration.AuthService /** * Default implementation of [RPCSecurityManager] adapting * [org.apache.shiro.mgt.SecurityManager] */ -class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager { +class RPCSecurityManagerImpl(config: AuthServiceConfig, cacheFactory: NamedCacheFactory) : RPCSecurityManager { override val id = config.id private val manager: DefaultSecurityManager init { - manager = buildImpl(config) + manager = buildImpl(config, cacheFactory) } @Throws(FailedLoginException::class) @@ -75,14 +74,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager { private val logger = loggerFor() - /** - * Instantiate RPCSecurityManager initialised with users data from a list of [User] - */ - fun fromUserList(id: AuthServiceId, users: List) = - RPCSecurityManagerImpl(AuthServiceConfig.fromUsers(users).copy(id = id)) - // Build internal Shiro securityManager instance - private fun buildImpl(config: AuthServiceConfig): DefaultSecurityManager { + private fun buildImpl(config: AuthServiceConfig, cacheFactory: NamedCacheFactory): DefaultSecurityManager { val realm = when (config.dataSource.type) { AuthDataSourceType.DB -> { logger.info("Constructing DB-backed security data source: ${config.dataSource.connection}") @@ -98,7 +91,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager { it.cacheManager = config.options?.cache?.let { CaffeineCacheManager( timeToLiveSeconds = it.expireAfterSecs, - maxSize = it.maxEntries) + maxSize = it.maxEntries, + cacheFactory = cacheFactory) } } } @@ -294,7 +288,8 @@ private fun Cache.toShiroCache() = object : ShiroCache * cache implementation in [com.github.benmanes.caffeine.cache.Cache] */ private class CaffeineCacheManager(val maxSize: Long, - val timeToLiveSeconds: Long) : CacheManager { + val timeToLiveSeconds: Long, + val cacheFactory: NamedCacheFactory) : CacheManager { private val instances = ConcurrentHashMap>() @@ -306,11 +301,7 @@ private class CaffeineCacheManager(val maxSize: Long, private fun buildCache(name: String): ShiroCache { logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s") - return Caffeine.newBuilder() - .expireAfterWrite(timeToLiveSeconds, TimeUnit.SECONDS) - .maximumSize(maxSize) - .buildNamed("RPCSecurityManagerShiroCache_$name") - .toShiroCache() + return cacheFactory.buildNamed(Caffeine.newBuilder(), "RPCSecurityManagerShiroCache_$name").toShiroCache() } companion object { 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 8d5583544f..7ccd6db8b9 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 @@ -6,6 +6,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping @@ -25,7 +26,6 @@ import net.corda.node.services.network.NetworkMapUpdater import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import java.security.PublicKey diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 46f29b1ffd..43de48c6b7 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -2,6 +2,7 @@ package net.corda.node.services.identity import net.corda.core.crypto.SecureHash import net.corda.core.identity.* +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.hash import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken @@ -10,7 +11,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.nodeapi.internal.persistence.CordaPersistence diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 835a751c73..574ca82aca 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -2,11 +2,11 @@ package net.corda.node.services.keys import net.corda.core.crypto.* import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.NamedCacheFactory import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt index a484261ba5..1390e14236 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt @@ -1,6 +1,7 @@ package net.corda.node.services.messaging import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.NamedCacheFactory import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.internal.nodeSerializationEnv @@ -20,7 +21,7 @@ class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serv private var locator: ServerLocator? = null private var rpcServer: RPCServer? = null - fun init(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) { + fun init(rpcOps: RPCOps, securityManager: RPCSecurityManager, cacheFactory: NamedCacheFactory) = synchronized(this) { val tcpTransport = ArtemisTcpTransport.rpcInternalClientTcpTransport(serverAddress, sslConfig) locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { @@ -32,7 +33,7 @@ class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serv isUseGlobalPools = nodeSerializationEnv != null } - rpcServer = RPCServer(rpcOps, NODE_RPC_USER, NODE_RPC_USER, locator!!, securityManager, nodeName, rpcServerConfiguration) + rpcServer = RPCServer(rpcOps, NODE_RPC_USER, NODE_RPC_USER, locator!!, securityManager, nodeName, rpcServerConfiguration, cacheFactory) } fun start(serverControl: ActiveMQServerControl) = synchronized(this) { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt index b8c29d8955..a5c2975e2a 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt @@ -2,9 +2,9 @@ package net.corda.node.services.messaging import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.NamedCacheFactory import net.corda.node.services.statemachine.DeduplicationId import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import java.time.Instant diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index 13b96c61b0..b2f54fc909 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.MetricRegistry import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.ThreadBox import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients @@ -27,7 +28,6 @@ import net.corda.node.services.statemachine.DeduplicationId import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.SenderDeduplicationId import net.corda.node.utilities.AffinityExecutor -import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 7c9ca38e93..d8e144e17b 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -13,7 +13,7 @@ import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId import net.corda.core.identity.CordaX500Name import net.corda.core.internal.LifeCycle -import net.corda.core.internal.buildNamed +import net.corda.core.internal.NamedCacheFactory import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults @@ -33,13 +33,8 @@ import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.contextDatabaseOrNull import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE -import org.apache.activemq.artemis.api.core.client.ClientConsumer -import org.apache.activemq.artemis.api.core.client.ClientMessage -import org.apache.activemq.artemis.api.core.client.ClientProducer -import org.apache.activemq.artemis.api.core.client.ClientSession -import org.apache.activemq.artemis.api.core.client.ClientSessionFactory -import org.apache.activemq.artemis.api.core.client.ServerLocator import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.CoreNotificationType import org.apache.activemq.artemis.api.core.management.ManagementHelper @@ -49,12 +44,7 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.time.Duration import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit +import java.util.concurrent.* import kotlin.concurrent.thread private typealias ObservableSubscriptionMap = Cache @@ -91,7 +81,8 @@ class RPCServer( private val serverLocator: ServerLocator, private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name, - private val rpcConfiguration: RPCServerConfiguration + private val rpcConfiguration: RPCServerConfiguration, + private val cacheFactory: NamedCacheFactory ) { private companion object { private val log = contextLogger() @@ -136,7 +127,7 @@ class RPCServer( private val responseMessageBuffer = ConcurrentHashMap() private val sendJobQueue = LinkedBlockingQueue() - private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry) + private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry, cacheFactory = cacheFactory) private var deduplicationIdentity: String? = null init { @@ -154,7 +145,7 @@ class RPCServer( log.debug { "Unsubscribing from Observable with id $key because of $cause" } value!!.subscription.unsubscribe() } - return Caffeine.newBuilder().removalListener(onObservableRemove).executor(SameThreadExecutor.getExecutor()).buildNamed("RPCServer_observableSubscription") + return cacheFactory.buildNamed(Caffeine.newBuilder().removalListener(onObservableRemove).executor(SameThreadExecutor.getExecutor()), "RPCServer_observableSubscription") } fun start(activeMqServerControl: ActiveMQServerControl) { diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index f873120c32..4eea7af890 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -6,6 +6,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture @@ -23,7 +24,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.node.utilities.NamedCacheFactory import net.corda.node.utilities.NonInvalidatingCache import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 034e91a2cf..2330d42571 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -3,6 +3,7 @@ package net.corda.node.services.persistence import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed @@ -15,7 +16,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.AppendOnlyPersistentMapBase -import net.corda.node.utilities.NamedCacheFactory import net.corda.node.utilities.WeightBasedAppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index ca96ef1ad5..31f937f62b 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -19,7 +19,6 @@ import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.serialization.* import net.corda.core.utilities.contextLogger import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser -import net.corda.node.utilities.NamedCacheFactory import net.corda.node.utilities.NonInvalidatingCache import net.corda.node.utilities.NonInvalidatingWeightBasedCache import net.corda.nodeapi.exceptions.DuplicateAttachmentException diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt index 2c48dfd6f9..386415f7c9 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt @@ -1,5 +1,6 @@ package net.corda.node.services.persistence +import net.corda.core.internal.NamedCacheFactory import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.NodePropertiesStore @@ -17,12 +18,12 @@ import javax.persistence.Table /** * Simple node properties key value store in DB. */ -class NodePropertiesPersistentStore(readPhysicalNodeId: () -> String, database: CordaPersistence) : NodePropertiesStore { +class NodePropertiesPersistentStore(readPhysicalNodeId: () -> String, database: CordaPersistence, cacheFactory: NamedCacheFactory) : NodePropertiesStore { private companion object { val logger = contextLogger() } - override val flowsDrainingMode = FlowsDrainingModeOperationsImpl(readPhysicalNodeId, database, logger) + override val flowsDrainingMode = FlowsDrainingModeOperationsImpl(readPhysicalNodeId, database, logger, cacheFactory) fun start() { flowsDrainingMode.map.preload() @@ -40,7 +41,7 @@ class NodePropertiesPersistentStore(readPhysicalNodeId: () -> String, database: ) } -class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private val persistence: CordaPersistence, logger: Logger) : FlowsDrainingModeOperations { +class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private val persistence: CordaPersistence, logger: Logger, cacheFactory: NamedCacheFactory) : FlowsDrainingModeOperations { private val nodeSpecificFlowsExecutionModeKey = "${readPhysicalNodeId()}_flowsExecutionMode" init { @@ -52,7 +53,8 @@ class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private { key -> key }, { entity -> entity.key to entity.value!! }, NodePropertiesPersistentStore::DBNodeProperty, - NodePropertiesPersistentStore.DBNodeProperty::class.java + NodePropertiesPersistentStore.DBNodeProperty::class.java, + cacheFactory ) override val values = PublishSubject.create>()!! diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index b6c8ebf437..122e10d3c3 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -9,6 +9,7 @@ import net.corda.core.flows.NotarisationRequestSignature import net.corda.core.flows.NotaryError import net.corda.core.flows.StateConsumptionDetails import net.corda.core.identity.Party +import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.notary.AsyncUniquenessProvider @@ -22,7 +23,6 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.currentDBSession diff --git a/node/src/main/kotlin/net/corda/node/services/upgrade/ContractUpgradeServiceImpl.kt b/node/src/main/kotlin/net/corda/node/services/upgrade/ContractUpgradeServiceImpl.kt index e6924dd3c7..9aa5739a61 100644 --- a/node/src/main/kotlin/net/corda/node/services/upgrade/ContractUpgradeServiceImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/upgrade/ContractUpgradeServiceImpl.kt @@ -2,6 +2,7 @@ package net.corda.node.services.upgrade import net.corda.core.contracts.StateRef import net.corda.core.contracts.UpgradedContract +import net.corda.core.internal.NamedCacheFactory import net.corda.core.node.services.ContractUpgradeService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.utilities.PersistentMap @@ -11,7 +12,7 @@ import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Table -class ContractUpgradeServiceImpl : ContractUpgradeService, SingletonSerializeAsToken() { +class ContractUpgradeServiceImpl(cacheFactory: NamedCacheFactory) : ContractUpgradeService, SingletonSerializeAsToken() { @Entity @Table(name = "${NODE_DATABASE_PREFIX}contract_upgrades") @@ -26,7 +27,7 @@ class ContractUpgradeServiceImpl : ContractUpgradeService, SingletonSerializeAsT ) private companion object { - fun createContractUpgradesMap(): PersistentMap { + fun createContractUpgradesMap(cacheFactory: NamedCacheFactory): PersistentMap { return PersistentMap( "ContractUpgradeService_upgrades", toPersistentEntityKey = { it }, @@ -37,12 +38,13 @@ class ContractUpgradeServiceImpl : ContractUpgradeService, SingletonSerializeAsT upgradedContractClassName = value } }, - persistentEntityClass = DBContractUpgrade::class.java + persistentEntityClass = DBContractUpgrade::class.java, + cacheFactory = cacheFactory ) } } - private val authorisedUpgrade = createContractUpgradesMap() + private val authorisedUpgrade = createContractUpgradesMap(cacheFactory) fun start() { authorisedUpgrade.preload() diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index 81f080e425..532a71f497 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -2,6 +2,7 @@ package net.corda.node.utilities import com.github.benmanes.caffeine.cache.LoadingCache import com.github.benmanes.caffeine.cache.Weigher +import net.corda.core.internal.NamedCacheFactory import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.contextTransaction @@ -12,7 +13,6 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference - /** * Implements a caching layer on top of an *append-only* table accessed via Hibernate mapping. Note that if the same key is [set] twice, * typically this will result in a duplicate insert if this is racing with another transaction. The flow framework will then retry. diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index d11c9667c2..5c963ac80d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -5,50 +5,77 @@ import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.LoadingCache -import net.corda.core.internal.buildNamed +import net.corda.core.internal.NamedCacheFactory import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.config.NodeConfiguration +import java.util.concurrent.TimeUnit /** - * Allow passing metrics and config to caching implementations. + * Allow passing metrics and config to caching implementations. This is needs to be distinct from [NamedCacheFactory] + * to avoid deterministic serialization from seeing metrics and config on method signatures. */ -interface NamedCacheFactory : SerializeAsToken { +interface BindableNamedCacheFactory : NamedCacheFactory, SerializeAsToken { /** * Build a new cache factory of the same type that incorporates metrics. */ - fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory + fun bindWithMetrics(metricRegistry: MetricRegistry): BindableNamedCacheFactory /** * Build a new cache factory of the same type that incorporates the associated configuration. */ - fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory - - fun buildNamed(caffeine: Caffeine, name: String): Cache - fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache + fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory } -class DefaultNamedCacheFactory private constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : NamedCacheFactory, SingletonSerializeAsToken() { +open class DefaultNamedCacheFactory private constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : BindableNamedCacheFactory, SingletonSerializeAsToken() { constructor() : this(null, null) - override fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration) - override fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration) + override fun bindWithMetrics(metricRegistry: MetricRegistry): BindableNamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration) + override fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration) - override fun buildNamed(caffeine: Caffeine, name: String): Cache { + protected fun configuredForNamed(caffeine: Caffeine, name: String): Caffeine { + return with(nodeConfiguration!!) { + when { + name.startsWith("RPCSecurityManagerShiroCache_") -> with(security?.authService?.options?.cache!!) { caffeine.maximumSize(maxEntries).expireAfterWrite(expireAfterSecs, TimeUnit.SECONDS) } + name == "RPCServer_observableSubscription" -> caffeine + name == "RpcClientProxyHandler_rpcObservable" -> caffeine + name == "SerializationScheme_attachmentClassloader" -> caffeine + name == "HibernateConfiguration_sessionFactories" -> caffeine.maximumSize(database.mappedSchemaCacheSize) + name == "DBTransactionStorage_transactions" -> caffeine.maximumWeight(transactionCacheSizeBytes) + name == "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(attachmentContentCacheSizeBytes) + name == "NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(attachmentCacheBound) + name == "PersistentIdentityService_partyByKey" -> caffeine.maximumSize(defaultCacheSize) + name == "PersistentIdentityService_partyByName" -> caffeine.maximumSize(defaultCacheSize) + name == "PersistentNetworkMap_nodesByKey" -> caffeine.maximumSize(defaultCacheSize) + name == "PersistentNetworkMap_idByLegalName" -> caffeine.maximumSize(defaultCacheSize) + name == "PersistentKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize) + name == "FlowDrainingMode_nodeProperties" -> caffeine.maximumSize(defaultCacheSize) + name == "ContractUpgradeService_upgrades" -> caffeine.maximumSize(defaultCacheSize) + name == "PersistentUniquenessProvider_transactions" -> caffeine.maximumSize(defaultCacheSize) + name == "P2PMessageDeduplicator_processedMessages" -> caffeine.maximumSize(defaultCacheSize) + name == "DeduplicationChecker_watermark" -> caffeine + name == "BFTNonValidatingNotaryService_transactions" -> caffeine.maximumSize(defaultCacheSize) + name == "RaftUniquenessProvider_transactions" -> caffeine.maximumSize(defaultCacheSize) + else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") + } + } + } + + protected fun checkState(name: String) { + checkCacheName(name) checkNotNull(metricRegistry) checkNotNull(nodeConfiguration) - return caffeine.maximumSize(1024).buildNamed(name) + } + + override fun buildNamed(caffeine: Caffeine, name: String): Cache { + checkState(name) + return configuredForNamed(caffeine, name).build() } override fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache { - checkNotNull(metricRegistry) - checkNotNull(nodeConfiguration) - val configuredCaffeine = when (name) { - "DBTransactionStorage_transactions" -> caffeine.maximumWeight(nodeConfiguration!!.transactionCacheSizeBytes) - "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(nodeConfiguration!!.attachmentContentCacheSizeBytes) - "NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(nodeConfiguration!!.attachmentCacheBound) - else -> caffeine.maximumSize(1024) - } - return configuredCaffeine.buildNamed(name, loader) + checkState(name) + return configuredForNamed(caffeine, name).build(loader) } + + protected val defaultCacheSize = 1024L } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt index 2cf4904282..25688233c2 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt @@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.LoadingCache import com.github.benmanes.caffeine.cache.Weigher +import net.corda.core.internal.NamedCacheFactory class NonInvalidatingCache private constructor( val cache: LoadingCache diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt index f16ec74f8f..634189a7b7 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt @@ -5,21 +5,21 @@ import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.LoadingCache import com.github.benmanes.caffeine.cache.RemovalListener -import net.corda.core.internal.buildNamed +import net.corda.core.internal.NamedCacheFactory class NonInvalidatingUnboundCache private constructor( val cache: LoadingCache ) : LoadingCache by cache { - constructor(name: String, loadFunction: (K) -> V, removalListener: RemovalListener = RemovalListener { _, _, _ -> }, + constructor(name: String, cacheFactory: NamedCacheFactory, loadFunction: (K) -> V, removalListener: RemovalListener = RemovalListener { _, _, _ -> }, keysToPreload: () -> Iterable = { emptyList() }) : - this(buildCache(name, loadFunction, removalListener, keysToPreload)) + this(buildCache(name, cacheFactory, loadFunction, removalListener, keysToPreload)) private companion object { - private fun buildCache(name: String, loadFunction: (K) -> V, removalListener: RemovalListener, + private fun buildCache(name: String, cacheFactory: NamedCacheFactory, loadFunction: (K) -> V, removalListener: RemovalListener, keysToPreload: () -> Iterable): LoadingCache { val builder = Caffeine.newBuilder().removalListener(removalListener).executor(SameThreadExecutor.getExecutor()) - return builder.buildNamed(name, NonInvalidatingCacheLoader(loadFunction)).apply { + return cacheFactory.buildNamed(builder, name, NonInvalidatingCacheLoader(loadFunction)).apply { getAll(keysToPreload()) } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt index 2f04ea7bbe..a006ee9883 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt @@ -2,6 +2,7 @@ package net.corda.node.utilities import com.github.benmanes.caffeine.cache.RemovalCause import com.github.benmanes.caffeine.cache.RemovalListener +import net.corda.core.internal.NamedCacheFactory import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.persistence.currentDBSession import java.util.* @@ -14,7 +15,8 @@ class PersistentMap( val toPersistentEntityKey: (K) -> EK, val fromPersistentEntity: (E) -> Pair, val toPersistentEntity: (key: K, value: V) -> E, - val persistentEntityClass: Class + val persistentEntityClass: Class, + cacheFactory: NamedCacheFactory ) : MutableMap, AbstractMap() { private companion object { @@ -24,7 +26,8 @@ class PersistentMap( private val cache = NonInvalidatingUnboundCache( name, loadFunction = { key -> Optional.ofNullable(loadValue(key)) }, - removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass) + removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass), + cacheFactory = cacheFactory ) /** Preload to allow [all] to take data only from the cache (cache is unbound) */ diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 8c8c794378..8d25b1ac66 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -38,10 +38,11 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.expect import net.corda.testing.core.expectEvents import net.corda.testing.core.sequence -import net.corda.testing.node.internal.cordappsForPackages +import net.corda.testing.internal.fromUserList import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.cordappsForPackages import net.corda.testing.node.testActor import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.* diff --git a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt index 7e92e706b6..d4e6a24186 100644 --- a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt @@ -7,7 +7,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.common.internal.relaxedThoroughness -import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.internal.ProcessUtilities.startJavaProcess import org.junit.Rule import org.junit.Test diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt index 6134aa348f..48a266ce31 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt @@ -18,6 +18,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties diff --git a/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt index f64d72ea87..64ed341993 100644 --- a/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt @@ -9,6 +9,8 @@ import net.corda.node.internal.security.tryAuthenticate import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.config.SecurityConfiguration import net.corda.nodeapi.internal.config.User +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.fromUserList import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import javax.security.auth.login.FailedLoginException @@ -134,7 +136,7 @@ class RPCSecurityManagerTest { private fun checkUserActions(permissions: Set, permitted: Set>) { val user = User(username = "user", password = "password", permissions = permissions) - val userRealms = RPCSecurityManagerImpl(SecurityConfiguration.AuthService.fromUsers(listOf(user))) + val userRealms = RPCSecurityManagerImpl(SecurityConfiguration.AuthService.fromUsers(listOf(user)), TestingNamedCacheFactory()) val disabled = allActions.filter { !permitted.contains(listOf(it)) } for (subject in listOf( userRealms.authenticate("user", Password("password")), diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 7cd1955776..b0279be977 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -10,13 +10,13 @@ import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.concurrent.openFuture import net.corda.core.node.ServicesForResolution import net.corda.core.utilities.days -import net.corda.node.internal.configureDatabase import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.statemachine.ExternalEvent import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.doLookup import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.spectator diff --git a/node/src/test/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepositoryTest.kt b/node/src/test/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepositoryTest.kt index f25588ea74..d478dc473d 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepositoryTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepositoryTest.kt @@ -4,9 +4,8 @@ import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.utilities.days -import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices import org.junit.Test import java.time.Instant diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index 186cfb13cc..bf8a16880f 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -7,7 +7,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.UnknownAnonymousPartyException -import net.corda.node.internal.configureDatabase import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities @@ -17,6 +16,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* import net.corda.testing.internal.DEV_INTERMEDIATE_CA import net.corda.testing.internal.DEV_ROOT_CA +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.makeTestIdentityService import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt index b52d8f39a8..5e36bd301c 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt @@ -2,11 +2,11 @@ package net.corda.node.services.persistence import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.loggerFor -import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After import org.junit.Assert.* diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index 7fd4072d38..12782348c2 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -3,16 +3,15 @@ package net.corda.node.services.persistence import net.corda.core.context.InvocationContext import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId -import net.corda.core.serialization.internal.CheckpointSerializationDefaults import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.internal.CheckpointSerializationDefaults import net.corda.core.serialization.internal.checkpointSerialize import net.corda.node.internal.CheckpointIncompatibleException import net.corda.node.internal.CheckpointVerifier -import net.corda.node.internal.configureDatabase import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.statemachine.Checkpoint -import net.corda.node.services.statemachine.SubFlowVersion import net.corda.node.services.statemachine.FlowStart +import net.corda.node.services.statemachine.SubFlowVersion import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig @@ -20,6 +19,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 44c92b6630..00b9356d21 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -7,13 +7,13 @@ import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction -import net.corda.node.internal.configureDatabase import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.createWireTransaction import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat 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 72071192cd..726d6b1cb6 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 @@ -29,7 +29,6 @@ import net.corda.finance.schemas.test.SampleCashSchemaV1 import net.corda.finance.schemas.test.SampleCashSchemaV2 import net.corda.finance.schemas.test.SampleCashSchemaV3 import net.corda.finance.utils.sumCash -import net.corda.node.internal.configureDatabase import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.schema.ContractStateAndRef @@ -41,6 +40,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.testing.core.* +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.vault.DummyDealStateSchemaV1 import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index e4f6549e48..255f0c323f 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -13,12 +13,12 @@ import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.Builder import net.corda.core.node.services.vault.Sort import net.corda.core.utilities.getOrThrow -import net.corda.node.internal.configureDatabase import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/TransactionCallbackTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/TransactionCallbackTest.kt index 7231c3afed..e6c4e8fdf2 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/TransactionCallbackTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/TransactionCallbackTest.kt @@ -1,7 +1,7 @@ package net.corda.node.services.persistence -import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After import org.junit.Test diff --git a/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt index f1abf5e618..9fac402e43 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt @@ -7,24 +7,21 @@ import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name -import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.node.services.api.SchemaService -import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.currentDBSession -import net.corda.testing.internal.LogHelper -import net.corda.testing.core.TestIdentity import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Test -import rx.subjects.PublishSubject import kotlin.test.assertEquals class PersistentStateServiceTests { diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 65401708d4..3c63c71847 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -8,7 +8,6 @@ import net.corda.core.flows.NotarisationRequestSignature import net.corda.core.flows.NotaryError import net.corda.core.identity.CordaX500Name import net.corda.core.internal.notary.NotaryInternalException -import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -17,6 +16,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.core.generateStateRef import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After import org.junit.Before 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 d7f890a7ab..7f55504ee9 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 @@ -24,13 +24,13 @@ import net.corda.finance.schemas.CashSchemaV1.PersistentCashState import net.corda.finance.schemas.CommercialPaperSchemaV1 import net.corda.finance.schemas.test.SampleCashSchemaV2 import net.corda.finance.schemas.test.SampleCashSchemaV3 -import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.testing.core.* import net.corda.testing.internal.TEST_TX_TIME import net.corda.testing.internal.chooseIdentity +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index d319c0b1d1..a28619a4d1 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -3,8 +3,8 @@ package net.corda.node.utilities import com.google.common.util.concurrent.SettableFuture import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.tee -import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.* +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/utilities/PersistentMapTests.kt b/node/src/test/kotlin/net/corda/node/utilities/PersistentMapTests.kt index baaf99a34f..69212e7369 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/PersistentMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/PersistentMapTests.kt @@ -1,9 +1,10 @@ package net.corda.node.utilities import net.corda.core.crypto.SecureHash -import net.corda.node.internal.configureDatabase import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices import org.junit.Test import kotlin.test.assertEquals @@ -25,7 +26,8 @@ class PersistentMapTests { upgradedContractClassName = value } }, - persistentEntityClass = ContractUpgradeServiceImpl.DBContractUpgrade::class.java + persistentEntityClass = ContractUpgradeServiceImpl.DBContractUpgrade::class.java, + cacheFactory = TestingNamedCacheFactory() ).apply { preload() } } diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 4b2e01d4e4..09946bef03 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -10,13 +10,14 @@ import net.corda.finance.DOLLARS import net.corda.finance.contracts.Fix import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* +import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.rigorousMock -import net.corda.testing.node.* +import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.createMockCordaService import org.junit.After import org.junit.Assert.* import org.junit.Before diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt index e94e2b95b4..9c78e01926 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt @@ -6,7 +6,6 @@ import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash -import net.corda.core.internal.buildNamed import net.corda.core.internal.copyBytes import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence @@ -77,7 +76,7 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe */ @DeleteForDJVM internal class AttachmentsClassLoaderBuilder(private val properties: Map, private val deserializationClassLoader: ClassLoader) { - private val cache: Cache, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).buildNamed("SerializationScheme_attachmentClassloader") + private val cache: Cache, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build() fun build(attachmentHashes: List): AttachmentsClassLoader? { val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContext ?: return null // Some tests don't set one. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index e78d0c12a3..9d7dfa7cf6 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -21,7 +21,6 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.ServicesForResolutionImpl -import net.corda.node.internal.configureDatabase import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.* import net.corda.node.services.identity.InMemoryIdentityService @@ -34,6 +33,7 @@ import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.MockCordappProvider +import net.corda.testing.internal.configureDatabase import net.corda.testing.node.internal.* import net.corda.testing.services.MockAttachmentStorage import java.security.KeyPair diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 38109bf114..d9c1cc3a2f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -24,12 +24,14 @@ import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT -import net.corda.testing.node.TestCordapp import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.fromUserList import net.corda.testing.node.NotarySpec +import net.corda.testing.node.TestCordapp import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages import org.apache.activemq.artemis.api.core.SimpleString @@ -485,7 +487,8 @@ data class RPCDriverDSL( locator, rpcSecurityManager, nodeLegalName, - configuration + configuration, + TestingNamedCacheFactory() ) driverDSL.shutdownManager.registerShutdown { rpcServer.close(queueDrainTimeout) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 4ddca172af..8f82cf3792 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -1,26 +1,42 @@ package net.corda.testing.internal +import net.corda.core.context.AuthServiceId import net.corda.core.contracts.* import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.NamedCacheFactory import net.corda.core.node.NodeInfo +import net.corda.core.schemas.MappedSchema import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.loggerFor +import net.corda.node.internal.createCordaPersistence +import net.corda.node.internal.security.RPCSecurityManagerImpl +import net.corda.node.internal.startHikariPool +import net.corda.node.services.api.SchemaService +import net.corda.node.services.config.SecurityConfiguration +import net.corda.node.services.schema.NodeSchemaService import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.internal.config.MutualSslConfiguration -import net.corda.nodeapi.internal.registerDevP2pCertificates +import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.createDevNodeCa -import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.loadDevCaTrustStore +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.serialization.internal.amqp.AMQP_ENABLED import net.corda.testing.internal.stubs.CertificateStoreStubs import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair +import java.util.* import javax.security.auth.x500.X500Principal @Suppress("unused") @@ -136,3 +152,24 @@ fun createWireTransaction(inputs: List, val componentGroups = WireTransaction.createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow) return WireTransaction(componentGroups, privacySalt) } + +/** + * Instantiate RPCSecurityManager initialised with users data from a list of [User] + */ +fun RPCSecurityManagerImpl.Companion.fromUserList(id: AuthServiceId, users: List) = + RPCSecurityManagerImpl(SecurityConfiguration.AuthService.fromUsers(users).copy(id = id), TestingNamedCacheFactory()) + +/** + * Convenience method for configuring a database for some tests. + */ +fun configureDatabase(hikariProperties: Properties, + databaseConfig: DatabaseConfig, + wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, + wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, + schemaService: SchemaService = NodeSchemaService(), + internalSchemas: Set = NodeSchemaService().internalSchemas(), + cacheFactory: NamedCacheFactory = TestingNamedCacheFactory()): CordaPersistence { + val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties, cacheFactory) + persistence.startHikariPool(hikariProperties, databaseConfig, internalSchemas) + return persistence +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt index 7908d5dd27..6802fd042e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestingNamedCacheFactory.kt @@ -5,21 +5,20 @@ import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.LoadingCache -import net.corda.core.internal.buildNamed import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.config.MB import net.corda.node.services.config.NodeConfiguration -import net.corda.node.utilities.NamedCacheFactory +import net.corda.node.utilities.BindableNamedCacheFactory -class TestingNamedCacheFactory private constructor(private val sizeOverride: Long, private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : NamedCacheFactory, SingletonSerializeAsToken() { +class TestingNamedCacheFactory private constructor(private val sizeOverride: Long, private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : BindableNamedCacheFactory, SingletonSerializeAsToken() { constructor(sizeOverride: Long = 1024) : this(sizeOverride, null, null) - override fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory = TestingNamedCacheFactory(sizeOverride, metricRegistry, this.nodeConfiguration) - override fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory = TestingNamedCacheFactory(sizeOverride, this.metricRegistry, nodeConfiguration) + override fun bindWithMetrics(metricRegistry: MetricRegistry): BindableNamedCacheFactory = TestingNamedCacheFactory(sizeOverride, metricRegistry, this.nodeConfiguration) + override fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory = TestingNamedCacheFactory(sizeOverride, this.metricRegistry, nodeConfiguration) override fun buildNamed(caffeine: Caffeine, name: String): Cache { // Does not check metricRegistry or nodeConfiguration, because for tests we don't care. - return caffeine.maximumSize(sizeOverride).buildNamed(name) + return caffeine.maximumSize(sizeOverride).build() } override fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache { @@ -29,6 +28,6 @@ class TestingNamedCacheFactory private constructor(private val sizeOverride: Lon "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(1.MB) else -> caffeine.maximumSize(sizeOverride) } - return configuredCaffeine.buildNamed(name, loader) + return configuredCaffeine.build(loader) } } \ No newline at end of file diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt index db17fc7749..97a8549688 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt @@ -14,7 +14,6 @@ import javafx.scene.image.WritableImage import javafx.scene.paint.Color import javafx.scene.text.TextAlignment import net.corda.core.crypto.SecureHash -import net.corda.core.internal.buildNamed /** * (The MIT License) @@ -76,7 +75,7 @@ object IdenticonRenderer { private const val renderingSize = 30.0 - private val cache = Caffeine.newBuilder().buildNamed("IdentIconRenderer_image", CacheLoader { key -> + private val cache = Caffeine.newBuilder().build(CacheLoader { key -> key.let { render(key.hashCode(), renderingSize) } }) From 4c3b9a067c235758cce4a817e4b5539258c97eff Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 17 Oct 2018 11:34:03 +0100 Subject: [PATCH 57/83] CORDA-535: Make notary implementations publishable --- experimental/notary-bft-smart/build.gradle | 1 + experimental/notary-raft/build.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/experimental/notary-bft-smart/build.gradle b/experimental/notary-bft-smart/build.gradle index bcd0614686..d9c0976b79 100644 --- a/experimental/notary-bft-smart/build.gradle +++ b/experimental/notary-bft-smart/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-jpa' apply plugin: 'idea' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' configurations { integrationTestCompile.extendsFrom testCompile diff --git a/experimental/notary-raft/build.gradle b/experimental/notary-raft/build.gradle index fc6a1894da..adf7118c1c 100644 --- a/experimental/notary-raft/build.gradle +++ b/experimental/notary-raft/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' configurations { integrationTestCompile.extendsFrom testCompile From 07c28c1fbf3ee359e497045a27d5ce22cf719af7 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 17 Oct 2018 17:24:27 +0100 Subject: [PATCH 58/83] Docs: improve organisation of the networks section. --- ...-a-network.rst => compatibility-zones.rst} | 13 ++-- docs/source/corda-networks-index.rst | 5 +- docs/source/corda-test-networks.rst | 77 ------------------- docs/source/corda-testnet-intro.rst | 18 ++--- 4 files changed, 16 insertions(+), 97 deletions(-) rename docs/source/{joining-a-network.rst => compatibility-zones.rst} (82%) delete mode 100644 docs/source/corda-test-networks.rst diff --git a/docs/source/joining-a-network.rst b/docs/source/compatibility-zones.rst similarity index 82% rename from docs/source/joining-a-network.rst rename to docs/source/compatibility-zones.rst index 6de161b781..060496c2ca 100644 --- a/docs/source/joining-a-network.rst +++ b/docs/source/compatibility-zones.rst @@ -4,12 +4,15 @@ -Connecting to a compatibility zone -================================== +Compatibility zones +=================== -Every Corda node is part of a network (also called a zone) that is *permissioned*. Production deployments require a -secure certificate authority. Most users will join an existing network such as the main Corda network or the Corda -TestNet. +Every Corda node is part of a "zone" (also sometimes called a Corda network) that is *permissioned*. Production +deployments require a secure certificate authority. Most users will join an existing network such as the main Corda +network or the Corda TestNet. We use the term "zone" to refer to a set of technically compatible nodes reachable +over a TCP/IP network like the internet. The word "network" is used in Corda but can be ambiguous with the concept +of a "business network", which is usually more like a membership list or subset of nodes in a zone that have agreed +to trade with each other. To connect to a compatibility zone you need to register with its certificate signing authority (doorman) by submitting a certificate signing request (CSR) to obtain a valid identity for the zone. You could do this out of band, for instance diff --git a/docs/source/corda-networks-index.rst b/docs/source/corda-networks-index.rst index d1bf86f03e..435beb7981 100644 --- a/docs/source/corda-networks-index.rst +++ b/docs/source/corda-networks-index.rst @@ -4,13 +4,12 @@ Networks .. toctree:: :maxdepth: 1 - joining-a-network - corda-test-networks + compatibility-zones + corda-testnet-intro running-a-notary permissioning network-map versioning - corda-testnet-intro azure-vm-explore aws-vm-explore gcp-vm diff --git a/docs/source/corda-test-networks.rst b/docs/source/corda-test-networks.rst deleted file mode 100644 index f5c2991fcf..0000000000 --- a/docs/source/corda-test-networks.rst +++ /dev/null @@ -1,77 +0,0 @@ -.. _log4j2: http://logging.apache.org/log4j/2.x/ - -Corda networks -============== - -A Corda network consists of a number of machines running nodes. These nodes communicate using persistent protocols in -order to create and validate transactions. - -There are three broader categories of functionality one such node may have. These pieces of functionality are provided -as services, and one node may run several of them. - -* Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a - double-spend or not -* Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of - transactions -* Regular node: All nodes have a vault and may start protocols communicating with other nodes, notaries and oracles and - evolve their private ledger - -Bootstrap your own test network -------------------------------- - -Certificates -~~~~~~~~~~~~ - -Every node in a given Corda network must have an identity certificate signed by the network's root CA. See -:doc:`permissioning` for more information. - -Configuration -~~~~~~~~~~~~~ - -A node can be configured by adding/editing ``node.conf`` in the node's directory. For details see :doc:`corda-configuration-file`. - -An example configuration: - -.. literalinclude:: example-code/src/main/resources/example-node.conf -:language: cfg - - The most important fields regarding network configuration are: - - * ``p2pAddress``: This specifies a host and port to which Artemis will bind for messaging with other nodes. Note that the - address bound will **NOT** be ``my-corda-node``, but rather ``::`` (all addresses on all network interfaces). The hostname specified - is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the - resolvable name of a machine in a VPN. -* ``rpcAddress``: The address to which Artemis will bind for RPC calls. -* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` and ``rpcAddress`` if they are on the same machine. - -Starting the nodes -~~~~~~~~~~~~~~~~~~ - -You will first need to create the local network by bootstrapping it with the bootstrapper. Details of how to do that -can be found in :doc:`network-bootstrapper`. - -Once that's done you may now start the nodes in any order. You should see a banner, some log lines and eventually -``Node started up and registered``, indicating that the node is fully started. - -.. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound. - -In terms of process management there is no prescribed method. You may start the jars by hand or perhaps use systemd and friends. - -Logging -~~~~~~~ - -Only a handful of important lines are printed to the console. For -details/diagnosing problems check the logs. - -Logging is standard log4j2_ and may be configured accordingly. Logs -are by default redirected to files in ``NODE_DIRECTORY/logs/``. - -Connecting to the nodes -~~~~~~~~~~~~~~~~~~~~~~~ - -Once a node has started up successfully you may connect to it as a client to initiate protocols/query state etc. -Depending on your network setup you may need to tunnel to do this remotely. - -See the :doc:`tutorial-clientrpc-api` on how to establish an RPC link. - -Sidenote: A client is always associated with a single node with a single identity, which only sees their part of the ledger. diff --git a/docs/source/corda-testnet-intro.rst b/docs/source/corda-testnet-intro.rst index ae62d02b80..0e8a5a34ff 100644 --- a/docs/source/corda-testnet-intro.rst +++ b/docs/source/corda-testnet-intro.rst @@ -22,7 +22,7 @@ Click on "Join the Corda Testnet". Select whether you want to register a company or as an individual on the Testnet. -This will create you an account with the Testnet onboarding application which will enable you to provision and manage multiple Corda nodes on Testnet. You will log in to this account to view and manage you Corda Testnet identitiy certificates. +This will create an account with the Testnet on-boarding application which will enable you to provision and manage multiple Corda nodes on Testnet. You will log in to this account to view and manage you Corda Testnet identity certificates. .. image:: resources/testnet-account-type.png @@ -30,23 +30,17 @@ Fill in the form with your details. .. note:: - Testnet is currently invitation only. If your request is approved you will receive an email. Please fill in as many details as possible as it helps us proritise requests. The approval process will take place daily by a member of the r3 operations team reviewing all invite requests and making a decision based on current rate of onboarding of new customers. + Testnet is currently invitation only. If your request is approved you will receive an email. Please fill in as many details as possible as it helps us prioritise requests. The approval process will take place daily by a member of the r3 operations team reviewing all invite requests and making a decision based on current rate of onboarding of new customers. .. image:: resources/testnet-form.png -.. note:: - - We currently only support federated login using Google email accounts. Please ensure the email you use to register is a Gmail account or is set up as a Google account and that you use this email to log in. - -Gmail is recommended. If you want to use a non-Gmail account you can enable your email for Google: https://support.google.com/accounts/answer/176347?hl=en - Once you have been approved, navigate to https://testnet.corda.network and click on "I have an invitation". -Sign in using the Google login service: +Sign in using either your email address and password, or "Sign in with Google": .. image:: resources/testnet-signin.png -When prompted approve the Testnet application: +If using Google accounts, approve the Testnet application when prompted: .. image:: resources/testnet-signin-auth.png @@ -66,7 +60,7 @@ Select the cloud provider you wish to use for documentation on how to specifical Once your cloud instance is set up you can install and run your Testnet pre-provisioned Corda node by clicking on "Copy" and pasting the one time link into your remote cloud terminal. -The installation script will download the Corda binaries as well as your PKI certificates, private keys and suporting files and will install and run Corda on your fresh cloud VM. Your node will register itself with the Corda Testnet when it first runs and be added to the global network map and be visible to counterparties after approximately 5 minutes. +The installation script will download the Corda binaries as well as your PKI certificates, private keys and supporting files and will install and run Corda on your fresh cloud VM. Your node will register itself with the Corda Testnet when it first runs and be added to the global network map and be visible to counterparties after approximately 5 minutes. Hosting a Corda node locally is possible but will require manually configuring firewall and port forwarding on your local router. If you want this option then click on the "Download" button to download a Zip file with a pre-configured Corda node. @@ -76,5 +70,5 @@ Hosting a Corda node locally is possible but will require manually configuring f A note on identities on Corda Testnet ------------------------------------- -Unlike the main Corda Network, which is designed for verified real world identities, The Corda Testnet automatically assigns a "distinguished name" as your identity on the network. This is to prevent name abuse such as the use of offensive language in the names or name squatting. This allows the provision of a node to be automatic and instantaneous. It also enables the same user to safely generate many nodes without accidental name conflicts. If you require a human readable name then please contact support and a partial organsation name can be approved. +Unlike the main Corda Network, which is designed for verified real world identities, The Corda Testnet automatically assigns a "distinguished name" as your identity on the network. This is to prevent name abuse such as the use of offensive language in the names or name squatting. This allows the provision of a node to be automatic and instantaneous. It also enables the same user to safely generate many nodes without accidental name conflicts. If you require a human readable name then please contact support and a partial organisation name can be approved. From 8af404427f79685831fea4f8e264b98037189774 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 18 Oct 2018 14:49:25 +0100 Subject: [PATCH 59/83] Address review comments --- docs/source/compatibility-zones.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/compatibility-zones.rst b/docs/source/compatibility-zones.rst index 060496c2ca..eb3e6680b2 100644 --- a/docs/source/compatibility-zones.rst +++ b/docs/source/compatibility-zones.rst @@ -8,8 +8,8 @@ Compatibility zones =================== Every Corda node is part of a "zone" (also sometimes called a Corda network) that is *permissioned*. Production -deployments require a secure certificate authority. Most users will join an existing network such as the main Corda -network or the Corda TestNet. We use the term "zone" to refer to a set of technically compatible nodes reachable +deployments require a secure certificate authority. Most users will join an existing network such as Corda +Network (the main network) or the Corda Testnet. We use the term "zone" to refer to a set of technically compatible nodes reachable over a TCP/IP network like the internet. The word "network" is used in Corda but can be ambiguous with the concept of a "business network", which is usually more like a membership list or subset of nodes in a zone that have agreed to trade with each other. From 7cfd44e38348421c1da5b2d3845ee4e3065ab0be Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 18 Oct 2018 15:39:42 +0100 Subject: [PATCH 60/83] CORDA-2113 - Include PNM ID in CSR (#4086) * CORDA-2113 - Include PNM ID in CSR If Compatibility Zone operator is using private networks and the node should be joining one, optionally the ID (a UUID) of that network can be included as part of the node's CSR to to the Doorman. * fix broken test --- .idea/compiler.xml | 2 +- docs/source/corda-configuration-file.rst | 1 + .../node/services/network/NetworkMapTest.kt | 12 ++++-- .../registration/NodeRegistrationTest.kt | 1 + .../net/corda/node/internal/NodeStartup.kt | 28 ++++++++----- .../node/services/config/NodeConfiguration.kt | 6 ++- .../HTTPNetworkRegistrationService.kt | 11 +++-- .../registration/NetworkRegistrationHelper.kt | 23 ++++++---- .../testing/node/internal/DriverDSLImpl.kt | 42 +++++++++++++++---- 9 files changed, 89 insertions(+), 37 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 4cfe9a6d84..1a8dc8210d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -239,4 +239,4 @@ - \ No newline at end of file + diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 8c6e003ee0..e5afa2f27b 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -194,6 +194,7 @@ absolute path to the node's base directory. :doormanURL: Root address of the network registration service. :networkMapURL: Root address of the network map service. + :pnm: Optional UUID of the private network operating within the compatibility zone this node should be joinging. .. note:: Only one of ``compatibilityZoneURL`` or ``networkServices`` should be used. diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 487135a000..943b2c0707 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -47,18 +47,22 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP @JvmStatic @Parameterized.Parameters(name = "{0}") fun runParams() = listOf( - { addr: URL, nms: NetworkMapServer -> - SharedCompatibilityZoneParams( + { + addr: URL, + nms: NetworkMapServer -> SharedCompatibilityZoneParams( addr, + pnm = null, publishNotaries = { nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2) } ) }, - { addr: URL, nms: NetworkMapServer -> - SplitCompatibilityZoneParams( + { + addr: URL, + nms: NetworkMapServer -> SplitCompatibilityZoneParams ( doormanURL = URL("http://I/Don't/Exist"), networkMapURL = addr, + pnm = null, publishNotaries = { nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2) } diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 40f01263c0..d86c365366 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -81,6 +81,7 @@ class NodeRegistrationTest { fun `node registration correct root cert`() { val compatibilityZone = SharedCompatibilityZoneParams( URL("http://$serverHostAndPort"), + null, publishNotaries = { server.networkParameters = testNetworkParameters(it) }, rootCert = DEV_ROOT_CA.certificate) internalDriver( 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 110a29cdca..3d0f1b8c58 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -152,7 +152,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { private val handleRegistrationError = { error: Exception -> when (error) { - is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.") + is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}") else -> error.logAsUnexpected("Exception during node registration") } } @@ -385,17 +385,23 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { logger.info(nodeStartedMessage) } - protected open fun registerWithNetwork(conf: NodeConfiguration, versionInfo: VersionInfo, nodeRegistrationConfig: NodeRegistrationOption) { - val compatibilityZoneURL = conf.networkServices?.doormanURL ?: throw RuntimeException( - "compatibilityZoneURL or networkServices must be configured!") + protected open fun registerWithNetwork( + conf: NodeConfiguration, + versionInfo: VersionInfo, + nodeRegistrationConfig: NodeRegistrationOption + ) { + println("\n" + + "******************************************************************\n" + + "* *\n" + + "* Registering as a new participant with a Corda network *\n" + + "* *\n" + + "******************************************************************\n") - println() - println("******************************************************************") - println("* *") - println("* Registering as a new participant with Corda network *") - println("* *") - println("******************************************************************") - NodeRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL, versionInfo), nodeRegistrationConfig).buildKeystore() + NodeRegistrationHelper(conf, + HTTPNetworkRegistrationService( + requireNotNull(conf.networkServices), + versionInfo), + nodeRegistrationConfig).buildKeystore() // Minimal changes to make registration tool create node identity. // TODO: Move node identity generation logic from node to registration helper. diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 5da2e9724f..f7cf92aa36 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -156,6 +156,8 @@ data class BFTSMaRtConfiguration( * * @property doormanURL The URL of the tls certificate signing service. * @property networkMapURL The URL of the Network Map service. + * @property pnm If the compatibility zone operator supports the private network map option, have the node + * at registration automatically join that private network. * @property inferred Non user setting that indicates weather the Network Services configuration was * set explicitly ([inferred] == false) or weather they have been inferred via the compatibilityZoneURL parameter * ([inferred] == true) where both the network map and doorman are running on the same endpoint. Only one, @@ -164,6 +166,7 @@ data class BFTSMaRtConfiguration( data class NetworkServicesConfig( val doormanURL: URL, val networkMapURL: URL, + val pnm: UUID? = null, val inferred : Boolean = false ) @@ -371,8 +374,9 @@ data class NodeConfigurationImpl( """.trimMargin()) } + // Support the deprecated method of configuring network services with a single compatibilityZoneURL option if (compatibilityZoneURL != null && networkServices == null) { - networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, true) + networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, inferred = true) } require(h2port == null || h2Settings == null) { "Cannot specify both 'h2port' and 'h2Settings' in configuration" } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index 3e422bc969..5de12f20c4 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.post import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.seconds import net.corda.node.VersionInfo +import net.corda.node.services.config.NetworkServicesConfig import net.corda.nodeapi.internal.crypto.X509CertificateFactory import okhttp3.CacheControl import okhttp3.Headers @@ -19,8 +20,11 @@ import java.util.* import java.util.zip.ZipInputStream import javax.naming.ServiceUnavailableException -class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: VersionInfo) : NetworkRegistrationService { - private val registrationURL = URL("$compatibilityZoneURL/certificate") +class HTTPNetworkRegistrationService( + val config : NetworkServicesConfig, + val versionInfo: VersionInfo +) : NetworkRegistrationService { + private val registrationURL = URL("${config.doormanURL}/certificate") companion object { private val TRANSIENT_ERROR_STATUS_CODES = setOf(HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT) @@ -54,7 +58,8 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: override fun submitRequest(request: PKCS10CertificationRequest): String { return String(registrationURL.post(OpaqueBytes(request.encoded), "Platform-Version" to "${versionInfo.platformVersion}", - "Client-Version" to versionInfo.releaseVersion)) + "Client-Version" to versionInfo.releaseVersion, + "Private-Network-Map" to (config.pnm?.toString() ?: ""))) } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 3d4e735498..eb1aac885b 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -96,10 +96,9 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, val requestId = try { submitOrResumeCertificateSigningRequest(keyPair) } catch (e: Exception) { - if (e is ConnectException || e is ServiceUnavailableException || e is IOException) { - throw NodeRegistrationException(e) - } - throw e + throw if (e is ConnectException || e is ServiceUnavailableException || e is IOException) { + NodeRegistrationException(e.message, e) + } else e } val certificates = try { @@ -200,7 +199,8 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, if (idlePeriodDuration != null) { Thread.sleep(idlePeriodDuration.toMillis()) } else { - throw NodeRegistrationException(e) + throw NodeRegistrationException("Compatibility Zone registration service is currently unavailable, " + + "try again later!.", e) } } } @@ -249,10 +249,17 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, protected open fun isTlsCrlIssuerCertRequired(): Boolean = false } -class NodeRegistrationException(cause: Throwable?) : IOException("Unable to contact node registration service", cause) +class NodeRegistrationException( + message: String?, + cause: Throwable? +) : IOException(message ?: "Unable to contact node registration service", cause) -class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) : - NetworkRegistrationHelper( +class NodeRegistrationHelper( + private val config: NodeConfiguration, + certService: NetworkRegistrationService, + regConfig: NodeRegistrationOption, + computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1)) +) : NetworkRegistrationHelper( config.certificatesDirectory, config.signingCertificateStore, config.myLegalName, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index d952d07414..e87557f716 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -222,7 +222,7 @@ class DriverDSLImpl( val registrationFuture = if (compatibilityZone?.rootCert != null) { // We don't need the network map to be available to be able to register the node - startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.doormanURL()) + startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.config()) } else { doneFuture(Unit) } @@ -275,14 +275,18 @@ class DriverDSLImpl( return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap, additionalCordapps, regenerateCordappsOnStart) } - private fun startNodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { + private fun startNodeRegistration( + providedName: CordaX500Name, + rootCert: X509Certificate, + networkServicesConfig: NetworkServicesConfig + ): CordaFuture { val baseDirectory = baseDirectory(providedName).createDirectories() val config = NodeConfig(ConfigHelper.loadConfig( baseDirectory = baseDirectory, allowMissingConfig = true, configOverrides = configOf( "p2pAddress" to portAllocation.nextHostAndPort().toString(), - "compatibilityZoneURL" to compatibilityZoneURL.toString(), + "compatibilityZoneURL" to networkServicesConfig.doormanURL.toString(), "myLegalName" to providedName.toString(), "rpcSettings" to mapOf( "address" to portAllocation.nextHostAndPort().toString(), @@ -305,7 +309,7 @@ class DriverDSLImpl( executorService.fork { NodeRegistrationHelper( config.corda, - HTTPNetworkRegistrationService(compatibilityZoneURL, versionInfo), + HTTPNetworkRegistrationService(networkServicesConfig, versionInfo), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword) ).buildKeystore() config @@ -371,7 +375,7 @@ class DriverDSLImpl( startNotaryIdentityGeneration() } else { // With a root cert specified we delegate generation of the notary identities to the CZ. - startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone.doormanURL()) + startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone) } notaryInfosFuture.map { notaryInfos -> compatibilityZone.publishNotaries(notaryInfos) @@ -422,16 +426,22 @@ class DriverDSLImpl( } } - private fun startAllNotaryRegistrations(rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture> { + private fun startAllNotaryRegistrations( + rootCert: X509Certificate, + compatibilityZone: CompatibilityZoneParams): CordaFuture> { // Start the registration process for all the notaries together then wait for their responses. return notarySpecs.map { spec -> require(spec.cluster == null) { "Registering distributed notaries not supported" } - startNotaryRegistration(spec, rootCert, compatibilityZoneURL) + startNotaryRegistration(spec, rootCert, compatibilityZone) }.transpose() } - private fun startNotaryRegistration(spec: NotarySpec, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { - return startNodeRegistration(spec.name, rootCert, compatibilityZoneURL).flatMap { config -> + private fun startNotaryRegistration( + spec: NotarySpec, + rootCert: X509Certificate, + compatibilityZone: CompatibilityZoneParams + ): CordaFuture { + return startNodeRegistration(spec.name, rootCert, compatibilityZone.config()).flatMap { config -> // Node registration only gives us the node CA cert, not the identity cert. That is only created on first // startup or when the node is told to just generate its node info file. We do that here. if (startNodesInProcess) { @@ -1067,6 +1077,7 @@ sealed class CompatibilityZoneParams( ) { abstract fun networkMapURL(): URL abstract fun doormanURL(): URL + abstract fun config() : NetworkServicesConfig } /** @@ -1074,11 +1085,18 @@ sealed class CompatibilityZoneParams( */ class SharedCompatibilityZoneParams( private val url: URL, + private val pnm : UUID?, publishNotaries: (List) -> Unit, rootCert: X509Certificate? = null ) : CompatibilityZoneParams(publishNotaries, rootCert) { + + val config : NetworkServicesConfig by lazy { + NetworkServicesConfig(url, url, pnm, false) + } + override fun doormanURL() = url override fun networkMapURL() = url + override fun config() : NetworkServicesConfig = config } /** @@ -1087,11 +1105,17 @@ class SharedCompatibilityZoneParams( class SplitCompatibilityZoneParams( private val doormanURL: URL, private val networkMapURL: URL, + private val pnm : UUID?, publishNotaries: (List) -> Unit, rootCert: X509Certificate? = null ) : CompatibilityZoneParams(publishNotaries, rootCert) { + val config : NetworkServicesConfig by lazy { + NetworkServicesConfig(doormanURL, networkMapURL, pnm, false) + } + override fun doormanURL() = doormanURL override fun networkMapURL() = networkMapURL + override fun config() : NetworkServicesConfig = config } fun internalDriver( From e99fa975f7f5e599b6f7ef1207868fba89948d3f Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 19 Oct 2018 11:17:20 +0100 Subject: [PATCH 61/83] CORDA-535: Allow notary implementations to specify a serialization filter (#4054) Only allow custom serialization filters in dev mode. --- docs/source/changelog.rst | 3 +++ docs/source/running-a-notary.rst | 4 +++- .../corda/notary/bftsmart/BFTSMaRtConfig.kt | 7 ------ .../notary/bftsmart/BftSmartNotaryService.kt | 12 +++++++++- .../net/corda/node/internal/AbstractNode.kt | 24 +++++++++++++++++++ .../net/corda/node/internal/NodeStartup.kt | 13 ++-------- 6 files changed, 43 insertions(+), 20 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 827dee6db5..c942bff9d3 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -11,6 +11,9 @@ Unreleased * Introduce minimum and target platform version for CorDapps. +* BFT-Smart and Raft notary implementations have been extracted out of node into ``experimental`` CorDapps to emphasise + their experimental nature. Moreover, the BFT-Smart notary will only work in dev mode due to its use of Java serialization. + * Vault storage of contract state constraints metadata and associated vault query functions to retrieve and sort by constraint type. * New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call. diff --git a/docs/source/running-a-notary.rst b/docs/source/running-a-notary.rst index cebfbfb459..567908c471 100644 --- a/docs/source/running-a-notary.rst +++ b/docs/source/running-a-notary.rst @@ -43,6 +43,8 @@ Byzantine fault-tolerant (experimental) A prototype BFT notary implementation based on `BFT-Smart `_ is available. You can try it out on our `notary demo `_ page. Note that it -is still experimental and there is active work ongoing for a production ready solution. +is still experimental and there is active work ongoing for a production ready solution. Additionally, BFT-Smart requires Java +serialization which is disabled by default in Corda due to security risks, and it will only work in dev mode where this can +be customised. We do not recommend using it in any long-running test or production deployments. \ No newline at end of file diff --git a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt index 489f190d6f..d31e6fb203 100644 --- a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt @@ -92,10 +92,3 @@ fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3 fun minCorrectReplicas(clusterSize: Int) = (2 * clusterSize + 3) / 3 fun minClusterSize(maxFaultyReplicas: Int) = maxFaultyReplicas * 3 + 1 -fun bftSMaRtSerialFilter(clazz: Class<*>): Boolean = clazz.name.let { - it.startsWith("bftsmart.") - || it.startsWith("java.security.") - || it.startsWith("java.util.") - || it.startsWith("java.lang.") - || it.startsWith("java.net.") -} diff --git a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt index 57560699cb..391b92a630 100644 --- a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt @@ -10,7 +10,6 @@ import net.corda.core.identity.Party import net.corda.core.internal.notary.NotaryInternalException import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.verifySignature -import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -41,6 +40,17 @@ class BftSmartNotaryService( ) : NotaryService() { companion object { private val log = contextLogger() + @JvmStatic + val serializationFilter + get() = { clazz: Class<*> -> + clazz.name.let { + it.startsWith("bftsmart.") + || it.startsWith("java.security.") + || it.startsWith("java.util.") + || it.startsWith("java.lang.") + || it.startsWith("java.net.") + } + } } private val notaryConfig = services.configuration.notary diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 950e2df204..c196bf639e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -32,6 +32,7 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.* import net.corda.node.CordaClock +import net.corda.node.SerialFilter import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.classloading.requireAnnotation @@ -86,6 +87,7 @@ import org.slf4j.Logger import rx.Observable import rx.Scheduler import java.io.IOException +import java.lang.UnsupportedOperationException import java.lang.reflect.InvocationTargetException import java.nio.file.Paths import java.security.KeyPair @@ -153,6 +155,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, schemaService, configuration.dataSourceProperties, cacheFactory) + init { // TODO Break cyclic dependency identityService.database = database @@ -766,6 +769,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("Unable to start notary service $serviceClass: notary identity not found") + + /** Some notary implementations only work with Java serialization. */ + maybeInstallSerializationFilter(serviceClass) + val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true } val service = constructor.newInstance(services, notaryKey) as NotaryService @@ -779,6 +786,23 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + /** Installs a custom serialization filter defined by a notary service implementation. Only supported in dev mode. */ + private fun maybeInstallSerializationFilter(serviceClass: Class) { + try { + @Suppress("UNCHECKED_CAST") + val filter = serviceClass.getDeclaredMethod("getSerializationFilter").invoke(null) as ((Class<*>) -> Boolean) + if (configuration.devMode) { + log.warn("Installing a custom Java serialization filter, required by ${serviceClass.name}. " + + "Note this is only supported in dev mode – a production node will fail to start if serialization filters are used.") + SerialFilter.install(filter) + } else { + throw UnsupportedOperationException("Unable to install a custom Java serialization filter, not in dev mode.") + } + } catch (e: NoSuchMethodException) { + // No custom serialization filter declared + } + } + private fun getNotaryServiceClass(className: String): Class { val loadedImplementations = cordappLoader.cordapps.mapNotNull { it.notaryService } log.debug("Notary service implementations found: ${loadedImplementations.joinToString(", ")}") 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 3d0f1b8c58..10fc55f7e5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -414,17 +414,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { protected open fun loadConfigFile(): Pair> = cmdLineOptions.loadConfig() protected open fun banJavaSerialisation(conf: NodeConfiguration) { - SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) - } - - /** This filter is required for BFT-Smart to work as it only supports Java serialization. */ - // TODO: move this filter out of the node, allow Cordapps to specify filters. - private fun bftSMaRtSerialFilter(clazz: Class<*>): Boolean = clazz.name.let { - it.startsWith("bftsmart.") - || it.startsWith("java.security.") - || it.startsWith("java.util.") - || it.startsWith("java.lang.") - || it.startsWith("java.net.") + // Note that in dev mode this filter can be overridden by a notary service implementation. + SerialFilter.install(::defaultSerialFilter) } protected open fun getVersionInfo(): VersionInfo { From f685df46b51a2593c238f587ab93ead704ac544c Mon Sep 17 00:00:00 2001 From: Thomas Schroeter Date: Fri, 19 Oct 2018 11:40:59 +0100 Subject: [PATCH 62/83] [ENT-1774] FlowAsyncOperation deduplication ID (#4068) --- .idea/compiler.xml | 2 + .../corda/core/internal/FlowAsyncOperation.kt | 12 +++-- .../core/internal/WaitForStateConsumption.kt | 4 +- .../flowstatemachines/SummingOperation.java | 2 +- .../SummingOperationThrowing.java | 2 +- .../TutorialFlowAsyncOperation.kt | 4 +- .../net/corda/node/flows/FlowRetryTest.kt | 51 ++++++++++++++++++- .../node/services/statemachine/Action.kt | 2 +- .../statemachine/ActionExecutorImpl.kt | 2 +- .../transitions/StartedFlowTransition.kt | 5 +- 10 files changed, 73 insertions(+), 13 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1a8dc8210d..6f25d0a850 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -134,6 +134,8 @@ + + diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt index 48959c885d..fb02a4da97 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt @@ -12,8 +12,14 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable interface FlowAsyncOperation { - /** Performs the operation in a non-blocking fashion. */ - fun execute(): CordaFuture + /** + * Performs the operation in a non-blocking fashion. + * @param deduplicationId If the flow restarts from a checkpoint (due to node restart, or via a visit to the flow + * hospital following an error) the execute method might be called more than once by the Corda flow state machine. + * For each duplicate call, the deduplicationId is guaranteed to be the same allowing duplicate requests to be + * de-duplicated if necessary inside the execute method. + */ + fun execute(deduplicationId: String): CordaFuture } // DOCEND FlowAsyncOperation @@ -24,4 +30,4 @@ fun FlowLogic.executeAsync(operation: FlowAsyncOperation, may val request = FlowIORequest.ExecuteAsyncOperation(operation) return stateMachine.suspend(request, maySkipCheckpoint) } -// DOCEND executeAsync \ No newline at end of file +// DOCEND executeAsync diff --git a/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt b/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt index 6c6dcb8605..ad6d94f93f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt +++ b/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt @@ -22,7 +22,7 @@ class WaitForStateConsumption(val stateRefs: Set, val services: Servic val logger = contextLogger() } - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { val futures = stateRefs.map { services.vaultService.whenConsumed(it).toCompletableFuture() } val completedFutures = futures.filter { it.isDone } @@ -40,4 +40,4 @@ class WaitForStateConsumption(val stateRefs: Set, val services: Servic return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { Unit }.asCordaFuture() } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java index d313fdb8ce..7b23e22efe 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java @@ -12,7 +12,7 @@ public final class SummingOperation implements FlowAsyncOperation { @NotNull @Override - public CordaFuture execute() { + public CordaFuture execute(String deduplicationId) { return CordaFutureImplKt.doneFuture(this.a + this.b); } diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java index 1a759074b0..91c30eaf4c 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java @@ -11,7 +11,7 @@ public final class SummingOperationThrowing implements FlowAsyncOperation execute() { + public CordaFuture execute(String deduplicationId) { throw new IllegalStateException("You shouldn't be calling me"); } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt index dfbf8c158d..c956a713ec 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt @@ -11,7 +11,7 @@ import net.corda.core.internal.executeAsync // DOCSTART SummingOperation class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { return doneFuture(a + b) } } @@ -19,7 +19,7 @@ class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation { // DOCSTART SummingOperationThrowing class SummingOperationThrowing(val a: Int, val b: Int) : FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { throw IllegalStateException("You shouldn't be calling me") } } diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt index 92334a7548..b1b0f0443f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt @@ -3,9 +3,13 @@ package net.corda.node.flows import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient import net.corda.core.CordaRuntimeException +import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.* import net.corda.core.identity.Party +import net.corda.core.internal.FlowAsyncOperation import net.corda.core.internal.IdempotentFlow +import net.corda.core.internal.concurrent.doneFuture +import net.corda.core.internal.executeAsync import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.ProgressTracker @@ -56,6 +60,21 @@ class FlowRetryTest { assertEquals("$numSessions:$numIterations", result) } + @Test + fun `async operation deduplication id is stable accross retries`() { + val user = User("mark", "dadada", setOf(Permissions.startFlow())) + driver(DriverParameters( + startNodesInProcess = isQuasarAgentSpecified(), + notarySpecs = emptyList() + )) { + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + + CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use { + it.proxy.startFlow(::AsyncRetryFlow).returnValue.getOrThrow() + } + } + } + @Test fun `flow gives up after number of exceptions, even if this is the first line of the flow`() { val user = User("mark", "dadada", setOf(Permissions.startFlow())) @@ -218,6 +237,36 @@ class RetryFlow() : FlowLogic(), IdempotentFlow { } } +@StartableByRPC +class AsyncRetryFlow() : FlowLogic(), IdempotentFlow { + companion object { + object FIRST_STEP : ProgressTracker.Step("Step one") + + fun tracker() = ProgressTracker(FIRST_STEP) + + val deduplicationIds = mutableSetOf() + } + + class RecordDeduplicationId: FlowAsyncOperation { + override fun execute(deduplicationId: String): CordaFuture { + val dedupeIdIsNew = deduplicationIds.add(deduplicationId) + if (dedupeIdIsNew) { + throw ExceptionToCauseFiniteRetry() + } + return doneFuture(deduplicationId) + } + } + + override val progressTracker = tracker() + + @Suspendable + override fun call(): String { + progressTracker.currentStep = FIRST_STEP + executeAsync(RecordDeduplicationId()) + return "Result" + } +} + @StartableByRPC class ThrowingFlow() : FlowLogic(), IdempotentFlow { companion object { @@ -237,4 +286,4 @@ class ThrowingFlow() : FlowLogic(), IdempotentFlow { progressTracker.currentStep = FIRST_STEP return "Result" } -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt index 73ac04b73e..a785347ff3 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt @@ -124,7 +124,7 @@ sealed class Action { /** * Execute the specified [operation]. */ - data class ExecuteAsyncOperation(val operation: FlowAsyncOperation<*>) : Action() + data class ExecuteAsyncOperation(val deduplicationId: String, val operation: FlowAsyncOperation<*>) : Action() /** * Release soft locks associated with given ID (currently the flow ID). diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index 00a0406dbe..bd2e5a5169 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -221,7 +221,7 @@ class ActionExecutorImpl( @Suspendable private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) { - val operationFuture = action.operation.execute() + val operationFuture = action.operation.execute(action.deduplicationId) operationFuture.thenMatch( success = { result -> fiber.scheduleEvent(Event.AsyncOperationCompletion(result)) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt index 7dd43cb299..2d4a9b6ddc 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt @@ -411,7 +411,10 @@ class StartedFlowTransition( private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult { return builder { - actions.add(Action.ExecuteAsyncOperation(flowIORequest.operation)) + // The `numberOfSuspends` is added to the deduplication ID in case an async + // operation is executed multiple times within the same flow. + val deduplicationId = context.id.toString() + ":" + currentState.checkpoint.numberOfSuspends.toString() + actions.add(Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation)) FlowContinuation.ProcessEvents } } From 73a4953ae95860f2115e9aeca546fdc8b9087975 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Fri, 19 Oct 2018 15:53:47 +0100 Subject: [PATCH 63/83] CORDA-2099: Define TypeIdentifier (#4081) * Corda-2099: Define TypeIdentifier * Comments, naming and formatting tweaks --- .../internal/model/TypeIdentifier.kt | 145 ++++++++++++++++++ .../internal/model/TypeIdentifierTests.kt | 53 +++++++ 2 files changed, 198 insertions(+) create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt create mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt new file mode 100644 index 0000000000..3e4816b0c6 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -0,0 +1,145 @@ +package net.corda.serialization.internal.model + +import com.google.common.reflect.TypeToken +import java.lang.reflect.* + +/** + * Used as a key for retrieving cached type information. We need slightly more information than the bare classname, + * and slightly less information than is captured by Java's [Type] (We drop type variance because in practice we resolve + * wildcards to their upper bounds, e.g. `? extends Foo` to `Foo`). We also need an identifier we can use even when the + * identified type is not visible from the current classloader. + * + * These identifiers act as the anchor for comparison between remote type information (prior to matching it to an actual + * local class) and local type information. + * + * [TypeIdentifier] provides a family of type identifiers, together with a [prettyPrint] method for displaying them. + */ +sealed class TypeIdentifier { + + /** + * The name of the type. + */ + abstract val name: String + + /** + * Obtain a nicely-formatted representation of the identified type, for help with debugging. + */ + fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) { + is TypeIdentifier.Unknown -> "?" + is TypeIdentifier.Top -> "*" + is TypeIdentifier.Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) + is TypeIdentifier.Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)" + is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint()}[]" + is TypeIdentifier.Parameterised -> + name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") { + it.prettyPrint() + } + } + + private fun String.simplifyClassNameIfRequired(simplifyClassNames: Boolean): String = + if (simplifyClassNames) split(".", "$").last() else this + + companion object { + /** + * Obtain the [TypeIdentifier] for an erased Java class. + * + * @param type The class to get a [TypeIdentifier] for. + */ + fun forClass(type: Class<*>): TypeIdentifier = when { + type.name == "java.lang.Object" -> Top + type.isArray -> ArrayOf(forClass(type.componentType)) + type.typeParameters.isEmpty() -> Unparameterised(type.name) + else -> Erased(type.name) + } + + /** + * Obtain the [TypeIdentifier] for a Java [Type] (typically obtained by calling one of + * [java.lang.reflect.Parameter.getAnnotatedType], + * [java.lang.reflect.Field.getGenericType] or + * [java.lang.reflect.Method.getGenericReturnType]). Wildcard types and type variables are converted to [Unknown]. + * + * @param type The [Type] to obtain a [TypeIdentifier] for. + * @param resolutionContext Optionally, a [Type] which can be used to resolve type variables, for example a + * class implementing a parameterised interface and specifying values for type variables which are referred to + * by methods defined in the interface. + */ + fun forGenericType(type: Type, resolutionContext: Type = type): TypeIdentifier = when(type) { + is ParameterizedType -> Parameterised((type.rawType as Class<*>).name, type.actualTypeArguments.map { + forGenericType(it.resolveAgainst(resolutionContext)) + }) + is Class<*> -> forClass(type) + is GenericArrayType -> ArrayOf(forGenericType(type.genericComponentType.resolveAgainst(resolutionContext))) + else -> Unknown + } + } + + /** + * The [TypeIdentifier] of [Any] / [java.lang.Object]. + */ + object Top : TypeIdentifier() { + override val name get() = "*" + override fun toString() = "Top" + } + + /** + * The [TypeIdentifier] of an unbounded wildcard. + */ + object Unknown : TypeIdentifier() { + override val name get() = "?" + override fun toString() = "Unknown" + } + + /** + * Identifies a class with no type parameters. + */ + data class Unparameterised(override val name: String) : TypeIdentifier() { + override fun toString() = "Unparameterised($name)" + } + + /** + * Identifies a parameterised class such as List, for which we cannot obtain the type parameters at runtime + * because they have been erased. + */ + data class Erased(override val name: String) : TypeIdentifier() { + override fun toString() = "Erased($name)" + } + + /** + * Identifies a type which is an array of some other type. + * + * @param componentType The [TypeIdentifier] of the component type of this array. + */ + data class ArrayOf(val componentType: TypeIdentifier) : TypeIdentifier() { + override val name get() = componentType.name + "[]" + override fun toString() = "ArrayOf(${componentType.prettyPrint()})" + } + + /** + * A parameterised class such as Map for which we have resolved type parameter values. + * + * @param parameters [TypeIdentifier]s for each of the resolved type parameter values of this type. + */ + data class Parameterised(override val name: String, val parameters: List) : TypeIdentifier() { + override fun toString() = "Parameterised(${prettyPrint()})" + } +} + +internal fun Type.resolveAgainst(context: Type): Type = when (this) { + is WildcardType -> this.upperBound + is ParameterizedType, + is TypeVariable<*> -> TypeToken.of(context).resolveType(this).type.upperBound + else -> this +} + +private val Type.upperBound: Type + get() = when (this) { + is TypeVariable<*> -> when { + this.bounds.isEmpty() || this.bounds.size > 1 -> this + else -> this.bounds[0] + } + is WildcardType -> when { + this.upperBounds.isEmpty() || this.upperBounds.size > 1 -> this + else -> this.upperBounds[0] + } + else -> this + } \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt new file mode 100644 index 0000000000..f07f88526a --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt @@ -0,0 +1,53 @@ +package net.corda.serialization.internal.model + +import com.google.common.reflect.TypeToken +import net.corda.serialization.internal.model.TypeIdentifier.* +import org.junit.Test +import java.lang.reflect.Type +import kotlin.test.assertEquals + +class TypeIdentifierTests { + + @Test + fun `primitive types and arrays`() { + assertIdentified(Int::class.javaPrimitiveType!!, "int") + assertIdentified("Integer") + assertIdentified("int[]") + assertIdentified>("Integer[]") + } + + @Test + fun `erased and unerased`() { + assertIdentified(List::class.java, "List (erased)") + assertIdentified>("List") + } + + @Test + fun `nested parameterised`() { + assertIdentified>>("List>") + } + + interface HasArray { + val array: Array> + } + + class HasStringArray(override val array: Array>): HasArray + + @Test + fun `resolved against an owning type`() { + val fieldType = HasArray::class.java.getDeclaredMethod("getArray").genericReturnType + assertIdentified(fieldType, "List<*>[]") + + assertEquals( + "List[]", + TypeIdentifier.forGenericType(fieldType, HasStringArray::class.java).prettyPrint()) + } + + private fun assertIdentified(type: Type, expected: String) = + assertEquals(expected, TypeIdentifier.forGenericType(type).prettyPrint()) + + private inline fun assertIdentified(expected: String) = + assertEquals(expected, TypeIdentifier.forGenericType(typeOf()).prettyPrint()) + + private inline fun typeOf() = object : TypeToken() {}.type +} \ No newline at end of file From e62a3edcd19c8e31a2a3948ca5bbc39e92c0aecf Mon Sep 17 00:00:00 2001 From: josecoll Date: Fri, 19 Oct 2018 16:40:06 +0100 Subject: [PATCH 64/83] Explicitly disable remote gradle build cache when building locally. (#4095) --- buildCacheSettings.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/buildCacheSettings.gradle b/buildCacheSettings.gradle index fcfc1513bf..b4d0175f6e 100644 --- a/buildCacheSettings.gradle +++ b/buildCacheSettings.gradle @@ -9,6 +9,7 @@ buildCache { enabled = !isCiServer } remote(HttpBuildCache) { + enabled = isCiServer url = gradleBuildCacheURL push = isCiServer } From e10119031cdc0b4b62068dcd2e70b98884d05e2c Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 19 Oct 2018 17:23:14 +0100 Subject: [PATCH 65/83] ENT-1906: Allow DJVM code to throw and catch sandbox exceptions. (#4088) * First phase of supporting exceptions within the DJVM. * Suppress unwanted inspection warnings about Kotlin/Java Map. * Add support for exception stack traces within the sandbox. * Simple review fixes. * Extra fixes after review. * Add DJVM support for String.intern(). * Partially restore implementation of SandboxClassLoader.loadClass(). * More review fixes. --- djvm/build.gradle | 2 +- .../java/sandbox/java/lang/Appendable.java | 8 +- .../java/sandbox/java/lang/CharSequence.java | 4 +- .../java/sandbox/java/lang/Comparable.java | 4 +- .../java/lang/DJVMThrowableWrapper.java | 37 ++++ .../src/main/java/sandbox/java/lang/Enum.java | 10 +- .../main/java/sandbox/java/lang/Iterable.java | 4 +- .../main/java/sandbox/java/lang/Object.java | 3 +- .../sandbox/java/lang/StackTraceElement.java | 46 +++++ .../main/java/sandbox/java/lang/String.java | 27 +++ .../java/sandbox/java/lang/StringBuffer.java | 4 +- .../java/sandbox/java/lang/StringBuilder.java | 4 +- .../java/sandbox/java/lang/Throwable.java | 137 ++++++++++++++ .../sandbox/java/nio/charset/Charset.java | 4 +- .../java/sandbox/java/util/Comparator.java | 4 +- .../main/java/sandbox/java/util/Locale.java | 4 +- .../sandbox/java/util/function/Function.java | 4 +- .../sandbox/java/util/function/Supplier.java | 4 +- .../net/corda/djvm/SandboxConfiguration.kt | 3 +- .../djvm/analysis/AnalysisConfiguration.kt | 171 +++++++++++++++--- .../djvm/analysis/ClassAndMemberVisitor.kt | 37 ++-- .../corda/djvm/analysis/ExceptionResolver.kt | 41 +++++ .../net/corda/djvm/analysis/PrefixTree.kt | 2 +- .../net/corda/djvm/analysis/Whitelist.kt | 3 - .../net/corda/djvm/code/ClassMutator.kt | 9 +- .../kotlin/net/corda/djvm/code/Emitter.kt | 6 +- .../net/corda/djvm/code/EmitterModule.kt | 8 + .../main/kotlin/net/corda/djvm/code/Types.kt | 11 ++ .../corda/djvm/code/instructions/TryBlock.kt | 8 + .../djvm/code/instructions/TryCatchBlock.kt | 6 +- .../djvm/code/instructions/TryFinallyBlock.kt | 5 +- .../net/corda/djvm/rewiring/ClassRewriter.kt | 55 ++++-- .../corda/djvm/rewiring/SandboxClassLoader.kt | 68 ++++++- .../djvm/rewiring/SandboxClassRemapper.kt | 6 - .../djvm/rewiring/ThrowableWrapperFactory.kt | 152 ++++++++++++++++ .../DisallowCatchingBlacklistedExceptions.kt | 44 +++-- .../HandleExceptionUnwrapper.kt | 46 +++++ .../implementation/ThrowExceptionWrapper.kt | 20 ++ .../instrumentation/TraceAllocations.kt | 5 +- .../instrumentation/TraceInvocations.kt | 5 +- .../instrumentation/TraceJumps.kt | 5 +- .../instrumentation/TraceThrows.kt | 5 +- .../net/corda/djvm/source/ClassSource.kt | 2 + .../corda/djvm/source/SourceClassLoader.kt | 12 +- djvm/src/main/kotlin/sandbox/Task.kt | 5 +- .../src/main/kotlin/sandbox/java/lang/DJVM.kt | 153 +++++++++++++++- .../kotlin/sandbox/java/lang/DJVMException.kt | 12 ++ .../djvm/costing/ThresholdViolationError.kt | 2 +- .../corda/djvm/rules/RuleViolationError.kt | 2 +- .../test/java/net/corda/djvm/WithJava.java | 24 +++ .../djvm/execution/SandboxEnumJavaTest.java | 106 +++++++++++ .../execution/SandboxThrowableJavaTest.java | 79 ++++++++ .../net/corda/djvm/DJVMExceptionTest.kt | 100 ++++++++++ .../test/kotlin/net/corda/djvm/DJVMTest.kt | 10 - .../test/kotlin/net/corda/djvm/TestBase.kt | 11 +- .../test/kotlin/net/corda/djvm/Utilities.kt | 26 +-- .../djvm/execution/SandboxExecutorTest.kt | 30 +-- .../djvm/execution/SandboxThrowableTest.kt | 95 ++++++++++ 58 files changed, 1508 insertions(+), 192 deletions(-) create mode 100644 djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java create mode 100644 djvm/src/main/java/sandbox/java/lang/StackTraceElement.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Throwable.java create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt create mode 100644 djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt create mode 100644 djvm/src/test/java/net/corda/djvm/WithJava.java create mode 100644 djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java create mode 100644 djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java create mode 100644 djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt diff --git a/djvm/build.gradle b/djvm/build.gradle index d40a4d5523..1b33bdd3ae 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -33,7 +33,6 @@ dependencies { // ASM: byte code manipulation library compile "org.ow2.asm:asm:$asm_version" - compile "org.ow2.asm:asm-tree:$asm_version" compile "org.ow2.asm:asm-commons:$asm_version" // ClassGraph: classpath scanning @@ -62,6 +61,7 @@ shadowJar { exclude 'sandbox/java/lang/Comparable.class' exclude 'sandbox/java/lang/Enum.class' exclude 'sandbox/java/lang/Iterable.class' + exclude 'sandbox/java/lang/StackTraceElement.class' exclude 'sandbox/java/lang/StringBuffer.class' exclude 'sandbox/java/lang/StringBuilder.class' exclude 'sandbox/java/nio/**' diff --git a/djvm/src/main/java/sandbox/java/lang/Appendable.java b/djvm/src/main/java/sandbox/java/lang/Appendable.java index 168607c511..c95eaf6e53 100644 --- a/djvm/src/main/java/sandbox/java/lang/Appendable.java +++ b/djvm/src/main/java/sandbox/java/lang/Appendable.java @@ -3,10 +3,10 @@ package sandbox.java.lang; import java.io.IOException; /** - * This is a dummy class that implements just enough of [java.lang.Appendable] - * to keep [sandbox.java.lang.StringBuilder], [sandbox.java.lang.StringBuffer] - * and [sandbox.java.lang.String] honest. - * Note that it does not extend [java.lang.Appendable]. + * This is a dummy class that implements just enough of {@link java.lang.Appendable} + * to keep {@link sandbox.java.lang.StringBuilder}, {@link sandbox.java.lang.StringBuffer} + * and {@link sandbox.java.lang.String} honest. + * Note that it does not extend {@link java.lang.Appendable}. */ public interface Appendable { diff --git a/djvm/src/main/java/sandbox/java/lang/CharSequence.java b/djvm/src/main/java/sandbox/java/lang/CharSequence.java index 1847103093..10b024d027 100644 --- a/djvm/src/main/java/sandbox/java/lang/CharSequence.java +++ b/djvm/src/main/java/sandbox/java/lang/CharSequence.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import org.jetbrains.annotations.NotNull; /** - * This is a dummy class that implements just enough of [java.lang.CharSequence] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.CharSequence} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface CharSequence extends java.lang.CharSequence { diff --git a/djvm/src/main/java/sandbox/java/lang/Comparable.java b/djvm/src/main/java/sandbox/java/lang/Comparable.java index 686539c1b4..59b3278a1b 100644 --- a/djvm/src/main/java/sandbox/java/lang/Comparable.java +++ b/djvm/src/main/java/sandbox/java/lang/Comparable.java @@ -1,8 +1,8 @@ package sandbox.java.lang; /** - * This is a dummy class that implements just enough of [java.lang.Comparable] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.Comparable} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface Comparable extends java.lang.Comparable { } diff --git a/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java b/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java new file mode 100644 index 0000000000..60de2e117d --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java @@ -0,0 +1,37 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +/** + * Pinned exceptions inherit from {@link java.lang.Throwable}, but we + * still need to be able to pass them through the sandbox's + * exception handlers. In which case we will wrap them inside + * one of these. + * + * Exceptions wrapped inside one of these cannot be caught. + * + * Also used for passing exceptions through finally blocks without + * any expensive unwrapping to {@link sandbox.java.lang.Throwable} + * based types. + */ +final class DJVMThrowableWrapper extends Throwable { + private final java.lang.Throwable throwable; + + DJVMThrowableWrapper(java.lang.Throwable t) { + throwable = t; + } + + /** + * Prevent this wrapper from creating its own stack trace. + */ + @Override + public final Throwable fillInStackTrace() { + return this; + } + + @Override + @NotNull + final java.lang.Throwable fromDJVM() { + return throwable; + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Enum.java b/djvm/src/main/java/sandbox/java/lang/Enum.java index ffcdd8c916..d3a4bf352e 100644 --- a/djvm/src/main/java/sandbox/java/lang/Enum.java +++ b/djvm/src/main/java/sandbox/java/lang/Enum.java @@ -1,11 +1,13 @@ package sandbox.java.lang; +import org.jetbrains.annotations.NotNull; + import java.io.Serializable; /** * This is a dummy class. We will load the actual Enum class at run-time. */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "WeakerAccess"}) public abstract class Enum> extends Object implements Comparable, Serializable { private final String name; @@ -24,4 +26,10 @@ public abstract class Enum> extends Object implements Comparab return ordinal; } + @Override + @NotNull + final java.lang.Enum fromDJVM() { + throw new UnsupportedOperationException("Dummy implementation"); + } + } diff --git a/djvm/src/main/java/sandbox/java/lang/Iterable.java b/djvm/src/main/java/sandbox/java/lang/Iterable.java index 6032fd97db..01f8108ac0 100644 --- a/djvm/src/main/java/sandbox/java/lang/Iterable.java +++ b/djvm/src/main/java/sandbox/java/lang/Iterable.java @@ -5,8 +5,8 @@ import org.jetbrains.annotations.NotNull; import java.util.Iterator; /** - * This is a dummy class that implements just enough of [java.lang.Iterable] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.Iterable} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface Iterable extends java.lang.Iterable { @Override diff --git a/djvm/src/main/java/sandbox/java/lang/Object.java b/djvm/src/main/java/sandbox/java/lang/Object.java index 4208a52a53..62ac16d4dd 100644 --- a/djvm/src/main/java/sandbox/java/lang/Object.java +++ b/djvm/src/main/java/sandbox/java/lang/Object.java @@ -54,8 +54,7 @@ public class Object { private static Class fromDJVM(Class type) { try { - java.lang.String name = type.getName(); - return Class.forName(name.startsWith("sandbox.") ? name.substring(8) : name); + return DJVM.fromDJVMType(type); } catch (ClassNotFoundException e) { throw new RuleViolationError(e.getMessage()); } diff --git a/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java b/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java new file mode 100644 index 0000000000..7b8173134a --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java @@ -0,0 +1,46 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +/** + * This is a dummy class. We will load the genuine class at runtime. + */ +public final class StackTraceElement extends Object implements java.io.Serializable { + + private final String className; + private final String methodName; + private final String fileName; + private final int lineNumber; + + public StackTraceElement(String className, String methodName, String fileName, int lineNumber) { + this.className = className; + this.methodName = methodName; + this.fileName = fileName; + this.lineNumber = lineNumber; + } + + public String getClassName() { + return className; + } + + public String getMethodName() { + return methodName; + } + + public String getFileName() { + return fileName; + } + + public int getLineNumber() { + return lineNumber; + } + + @Override + @NotNull + public String toDJVMString() { + return String.toDJVM( + className.toString() + ':' + methodName.toString() + + (fileName != null ? '(' + fileName.toString() + ':' + lineNumber + ')' : "") + ); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/String.java b/djvm/src/main/java/sandbox/java/lang/String.java index 4cce494d30..476669bfe9 100644 --- a/djvm/src/main/java/sandbox/java/lang/String.java +++ b/djvm/src/main/java/sandbox/java/lang/String.java @@ -7,6 +7,8 @@ import sandbox.java.util.Locale; import java.io.Serializable; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.util.Map; @SuppressWarnings("unused") public final class String extends Object implements Comparable, CharSequence, Serializable { @@ -22,6 +24,18 @@ public final class String extends Object implements Comparable, CharSequ private static final String TRUE = new String("true"); private static final String FALSE = new String("false"); + private static final Map INTERNAL = new java.util.HashMap<>(); + private static final Constructor SHARED; + + static { + try { + SHARED = java.lang.String.class.getDeclaredConstructor(char[].class, java.lang.Boolean.TYPE); + SHARED.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + } + private final java.lang.String value; public String() { @@ -88,6 +102,17 @@ public final class String extends Object implements Comparable, CharSequ this.value = builder.toString(); } + String(char[] value, boolean share) { + java.lang.String newValue; + try { + // This is (presumably) an optimisation for memory usage. + newValue = (java.lang.String) SHARED.newInstance(value, share); + } catch (Exception e) { + newValue = new java.lang.String(value); + } + this.value = newValue; + } + @Override public char charAt(int index) { return value.charAt(index); @@ -310,6 +335,8 @@ public final class String extends Object implements Comparable, CharSequ return toDJVM(value.trim()); } + public String intern() { return INTERNAL.computeIfAbsent(value, s -> this); } + public char[] toCharArray() { return value.toCharArray(); } diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuffer.java b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java index e9cbcad328..4d8fea7e1d 100644 --- a/djvm/src/main/java/sandbox/java/lang/StringBuffer.java +++ b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import java.io.Serializable; /** - * This is a dummy class that implements just enough of [java.lang.StringBuffer] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.StringBuffer} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class StringBuffer extends Object implements CharSequence, Appendable, Serializable { diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuilder.java b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java index ed80b2e508..a90fef7dde 100644 --- a/djvm/src/main/java/sandbox/java/lang/StringBuilder.java +++ b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import java.io.Serializable; /** - * This is a dummy class that implements just enough of [java.lang.StringBuilder] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.StringBuilder} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class StringBuilder extends Object implements Appendable, CharSequence, Serializable { diff --git a/djvm/src/main/java/sandbox/java/lang/Throwable.java b/djvm/src/main/java/sandbox/java/lang/Throwable.java new file mode 100644 index 0000000000..df19e2fc6e --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Throwable.java @@ -0,0 +1,137 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; +import sandbox.TaskTypes; + +import java.io.Serializable; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class Throwable extends Object implements Serializable { + private static final StackTraceElement[] NO_STACK_TRACE = new StackTraceElement[0]; + + private String message; + private Throwable cause; + private StackTraceElement[] stackTrace; + + public Throwable() { + this.cause = this; + fillInStackTrace(); + } + + public Throwable(String message) { + this(); + this.message = message; + } + + public Throwable(Throwable cause) { + this.cause = cause; + this.message = (cause == null) ? null : cause.toDJVMString(); + fillInStackTrace(); + } + + public Throwable(String message, Throwable cause) { + this.message = message; + this.cause = cause; + fillInStackTrace(); + } + + protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + if (writableStackTrace) { + fillInStackTrace(); + } else { + stackTrace = NO_STACK_TRACE; + } + this.message = message; + this.cause = cause; + } + + public String getMessage() { + return message; + } + + public String getLocalizedMessage() { + return getMessage(); + } + + public Throwable getCause() { + return (cause == this) ? null : cause; + } + + public Throwable initCause(Throwable cause) { + if (this.cause != this) { + throw new java.lang.IllegalStateException( + "Can't overwrite cause with " + java.util.Objects.toString(cause, "a null"), fromDJVM()); + } + if (cause == this) { + throw new java.lang.IllegalArgumentException("Self-causation not permitted", fromDJVM()); + } + this.cause = cause; + return this; + } + + @Override + @NotNull + public String toDJVMString() { + java.lang.String s = getClass().getName(); + String localized = getLocalizedMessage(); + return String.valueOf((localized != null) ? (s + ": " + localized.toString()) : s); + } + + public StackTraceElement[] getStackTrace() { + return (stackTrace == NO_STACK_TRACE) ? stackTrace : stackTrace.clone(); + } + + public void setStackTrace(StackTraceElement[] stackTrace) { + StackTraceElement[] traceCopy = stackTrace.clone(); + + for (int i = 0; i < traceCopy.length; ++i) { + if (traceCopy[i] == null) { + throw new java.lang.NullPointerException("stackTrace[" + i + ']'); + } + } + + this.stackTrace = traceCopy; + } + + @SuppressWarnings({"ThrowableNotThrown", "UnusedReturnValue"}) + public Throwable fillInStackTrace() { + if (stackTrace == null) { + /* + * We have been invoked from within this exception's constructor. + * Work our way up the stack trace until we find this constructor, + * and then find out who actually invoked it. This is where our + * sandboxed stack trace will start from. + * + * Our stack trace will end at the point where we entered the sandbox. + */ + final java.lang.StackTraceElement[] elements = new java.lang.Throwable().getStackTrace(); + final java.lang.String exceptionName = getClass().getName(); + int startIdx = 1; + while (startIdx < elements.length && !isConstructorFor(elements[startIdx], exceptionName)) { + ++startIdx; + } + while (startIdx < elements.length && isConstructorFor(elements[startIdx], exceptionName)) { + ++startIdx; + } + + int endIdx = startIdx; + while (endIdx < elements.length && !TaskTypes.isEntryPoint(elements[endIdx])) { + ++endIdx; + } + stackTrace = (startIdx == elements.length) ? NO_STACK_TRACE : DJVM.copyToDJVM(elements, startIdx, endIdx); + } + return this; + } + + private static boolean isConstructorFor(java.lang.StackTraceElement elt, java.lang.String className) { + return elt.getClassName().equals(className) && elt.getMethodName().equals(""); + } + + public void printStackTrace() {} + + @Override + @NotNull + java.lang.Throwable fromDJVM() { + return DJVM.fromDJVM(this); + } +} diff --git a/djvm/src/main/java/sandbox/java/nio/charset/Charset.java b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java index 371a21404a..453006bb7f 100644 --- a/djvm/src/main/java/sandbox/java/nio/charset/Charset.java +++ b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java @@ -1,8 +1,8 @@ package sandbox.java.nio.charset; /** - * This is a dummy class that implements just enough of [java.nio.charset.Charset] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.nio.charset.Charset} + * to allow us to compile {@link sandbox.java.lang.String}. */ @SuppressWarnings("unused") public abstract class Charset extends sandbox.java.lang.Object { diff --git a/djvm/src/main/java/sandbox/java/util/Comparator.java b/djvm/src/main/java/sandbox/java/util/Comparator.java index 20679dee59..f6363d9c34 100644 --- a/djvm/src/main/java/sandbox/java/util/Comparator.java +++ b/djvm/src/main/java/sandbox/java/util/Comparator.java @@ -1,8 +1,8 @@ package sandbox.java.util; /** - * This is a dummy class that implements just enough of [java.util.Comparator] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.util.Comparator} + * to allow us to compile {@link sandbox.java.lang.String}. */ @FunctionalInterface public interface Comparator extends java.util.Comparator { diff --git a/djvm/src/main/java/sandbox/java/util/Locale.java b/djvm/src/main/java/sandbox/java/util/Locale.java index 3ceaea9382..ed06b79058 100644 --- a/djvm/src/main/java/sandbox/java/util/Locale.java +++ b/djvm/src/main/java/sandbox/java/util/Locale.java @@ -1,8 +1,8 @@ package sandbox.java.util; /** - * This is a dummy class that implements just enough of [java.util.Locale] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.util.Locale} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class Locale extends sandbox.java.lang.Object { public abstract sandbox.java.lang.String toLanguageTag(); diff --git a/djvm/src/main/java/sandbox/java/util/function/Function.java b/djvm/src/main/java/sandbox/java/util/function/Function.java index 5cd806a01e..ce26393f67 100644 --- a/djvm/src/main/java/sandbox/java/util/function/Function.java +++ b/djvm/src/main/java/sandbox/java/util/function/Function.java @@ -1,8 +1,8 @@ package sandbox.java.util.function; /** - * This is a dummy class that implements just enough of [java.util.function.Function] - * to allow us to compile [sandbox.Task]. + * This is a dummy class that implements just enough of {@link java.util.function.Function} + * to allow us to compile {@link sandbox.Task}. */ @FunctionalInterface public interface Function { diff --git a/djvm/src/main/java/sandbox/java/util/function/Supplier.java b/djvm/src/main/java/sandbox/java/util/function/Supplier.java index 31f236bae6..0ff9f56dfb 100644 --- a/djvm/src/main/java/sandbox/java/util/function/Supplier.java +++ b/djvm/src/main/java/sandbox/java/util/function/Supplier.java @@ -1,8 +1,8 @@ package sandbox.java.util.function; /** - * This is a dummy class that implements just enough of [java.util.function.Supplier] - * to allow us to compile [sandbox.java.lang.ThreadLocal]. + * This is a dummy class that implements just enough of @{link java.util.function.Supplier} + * to allow us to compile {@link sandbox.java.lang.ThreadLocal}. */ @FunctionalInterface public interface Supplier { diff --git a/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt index da7cd0d553..ffd233df25 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt @@ -2,6 +2,7 @@ package net.corda.djvm import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.code.DefinitionProvider +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.execution.ExecutionProfile import net.corda.djvm.rules.Rule @@ -51,7 +52,7 @@ class SandboxConfiguration private constructor( executionProfile = profile, rules = rules, emitters = (emitters ?: Discovery.find()).filter { - enableTracing || !it.isTracer + enableTracing || it.priority > EMIT_TRACING }, definitionProviders = definitionProviders, analysisConfiguration = analysisConfiguration diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt index f8d87fd1ea..ff71421a54 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt @@ -58,11 +58,21 @@ class AnalysisConfiguration( */ val stitchedInterfaces: Map> get() = STITCHED_INTERFACES + /** + * These classes have extra methods added as they are mapped into the sandbox. + */ + val stitchedClasses: Map> get() = STITCHED_CLASSES + /** * Functionality used to resolve the qualified name and relevant information about a class. */ val classResolver: ClassResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX) + /** + * Resolves the internal names of synthetic exception classes. + */ + val exceptionResolver: ExceptionResolver = ExceptionResolver(JVM_EXCEPTIONS, pinnedClasses, SANDBOX_PREFIX) + private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) } val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader) @@ -76,11 +86,14 @@ class AnalysisConfiguration( fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES fun isPinnedClass(className: String): Boolean = className in pinnedClasses + fun isJvmException(className: String): Boolean = className in JVM_EXCEPTIONS + fun isSandboxClass(className: String): Boolean = className.startsWith(SANDBOX_PREFIX) && !isPinnedClass(className) + companion object { /** * The package name prefix to use for classes loaded into a sandbox. */ - private const val SANDBOX_PREFIX: String = "sandbox/" + const val SANDBOX_PREFIX: String = "sandbox/" /** * These class must belong to the application class loader. @@ -111,60 +124,162 @@ class AnalysisConfiguration( java.lang.String.CASE_INSENSITIVE_ORDER::class.java, java.lang.System::class.java, java.lang.ThreadLocal::class.java, + java.lang.Throwable::class.java, kotlin.Any::class.java, sun.misc.JavaLangAccess::class.java, sun.misc.SharedSecrets::class.java ).sandboxed() + setOf( "sandbox/Task", + "sandbox/TaskTypes", "sandbox/java/lang/DJVM", + "sandbox/java/lang/DJVMException", + "sandbox/java/lang/DJVMThrowableWrapper", "sandbox/sun/misc/SharedSecrets\$1", "sandbox/sun/misc/SharedSecrets\$JavaLangAccessImpl" ) + /** + * These are thrown by the JVM itself, and so + * we need to handle them without wrapping them. + * + * Note that this set is closed, i.e. every one + * of these exceptions' [Throwable] super classes + * is also within this set. + * + * The full list of exceptions is determined by: + * hotspot/src/share/vm/classfile/vmSymbols.hpp + */ + val JVM_EXCEPTIONS: Set = setOf( + java.io.IOException::class.java, + java.lang.AbstractMethodError::class.java, + java.lang.ArithmeticException::class.java, + java.lang.ArrayIndexOutOfBoundsException::class.java, + java.lang.ArrayStoreException::class.java, + java.lang.ClassCastException::class.java, + java.lang.ClassCircularityError::class.java, + java.lang.ClassFormatError::class.java, + java.lang.ClassNotFoundException::class.java, + java.lang.CloneNotSupportedException::class.java, + java.lang.Error::class.java, + java.lang.Exception::class.java, + java.lang.ExceptionInInitializerError::class.java, + java.lang.IllegalAccessError::class.java, + java.lang.IllegalAccessException::class.java, + java.lang.IllegalArgumentException::class.java, + java.lang.IllegalStateException::class.java, + java.lang.IncompatibleClassChangeError::class.java, + java.lang.IndexOutOfBoundsException::class.java, + java.lang.InstantiationError::class.java, + java.lang.InstantiationException::class.java, + java.lang.InternalError::class.java, + java.lang.LinkageError::class.java, + java.lang.NegativeArraySizeException::class.java, + java.lang.NoClassDefFoundError::class.java, + java.lang.NoSuchFieldError::class.java, + java.lang.NoSuchFieldException::class.java, + java.lang.NoSuchMethodError::class.java, + java.lang.NoSuchMethodException::class.java, + java.lang.NullPointerException::class.java, + java.lang.OutOfMemoryError::class.java, + java.lang.ReflectiveOperationException::class.java, + java.lang.RuntimeException::class.java, + java.lang.StackOverflowError::class.java, + java.lang.StringIndexOutOfBoundsException::class.java, + java.lang.ThreadDeath::class.java, + java.lang.Throwable::class.java, + java.lang.UnknownError::class.java, + java.lang.UnsatisfiedLinkError::class.java, + java.lang.UnsupportedClassVersionError::class.java, + java.lang.UnsupportedOperationException::class.java, + java.lang.VerifyError::class.java, + java.lang.VirtualMachineError::class.java + ).sandboxed() + setOf( + // Mentioned here to prevent the DJVM from generating a synthetic wrapper. + "sandbox/java/lang/DJVMThrowableWrapper" + ) + /** * These interfaces will be modified as follows when * added to the sandbox: * * interface sandbox.A extends A */ - private val STITCHED_INTERFACES: Map> = mapOf( - sandboxed(CharSequence::class.java) to listOf( - object : MethodBuilder( - access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, - className = "sandbox/java/lang/CharSequence", - memberName = "subSequence", - descriptor = "(II)Ljava/lang/CharSequence;" - ) { - override fun writeBody(emitter: EmitterModule) = with(emitter) { - pushObject(0) - pushInteger(1) - pushInteger(2) - invokeInterface(className, memberName, "(II)L$className;") - returnObject() - } - }.withBody() - .build(), - MethodBuilder( - access = ACC_PUBLIC or ACC_ABSTRACT, - className = "sandbox/java/lang/CharSequence", - memberName = "toString", - descriptor = "()Ljava/lang/String;" - ).build() - ), + private val STITCHED_INTERFACES: Map> = listOf( + object : MethodBuilder( + access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, + className = sandboxed(CharSequence::class.java), + memberName = "subSequence", + descriptor = "(II)Ljava/lang/CharSequence;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + pushInteger(1) + pushInteger(2) + invokeInterface(className, memberName, "(II)L$className;") + returnObject() + } + }.withBody() + .build(), + + MethodBuilder( + access = ACC_PUBLIC or ACC_ABSTRACT, + className = sandboxed(CharSequence::class.java), + memberName = "toString", + descriptor = "()Ljava/lang/String;" + ).build() + ).mapByClassName() + mapOf( sandboxed(Comparable::class.java) to emptyList(), sandboxed(Comparator::class.java) to emptyList(), sandboxed(Iterable::class.java) to emptyList() ) - private fun sandboxed(clazz: Class<*>) = SANDBOX_PREFIX + Type.getInternalName(clazz) + /** + * These classes have extra methods added when mapped into the sandbox. + */ + private val STITCHED_CLASSES: Map> = listOf( + object : MethodBuilder( + access = ACC_FINAL, + className = sandboxed(Enum::class.java), + memberName = "fromDJVM", + descriptor = "()Ljava/lang/Enum;", + signature = "()Ljava/lang/Enum<*>;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + invokeStatic("sandbox/java/lang/DJVM", "fromDJVMEnum", "(Lsandbox/java/lang/Enum;)Ljava/lang/Enum;") + returnObject() + } + }.withBody() + .build(), + + object : MethodBuilder( + access = ACC_BRIDGE or ACC_SYNTHETIC, + className = sandboxed(Enum::class.java), + memberName = "fromDJVM", + descriptor = "()Ljava/lang/Object;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + invokeVirtual(className, memberName, "()Ljava/lang/Enum;") + returnObject() + } + }.withBody() + .build() + ).mapByClassName() + + private fun sandboxed(clazz: Class<*>): String = (SANDBOX_PREFIX + Type.getInternalName(clazz)).intern() private fun Set>.sandboxed(): Set = map(Companion::sandboxed).toSet() + private fun Iterable.mapByClassName(): Map> + = groupBy(Member::className).mapValues(Map.Entry>::value) } private open class MethodBuilder( protected val access: Int, protected val className: String, protected val memberName: String, - protected val descriptor: String) { + protected val descriptor: String, + protected val signature: String = "" + ) { private val bodies = mutableListOf() protected open fun writeBody(emitter: EmitterModule) {} @@ -179,7 +294,7 @@ class AnalysisConfiguration( className = className, memberName = memberName, signature = descriptor, - genericsDetails = "", + genericsDetails = signature, body = bodies ) } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt index 8bfb997ae7..3ab04895e4 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt @@ -2,6 +2,7 @@ package net.corda.djvm.analysis import net.corda.djvm.code.EmitterModule import net.corda.djvm.code.Instruction +import net.corda.djvm.code.emptyAsNull import net.corda.djvm.code.instructions.* import net.corda.djvm.messages.Message import net.corda.djvm.references.* @@ -232,7 +233,7 @@ open class ClassAndMemberVisitor( analysisContext.classes.add(visitedClass) super.visit( version, access, visitedClass.name, signature, - visitedClass.superClass.nullIfEmpty(), + visitedClass.superClass.emptyAsNull, visitedClass.interfaces.toTypedArray() ) } @@ -285,12 +286,19 @@ open class ClassAndMemberVisitor( ): MethodVisitor? { var visitedMember: Member? = null val clazz = currentClass!! - val member = Member(access, clazz.name, name, desc, signature ?: "") + val member = Member( + access = access, + className = clazz.name, + memberName = name, + signature = desc, + genericsDetails = signature ?: "", + exceptions = exceptions?.toMutableSet() ?: mutableSetOf() + ) currentMember = member sourceLocation = sourceLocation.copy( - memberName = name, - signature = desc, - lineNumber = 0 + memberName = name, + signature = desc, + lineNumber = 0 ) val processMember = captureExceptions { visitedMember = visitMethod(clazz, member) @@ -320,12 +328,19 @@ open class ClassAndMemberVisitor( ): FieldVisitor? { var visitedMember: Member? = null val clazz = currentClass!! - val member = Member(access, clazz.name, name, desc, "", value = value) + val member = Member( + access = access, + className = clazz.name, + memberName = name, + signature = desc, + genericsDetails = "", + value = value + ) currentMember = member sourceLocation = sourceLocation.copy( - memberName = name, - signature = desc, - lineNumber = 0 + memberName = name, + signature = desc, + lineNumber = 0 ) val processMember = captureExceptions { visitedMember = visitField(clazz, member) @@ -578,10 +593,6 @@ open class ClassAndMemberVisitor( */ const val API_VERSION: Int = Opcodes.ASM6 - private fun String.nullIfEmpty(): String? { - return if (this.isEmpty()) { null } else { this } - } - } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt new file mode 100644 index 0000000000..0bfeb103e2 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt @@ -0,0 +1,41 @@ +package net.corda.djvm.analysis + +import org.objectweb.asm.Type + +class ExceptionResolver( + private val jvmExceptionClasses: Set, + private val pinnedClasses: Set, + private val sandboxPrefix: String +) { + companion object { + private const val DJVM_EXCEPTION_NAME = "\$1DJVM" + + fun isDJVMException(className: String): Boolean = className.endsWith(DJVM_EXCEPTION_NAME) + fun getDJVMException(className: String): String = className + DJVM_EXCEPTION_NAME + fun getDJVMExceptionOwner(className: String): String = className.dropLast(DJVM_EXCEPTION_NAME.length) + } + + fun getThrowableName(clazz: Class<*>): String { + return getDJVMException(Type.getInternalName(clazz)) + } + + fun getThrowableSuperName(clazz: Class<*>): String { + return getThrowableOwnerName(Type.getInternalName(clazz.superclass)) + } + + fun getThrowableOwnerName(className: String): String { + return if (className in jvmExceptionClasses) { + className.unsandboxed + } else if (className in pinnedClasses) { + className + } else { + getDJVMException(className) + } + } + + private val String.unsandboxed: String get() = if (startsWith(sandboxPrefix)) { + drop(sandboxPrefix.length) + } else { + this + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt index a063965b76..26679cc133 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt @@ -35,4 +35,4 @@ class PrefixTree { return false } -} \ No newline at end of file +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt index c19cc8111e..ed3fb32e88 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt @@ -100,9 +100,6 @@ open class Whitelist private constructor( "^java/lang/Cloneable(\\..*)?\$".toRegex(), "^java/lang/Object(\\..*)?\$".toRegex(), "^java/lang/Override(\\..*)?\$".toRegex(), - // TODO: sandbox exception handling! - "^java/lang/StackTraceElement\$".toRegex(), - "^java/lang/Throwable\$".toRegex(), "^java/lang/Void\$".toRegex(), "^java/lang/invoke/LambdaMetafactory\$".toRegex(), "^java/lang/invoke/MethodHandles(\\\$.*)?\$".toRegex(), diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt index 3c800d9859..4d8f2b6307 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt @@ -41,7 +41,11 @@ class ClassMutator( } } - private val emitters: List = emitters + PrependClassInitializer() + /* + * Some emitters must be executed before others. E.g. we need to apply + * the tracing emitters before the non-tracing ones. + */ + private val emitters: List = (emitters + PrependClassInitializer()).sortedBy(Emitter::priority) private val initializers = mutableListOf() /** @@ -128,8 +132,7 @@ class ClassMutator( */ override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { val context = EmitterContext(currentAnalysisContext(), configuration, emitter) - // We need to apply the tracing emitters before the non-tracing ones. - Processor.processEntriesOfType(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) { + Processor.processEntriesOfType(emitters, analysisContext.messages) { it.emit(context, instruction) } if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt index f904d276b7..2eb5e0de5d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt @@ -18,10 +18,10 @@ interface Emitter { fun emit(context: EmitterContext, instruction: Instruction) /** - * Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox. + * Determines the order in which emitters are executed within the sandbox. */ @JvmDefault - val isTracer: Boolean - get() = false + val priority: Int + get() = EMIT_DEFAULT } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt index 2e2d2fc2e4..e51647830b 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt @@ -168,6 +168,14 @@ class EmitterModule( inline fun throwException(message: String) = throwException(T::class.java, message) + /** + * Attempt to cast the object on the top of the stack to the given class. + */ + fun castObjectTo(className: String) { + methodVisitor.visitTypeInsn(CHECKCAST, className) + hasEmittedCustomCode = true + } + /** * Emit instruction for returning from "void" method. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt index 93a9c5bf7d..3d3b86d2af 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt @@ -2,11 +2,22 @@ package net.corda.djvm.code import org.objectweb.asm.Type +import sandbox.java.lang.DJVMException import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.rules.RuleViolationError +/** + * These are the priorities for executing [Emitter] instances. + * Tracing emitters are executed first. + */ +const val EMIT_TRACING: Int = 0 +const val EMIT_TRAPPING_EXCEPTIONS: Int = EMIT_TRACING + 1 +const val EMIT_HANDLING_EXCEPTIONS: Int = EMIT_TRAPPING_EXCEPTIONS + 1 +const val EMIT_DEFAULT: Int = 10 + val ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java) val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::class.java) +val djvmException: String = Type.getInternalName(DJVMException::class.java) /** * Local extension method for normalizing a class name. diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt new file mode 100644 index 0000000000..a984766d29 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt @@ -0,0 +1,8 @@ +package net.corda.djvm.code.instructions + +import org.objectweb.asm.Label + +open class TryBlock( + val handler: Label, + val typeName: String +) : NoOperationInstruction() \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt index ac9b9e643f..943d745c80 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt @@ -9,6 +9,6 @@ import org.objectweb.asm.Label * @property handler The label of the exception handler. */ class TryCatchBlock( - val typeName: String, - val handler: Label -) : NoOperationInstruction() + typeName: String, + handler: Label +) : TryBlock(handler, typeName) diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt index 808575b05d..7ec2149b73 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt @@ -7,7 +7,6 @@ import org.objectweb.asm.Label * * @property handler The handler for the finally-block. */ -@Suppress("MemberVisibilityCanBePrivate") class TryFinallyBlock( - val handler: Label -) : NoOperationInstruction() + handler: Label +) : TryBlock(handler, "") diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt index 081bff4fa5..4804074457 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt @@ -1,7 +1,6 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration -import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import net.corda.djvm.code.ClassMutator @@ -11,6 +10,8 @@ import net.corda.djvm.references.Member import net.corda.djvm.utilities.loggerFor import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor /** * Functionality for rewriting parts of a class as it is being loaded. @@ -22,6 +23,7 @@ open class ClassRewriter( private val configuration: SandboxConfiguration, private val classLoader: ClassLoader ) { + private val analysisConfig = configuration.analysisConfiguration /** * Process class and allow user to rewrite parts/all of its content through provided hooks. @@ -32,13 +34,15 @@ open class ClassRewriter( fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode { logger.debug("Rewriting class {}...", reader.className) val writer = SandboxClassWriter(reader, classLoader) - val analysisConfiguration = configuration.analysisConfiguration - val classRemapper = SandboxClassRemapper(InterfaceStitcher(writer, analysisConfiguration), analysisConfiguration) + val classRemapper = SandboxClassRemapper( + ClassExceptionRemapper(SandboxStitcher(writer)), + analysisConfig + ) val visitor = ClassMutator( - classRemapper, - analysisConfiguration, - configuration.definitionProviders, - configuration.emitters + classRemapper, + analysisConfig, + configuration.definitionProviders, + configuration.emitters ) visitor.analyze(reader, context, options = ClassReader.EXPAND_FRAMES) return ByteCode(writer.toByteArray(), visitor.hasBeenModified) @@ -50,25 +54,30 @@ open class ClassRewriter( /** * Extra visitor that is applied after [SandboxRemapper]. This "stitches" the original - * unmapped interface as a super-interface of the mapped version. + * unmapped interface as a super-interface of the mapped version, as well as adding + * any extra methods that are needed. */ - private class InterfaceStitcher(parent: ClassVisitor, private val configuration: AnalysisConfiguration) + private inner class SandboxStitcher(parent: ClassVisitor) : ClassVisitor(API_VERSION, parent) { private val extraMethods = mutableListOf() override fun visit(version: Int, access: Int, className: String, signature: String?, superName: String?, interfaces: Array?) { - val stitchedInterfaces = configuration.stitchedInterfaces[className]?.let { methods -> + val stitchedInterfaces = analysisConfig.stitchedInterfaces[className]?.let { methods -> extraMethods += methods - arrayOf(*(interfaces ?: emptyArray()), configuration.classResolver.reverse(className)) + arrayOf(*(interfaces ?: emptyArray()), analysisConfig.classResolver.reverse(className)) } ?: interfaces + analysisConfig.stitchedClasses[className]?.also { methods -> + extraMethods += methods + } + super.visit(version, access, className, signature, superName, stitchedInterfaces) } override fun visitEnd() { for (method in extraMethods) { - method.apply { + with(method) { visitMethod(access, memberName, signature, genericsDetails.emptyAsNull, exceptions.toTypedArray())?.also { mv -> mv.visitCode() EmitterModule(mv).writeByteCode(body) @@ -81,4 +90,26 @@ open class ClassRewriter( super.visitEnd() } } + + /** + * Map exceptions in method signatures to their sandboxed equivalents. + */ + private inner class ClassExceptionRemapper(parent: ClassVisitor) : ClassVisitor(API_VERSION, parent) { + override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array?): MethodVisitor? { + val mappedExceptions = exceptions?.map(analysisConfig.exceptionResolver::getThrowableOwnerName)?.toTypedArray() + return super.visitMethod(access, name, descriptor, signature, mappedExceptions)?.let { + MethodExceptionRemapper(it) + } + } + } + + /** + * Map exceptions in method try-catch blocks to their sandboxed equivalents. + */ + private inner class MethodExceptionRemapper(parent: MethodVisitor) : MethodVisitor(API_VERSION, parent) { + override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, exceptionType: String?) { + val mappedExceptionType = exceptionType?.let(analysisConfig.exceptionResolver::getThrowableOwnerName) + super.visitTryCatchBlock(start, end, handler, mappedExceptionType) + } + } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt index 7f2abccb6a..4dbeae7ab2 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt @@ -3,6 +3,9 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException +import net.corda.djvm.code.asPackagePath import net.corda.djvm.code.asResourcePath import net.corda.djvm.references.ClassReference import net.corda.djvm.source.ClassSource @@ -33,7 +36,7 @@ class SandboxClassLoader( /** * The analyzer used to traverse the class hierarchy. */ - val analyzer: ClassAndMemberVisitor + private val analyzer: ClassAndMemberVisitor get() = ruleValidator /** @@ -56,6 +59,18 @@ class SandboxClassLoader( */ private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader) + /** + * We need to load this class up front, so that we can identify sandboxed exception classes. + */ + private val throwableClass: Class<*> + + init { + // Bootstrap the loading of the sandboxed Throwable class. + loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Object"), context) + loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.StackTraceElement"), context) + throwableClass = loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Throwable"), context).type + } + /** * Given a class name, provide its corresponding [LoadedClass] for the sandbox. */ @@ -77,11 +92,43 @@ class SandboxClassLoader( */ @Throws(ClassNotFoundException::class) override fun loadClass(name: String, resolve: Boolean): Class<*> { - val source = ClassSource.fromClassName(name) - return if (name.startsWith("sandbox.") && !analysisConfiguration.isPinnedClass(source.internalClassName)) { - loadClassAndBytes(source, context).type + var clazz = findLoadedClass(name) + if (clazz == null) { + val source = ClassSource.fromClassName(name) + clazz = if (analysisConfiguration.isSandboxClass(source.internalClassName)) { + loadSandboxClass(source, context).type + } else { + super.loadClass(name, resolve) + } + } + if (resolve) { + resolveClass(clazz) + } + return clazz + } + + private fun loadSandboxClass(source: ClassSource, context: AnalysisContext): LoadedClass { + return if (isDJVMException(source.internalClassName)) { + /** + * We need to load a DJVMException's owner class before we can create + * its wrapper exception. And loading the owner should also create the + * wrapper class automatically. + */ + loadedClasses.getOrElse(source.internalClassName) { + loadSandboxClass(ClassSource.fromClassName(getDJVMExceptionOwner(source.qualifiedClassName)), context) + loadedClasses[source.internalClassName] + } ?: throw ClassNotFoundException(source.qualifiedClassName) } else { - super.loadClass(name, resolve) + loadClassAndBytes(source, context).also { clazz -> + /** + * Check whether we've just loaded an unpinned sandboxed throwable class. + * If we have, we may also need to synthesise a throwable wrapper for it. + */ + if (throwableClass.isAssignableFrom(clazz.type) && !analysisConfiguration.isJvmException(source.internalClassName)) { + logger.debug("Generating synthetic throwable for ${source.qualifiedClassName}") + loadWrapperFor(clazz.type) + } + } } } @@ -134,7 +181,7 @@ class SandboxClassLoader( } // Try to define the transformed class. - val clazz = try { + val clazz: Class<*> = try { when { whitelistedClasses.matches(sourceName.asResourcePath) -> supportingClassLoader.loadClass(sourceName) else -> defineClass(resolvedName, byteCode.bytes, 0, byteCode.bytes.size) @@ -167,6 +214,15 @@ class SandboxClassLoader( } } + private fun loadWrapperFor(throwable: Class<*>): LoadedClass { + val className = analysisConfiguration.exceptionResolver.getThrowableName(throwable) + return loadedClasses.getOrPut(className) { + val superName = analysisConfiguration.exceptionResolver.getThrowableSuperName(throwable) + val byteCode = ThrowableWrapperFactory.toByteCode(className, superName) + LoadedClass(defineClass(className.asPackagePath, byteCode.bytes, 0, byteCode.bytes.size), byteCode) + } + } + private companion object { private val logger = loggerFor() private val UNMODIFIED = ByteCode(ByteArray(0), false) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt index 7412999727..9f90981f57 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt @@ -3,7 +3,6 @@ package net.corda.djvm.rewiring import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import org.objectweb.asm.ClassVisitor -import org.objectweb.asm.Label import org.objectweb.asm.MethodVisitor import org.objectweb.asm.commons.ClassRemapper @@ -35,11 +34,6 @@ class SandboxClassRemapper(cv: ClassVisitor, private val configuration: Analysis return mapperFor(method).visitMethodInsn(opcode, owner, name, descriptor, isInterface) } - override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, type: String?) { - // Don't map caught exception names - these could be thrown by the JVM itself. - nonmapper.visitTryCatchBlock(start, end, handler, type) - } - override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { val field = Element(owner, name, descriptor) return mapperFor(field).visitFieldInsn(opcode, owner, name, descriptor) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt new file mode 100644 index 0000000000..3557ed50cf --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt @@ -0,0 +1,152 @@ +package net.corda.djvm.rewiring + +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException +import net.corda.djvm.code.djvmException +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes.* + +/** + * Generates a synthetic [Throwable] class that will wrap a [sandbox.java.lang.Throwable]. + * Only exceptions which are NOT thrown by the JVM will be accompanied one of these. + */ +class ThrowableWrapperFactory( + private val className: String, + private val superName: String +) { + companion object { + const val CONSTRUCTOR_DESCRIPTOR = "(Lsandbox/java/lang/Throwable;)V" + const val FIELD_TYPE = "Lsandbox/java/lang/Throwable;" + const val THROWABLE_FIELD = "t" + + fun toByteCode(className: String, superName: String): ByteCode { + val bytecode: ByteArray = with(ClassWriter(0)) { + ThrowableWrapperFactory(className, superName).accept(this) + toByteArray() + } + return ByteCode(bytecode, true) + } + } + + /** + * Write bytecode for synthetic throwable wrapper class. All of + * these classes implement [sandbox.java.lang.DJVMException], + * either directly or indirectly. + */ + fun accept(writer: ClassWriter) = with(writer) { + if (isDJVMException(superName)) { + childClass() + } else { + baseClass() + } + } + + /** + * This is a "base" wrapper class that inherits from a JVM exception. + * + * + * public class CLASSNAME extends JAVA_EXCEPTION implements DJVMException { + * private final sandbox.java.lang.Throwable t; + * + * public CLASSNAME(sandbox.java.lang.Throwable t) { + * this.t = t; + * } + * + * @Override + * public final sandbox.java.lang.Throwable getThrowable() { + * return t; + * } + * + * @Override + * public final java.lang.Throwable fillInStackTrace() { + * return this; + * } + * } + * + */ + private fun ClassWriter.baseClass() { + // Class definition + visit( + V1_8, + ACC_SYNTHETIC or ACC_PUBLIC, + className, + null, + superName, + arrayOf(djvmException) + ) + + // Private final field to hold the sandbox throwable object. + visitField(ACC_PRIVATE or ACC_FINAL, THROWABLE_FIELD, FIELD_TYPE, null, null) + + // Constructor + visitMethod(ACC_PUBLIC, "", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitMethodInsn(INVOKESPECIAL, superName, "", "()V", false) + mv.visitVarInsn(ALOAD, 0) + mv.visitVarInsn(ALOAD, 1) + mv.visitFieldInsn(PUTFIELD, className, THROWABLE_FIELD, FIELD_TYPE) + mv.visitInsn(RETURN) + mv.visitMaxs(2, 2) + mv.visitEnd() + } + + // Getter method for the sandbox throwable object. + visitMethod(ACC_PUBLIC or ACC_FINAL, "getThrowable", "()$FIELD_TYPE", null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitFieldInsn(GETFIELD, className, THROWABLE_FIELD, FIELD_TYPE) + mv.visitInsn(ARETURN) + mv.visitMaxs(1, 1) + mv.visitEnd() + } + + // Prevent these wrappers from generating their own stack traces. + visitMethod(ACC_PUBLIC or ACC_FINAL, "fillInStackTrace", "()Ljava/lang/Throwable;", null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitInsn(ARETURN) + mv.visitMaxs(1, 1) + mv.visitEnd() + } + + // End of class + visitEnd() + } + + /** + * This wrapper class inherits from another wrapper class. + * + * + * public class CLASSNAME extends SUPERNAME { + * public CLASSNAME(sandbox.java.lang.Throwable t) { + * super(t); + * } + * } + * + */ + private fun ClassWriter.childClass() { + // Class definition + visit( + V1_8, + ACC_SYNTHETIC or ACC_PUBLIC, + className, + null, + superName, + arrayOf() + ) + + // Constructor + visitMethod(ACC_PUBLIC, "", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitVarInsn(ALOAD, 1) + mv.visitMethodInsn(INVOKESPECIAL, superName, "", CONSTRUCTOR_DESCRIPTOR, false) + mv.visitInsn(RETURN) + mv.visitMaxs(2, 2) + mv.visitEnd() + } + + // End of class + visitEnd() + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt index a5524ec12b..d898a747b0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt @@ -27,30 +27,36 @@ class DisallowCatchingBlacklistedExceptions : Emitter { companion object { private val disallowedExceptionTypes = setOf( - ruleViolationError, - thresholdViolationError, + ruleViolationError, + thresholdViolationError, - /** - * These errors indicate that the JVM is failing, - * so don't allow these to be caught either. - */ - "java/lang/StackOverflowError", - "java/lang/OutOfMemoryError", + /** + * These errors indicate that the JVM is failing, + * so don't allow these to be caught either. + */ + "java/lang/StackOverflowError", + "java/lang/OutOfMemoryError", - /** - * These are immediate super-classes for our explicit errors. - */ - "java/lang/VirtualMachineError", - "java/lang/ThreadDeath", + /** + * These are immediate super-classes for our explicit errors. + */ + "java/lang/VirtualMachineError", + "java/lang/ThreadDeath", - /** - * Any of [ThreadDeath] and [VirtualMachineError]'s throwable - * super-classes also need explicit checking. - */ - "java/lang/Throwable", - "java/lang/Error" + /** + * Any of [ThreadDeath] and [VirtualMachineError]'s throwable + * super-classes also need explicit checking. + */ + "java/lang/Throwable", + "java/lang/Error" ) } + /** + * We need to invoke this emitter before the [HandleExceptionUnwrapper] + * so that we don't unwrap exceptions we don't want to catch. + */ + override val priority: Int + get() = EMIT_TRAPPING_EXCEPTIONS } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt new file mode 100644 index 0000000000..b616128b85 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt @@ -0,0 +1,46 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.EMIT_HANDLING_EXCEPTIONS +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.CodeLabel +import net.corda.djvm.code.instructions.TryBlock +import org.objectweb.asm.Label + +/** + * Converts an exception from [java.lang.Throwable] to [sandbox.java.lang.Throwable] + * at the beginning of either a catch block or a finally block. + */ +class HandleExceptionUnwrapper : Emitter { + private val handlers = mutableMapOf() + + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is TryBlock) { + handlers[instruction.handler] = instruction.typeName + } else if (instruction is CodeLabel) { + handlers[instruction.label]?.let { exceptionType -> + if (exceptionType.isNotEmpty()) { + /** + * This is a catch block; the wrapping function is allowed to throw exceptions. + */ + invokeStatic("sandbox/java/lang/DJVM", "catch", "(Ljava/lang/Throwable;)Lsandbox/java/lang/Throwable;") + + /** + * When catching exceptions, we also need to tell the verifier which + * which kind of [sandbox.java.lang.Throwable] to expect this to be. + */ + castObjectTo(exceptionType) + } else { + /** + * This is a finally block; the wrapping function MUST NOT throw exceptions. + */ + invokeStatic("sandbox/java/lang/DJVM", "finally", "(Ljava/lang/Throwable;)Lsandbox/java/lang/Throwable;") + } + } + } + } + + override val priority: Int + get() = EMIT_HANDLING_EXCEPTIONS +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt new file mode 100644 index 0000000000..037b4012b0 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt @@ -0,0 +1,20 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import org.objectweb.asm.Opcodes.ATHROW + +/** + * Converts a [sandbox.java.lang.Throwable] into a [java.lang.Throwable] + * so that the JVM can throw it. + */ +class ThrowExceptionWrapper : Emitter { + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + when (instruction.operation) { + ATHROW -> { + invokeStatic("sandbox/java/lang/DJVM", "fromDJVM", "(Lsandbox/java/lang/Throwable;)Ljava/lang/Throwable;") + } + } + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt index 839ac609bc..a8577c19ac 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -40,7 +41,7 @@ class TraceAllocations : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt index b71f1f4657..cafafba1ea 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceInvocations : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt index 1d7695380b..ce4e41eaa8 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceJumps : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt index b4756b272e..dc8064ff15 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceThrows : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt index 99ef5319fb..4cb15b9194 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt @@ -21,12 +21,14 @@ class ClassSource private constructor( /** * Instantiate a [ClassSource] from a fully qualified class name. */ + @JvmStatic fun fromClassName(className: String, origin: String? = null) = ClassSource(className, origin) /** * Instantiate a [ClassSource] from a file on disk. */ + @JvmStatic fun fromPath(path: Path) = PathClassSource(path) /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt index 8b4789f8df..fbc0f1b0f0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt @@ -2,6 +2,8 @@ package net.corda.djvm.source import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassResolver +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException import net.corda.djvm.analysis.SourceLocation import net.corda.djvm.code.asResourcePath import net.corda.djvm.messages.Message @@ -61,7 +63,15 @@ abstract class AbstractSourceClassLoader( */ override fun loadClass(name: String, resolve: Boolean): Class<*> { logger.trace("Loading class {}, resolve={}...", name, resolve) - val originalName = classResolver.reverseNormalized(name) + val originalName = classResolver.reverseNormalized(name).let { n -> + // A synthetic exception should be mapped back to its + // corresponding exception in the original hierarchy. + if (isDJVMException(n)) { + getDJVMExceptionOwner(n) + } else { + n + } + } return super.loadClass(originalName, resolve) } diff --git a/djvm/src/main/kotlin/sandbox/Task.kt b/djvm/src/main/kotlin/sandbox/Task.kt index 8a2bbab78a..0be04225bf 100644 --- a/djvm/src/main/kotlin/sandbox/Task.kt +++ b/djvm/src/main/kotlin/sandbox/Task.kt @@ -6,7 +6,10 @@ import sandbox.java.lang.unsandbox typealias SandboxFunction = sandbox.java.util.function.Function -@Suppress("unused") +internal fun isEntryPoint(elt: java.lang.StackTraceElement): Boolean { + return elt.className == "sandbox.Task" && elt.methodName == "apply" +} + class Task(private val function: SandboxFunction?) : SandboxFunction { /** diff --git a/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt index b6a3acdc77..a098d78020 100644 --- a/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt +++ b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt @@ -2,19 +2,25 @@ @file:Suppress("unused") package sandbox.java.lang +import net.corda.djvm.analysis.AnalysisConfiguration.Companion.JVM_EXCEPTIONS +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMException +import net.corda.djvm.rules.implementation.* import org.objectweb.asm.Opcodes.ACC_ENUM +import org.objectweb.asm.Type +import sandbox.isEntryPoint +import sandbox.net.corda.djvm.rules.RuleViolationError private const val SANDBOX_PREFIX = "sandbox." fun Any.unsandbox(): Any { return when (this) { - is Enum<*> -> fromDJVMEnum() is Object -> fromDJVM() is Array<*> -> fromDJVMArray() else -> this } } +@Throws(ClassNotFoundException::class) fun Any.sandbox(): Any { return when (this) { is kotlin.String -> String.toDJVM(this) @@ -27,6 +33,7 @@ fun Any.sandbox(): Any { is kotlin.Double -> Double.toDJVM(this) is kotlin.Boolean -> Boolean.toDJVM(this) is kotlin.Enum<*> -> toDJVMEnum() + is kotlin.Throwable -> toDJVMThrowable() is Array<*> -> toDJVMArray() else -> this } @@ -38,8 +45,11 @@ private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this) * These functions use the "current" classloader, i.e. classloader * that owns this DJVM class. */ -private fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) -private fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) +@Throws(ClassNotFoundException::class) +internal fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) + +@Throws(ClassNotFoundException::class) +internal fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) private fun kotlin.String.toSandboxPackage(): kotlin.String { return if (startsWith(SANDBOX_PREFIX)) { @@ -66,10 +76,12 @@ private inline fun Array<*>.toDJVMArray(): Array { } } -private fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> { +@Throws(ClassNotFoundException::class) +internal fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> { return javaClass.fromDJVMType().enumConstants[ordinal()] as kotlin.Enum<*> } +@Throws(ClassNotFoundException::class) private fun kotlin.Enum<*>.toDJVMEnum(): Enum<*> { @Suppress("unchecked_cast") return (getEnumConstants(javaClass.toDJVMType() as Class>) as Array>)[ordinal] @@ -87,10 +99,11 @@ fun getEnumConstants(clazz: Class>): Array<*>? { internal fun enumConstantDirectory(clazz: Class>): sandbox.java.util.Map>? { // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. + @Suppress("ReplaceGetOrSet") return allEnumDirectories.get(clazz) ?: createEnumDirectory(clazz) } -@Suppress("unchecked_cast") +@Suppress("unchecked_cast", "ReplaceGetOrSet") internal fun getEnumConstantsShared(clazz: Class>): Array>? { return if (isEnum(clazz)) { // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. @@ -100,7 +113,7 @@ internal fun getEnumConstantsShared(clazz: Class>): Array>): Array>? { return clazz.getMethod("values").let { method -> method.isAccessible = true @@ -109,6 +122,7 @@ private fun createEnum(clazz: Class>): Array>? { }?.apply { allEnums.put(clazz, this) } } +@Suppress("ReplacePutWithAssignment") private fun createEnumDirectory(clazz: Class>): sandbox.java.util.Map> { val universe = getEnumConstantsShared(clazz) ?: throw IllegalArgumentException("${clazz.name} is not an enum type") val directory = sandbox.java.util.LinkedHashMap>(2 * universe.size) @@ -154,5 +168,130 @@ private fun toSandbox(className: kotlin.String): kotlin.String { private val bannedClasses = setOf( "^java\\.lang\\.DJVM(.*)?\$".toRegex(), "^net\\.corda\\.djvm\\..*\$".toRegex(), - "^Task\$".toRegex() + "^Task(.*)?\$".toRegex() ) + +/** + * Exception Management. + * + * This function converts a [sandbox.java.lang.Throwable] into a + * [java.lang.Throwable] that the JVM can actually throw. + */ +fun fromDJVM(t: Throwable?): kotlin.Throwable { + return if (t is DJVMThrowableWrapper) { + // We must be exiting a finally block. + t.fromDJVM() + } else { + try { + /** + * Someone has created a [sandbox.java.lang.Throwable] + * and is (re?)throwing it. + */ + val sandboxedName = t!!.javaClass.name + if (Type.getInternalName(t.javaClass) in JVM_EXCEPTIONS) { + // We map these exceptions to their equivalent JVM classes. + Class.forName(sandboxedName.fromSandboxPackage()).createJavaThrowable(t) + } else { + // Whereas the sandbox creates a synthetic throwable wrapper for these. + Class.forName(getDJVMException(sandboxedName)) + .getDeclaredConstructor(sandboxThrowable) + .newInstance(t) as kotlin.Throwable + } + } catch (e: Exception) { + RuleViolationError(e.message) + } + } +} + +/** + * Wraps a [java.lang.Throwable] inside a [sandbox.java.lang.Throwable]. + * This function is invoked at the beginning of a finally block, and + * so does not need to return a reference to the equivalent sandboxed + * exception. The finally block only needs to be able to re-throw the + * original exception when it finishes. + */ +fun finally(t: kotlin.Throwable): Throwable = DJVMThrowableWrapper(t) + +/** + * Converts a [java.lang.Throwable] into a [sandbox.java.lang.Throwable]. + * It is invoked at the start of each catch block. + * + * Note: [DisallowCatchingBlacklistedExceptions] means that we don't + * need to handle [ThreadDeath] here. + */ +fun catch(t: kotlin.Throwable): Throwable { + try { + return t.toDJVMThrowable() + } catch (e: Exception) { + throw RuleViolationError(e.message) + } +} + +/** + * Worker functions to convert [java.lang.Throwable] into [sandbox.java.lang.Throwable]. + */ +private fun kotlin.Throwable.toDJVMThrowable(): Throwable { + return (this as? DJVMException)?.getThrowable() ?: javaClass.toDJVMType().createDJVMThrowable(this) +} + +/** + * Creates a new [sandbox.java.lang.Throwable] from a [java.lang.Throwable], + * which was probably thrown by the JVM itself. + */ +private fun Class<*>.createDJVMThrowable(t: kotlin.Throwable): Throwable { + return (try { + getDeclaredConstructor(String::class.java).newInstance(String.toDJVM(t.message)) + } catch (e: NoSuchMethodException) { + newInstance() + } as Throwable).apply { + t.cause?.also { + initCause(it.toDJVMThrowable()) + } + stackTrace = sanitiseToDJVM(t.stackTrace) + } +} + +private fun Class<*>.createJavaThrowable(t: Throwable): kotlin.Throwable { + return (try { + getDeclaredConstructor(kotlin.String::class.java).newInstance(String.fromDJVM(t.message)) + } catch (e: NoSuchMethodException) { + newInstance() + } as kotlin.Throwable).apply { + t.cause?.also { + initCause(fromDJVM(it)) + } + stackTrace = copyFromDJVM(t.stackTrace) + } +} + +private fun sanitiseToDJVM(source: Array): Array { + var idx = 0 + while (idx < source.size && !isEntryPoint(source[idx])) { + ++idx + } + return copyToDJVM(source, 0, idx) +} + +internal fun copyToDJVM(source: Array, fromIdx: Int, toIdx: Int): Array { + return source.sliceArray(fromIdx until toIdx).map(::toDJVM).toTypedArray() +} + +private fun toDJVM(elt: java.lang.StackTraceElement) = StackTraceElement( + String.toDJVM(elt.className), + String.toDJVM(elt.methodName), + String.toDJVM(elt.fileName), + elt.lineNumber +) + +private fun copyFromDJVM(source: Array): Array { + return source.map(::fromDJVM).toTypedArray() +} + +private fun fromDJVM(elt: StackTraceElement) = java.lang.StackTraceElement( + String.fromDJVM(elt.className), + String.fromDJVM(elt.methodName), + String.fromDJVM(elt.fileName), + elt.lineNumber +) + +private val sandboxThrowable: Class<*> = Throwable::class.java diff --git a/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt b/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt new file mode 100644 index 0000000000..553e8533ab --- /dev/null +++ b/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt @@ -0,0 +1,12 @@ +package sandbox.java.lang + +/** + * All synthetic [Throwable] classes wrapping non-JVM exceptions + * will implement this interface. + */ +interface DJVMException { + /** + * Returns the [sandbox.java.lang.Throwable] instance inside the wrapper. + */ + fun getThrowable(): Throwable +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt index 0fe4283caf..b312a4091f 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt @@ -6,4 +6,4 @@ package sandbox.net.corda.djvm.costing * * @property message The description of the condition causing the problem. */ -class ThresholdViolationError(override val message: String) : ThreadDeath() +class ThresholdViolationError(override val message: String?) : ThreadDeath() diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt index 24b0e73775..1bd39bdf39 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt @@ -7,4 +7,4 @@ package sandbox.net.corda.djvm.rules * * @property message The description of the condition causing the problem. */ -class RuleViolationError(override val message: String) : ThreadDeath() \ No newline at end of file +class RuleViolationError(override val message: String?) : ThreadDeath() \ No newline at end of file diff --git a/djvm/src/test/java/net/corda/djvm/WithJava.java b/djvm/src/test/java/net/corda/djvm/WithJava.java new file mode 100644 index 0000000000..3e8cf05145 --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/WithJava.java @@ -0,0 +1,24 @@ +package net.corda.djvm; + +import net.corda.djvm.execution.ExecutionSummaryWithResult; +import net.corda.djvm.execution.SandboxExecutor; +import net.corda.djvm.source.ClassSource; + +import java.util.function.Function; + +public interface WithJava { + + static ExecutionSummaryWithResult run( + SandboxExecutor executor, Class> task, T input) { + try { + return executor.run(ClassSource.fromClassName(task.getName(), null), input); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeException(e.getMessage(), e); + } + } + } + +} diff --git a/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java b/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java new file mode 100644 index 0000000000..0343d9517d --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java @@ -0,0 +1,106 @@ +package net.corda.djvm.execution; + +import net.corda.djvm.TestBase; +import net.corda.djvm.WithJava; +import static net.corda.djvm.messages.Severity.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import org.junit.Test; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +import static java.util.Collections.emptySet; + +public class SandboxEnumJavaTest extends TestBase { + + @Test + public void testEnumInsideSandbox() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, TransformEnum.class, 0); + assertThat(output.getResult()) + .isEqualTo(new String[]{ "ONE", "TWO", "THREE" }); + return null; + }); + } + + @Test + public void testReturnEnumFromSandbox() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, FetchEnum.class, "THREE"); + assertThat(output.getResult()) + .isEqualTo(ExampleEnum.THREE); + return null; + }); + } + + @Test + public void testWeCanIdentifyClassAsEnum() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, AssertEnum.class, ExampleEnum.THREE); + assertThat(output.getResult()).isTrue(); + return null; + }); + } + + @Test + public void testWeCanCreateEnumMap() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, UseEnumMap.class, ExampleEnum.TWO); + assertThat(output.getResult()).isEqualTo(1); + return null; + }); + } + + @Test + public void testWeCanCreateEnumSet() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, UseEnumSet.class, ExampleEnum.ONE); + assertThat(output.getResult()).isTrue(); + return null; + }); + } + + public static class AssertEnum implements Function { + @Override + public Boolean apply(ExampleEnum input) { + return input.getClass().isEnum(); + } + } + + public static class TransformEnum implements Function { + @Override + public String[] apply(Integer input) { + return Stream.of(ExampleEnum.values()).map(ExampleEnum::name).toArray(String[]::new); + } + } + + public static class FetchEnum implements Function { + public ExampleEnum apply(String input) { + return ExampleEnum.valueOf(input); + } + } + + public static class UseEnumMap implements Function { + @Override + public Integer apply(ExampleEnum input) { + Map map = new EnumMap<>(ExampleEnum.class); + map.put(input, input.name()); + return map.size(); + } + } + + public static class UseEnumSet implements Function { + @Override + public Boolean apply(ExampleEnum input) { + return EnumSet.allOf(ExampleEnum.class).contains(input); + } + } +} diff --git a/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java b/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java new file mode 100644 index 0000000000..26203f641b --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java @@ -0,0 +1,79 @@ +package net.corda.djvm.execution; + +import net.corda.djvm.TestBase; +import net.corda.djvm.WithJava; +import static net.corda.djvm.messages.Severity.*; + +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; + +import static java.util.Collections.emptySet; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class SandboxThrowableJavaTest extends TestBase { + + @Test + public void testUserExceptionHandling() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, ThrowAndCatchJavaExample.class, "Hello World!"); + assertThat(output.getResult()) + .isEqualTo(new String[]{ "FIRST FINALLY", "BASE EXCEPTION", "Hello World!", "SECOND FINALLY" }); + return null; + }); + } + + @Test + public void testCheckedExceptions() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + + ExecutionSummaryWithResult success = WithJava.run(executor, JavaWithCheckedExceptions.class, "http://localhost:8080/hello/world"); + assertThat(success.getResult()).isEqualTo("/hello/world"); + + ExecutionSummaryWithResult failure = WithJava.run(executor, JavaWithCheckedExceptions.class, "nasty string"); + assertThat(failure.getResult()).isEqualTo("CATCH:Illegal character in path at index 5: nasty string"); + + return null; + }); + } + + public static class ThrowAndCatchJavaExample implements Function { + @Override + public String[] apply(String input) { + List data = new LinkedList<>(); + try { + try { + throw new MyExampleException(input); + } finally { + data.add("FIRST FINALLY"); + } + } catch (MyBaseException e) { + data.add("BASE EXCEPTION"); + data.add(e.getMessage()); + } catch (Exception e) { + data.add("NOT THIS ONE!"); + } finally { + data.add("SECOND FINALLY"); + } + + return data.toArray(new String[0]); + } + } + + public static class JavaWithCheckedExceptions implements Function { + @Override + public String apply(String input) { + try { + return new URI(input).getPath(); + } catch (URISyntaxException e) { + return "CATCH:" + e.getMessage(); + } + } + } +} diff --git a/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt b/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt new file mode 100644 index 0000000000..886b1efacc --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt @@ -0,0 +1,100 @@ +package net.corda.djvm + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test +import sandbox.SandboxFunction +import sandbox.Task +import sandbox.java.lang.sandbox + +class DJVMExceptionTest { + @Test + fun testSingleException() { + val result = Task(SingleExceptionTask()).apply("Hello World") + assertThat(result).isInstanceOf(Throwable::class.java) + result as Throwable + + assertThat(result.message).isEqualTo("Hello World") + assertThat(result.cause).isNull() + assertThat(result.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + } + + @Test + fun testMultipleExceptions() { + val result = Task(MultipleExceptionsTask()).apply("Hello World") + assertThat(result).isInstanceOf(Throwable::class.java) + result as Throwable + + assertThat(result.message).isEqualTo("Hello World(1)(2)") + assertThat(result.cause).isInstanceOf(Throwable::class.java) + assertThat(result.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + val resultLineNumbers = result.stackTrace.toLineNumbers() + + val firstCause = result.cause as Throwable + assertThat(firstCause.message).isEqualTo("Hello World(1)") + assertThat(firstCause.cause).isInstanceOf(Throwable::class.java) + assertThat(firstCause.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + val firstCauseLineNumbers = firstCause.stackTrace.toLineNumbers() + + val rootCause = firstCause.cause as Throwable + assertThat(rootCause.message).isEqualTo("Hello World") + assertThat(rootCause.cause).isNull() + assertThat(rootCause.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + val rootCauseLineNumbers = rootCause.stackTrace.toLineNumbers() + + // These stack traces should share one line number and have one distinct line number each. + assertThat(resultLineNumbers.toSet() + firstCauseLineNumbers.toSet() + rootCauseLineNumbers.toSet()) + .hasSize(4) + } + + @Test + fun testJavaThrowableToSandbox() { + val result = Throwable("Hello World").sandbox() + assertThat(result).isInstanceOf(sandbox.java.lang.Throwable::class.java) + result as sandbox.java.lang.Throwable + + assertThat(result.message).isEqualTo("Hello World".toDJVM()) + assertThat(result.stackTrace).isNotEmpty() + assertThat(result.cause).isNull() + } + + @Test + fun testWeTryToCreateCorrectSandboxExceptionsAtRuntime() { + assertThatExceptionOfType(ClassNotFoundException::class.java) + .isThrownBy { Exception("Hello World").sandbox() } + .withMessage("sandbox.java.lang.Exception") + assertThatExceptionOfType(ClassNotFoundException::class.java) + .isThrownBy { RuntimeException("Hello World").sandbox() } + .withMessage("sandbox.java.lang.RuntimeException") + } +} + +class SingleExceptionTask : SandboxFunction { + override fun apply(input: Any?): sandbox.java.lang.Throwable? { + return sandbox.java.lang.Throwable(input as? sandbox.java.lang.String) + } +} + +class MultipleExceptionsTask : SandboxFunction { + override fun apply(input: Any?): sandbox.java.lang.Throwable? { + val root = sandbox.java.lang.Throwable(input as? sandbox.java.lang.String) + val nested = sandbox.java.lang.Throwable(root.message + "(1)", root) + return sandbox.java.lang.Throwable(nested.message + "(2)", nested) + } +} + +private infix operator fun sandbox.java.lang.String.plus(s: String): sandbox.java.lang.String { + return (toString() + s).toDJVM() +} + +private fun Array.toLineNumbers(): IntArray { + return map(StackTraceElement::getLineNumber).toIntArray() +} \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt index d71fa1a36a..37048ee7f2 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt @@ -113,14 +113,4 @@ class DJVMTest { assertArrayEquals(ByteArray(1) { 127.toByte() }, result[9] as ByteArray) assertArrayEquals(CharArray(1) { '?' }, result[10] as CharArray) } - - private fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this) - private fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this) - private fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this) - private fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this) - private fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this) - private fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this) - private fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this) - private fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this) - private fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this) } \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt index a771798655..ad16eee53a 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt @@ -37,22 +37,29 @@ abstract class TestBase { val ALL_EMITTERS = Discovery.find() // We need at least these emitters to handle the Java API classes. + @JvmField val BASIC_EMITTERS: List = listOf( ArgumentUnwrapper(), + HandleExceptionUnwrapper(), ReturnTypeWrapper(), RewriteClassMethods(), - StringConstantWrapper() + StringConstantWrapper(), + ThrowExceptionWrapper() ) val ALL_DEFINITION_PROVIDERS = Discovery.find() // We need at least these providers to handle the Java API classes. + @JvmField val BASIC_DEFINITION_PROVIDERS: List = listOf(StaticConstantRemover()) + @JvmField val BLANK = emptySet() + @JvmField val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass) + @JvmField val DETERMINISTIC_RT: Path = Paths.get( System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set")) @@ -89,7 +96,7 @@ abstract class TestBase { val reader = ClassReader(T::class.java.name) AnalysisConfiguration( minimumSeverityLevel = minimumSeverityLevel, - classPath = listOf(DETERMINISTIC_RT) + bootstrapJar = DETERMINISTIC_RT ).use { analysisConfiguration -> val validator = RuleValidator(ALL_RULES, analysisConfiguration) val context = AnalysisContext.fromConfiguration(analysisConfiguration) diff --git a/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt index d493238723..6313661b0c 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt @@ -1,22 +1,24 @@ +@file:JvmName("UtilityFunctions") package net.corda.djvm import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.rules.RuleViolationError +/** + * Allows us to create a [Utilities] object that we can pin inside the sandbox. + */ object Utilities { fun throwRuleViolationError(): Nothing = throw RuleViolationError("Can't catch this!") fun throwThresholdViolationError(): Nothing = throw ThresholdViolationError("Can't catch this!") - - fun throwContractConstraintViolation(): Nothing = throw IllegalArgumentException("Contract constraint violated") - - fun throwError(): Nothing = throw Error() - - fun throwThrowable(): Nothing = throw Throwable() - - fun throwThreadDeath(): Nothing = throw ThreadDeath() - - fun throwStackOverflowError(): Nothing = throw StackOverflowError("FAKE OVERFLOW!") - - fun throwOutOfMemoryError(): Nothing = throw OutOfMemoryError("FAKE OOM!") } + +fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this) +fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this) +fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this) +fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this) +fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this) +fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this) +fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this) +fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this) +fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this) \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt index a3919c964c..32fa876195 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt @@ -6,14 +6,8 @@ import foo.bar.sandbox.toNumber import net.corda.djvm.TestBase import net.corda.djvm.analysis.Whitelist import net.corda.djvm.Utilities -import net.corda.djvm.Utilities.throwContractConstraintViolation -import net.corda.djvm.Utilities.throwError -import net.corda.djvm.Utilities.throwOutOfMemoryError import net.corda.djvm.Utilities.throwRuleViolationError -import net.corda.djvm.Utilities.throwStackOverflowError -import net.corda.djvm.Utilities.throwThreadDeath import net.corda.djvm.Utilities.throwThresholdViolationError -import net.corda.djvm.Utilities.throwThrowable import net.corda.djvm.assertions.AssertionExtensions.withProblem import net.corda.djvm.rewiring.SandboxClassLoadingException import org.assertj.core.api.Assertions.assertThat @@ -55,7 +49,7 @@ class SandboxExecutorTest : TestBase() { class Contract : Function { override fun apply(input: Transaction) { - throwContractConstraintViolation() + throw IllegalArgumentException("Contract constraint violated") } } @@ -74,11 +68,7 @@ class SandboxExecutorTest : TestBase() { val obj = Object() val hash1 = obj.hashCode() val hash2 = obj.hashCode() - //require(hash1 == hash2) - // TODO: Replace require() once we have working exception support. - if (hash1 != hash2) { - throwError() - } + require(hash1 == hash2) return Object().hashCode() } } @@ -180,7 +170,7 @@ class SandboxExecutorTest : TestBase() { class TestCatchThreadDeath : Function { override fun apply(input: Int): Int { return try { - throwThreadDeath() + throw ThreadDeath() } catch (exception: ThreadDeath) { 1 } @@ -261,8 +251,8 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throwThrowable() - 2 -> throwError() + 1 -> throw Throwable() + 2 -> throw Error() else -> 0 } } catch (exception: Error) { @@ -277,20 +267,20 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throwThrowable() - 2 -> throwError() + 1 -> throw Throwable() + 2 -> throw Error() 3 -> try { - throwThreadDeath() + throw ThreadDeath() } catch (ex: ThreadDeath) { 3 } 4 -> try { - throwStackOverflowError() + throw StackOverflowError("FAKE OVERFLOW!") } catch (ex: StackOverflowError) { 4 } 5 -> try { - throwOutOfMemoryError() + throw OutOfMemoryError("FAKE OOM!") } catch (ex: OutOfMemoryError) { 5 } diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt new file mode 100644 index 0000000000..ae013a9c1e --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt @@ -0,0 +1,95 @@ +package net.corda.djvm.execution + +import net.corda.djvm.TestBase +import org.assertj.core.api.Assertions.* +import org.junit.Test +import java.util.function.Function + +class SandboxThrowableTest : TestBase() { + + @Test + fun `test user exception handling`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + contractExecutor.run("Hello World").apply { + assertThat(result) + .isEqualTo(arrayOf("FIRST FINALLY", "BASE EXCEPTION", "Hello World", "SECOND FINALLY")) + } + } + + @Test + fun `test rethrowing an exception`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + contractExecutor.run("Hello World").apply { + assertThat(result) + .isEqualTo(arrayOf("FIRST CATCH", "FIRST FINALLY", "SECOND CATCH", "Hello World", "SECOND FINALLY")) + } + } + + @Test + fun `test JVM exceptions still propagate`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(-1).apply { + assertThat(result) + .isEqualTo("sandbox.java.lang.ArrayIndexOutOfBoundsException:-1") + } + } +} + +class ThrowAndRethrowExample : Function> { + override fun apply(input: String): Array { + val data = mutableListOf() + try { + try { + throw MyExampleException(input) + } catch (e: Exception) { + data += "FIRST CATCH" + throw e + } finally { + data += "FIRST FINALLY" + } + } catch (e: MyExampleException) { + data += "SECOND CATCH" + e.message?.apply { data += this } + } finally { + data += "SECOND FINALLY" + } + + return data.toTypedArray() + } +} + +class ThrowAndCatchExample : Function> { + override fun apply(input: String): Array { + val data = mutableListOf() + try { + try { + throw MyExampleException(input) + } finally { + data += "FIRST FINALLY" + } + } catch (e: MyBaseException) { + data += "BASE EXCEPTION" + e.message?.apply { data += this } + } catch (e: Exception) { + data += "NOT THIS ONE!" + } finally { + data += "SECOND FINALLY" + } + + return data.toTypedArray() + } +} + +class TriggerJVMException : Function { + override fun apply(input: Int): String { + return try { + arrayOf(0, 1, 2)[input] + "No Error" + } catch (e: Exception) { + e.javaClass.name + ':' + (e.message ?: "") + } + } +} + +open class MyBaseException(message: String) : Exception(message) +class MyExampleException(message: String) : MyBaseException(message) \ No newline at end of file From 72cab905770e84e8c341701b1d31d96829a4e5fd Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 19 Oct 2018 18:34:32 +0100 Subject: [PATCH 66/83] [CORDA-738] Ensure encumbrances are bi-directional (#4089) --- .../TransactionVerificationException.kt | 23 ++- .../flows/AbstractStateReplacementFlow.kt | 1 + .../net/corda/core/flows/NotaryChangeFlow.kt | 8 +- .../core/transactions/LedgerTransaction.kt | 89 +++++++-- .../transactions/NotaryChangeTransactions.kt | 28 +-- .../net/corda/core/utilities/KotlinUtils.kt | 1 - .../TransactionEncumbranceTests.kt | 178 +++++++++++++++--- .../corda/node/services/NotaryChangeTests.kt | 22 +-- 8 files changed, 271 insertions(+), 79 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 29db493d86..980589e9cb 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -116,13 +116,32 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction) : TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null) + /** + * If two or more states refer to another state (as their encumbrance), then the bi-directionality property cannot + * be satisfied. + */ + @KeepForDJVM + class TransactionDuplicateEncumbranceException(txId: SecureHash, index: Int) + : TransactionVerificationException(txId, "The bi-directionality property of encumbered output states " + + "is not satisfied. Index $index is referenced more than once", null) + + /** + * An encumbered state should also be referenced as the encumbrance of another state in order to satisfy the + * bi-directionality property (a full cycle should be present). + */ + @KeepForDJVM + class TransactionNonMatchingEncumbranceException(txId: SecureHash, nonMatching: Collection) + : TransactionVerificationException(txId, "The bi-directionality property of encumbered output states " + + "is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " + + "a full cycle. Offending indices $nonMatching", null) + /** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */ @CordaSerializable @KeepForDJVM enum class Direction { - /** Issue in the inputs list */ + /** Issue in the inputs list. */ INPUT, - /** Issue in the outputs list */ + /** Issue in the outputs list. */ OUTPUT } diff --git a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt index 55946b4817..53f547076b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt @@ -62,6 +62,7 @@ abstract class AbstractStateReplacementFlow { @Throws(StateReplacementException::class) override fun call(): StateAndRef { val (stx) = assembleTx() + stx.verify(serviceHub, checkSufficientSignatures = false) val participantSessions = getParticipantSessions() progressTracker.currentStep = SIGNING diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt index 7690f01843..18b98ae2d4 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt @@ -46,14 +46,14 @@ class NotaryChangeFlow( return AbstractStateReplacementFlow.UpgradeTx(stx) } - /** Resolves the encumbrance state chain for the given [state] */ + /** Resolves the encumbrance state chain for the given [state]. */ private fun resolveEncumbrances(state: StateAndRef): List> { - val states = mutableListOf(state) + val states = mutableSetOf(state) while (states.last().state.encumbrance != null) { val encumbranceStateRef = StateRef(states.last().ref.txhash, states.last().state.encumbrance!!) val encumbranceState = serviceHub.toStateAndRef(encumbranceStateRef) - states.add(encumbranceState) + if (!states.add(encumbranceState)) break // Stop if there is a cycle. } - return states + return states.toList() } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index fdb255348b..5c97af83a4 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -169,30 +169,79 @@ data class LedgerTransaction @JvmOverloads constructor( private fun checkEncumbrancesValid() { // Validate that all encumbrances exist within the set of input states. - val encumberedInputs = inputs.filter { it.state.encumbrance != null } - encumberedInputs.forEach { (state, ref) -> - val encumbranceStateExists = inputs.any { - it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance - } - if (!encumbranceStateExists) { + inputs.filter { it.state.encumbrance != null } + .forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) } + + // Check that in the outputs, + // a) an encumbered state does not refer to itself as the encumbrance + // b) the number of outputs can contain the encumbrance + // c) the bi-directionality (full cycle) property is satisfied. + val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) } + if (!statesAndEncumbrance.isEmpty()) { + checkOutputEncumbrances(statesAndEncumbrance) + } + } + + private fun checkInputEncumbranceStateExists(state: TransactionState, ref: StateRef) { + val encumbranceStateExists = inputs.any { + it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance + } + if (!encumbranceStateExists) { + throw TransactionVerificationException.TransactionMissingEncumbranceException( + id, + state.encumbrance!!, + TransactionVerificationException.Direction.INPUT + ) + } + } + + // Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional + // encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship + // can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by + // other states, a full cycle detection will fail. As a result, all of the encumbered states must be present + // as "from" and "to" only once (or zero times if no encumbrance takes place). For instance, + // a -> b + // c -> b and a -> b + // b -> a b -> c + // do not satisfy the bi-directionality (full cycle) property. + // + // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. + // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. + // + // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. + // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. + // + // On other hand the following are valid constructions: + // a -> b a -> c + // b -> c and c -> b + // c -> a b -> a + // and form a full cycle, meaning that the bi-directionality property is satisfied. + private fun checkOutputEncumbrances(statesAndEncumbrance: List>) { + // [Set] of "from" (encumbered states). + val encumberedSet = mutableSetOf() + // [Set] of "to" (encumbrance states). + val encumbranceSet = mutableSetOf() + // Update both [Set]s. + statesAndEncumbrance.forEach { (statePosition, encumbrance) -> + // Check it does not refer to itself. + if (statePosition == encumbrance || encumbrance >= outputs.size) { throw TransactionVerificationException.TransactionMissingEncumbranceException( id, - state.encumbrance!!, - TransactionVerificationException.Direction.INPUT - ) + encumbrance, + TransactionVerificationException.Direction.OUTPUT) + } else { + encumberedSet.add(statePosition) // Guaranteed to have unique elements. + if (!encumbranceSet.add(encumbrance)) { + throw TransactionVerificationException.TransactionDuplicateEncumbranceException(id, encumbrance) + } } } - - // Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance, - // and that the number of outputs can contain the encumbrance. - for ((i, output) in outputs.withIndex()) { - val encumbranceIndex = output.encumbrance ?: continue - if (encumbranceIndex == i || encumbranceIndex >= outputs.size) { - throw TransactionVerificationException.TransactionMissingEncumbranceException( - id, - encumbranceIndex, - TransactionVerificationException.Direction.OUTPUT) - } + // At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their + // elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union). + val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet) + if (symmetricDifference.isNotEmpty()) { + // At least one encumbered state is not in the [encumbranceSet] and vice versa. + throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(id, symmetricDifference) } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index 2603b6ca3c..55f329540d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -102,13 +102,21 @@ data class NotaryChangeLedgerTransaction( override val references: List> = emptyList() - /** We compute the outputs on demand by applying the notary field modification to the inputs */ + /** We compute the outputs on demand by applying the notary field modification to the inputs. */ override val outputs: List> - get() = inputs.mapIndexed { pos, (state) -> + get() = computeOutputs() + + private fun computeOutputs(): List> { + val inputPositionIndex: Map = inputs.mapIndexed { index, stateAndRef -> stateAndRef.ref to index }.toMap() + return inputs.map { (state, ref) -> if (state.encumbrance != null) { - state.copy(notary = newNotary, encumbrance = pos + 1) + val encumbranceStateRef = StateRef(ref.txhash, state.encumbrance) + val encumbrancePosition = inputPositionIndex[encumbranceStateRef] + ?: throw IllegalStateException("Unable to generate output states – transaction not constructed correctly.") + state.copy(notary = newNotary, encumbrance = encumbrancePosition) } else state.copy(notary = newNotary) } + } override val requiredSigningKeys: Set get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey @@ -118,18 +126,16 @@ data class NotaryChangeLedgerTransaction( } /** - * Check that encumbrances have been included in the inputs. The [NotaryChangeFlow] guarantees that an encumbrance - * will follow its encumbered state in the inputs. + * Check that encumbrances have been included in the inputs. */ private fun checkEncumbrances() { - inputs.forEachIndexed { i, (state, ref) -> - state.encumbrance?.let { - val nextIndex = i + 1 - fun nextStateIsEncumbrance() = (inputs[nextIndex].ref.txhash == ref.txhash) && (inputs[nextIndex].ref.index == it) - if (nextIndex >= inputs.size || !nextStateIsEncumbrance()) { + val encumberedStates = inputs.asSequence().filter { it.state.encumbrance != null }.associateBy { it.ref } + if (encumberedStates.isNotEmpty()) { + inputs.forEach { (state, ref) -> + if (StateRef(ref.txhash, state.encumbrance!!) !in encumberedStates) { throw TransactionVerificationException.TransactionMissingEncumbranceException( id, - it, + state.encumbrance, TransactionVerificationException.Direction.INPUT) } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 240edbfd1e..97b9248be2 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -134,4 +134,3 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { } catch (e: ExecutionException) { throw e.cause!! } - diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt index 468f9cee95..6cbd8e3483 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt @@ -4,6 +4,7 @@ import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.requireThat import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name @@ -21,33 +22,43 @@ import org.junit.Rule import org.junit.Test import java.time.Instant import java.time.temporal.ChronoUnit +import kotlin.test.assertFailsWith const val TEST_TIMELOCK_ID = "net.corda.core.transactions.TransactionEncumbranceTests\$DummyTimeLock" class TransactionEncumbranceTests { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + private companion object { val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party val MEGA_CORP get() = megaCorp.party val MEGA_CORP_PUBKEY get() = megaCorp.publicKey + + val defaultIssuer = MEGA_CORP.ref(1) + + val state = Cash.State( + amount = 1000.DOLLARS `issued by` defaultIssuer, + owner = MEGA_CORP + ) + + val stateWithNewOwner = state.copy(owner = MINI_CORP) + val extraCashState = state.copy(amount = state.amount * 3) + + val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z") + val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS) + val timeLock = DummyTimeLock.State(FIVE_PM) + + + val ledgerServices = MockServices(listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), MEGA_CORP.name, + rigorousMock().also { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + }) } - @Rule - @JvmField - val testSerialization = SerializationEnvironmentRule() - val defaultIssuer = MEGA_CORP.ref(1) - - val state = Cash.State( - amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = MEGA_CORP - ) - val stateWithNewOwner = state.copy(owner = MINI_CORP) - - val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z") - val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS) - val timeLock = DummyTimeLock.State(FIVE_PM) - class DummyTimeLock : Contract { override fun verify(tx: LedgerTransaction) { val timeLockInput = tx.inputsOfType().singleOrNull() ?: return @@ -65,23 +76,136 @@ class TransactionEncumbranceTests { } } - private val ledgerServices = MockServices(listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), MEGA_CORP.name, - rigorousMock().also { - doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - }) - @Test - fun `state can be encumbered`() { + fun `states can be bi-directionally encumbered`() { + // Basic encumbrance example for encumbrance index links 0 -> 1 and 1 -> 0 ledgerServices.ledger(DUMMY_NOTARY) { transaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) input(Cash.PROGRAM_ID, state) - output(Cash.PROGRAM_ID, encumbrance = 1, contractState = stateWithNewOwner) - output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", 0, timeLock) command(MEGA_CORP.owningKey, Cash.Commands.Move()) verifies() } } + + // Full cycle example with 4 elements 0 -> 1, 1 -> 2, 2 -> 3 and 3 -> 0 + // All 3 Cash states and the TimeLock are linked and should be consumed in the same transaction. + // Note that all of the Cash states are encumbered both together and with time lock. + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, extraCashState) + output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 2", encumbrance = 2, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", 0, timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + + // A transaction that includes multiple independent encumbrance chains. + // Each Cash state is encumbered with its own TimeLock. + // Note that all of the Cash states are encumbered both together and with time lock. + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, extraCashState) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock A", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock B", encumbrance = 4, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock C", encumbrance = 5, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock A", 0, timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock B", 1, timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock C", 2, timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + + // Full cycle example with 4 elements (different combination) 0 -> 3, 1 -> 2, 2 -> 0 and 3 -> 1 + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, extraCashState) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 2", encumbrance = 2, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 0", encumbrance = 0, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", 1, timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + @Test + fun `non bi-directional encumbrance will fail`() { + // Single encumbrance with no back link. + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + // Full cycle fails due to duplicate encumbrance reference. + // 0 -> 1, 1 -> 3, 2 -> 3 (thus 3 is referenced two times). + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3 again", encumbrance = 3, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + // No Full cycle due to non-matching encumbered-encumbrance elements. + // 0 -> 1, 1 -> 3, 2 -> 0 (thus offending indices [2, 3], because 2 is not referenced and 3 is not encumbered). + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 0", encumbrance = 0, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + // No Full cycle in one of the encumbrance chains due to non-matching encumbered-encumbrance elements. + // 0 -> 2, 2 -> 0 is valid. On the other hand, there is 1 -> 3 only and 3 -> 1 does not exist. + // (thus offending indices [1, 3], because 1 is not referenced and 3 is not encumbered). + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock A", encumbrance = 2, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock B", encumbrance = 3, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock A", 0, timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock B", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } } @Test @@ -132,7 +256,7 @@ class TransactionEncumbranceTests { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = state) - output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock",0, timeLock) } transaction { attachments(Cash.PROGRAM_ID) @@ -151,7 +275,7 @@ class TransactionEncumbranceTests { transaction { attachments(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, state) - output(Cash.PROGRAM_ID, encumbrance = 0, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by itself", encumbrance = 0, contractState = stateWithNewOwner) command(MEGA_CORP.owningKey, Cash.Commands.Move()) this `fails with` "Missing required encumbrance 0 in OUTPUT" } @@ -164,7 +288,7 @@ class TransactionEncumbranceTests { transaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) input(Cash.PROGRAM_ID, state) - output(TEST_TIMELOCK_ID, encumbrance = 2, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "state encumbered by state 2 which does not exist", encumbrance = 2, contractState = stateWithNewOwner) output(TEST_TIMELOCK_ID, timeLock) command(MEGA_CORP.owningKey, Cash.Commands.Move()) this `fails with` "Missing required encumbrance 2 in OUTPUT" @@ -178,7 +302,7 @@ class TransactionEncumbranceTests { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1, contractState = state) - output(Cash.PROGRAM_ID, "some other state", state) + output(Cash.PROGRAM_ID, "some other state", encumbrance = 0, contractState = state) output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) } transaction { diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 6ac3f82c7e..04fd10546c 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -109,20 +109,13 @@ class NotaryChangeTests { // Check that all encumbrances have been propagated to the outputs val originalOutputs = issueTx.outputStates val newOutputs = notaryChangeTx.outputStates - assertTrue(originalOutputs.minus(newOutputs).isEmpty()) + assertTrue(originalOutputs.size == newOutputs.size && originalOutputs.containsAll(newOutputs)) - // Check that encumbrance links aren't broken after notary change - val encumbranceLink = HashMap() - issueTx.outputs.forEach { - val currentState = it.data - val encumbranceState = it.encumbrance?.let { issueTx.outputs[it].data } - encumbranceLink[currentState] = encumbranceState - } - notaryChangeTx.outputs.forEach { - val currentState = it.data - val encumbranceState = it.encumbrance?.let { notaryChangeTx.outputs[it].data } - assertEquals(encumbranceLink[currentState], encumbranceState) - } + // Check if encumbrance linking between states has not changed. + val originalLinkedStates = issueTx.outputs.asSequence().filter { it.encumbrance != null }.map { Pair(it.data, issueTx.outputs[it.encumbrance!!].data) }.toSet() + val notaryChangeLinkedStates = notaryChangeTx.outputs.asSequence().filter { it.encumbrance != null }.map { Pair(it.data, notaryChangeTx.outputs[it.encumbrance!!].data) }.toSet() + + assertTrue { originalLinkedStates.size == notaryChangeLinkedStates.size && originalLinkedStates.containsAll(notaryChangeLinkedStates) } } @Test @@ -172,10 +165,11 @@ class NotaryChangeTests { val stateB = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) val stateC = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) + // Ensure encumbrances form a cycle. val tx = TransactionBuilder(null).apply { addCommand(Command(DummyContract.Commands.Create(), owner.party.owningKey)) addOutputState(stateA, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 2) // Encumbered by stateB - addOutputState(stateC, DummyContract.PROGRAM_ID, notaryIdentity) + addOutputState(stateC, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 0) // Encumbered by stateA addOutputState(stateB, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 1) // Encumbered by stateC } val stx = services.signInitialTransaction(tx) From 3a8fd51a08e82fc6fd0f0113ad5e9a5b88694a46 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Fri, 19 Oct 2018 20:30:08 +0100 Subject: [PATCH 67/83] ENT-2608 - Fix deserialization of evolution (#4096) Fix deserialization could mix together the object trees of two threads when passing through evolution (ENT-2608). --- .../serialization/internal/amqp/EvolutionSerializer.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 4604af4505..700a3b51bb 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -116,7 +116,6 @@ abstract class EvolutionSerializer( factory: SerializerFactory, constructor: KFunction, readersAsSerialized: Map): AMQPSerializer { - val constructorArgs = arrayOfNulls(constructor.parameters.size) // Java doesn't care about nullability unless it's a primitive in which // case it can't be referenced. Unfortunately whilst Kotlin does apply @@ -144,7 +143,7 @@ abstract class EvolutionSerializer( } } } - return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor, constructorArgs) + return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor) } private fun makeWithSetters( @@ -210,8 +209,7 @@ class EvolutionSerializerViaConstructor( clazz: Type, factory: SerializerFactory, oldReaders: Map, - kotlinConstructor: KFunction, - private val constructorArgs: Array) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { + kotlinConstructor: KFunction) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { /** * Unlike a normal [readObject] call where we simply apply the parameter deserialisers * to the object list of values we need to map that list, which is ordered per the @@ -226,6 +224,7 @@ class EvolutionSerializerViaConstructor( ): Any { if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj") + val constructorArgs : Array = arrayOfNulls(kotlinConstructor.parameters.size) // *must* read all the parameters in the order they were serialized oldReaders.values.zip(obj).map { it.first.readProperty(it.second, schemas, input, constructorArgs, context) } From dd60ae27f2dadd12d121d3ecbb951bdaf2725272 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Sat, 20 Oct 2018 10:52:24 +0100 Subject: [PATCH 68/83] FungibleState and design document for tokens (#4049) --- .ci/api-current.txt | 9 ++++- .../net/corda/core/contracts/FungibleAsset.kt | 4 +- .../net/corda/core/contracts/FungibleState.kt | 37 ++++++++++++++++++ .../corda/core/node/services/VaultService.kt | 18 +++++---- .../core/node/services/vault/QueryCriteria.kt | 16 ++++++++ docs/source/api-states.rst | 31 +++++++++++++++ docs/source/changelog.rst | 9 +++++ docs/source/resources/state-hierarchy.png | Bin 182078 -> 174750 bytes .../node/services/schema/NodeSchemaService.kt | 11 ++++++ .../node/services/vault/NodeVaultService.kt | 37 ++++++++++++------ .../corda/node/services/vault/VaultSchema.kt | 4 +- .../vault-schema.changelog-master.xml | 1 + .../migration/vault-schema.changelog-v7.xml | 8 ++++ .../services/vault/NodeVaultServiceTest.kt | 26 ++++++++++++ 14 files changed, 186 insertions(+), 25 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt create mode 100644 node/src/main/resources/migration/vault-schema.changelog-v7.xml diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 8d3efd6638..6aecda8fa6 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -561,7 +561,7 @@ public final class net.corda.core.contracts.ContractsDSL extends java.lang.Objec public static final java.util.List> select(java.util.Collection>, Class, java.util.Collection, java.util.Collection) ## @CordaSerializable -public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.OwnableState +public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.FungibleState, net.corda.core.contracts.OwnableState @NotNull public abstract net.corda.core.contracts.Amount> getAmount() @NotNull @@ -569,6 +569,11 @@ public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.c @NotNull public abstract net.corda.core.contracts.FungibleAsset withNewOwnerAndAmount(net.corda.core.contracts.Amount>, net.corda.core.identity.AbstractParty) ## +@CordaSerializable +public interface net.corda.core.contracts.FungibleState extends net.corda.core.contracts.ContractState + @NotNull + public abstract net.corda.core.contracts.Amount getAmount() +## @DoNotImplement @CordaSerializable public final class net.corda.core.contracts.HashAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint @@ -3366,7 +3371,7 @@ public interface net.corda.core.node.services.VaultService public abstract net.corda.core.messaging.DataFeed, net.corda.core.node.services.Vault$Update> trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) @Suspendable @NotNull - public abstract java.util.List> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount, Class) + public abstract java.util.List> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount, Class) @NotNull public abstract net.corda.core.concurrent.CordaFuture> whenConsumed(net.corda.core.contracts.StateRef) ## diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index ddcd04be56..78878461f4 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -28,12 +28,12 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). */ @KeepForDJVM -interface FungibleAsset : OwnableState { +interface FungibleAsset : FungibleState>, OwnableState { /** * Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally * anything else that's quantifiable with integer quantities. See [Issued] and [Amount] for more details. */ - val amount: Amount> + override val amount: Amount> /** * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt new file mode 100644 index 0000000000..0e4a241ccd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt @@ -0,0 +1,37 @@ +package net.corda.core.contracts + +import net.corda.core.KeepForDJVM + +/** + * Interface to represent things which are fungible, this means that there is an expectation that these things can + * be split and merged. That's the only assumption made by this interface. + * + * This interface has been defined in addition to [FungibleAsset] to provide some additional flexibility which + * [FungibleAsset] lacks, in particular: + * + * - [FungibleAsset] defines an amount property of type Amount>, therefore there is an assumption that all + * fungible things are issued by a single well known party but this is not always the case. For example, + * crypto-currencies like Bitcoin are generated periodically by a pool of pseudo-anonymous miners + * and Corda can support such crypto-currencies. + * - [FungibleAsset] implements [OwnableState], as such there is an assumption that all fungible things are ownable. + * This is not always true as fungible derivative contracts exist, for example. + * + * The expectation is that this interface should be combined with the other core state interfaces such as + * [OwnableState] and others created at the application layer. + * + * @param T a type that represents the fungible thing in question. This should describe the basic type of the asset + * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). An + * upper-bound is not specified for [T] to ensure flexibility. Typically, a class would be provided that implements + * [TokenizableAssetInfo]. + */ +// DOCSTART 1 +@KeepForDJVM +interface FungibleState : ContractState { + /** + * Amount represents a positive quantity of some token which can be cash, tokens, stock, agreements, or generally + * anything else that's quantifiable with integer quantities. See [Amount] for more details. + */ + val amount: Amount +} +// DOCEND 1 + diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 43b71bf966..a9087e12af 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -334,24 +334,26 @@ interface VaultService { /** * Helper function to determine spendable states and soft locking them. - * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`. - * However, this is fully generic and can operate with custom [FungibleAsset] states. + * Currently performance will be worse than for the hand optimised version in + * [Cash.unconsumedCashStatesForSpending]. However, this is fully generic and can operate with custom [FungibleState] + * and [FungibleAsset] states. * @param lockId The [FlowLogic.runId]'s [UUID] of the current flow used to soft lock the states. * @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the * [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the * [StateStatus.UNCONSUMED], soft lock and contract type requirements. - * @param amount The required amount of the asset, but with the issuer stripped off. - * It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery]. + * @param amount The required amount of the asset. It is assumed that compatible issuer states will be filtered out + * by the [eligibleStatesQuery]. This method accepts both Amount> and Amount<*>. Amount> is + * automatically unwrapped to Amount<*>. * @param contractStateType class type of the result set. * @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount, * or else an empty list and no change in the stored lock states when their are insufficient resources available. */ @Suspendable @Throws(StatesNotAvailableException::class) - fun , U : Any> tryLockFungibleStatesForSpending(lockId: UUID, - eligibleStatesQuery: QueryCriteria, - amount: Amount, - contractStateType: Class): List> + fun > tryLockFungibleStatesForSpending(lockId: UUID, + eligibleStatesQuery: QueryCriteria, + amount: Amount<*>, + contractStateType: Class): List> // DOCSTART VaultQueryAPI /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 2ee555dee1..553320c7fa 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -168,6 +168,22 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + val quantity: ColumnPredicate? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + ) : CommonQueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + super.visit(parser) + return parser.parseCriteria(this) + } + } + /** * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates] */ diff --git a/docs/source/api-states.rst b/docs/source/api-states.rst index ad05640b16..156a38fcd7 100644 --- a/docs/source/api-states.rst +++ b/docs/source/api-states.rst @@ -100,6 +100,37 @@ Because ``OwnableState`` models fungible assets that can be merged and split ove not have a ``linearId``. $5 of cash created by one transaction is considered to be identical to $5 of cash produced by another transaction. +FungibleState +~~~~~~~~~~~~~ + +`FungibleState` is an interface to represent things which are fungible, this means that there is an expectation that +these things can be split and merged. That's the only assumption made by this interface. This interface should be +implemented if you want to represent fractional ownership in a thing, or if you have many things. Examples: + +* There is only one Mona Lisa which you wish to issue 100 tokens, each representing a 1% interest in the Mona Lisa +* A company issues 1000 shares with a nominal value of 1, in one batch of 1000. This means the single batch of 1000 + shares could be split up into 1000 units of 1 share. + +The interface is defined as follows: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +As seen, the interface takes a type parameter `T` that represents the fungible thing in question. This should describe +the basic type of the asset e.g. GBP, USD, oil, shares in company , etc. and any additional metadata (issuer, grade, +class, etc.). An upper-bound is not specified for `T` to ensure flexibility. Typically, a class would be provided that +implements `TokenizableAssetInfo` so the thing can be easily added and subtracted using the `Amount` class. + +This interface has been added in addition to `FungibleAsset` to provide some additional flexibility which +`FungibleAsset` lacks, in particular: +* `FungibleAsset` defines an amount property of type Amount>, therefore there is an assumption that all + fungible things are issued by a single well known party but this is not always the case. +* `FungibleAsset` implements `OwnableState`, as such there is an assumption that all fungible things are ownable. + Other interfaces ^^^^^^^^^^^^^^^^ You can also customize your state by implementing the following interfaces: diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c942bff9d3..51330fffd3 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -225,6 +225,15 @@ Unreleased normal state when it occurs in an input or output position. *This feature is only available on Corda networks running with a minimum platform version of 4.* +* Removed type parameter `U` from `tryLockFungibleStatesForSpending` to allow the function to be used with `FungibleState` + as well as `FungibleAsset`. This _might_ cause a compile failure in some obscure cases due to the removal of the type + parameter from the method. If your CorDapp does specify types explicitly when using this method then updating the types + will allow your app to compile successfully. However, those using type inference (e.g. using Kotlin) should not experience + any changes. Old CorDapp JARs will still work regardless. + +* `issuer_ref` column in `FungibleStateSchema` was updated to be nullable to support the introduction of the + `FungibleState` interface. The `vault_fungible_states` table can hold both `FungibleAssets` and `FungibleStates`. + Version 3.3 ----------- diff --git a/docs/source/resources/state-hierarchy.png b/docs/source/resources/state-hierarchy.png index a1c950683ad4fe844e28dba5da39d3d5a083e96c..232ac7d1f59530091bc77047725d9753376305e0 100644 GIT binary patch literal 174750 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 rEJRGmXPZ)}&-SD%#7IlTTp-*_)h*NSh9Iy87 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 #dT^CfQ%;@cgOs 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

    RcD)9-++k+bjE0le1p-rqJ*!$Z296mU;(<( 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 bK%iuGJ}(*lhV|hQ;Gdj~;@zyG{4Jzj diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index f75a6ad83a..0310e9aa7c 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -2,6 +2,7 @@ package net.corda.node.services.schema import net.corda.core.contracts.ContractState import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.FungibleState import net.corda.core.contracts.LinearState import net.corda.core.schemas.* import net.corda.core.schemas.MappedSchemaValidator.crossReferencesToOtherMappedSchema @@ -65,6 +66,8 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() if (state is LinearState) schemas += VaultSchemaV1 // VaultLinearStates if (state is FungibleAsset<*>) + schemas += VaultSchemaV1 // VaultFungibleAssets + if (state is FungibleState<*>) schemas += VaultSchemaV1 // VaultFungibleStates return schemas @@ -76,6 +79,14 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() return VaultSchemaV1.VaultLinearStates(state.linearId, state.participants) if ((schema === VaultSchemaV1) && (state is FungibleAsset<*>)) return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants) + if ((schema === VaultSchemaV1) && (state is FungibleState<*>)) + return VaultSchemaV1.VaultFungibleStates( + participants = state.participants.toMutableSet(), + owner = null, + quantity = state.amount.quantity, + issuer = null, + issuerRef = null + ) return (state as QueryableState).generateMappedObject(schema) } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 1d20d8311f..c893472a6f 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -10,13 +10,10 @@ import net.corda.core.messaging.DataFeed import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord import net.corda.core.node.services.* -import net.corda.core.node.services.vault.* import net.corda.core.node.services.Vault.ConstraintInfo.Companion.constraintInfo +import net.corda.core.node.services.vault.* import net.corda.core.schemas.PersistentStateRef -import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize import net.corda.core.transactions.* import net.corda.core.utilities.* import net.corda.node.services.api.SchemaService @@ -278,7 +275,10 @@ class NodeVaultService( val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid val vaultUpdate = if (uuid != null) netUpdate.copy(flowId = uuid) else netUpdate if (uuid != null) { - val fungible = netUpdate.produced.filter { it.state.data is FungibleAsset<*> } + val fungible = netUpdate.produced.filter { stateAndRef -> + val state = stateAndRef.state.data + state is FungibleAsset<*> || state is FungibleState<*> + } if (fungible.isNotEmpty()) { val stateRefs = fungible.map { it.ref }.toNonEmptySet() log.trace { "Reserving soft locks for flow id $uuid and states $stateRefs" } @@ -397,14 +397,27 @@ class NodeVaultService( @Suspendable @Throws(StatesNotAvailableException::class) - override fun , U : Any> tryLockFungibleStatesForSpending(lockId: UUID, - eligibleStatesQuery: QueryCriteria, - amount: Amount, - contractStateType: Class): List> { + override fun > tryLockFungibleStatesForSpending( + lockId: UUID, + eligibleStatesQuery: QueryCriteria, + amount: Amount<*>, + contractStateType: Class + ): List> { if (amount.quantity == 0L) { return emptyList() } + // Helper to unwrap the token from the Issued object if one exists. + fun unwrapIssuedAmount(amount: Amount<*>): Any { + val token = amount.token + return when (token) { + is Issued<*> -> token.product + else -> token + } + } + + val unwrappedToken = unwrapIssuedAmount(amount) + // Enrich QueryCriteria with additional default attributes (such as soft locks). // We only want to return RELEVANT states here. val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) @@ -419,8 +432,10 @@ class NodeVaultService( var claimedAmount = 0L val claimedStates = mutableListOf>() for (state in results.states) { - val issuedAssetToken = state.state.data.amount.token - if (issuedAssetToken.product == amount.token) { + // This method handles Amount> in FungibleAsset and Amount in FungibleState. + val issuedAssetToken = unwrapIssuedAmount(state.state.data.amount) + + if (issuedAssetToken == unwrappedToken) { claimedStates += state claimedAmount += state.state.data.amount.quantity if (claimedAmount > amount.quantity) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 8755eccd94..db23815db2 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -145,9 +145,9 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio @Column(name = "issuer_name", nullable = true) var issuer: AbstractParty?, - @Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = false) + @Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = true) @Type(type = "corda-wrapper-binary") - var issuerRef: ByteArray + var issuerRef: ByteArray? ) : PersistentState() { constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : this(owner = _owner, diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index 0c3d274098..062eb5a2ec 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -10,4 +10,5 @@ + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v7.xml b/node/src/main/resources/migration/vault-schema.changelog-v7.xml new file mode 100644 index 0000000000..5b85396391 --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v7.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 41a244990a..5bc7e6b287 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -129,6 +129,32 @@ class NodeVaultServiceTest { return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java) } + @Test + fun `fungible state selection test`() { + val issuerParty = services.myInfo.legalIdentities.first() + class FungibleFoo(override val amount: Amount, override val participants: List) : FungibleState + val fungibleFoo = FungibleFoo(100.DOLLARS, listOf(issuerParty)) + services.apply { + val tx = signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addCommand(Command(DummyContract.Commands.Create(), issuerParty.owningKey)) + addOutputState(fungibleFoo, DummyContract.PROGRAM_ID) + }) + recordTransactions(listOf(tx)) + } + + val baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = listOf(DUMMY_NOTARY)) + + database.transaction { + val states = services.vaultService.tryLockFungibleStatesForSpending( + lockId = UUID.randomUUID(), + eligibleStatesQuery = baseCriteria, + amount = 10.DOLLARS, + contractStateType = FungibleFoo::class.java + ) + assertEquals(states.single().state.data.amount, 100.DOLLARS) + } + } + @Test fun `duplicate insert of transaction does not fail`() { database.transaction { From 88f368134f4451304e0d4bb6f2f9c84ce1593217 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 22 Oct 2018 07:11:27 +0100 Subject: [PATCH 69/83] ENT-2610: Separate passwords for store and for private keys in Corda OS. (#4090) * ENT-2610: Separate passwords for store and for private keys in Corda OS. When it comes to KeyStores there are *2* passwords: 1 for the keyStore as a whole and separately there is one private keys within this keyStore. Unfortunately, those 2 passwords have to be the same due to Artemis limitation, for more details please see: `org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport.loadKeyManagerFactory` where it is calling `KeyManagerFactory.init()` with store password. Before change in this PR, throughout our codebase there are multiple places where we assume that storePassword is the same as keyPassword, even in the classes that have nothing to do with Artemis. This is of course less than ideal as TLS communication may be used not only for Artemis connectivity (e.g. Bridge/Float interaction in Ent) and it is unfair to impose same passwords constraint on that communication channel. Therefore this PR is removing this limitation and properly separating storePassword from keyPassword. Linked Jira(https://r3-cev.atlassian.net/browse/ENT-2610) has for more background info. Suggest to start review from `net.corda.core.crypto.X509NameConstraintsTest` to get an idea about the nature of the changes made. * ENT-2610: Address PR input from @kchalkias * ENT-2610: Address PR input from @kchalkias, s/privateKeyPassword/entryPassword/ * ENT-2610: Address PR input from @kchalkias, s/keyPassword/entryPassword/ In the implementation of `CertificateStoreSupplier` --- .../core/crypto/X509NameConstraintsTest.kt | 31 ++- .../notary/raft/RaftUniquenessProvider.kt | 4 +- .../nodeapi/internal/DevIdentityGenerator.kt | 13 +- .../nodeapi/internal/KeyStoreConfigHelpers.kt | 18 +- .../internal/config/CertificateStore.kt | 11 +- .../config/CertificateStoreSupplier.kt | 4 +- .../internal/config/ConfigUtilities.kt | 2 +- .../nodeapi/internal/crypto/X509KeyStore.kt | 6 +- .../internal/protonwrapper/netty/SSLHelper.kt | 4 +- .../internal/crypto/X509UtilitiesTest.kt | 7 +- .../protonwrapper/netty/SSLHelperTest.kt | 6 +- .../net/corda/node/NodeKeystoreCheckTest.kt | 6 +- .../CertificateRevocationListNodeTests.kt | 8 +- .../messaging/MQSecurityAsNodeTest.kt | 4 +- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../node/services/config/ConfigUtilities.kt | 6 +- .../node/services/config/NodeConfiguration.kt | 12 +- .../registration/NetworkRegistrationHelper.kt | 14 +- .../testing/node/internal/DriverDSLImpl.kt | 4 +- .../internal/UnsafeCertificatesFactory.kt | 215 ------------------ .../internal/stubs/CertificateStoreStubs.kt | 54 +++-- 21 files changed, 134 insertions(+), 297 deletions(-) delete mode 100644 testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index 4e5a56149b..acb2f014b8 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -11,23 +11,31 @@ import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test +import java.security.UnrecoverableKeyException import java.security.cert.CertPathValidator import java.security.cert.CertPathValidatorException import java.security.cert.PKIXParameters import javax.security.auth.x500.X500Principal +import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue class X509NameConstraintsTest { + companion object { + private const val storePassword = "storePassword" + private const val keyPassword = "entryPassword" + } + private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() - val trustStore = X509KeyStore("password").apply { + + val trustStore = X509KeyStore(storePassword).apply { setCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate) } - val keyStore = X509KeyStore("password").apply { + val keyStore = X509KeyStore(storePassword).apply { val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val nodeCaCert = X509Utilities.createCertificate( CertificateType.NODE_CA, @@ -43,7 +51,7 @@ class X509NameConstraintsTest { nodeCaKeyPair, X500Principal(subjectName.encoded), tlsKeyPair.public) - setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCa.certificate)) + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCa.certificate), keyPassword) } return Pair(keyStore, trustStore) @@ -90,7 +98,6 @@ class X509NameConstraintsTest { .map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray() val nameConstraints = NameConstraints(acceptableNames, arrayOf()) - Crypto.ECDSA_SECP256R1_SHA256 val pathValidator = CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME) assertFailsWith(CertPathValidatorException::class) { @@ -127,4 +134,20 @@ class X509NameConstraintsTest { true } } + + @Test + fun `test private key retrieval`() { + val acceptableNames = listOf("CN=Bank A TLS, UID=", "O=Bank A") + .map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray() + + val nameConstraints = NameConstraints(acceptableNames, arrayOf()) + val (keystore, _) = makeKeyStores(X500Name("CN=Bank A"), nameConstraints) + + val privateKey = keystore.getPrivateKey(X509Utilities.CORDA_CLIENT_TLS, keyPassword) + assertEquals(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME.algorithmName, privateKey.algorithm) + + assertFailsWith(UnrecoverableKeyException::class) { + keystore.getPrivateKey(X509Utilities.CORDA_CLIENT_TLS, "gibberish") + } + } } diff --git a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt index 8146178559..e9aac5a99e 100644 --- a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt @@ -161,9 +161,9 @@ class RaftUniquenessProvider( .withSsl() .withSslProtocol(SslProtocol.TLSv1_2) .withKeyStorePath(config.keyStore.path.toString()) - .withKeyStorePassword(config.keyStore.password) + .withKeyStorePassword(config.keyStore.storePassword) .withTrustStorePath(config.trustStore.path.toString()) - .withTrustStorePassword(config.trustStore.password) + .withTrustStorePassword(config.trustStore.storePassword) .build() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index 456db77e9b..562af4472f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -30,9 +30,9 @@ object DevIdentityGenerator { /** Install a node key store for the given node directory using the given legal name. */ fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name): Party { val certificatesDirectory = nodeDir / "certificates" - val signingCertStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "nodekeystore.jks", "cordacadevpass") - val p2pKeyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "sslkeystore.jks", "cordacadevpass") - val p2pTrustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "truststore.jks", "trustpass") + val signingCertStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "nodekeystore.jks", DEV_CA_KEY_STORE_PASS, DEV_CA_KEY_STORE_PASS) + val p2pKeyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "sslkeystore.jks", DEV_CA_KEY_STORE_PASS, DEV_CA_KEY_STORE_PASS) + val p2pTrustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "truststore.jks", DEV_CA_TRUST_STORE_PASS, DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS) val p2pSslConfig = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore) certificatesDirectory.createDirectories() @@ -77,13 +77,16 @@ object DevIdentityGenerator { publicKey) } val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks" - X509KeyStore.fromFile(distServKeyStoreFile, "cordacadevpass", createNew = true).update { + X509KeyStore.fromFile(distServKeyStoreFile, DEV_CA_KEY_STORE_PASS, createNew = true).update { setCertificate("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert) setPrivateKey( "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key", keyPair.private, listOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate), - "cordacadevkeypass") + DEV_CA_KEY_STORE_PASS // Unfortunately we have to use the same password for private key due to Artemis limitation, for more details please see: + // org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport.loadKeyManagerFactory + // where it is calling `KeyManagerFactory.init()` with store password + /*DEV_CA_PRIVATE_KEY_PASS*/) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index d8a856afa5..03caed3ca2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -27,7 +27,8 @@ fun CertificateStore.registerDevSigningCertificates(legalName: CordaX500Name, devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) { update { - setPrivateKey(X509Utilities.CORDA_CLIENT_CA, devNodeCa.keyPair.private, listOf(devNodeCa.certificate, intermediateCa.certificate, rootCert)) + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, devNodeCa.keyPair.private, listOf(devNodeCa.certificate, intermediateCa.certificate, rootCert), + this@registerDevSigningCertificates.entryPassword) } } @@ -39,7 +40,8 @@ fun CertificateStore.registerDevP2pCertificates(legalName: CordaX500Name, update { val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, devNodeCa.certificate, devNodeCa.keyPair, legalName.x500Principal, tlsKeyPair.public) - setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, devNodeCa.certificate, intermediateCa.certificate, rootCert)) + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, devNodeCa.certificate, intermediateCa.certificate, rootCert), + this@registerDevP2pCertificates.entryPassword) } } @@ -47,15 +49,14 @@ fun CertificateStore.storeLegalIdentity(alias: String, keyPair: KeyPair = Crypto val identityCertPath = query { val nodeCaCertPath = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) // Assume key password = store password. - val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, this@storeLegalIdentity.entryPassword) // Create new keys and store in keystore. val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCaCertAndKeyPair.certificate, nodeCaCertAndKeyPair.keyPair, nodeCaCertAndKeyPair.certificate.subjectX500Principal, keyPair.public) // TODO: X509Utilities.validateCertificateChain() - // Assume key password = store password. listOf(identityCert) + nodeCaCertPath } update { - setPrivateKey(alias, keyPair.private, identityCertPath) + setPrivateKey(alias, keyPair.private, identityCertPath, this@storeLegalIdentity.entryPassword) } return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath)) } @@ -96,6 +97,7 @@ const val DEV_CA_KEY_STORE_FILE: String = "cordadevcakeys.jks" const val DEV_CA_KEY_STORE_PASS: String = "cordacadevpass" const val DEV_CA_TRUST_STORE_FILE: String = "cordatruststore.jks" const val DEV_CA_TRUST_STORE_PASS: String = "trustpass" +const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass" // We need a class so that we can get hold of the class loader internal object DevCaHelper { @@ -104,6 +106,8 @@ internal object DevCaHelper { } } -fun loadDevCaKeyStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_KEY_STORE_FILE", DEV_CA_KEY_STORE_PASS, classLoader) +fun loadDevCaKeyStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource( + "certificates/$DEV_CA_KEY_STORE_FILE", DEV_CA_KEY_STORE_PASS, DEV_CA_PRIVATE_KEY_PASS, classLoader) -fun loadDevCaTrustStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_TRUST_STORE_FILE", DEV_CA_TRUST_STORE_PASS, classLoader) +fun loadDevCaTrustStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource( + "certificates/$DEV_CA_TRUST_STORE_FILE", DEV_CA_TRUST_STORE_PASS, DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS, classLoader) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt index 3ca6be6d6b..526fa8eb9b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt @@ -14,17 +14,18 @@ interface CertificateStore : Iterable> { companion object { - fun of(store: X509KeyStore, password: String): CertificateStore = DelegatingCertificateStore(store, password) + fun of(store: X509KeyStore, password: String, entryPassword: String): CertificateStore = DelegatingCertificateStore(store, password, entryPassword) - fun fromFile(storePath: Path, password: String, createNew: Boolean): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromFile(storePath, password, createNew), password) + fun fromFile(storePath: Path, password: String, entryPassword: String, createNew: Boolean): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromFile(storePath, password, createNew), password, entryPassword) - fun fromInputStream(stream: InputStream, password: String): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromInputStream(stream, password), password) + fun fromInputStream(stream: InputStream, password: String, entryPassword: String): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromInputStream(stream, password), password, entryPassword) - fun fromResource(storeResourceName: String, password: String, classLoader: ClassLoader = Thread.currentThread().contextClassLoader): CertificateStore = fromInputStream(classLoader.getResourceAsStream(storeResourceName), password) + fun fromResource(storeResourceName: String, password: String, entryPassword: String, classLoader: ClassLoader = Thread.currentThread().contextClassLoader): CertificateStore = fromInputStream(classLoader.getResourceAsStream(storeResourceName), password, entryPassword) } val value: X509KeyStore val password: String + val entryPassword: String fun writeTo(stream: OutputStream) = value.internal.store(stream, password.toCharArray()) @@ -79,4 +80,4 @@ interface CertificateStore : Iterable> { } } -private class DelegatingCertificateStore(override val value: X509KeyStore, override val password: String) : CertificateStore \ No newline at end of file +private class DelegatingCertificateStore(override val value: X509KeyStore, override val password: String, override val entryPassword: String) : CertificateStore \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt index 3703742813..7cdfeee79b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt @@ -18,7 +18,7 @@ interface CertificateStoreSupplier { } // TODO replace reference to FileBasedCertificateStoreSupplier with CertificateStoreSupplier, after coming up with a way of passing certificate stores to Artemis. -class FileBasedCertificateStoreSupplier(val path: Path, val password: String) : CertificateStoreSupplier { +class FileBasedCertificateStoreSupplier(val path: Path, val storePassword: String, val entryPassword: String) : CertificateStoreSupplier { - override fun get(createNew: Boolean) = CertificateStore.fromFile(path, password, createNew) + override fun get(createNew: Boolean) = CertificateStore.fromFile(path, storePassword, entryPassword, createNew) } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt index cf3245837c..7286fe8abc 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt @@ -151,7 +151,7 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set private fun ConfigException.Missing.relative(path: String, nestedPath: String?): ConfigException.Missing { return when { - nestedPath != null -> throw ConfigException.Missing("$nestedPath.$path") + nestedPath != null -> throw ConfigException.Missing("$nestedPath.$path", this) else -> this } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt index 5c0c4b501b..f039af0b3d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt @@ -53,17 +53,17 @@ class X509KeyStore private constructor(val internal: KeyStore, private val store return uncheckedCast(certArray.asList()) } - fun getCertificateAndKeyPair(alias: String, keyPassword: String = storePassword): CertificateAndKeyPair { + fun getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair { val cert = getCertificate(alias) val publicKey = Crypto.toSupportedPublicKey(cert.publicKey) return CertificateAndKeyPair(cert, KeyPair(publicKey, getPrivateKey(alias, keyPassword))) } - fun getPrivateKey(alias: String, keyPassword: String = storePassword): PrivateKey { + fun getPrivateKey(alias: String, keyPassword: String): PrivateKey { return internal.getSupportedKey(alias, keyPassword) } - fun setPrivateKey(alias: String, key: PrivateKey, certificates: List, keyPassword: String = storePassword) { + fun setPrivateKey(alias: String, key: PrivateKey, certificates: List, keyPassword: String) { internal.setKeyEntry(alias, key, keyPassword.toCharArray(), certificates.toTypedArray()) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index 2024be3359..75d623d278 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -159,7 +159,9 @@ internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateSto return CertPathTrustManagerParameters(pkixParams) } -fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.password.toCharArray()) +// As per Javadoc in: https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/KeyManagerFactory.html `init` method +// 2nd parameter `password` - the password for recovering keys in the KeyStore +fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.entryPassword.toCharArray()) fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 3db9072041..e2cfff84ca 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -235,8 +235,9 @@ class X509UtilitiesTest { signingCertStore.get(createNew = true).also { it.registerDevSigningCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } p2pSslConfig.keyStore.get(createNew = true).also { it.registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } // Load back server certificate - val serverKeyStore = signingCertStore.get().value - val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + val certStore = signingCertStore.get() + val serverKeyStore = certStore.value + val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, certStore.entryPassword) serverCert.checkValidity() serverCert.verify(intermediateCa.certificate.publicKey) @@ -244,7 +245,7 @@ class X509UtilitiesTest { // Load back SSL certificate val sslKeyStoreReloaded = p2pSslConfig.keyStore.get() - val (sslCert) = sslKeyStoreReloaded.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, p2pSslConfig.keyStore.password) } + val (sslCert) = sslKeyStoreReloaded.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslKeyStoreReloaded.entryPassword) } sslCert.checkValidity() sslCert.verify(serverCert.publicKey) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt index 54d42edd0d..1791a48f4c 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt @@ -20,8 +20,10 @@ class SSLHelperTest { val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(CertificateStore.fromFile(sslConfig.keyStore.path, sslConfig.keyStore.password, false)) - trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(CertificateStore.fromFile(sslConfig.trustStore.path, sslConfig.trustStore.password, false), false)) + val keyStore = sslConfig.keyStore + keyManagerFactory.init(CertificateStore.fromFile(keyStore.path, keyStore.storePassword, keyStore.entryPassword, false)) + val trustStore = sslConfig.trustStore + trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(CertificateStore.fromFile(trustStore.path, trustStore.storePassword, trustStore.entryPassword, false), false)) val sslHandler = createClientSslHelper(NetworkHostAndPort("localhost", 1234), setOf(legalName), keyManagerFactory, trustManagerFactory) val legalNameHash = SecureHash.sha256(legalName.toString()).toString().take(32).toLowerCase() diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt index cf438f16f8..0abeec4144 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -25,7 +25,7 @@ class NodeKeystoreCheckTest { } @Test - fun `node should throw exception if cert path doesn't chain to the trust root`() { + fun `node should throw exception if cert path does not chain to the trust root`() { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { // Create keystores. val keystorePassword = "password" @@ -49,9 +49,9 @@ class NodeKeystoreCheckTest { // Self signed root. val badRootKeyPair = Crypto.generateKeyPair() val badRoot = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Bad Root,L=Lodnon,C=GB"), badRootKeyPair) - val nodeCA = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + val nodeCA = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, signingCertStore.entryPassword) val badNodeCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, badRoot, badRootKeyPair, ALICE_NAME.x500Principal, nodeCA.keyPair.public) - setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, listOf(badNodeCACert, badRoot)) + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, listOf(badNodeCACert, badRoot), signingCertStore.entryPassword) } assertThatThrownBy { diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt index 66682cffa4..4b18fa28be 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt @@ -423,17 +423,17 @@ class CertificateRevocationListNodeTests { val signingCertificateStore = first val p2pSslConfiguration = second val nodeKeyStore = signingCertificateStore.get() - val (nodeCert, nodeKeys) = nodeKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) } + val (nodeCert, nodeKeys) = nodeKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, nodeKeyStore.entryPassword) } val newNodeCert = replaceCrlDistPointCaCertificate(nodeCert, CertificateType.NODE_CA, INTERMEDIATE_CA.keyPair, nodeCaCrlDistPoint) val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.drop(2).toTypedArray()) nodeKeyStore.update { internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA) } nodeKeyStore.update { - setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain) + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain, nodeKeyStore.entryPassword) } val sslKeyStore = p2pSslConfiguration.keyStore.get() - val (tlsCert, tlsKeys) = sslKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) } + val (tlsCert, tlsKeys) = sslKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslKeyStore.entryPassword) } val newTlsCert = replaceCrlDistPointCaCertificate(tlsCert, CertificateType.TLS, nodeKeys, tlsCrlDistPoint, X500Name.getInstance(ROOT_CA.certificate.subjectX500Principal.encoded)) val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.drop(3).toTypedArray()) @@ -441,7 +441,7 @@ class CertificateRevocationListNodeTests { internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS) } sslKeyStore.update { - setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain) + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain, sslKeyStore.entryPassword) } return newNodeCert } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 7fb2986bc4..df860ef06a 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -105,11 +105,11 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKeyPair, CordaX500Name("MiniCorp", "London", "GB").x500Principal, tlsKeyPair.public) signingCertStore.get(createNew = true).update { - setPrivateKey(X509Utilities.CORDA_CLIENT_CA, clientKeyPair.private, listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, clientKeyPair.private, listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate), signingCertStore.entryPassword) } p2pSslConfig.keyStore.get(createNew = true).update { - setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate), p2pSslConfig.keyStore.entryPassword) } val attacker = clientTo(alice.node.configuration.p2pAddress, p2pSslConfig) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index c196bf639e..72eb7e1bc1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -862,7 +862,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, keyStore.storeLegalIdentity(privateKeyAlias, generateKeyPair()) } - val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias) } + val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias, keyStore.entryPassword) } // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. val compositeKeyAlias = "$id-composite-key" diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 1b8d0507b8..bfae0e983c 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -86,9 +86,9 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N } if (keyStore.getOptional() == null || signingCertificateStore.getOptional() == null) { - val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.password).get(true).also { it.registerDevSigningCertificates(myLegalName) } + val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.storePassword, signingCertificateStore.entryPassword).get(true).also { it.registerDevSigningCertificates(myLegalName) } - FileBasedCertificateStoreSupplier(keyStore.path, keyStore.password).get(true).also { it.registerDevP2pCertificates(myLegalName) } + FileBasedCertificateStoreSupplier(keyStore.path, keyStore.storePassword, keyStore.entryPassword).get(true).also { it.registerDevP2pCertificates(myLegalName) } // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks" @@ -97,7 +97,7 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N signingKeyStore.update { serviceKeystore.aliases().forEach { if (serviceKeystore.internal.isKeyEntry(it)) { - setPrivateKey(it, serviceKeystore.getPrivateKey(it, DEV_CA_PRIVATE_KEY_PASS), serviceKeystore.getCertificateChain(it)) + setPrivateKey(it, serviceKeystore.getPrivateKey(it, DEV_CA_KEY_STORE_PASS), serviceKeystore.getCertificateChain(it), signingKeyStore.entryPassword) } else { setCertificate(it, serviceKeystore.getCertificate(it)) } diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index f7cf92aa36..ecf0c407f9 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -260,12 +260,16 @@ data class NodeConfigurationImpl( override val certificatesDirectory = baseDirectory / "certificates" private val signingCertificateStorePath = certificatesDirectory / "nodekeystore.jks" - override val signingCertificateStore = FileBasedCertificateStoreSupplier(signingCertificateStorePath, keyStorePassword) - private val p2pKeystorePath: Path get() = certificatesDirectory / "sslkeystore.jks" - private val p2pKeyStore = FileBasedCertificateStoreSupplier(p2pKeystorePath, keyStorePassword) + + // TODO: There are two implications here: + // 1. "signingCertificateStore" and "p2pKeyStore" have the same passwords. In the future we should re-visit this "rule" and see of they can be made different; + // 2. The passwords for store and for keys in this store are the same, this is due to limitations of Artemis. + override val signingCertificateStore = FileBasedCertificateStoreSupplier(signingCertificateStorePath, keyStorePassword, keyStorePassword) + private val p2pKeyStore = FileBasedCertificateStoreSupplier(p2pKeystorePath, keyStorePassword, keyStorePassword) + private val p2pTrustStoreFilePath: Path get() = certificatesDirectory / "truststore.jks" - private val p2pTrustStore = FileBasedCertificateStoreSupplier(p2pTrustStoreFilePath, trustStorePassword) + private val p2pTrustStore = FileBasedCertificateStoreSupplier(p2pTrustStoreFilePath, trustStorePassword, trustStorePassword) override val p2pSslOptions: MutualSslConfiguration = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore) override val rpcOptions: NodeRpcOptions diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index eb1aac885b..e8f916dbb7 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -161,7 +161,7 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, println("Private key '$keyAlias' and certificate stored in node signing keystore.") } - private fun CertificateStore.loadOrCreateKeyPair(alias: String, privateKeyPassword: String = password): KeyPair { + private fun CertificateStore.loadOrCreateKeyPair(alias: String, entryPassword: String = password): KeyPair { // Create or load self signed keypair from the key store. // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. if (alias !in this) { @@ -170,11 +170,11 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair) // Save to the key store. with(value) { - setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) + setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = entryPassword) save() } } - return query { getCertificateAndKeyPair(alias, privateKeyPassword) }.keyPair + return query { getCertificateAndKeyPair(alias, entryPassword) }.keyPair } /** @@ -281,7 +281,9 @@ class NodeRegistrationHelper( } private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List, tlsCertCrlIssuer: X500Name?) { - config.p2pSslOptions.keyStore.get(createNew = true).update { + val keyStore = config.p2pSslOptions.keyStore + val certificateStore = keyStore.get(createNew = true) + certificateStore.update { println("Generating SSL certificate for node messaging service.") val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val sslCert = X509Utilities.createCertificate( @@ -293,9 +295,9 @@ class NodeRegistrationHelper( crlDistPoint = config.tlsCertCrlDistPoint?.toString(), crlIssuer = tlsCertCrlIssuer) logger.info("Generated TLS certificate: $sslCert") - setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) + setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates, certificateStore.entryPassword) } - println("SSL private key and certificate stored in ${config.p2pSslOptions.keyStore.path}.") + println("SSL private key and certificate stored in ${keyStore.path}.") } private fun createTruststore(rootCertificate: X509Certificate) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index e87557f716..cd46caf2fb 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -845,10 +845,10 @@ class DriverDSLImpl( config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString() config += "keyStorePath" to configuration.p2pSslOptions.keyStore.path.toString() - config += "keyStorePassword" to configuration.p2pSslOptions.keyStore.password + config += "keyStorePassword" to configuration.p2pSslOptions.keyStore.storePassword config += "trustStorePath" to configuration.p2pSslOptions.trustStore.path.toString() - config += "trustStorePassword" to configuration.p2pSslOptions.trustStore.password + config += "trustStorePassword" to configuration.p2pSslOptions.trustStore.storePassword return config } diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt deleted file mode 100644 index bce2152d57..0000000000 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt +++ /dev/null @@ -1,215 +0,0 @@ -package net.corda.testing.common.internal - -import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.createFile -import net.corda.core.internal.deleteIfExists -import net.corda.core.internal.div -import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier -import net.corda.nodeapi.internal.config.SslConfiguration -import net.corda.nodeapi.internal.config.MutualSslConfiguration -import net.corda.nodeapi.internal.crypto.* -import org.apache.commons.io.FileUtils -import sun.security.tools.keytool.CertAndKeyGen -import sun.security.x509.X500Name -import java.nio.file.Files -import java.nio.file.Path -import java.security.KeyPair -import java.security.KeyStore -import java.security.PrivateKey -import java.security.cert.X509Certificate -import java.time.Duration -import java.time.Instant -import java.time.Instant.now -import java.time.temporal.ChronoUnit -import java.util.* -import javax.security.auth.x500.X500Principal - -class UnsafeCertificatesFactory( - defaults: Defaults = defaults(), - private val keyType: String = defaults.keyType, - private val signatureAlgorithm: String = defaults.signatureAlgorithm, - private val keySize: Int = defaults.keySize, - private val certificatesValidityWindow: CertificateValidityWindow = defaults.certificatesValidityWindow, - private val keyStoreType: String = defaults.keyStoreType) { - - companion object { - private const val KEY_TYPE_RSA = "RSA" - private const val SIG_ALG_SHA_RSA = "SHA1WithRSA" - private const val KEY_SIZE = 1024 - private val DEFAULT_DURATION = Duration.of(365, ChronoUnit.DAYS) - private const val DEFAULT_KEYSTORE_TYPE = "JKS" - - fun defaults() = Defaults(KEY_TYPE_RSA, SIG_ALG_SHA_RSA, KEY_SIZE, CertificateValidityWindow(now(), DEFAULT_DURATION), DEFAULT_KEYSTORE_TYPE) - } - - data class Defaults( - val keyType: String, - val signatureAlgorithm: String, - val keySize: Int, - val certificatesValidityWindow: CertificateValidityWindow, - val keyStoreType: String) - - fun createSelfSigned(name: X500Name): UnsafeCertificate = createSelfSigned(name, keyType, signatureAlgorithm, keySize, certificatesValidityWindow) - - fun createSelfSigned(name: CordaX500Name) = createSelfSigned(name.asX500Name()) - - fun createSignedBy(subject: X500Principal, issuer: UnsafeCertificate): UnsafeCertificate = issuer.createSigned(subject, keyType, signatureAlgorithm, keySize, certificatesValidityWindow) - - fun createSignedBy(name: CordaX500Name, issuer: UnsafeCertificate): UnsafeCertificate = issuer.createSigned(name, keyType, signatureAlgorithm, keySize, certificatesValidityWindow) - - fun newKeyStore(password: String) = UnsafeKeyStore.create(keyStoreType, password) - - fun newKeyStores(keyStorePassword: String, trustStorePassword: String): KeyStores = KeyStores(newKeyStore(keyStorePassword), newKeyStore(trustStorePassword)) -} - -class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) { - fun save(directory: Path = Files.createTempDirectory(null)): AutoClosableSSLConfiguration { - val keyStoreFile = keyStore.toTemporaryFile("sslkeystore", directory = directory) - val trustStoreFile = trustStore.toTemporaryFile("truststore", directory = directory) - - val sslConfiguration = sslConfiguration(keyStoreFile, trustStoreFile) - - return object : AutoClosableSSLConfiguration { - override val value = sslConfiguration - - override fun close() { - keyStoreFile.close() - trustStoreFile.close() - } - } - } - - private fun sslConfiguration(keyStoreFile: TemporaryFile, trustStoreFile: TemporaryFile): MutualSslConfiguration { - - val keyStore = FileBasedCertificateStoreSupplier(keyStoreFile.file, keyStore.password) - val trustStore = FileBasedCertificateStoreSupplier(trustStoreFile.file, trustStore.password) - return SslConfiguration.mutual(keyStore, trustStore) - } -} - -interface AutoClosableSSLConfiguration : AutoCloseable { - val value: MutualSslConfiguration -} - -typealias KeyStoreEntry = Pair - -data class UnsafeKeyStore(private val delegate: KeyStore, val password: String) : Iterable { - companion object { - private const val JKS_TYPE = "JKS" - - fun create(type: String, password: String) = UnsafeKeyStore(newKeyStore(type, password), password) - - fun createJKS(password: String) = create(JKS_TYPE, password) - } - - operator fun plus(entry: KeyStoreEntry) = set(entry.first, entry.second) - - override fun iterator(): Iterator> = delegate.aliases().toList().map { alias -> alias to get(alias) }.iterator() - - operator fun get(alias: String): UnsafeCertificate { - return when { - delegate.isKeyEntry(alias) -> delegate.getCertificateAndKeyPair(alias, password).unsafe() - else -> UnsafeCertificate(delegate.getX509Certificate(alias), null) - } - } - - operator fun set(alias: String, certificate: UnsafeCertificate) { - delegate.setCertificateEntry(alias, certificate.value) - delegate.setKeyEntry(alias, certificate.privateKey, password.toCharArray(), arrayOf(certificate.value)) - } - - fun save(path: Path) = delegate.save(path, password) - - fun toTemporaryFile(fileName: String, fileExtension: String? = delegate.type.toLowerCase(), directory: Path): TemporaryFile { - return TemporaryFile("$fileName.$fileExtension", directory).also { save(it.file) } - } -} - -class TemporaryFile(fileName: String, val directory: Path) : AutoCloseable { - val file: Path = (directory / fileName).createFile().toAbsolutePath() - - init { - file.toFile().deleteOnExit() - } - - override fun close() { - file.deleteIfExists() - } -} - -data class UnsafeCertificate(val value: X509Certificate, val privateKey: PrivateKey?) { - val keyPair = KeyPair(value.publicKey, privateKey) - - val principal: X500Principal get() = value.subjectX500Principal - - val issuer: X500Principal get() = value.issuerX500Principal - - fun createSigned(subject: X500Principal, keyType: String, signatureAlgorithm: String, keySize: Int, certificatesValidityWindow: CertificateValidityWindow): UnsafeCertificate { - val keyGen = keyGen(keyType, signatureAlgorithm, keySize) - - return UnsafeCertificate(X509Utilities.createCertificate( - certificateType = CertificateType.TLS, - issuer = value.subjectX500Principal, - issuerKeyPair = keyPair, - validityWindow = certificatesValidityWindow.datePair, - subject = subject, - subjectPublicKey = keyGen.publicKey - ), keyGen.privateKey) - } - - fun createSigned(name: CordaX500Name, keyType: String, signatureAlgorithm: String, keySize: Int, certificatesValidityWindow: CertificateValidityWindow) = createSigned(name.x500Principal, keyType, signatureAlgorithm, keySize, certificatesValidityWindow) -} - -data class CertificateValidityWindow(val from: Instant, val to: Instant) { - constructor(from: Instant, duration: Duration) : this(from, from.plus(duration)) - - val duration = Duration.between(from, to)!! - - val datePair = Date.from(from) to Date.from(to) -} - -private fun createSelfSigned(name: X500Name, keyType: String, signatureAlgorithm: String, keySize: Int, certificatesValidityWindow: CertificateValidityWindow): UnsafeCertificate { - val keyGen = keyGen(keyType, signatureAlgorithm, keySize) - return UnsafeCertificate(keyGen.getSelfCertificate(name, certificatesValidityWindow.duration.toMillis()), keyGen.privateKey) -} - -private fun CordaX500Name.asX500Name(): X500Name = X500Name.asX500Name(x500Principal) - -private fun CertificateAndKeyPair.unsafe() = UnsafeCertificate(certificate, keyPair.private) - -private fun keyGen(keyType: String, signatureAlgorithm: String, keySize: Int): CertAndKeyGen { - val keyGen = CertAndKeyGen(keyType, signatureAlgorithm) - keyGen.generate(keySize) - return keyGen -} - -private fun newKeyStore(type: String, password: String): KeyStore { - val keyStore = KeyStore.getInstance(type) - // Loading creates the store, can't do anything with it until it's loaded - keyStore.load(null, password.toCharArray()) - - return keyStore -} - -fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: MutualSslConfiguration, clientSslOptions: MutualSslConfiguration) -> Unit) { - val serverDir = Files.createTempDirectory(null) - FileUtils.forceDeleteOnExit(serverDir.toFile()) - - val clientDir = Files.createTempDirectory(null) - FileUtils.forceDeleteOnExit(clientDir.toFile()) - - server.save(serverDir).use { serverSslConfiguration -> - client.save(clientDir).use { clientSslConfiguration -> - action(serverSslConfiguration.value, clientSslConfiguration.value) - } - } - clientDir.deleteIfExists() - serverDir.deleteIfExists() -} - -fun withCertificates(factoryDefaults: UnsafeCertificatesFactory.Defaults = UnsafeCertificatesFactory.defaults(), action: (server: KeyStores, client: KeyStores, createSelfSigned: (name: CordaX500Name) -> UnsafeCertificate, createSignedBy: (name: CordaX500Name, issuer: UnsafeCertificate) -> UnsafeCertificate) -> Unit) { - val factory = UnsafeCertificatesFactory(factoryDefaults) - val server = factory.newKeyStores("serverKeyStorePass", "serverTrustKeyStorePass") - val client = factory.newKeyStores("clientKeyStorePass", "clientTrustKeyStorePass") - action(server, client, factory::createSelfSigned, factory::createSignedBy) -} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt index 62f871ca8c..dfe21245f1 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt @@ -1,6 +1,9 @@ package net.corda.testing.internal.stubs import net.corda.core.internal.div +import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS +import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PASS +import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.config.MutualSslConfiguration @@ -11,28 +14,27 @@ class CertificateStoreStubs { companion object { const val DEFAULT_CERTIFICATES_DIRECTORY_NAME = "certificates" - - @JvmStatic - fun withStoreAt(certificateStorePath: Path, password: String): FileBasedCertificateStoreSupplier = FileBasedCertificateStoreSupplier(certificateStorePath, password) } class Signing { companion object { - const val DEFAULT_STORE_FILE_NAME = "nodekeystore.jks" - const val DEFAULT_STORE_PASSWORD = "cordacadevpass" + private const val DEFAULT_STORE_FILE_NAME = "nodekeystore.jks" + private const val DEFAULT_STORE_PASSWORD = DEV_CA_KEY_STORE_PASS @JvmStatic - fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, + keyPassword: String = password, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { - return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password, keyPassword) } @JvmStatic - fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, keyPassword: String = password, + certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { - return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password, keyPassword) } } } @@ -42,10 +44,13 @@ class CertificateStoreStubs { companion object { @JvmStatic - fun withCertificatesDirectory(certificatesDirectory: Path, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { + fun withCertificatesDirectory(certificatesDirectory: Path, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, + keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, keyPassword: String = keyStorePassword, + trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD, trustStoreKeyPassword: String = TrustStore.DEFAULT_KEY_PASSWORD, + useOpenSsl: Boolean = false): MutualSslConfiguration { - val keyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / keyStoreFileName, keyStorePassword) - val trustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / trustStoreFileName, trustStorePassword) + val keyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / keyStoreFileName, keyStorePassword, keyPassword) + val trustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / trustStoreFileName, trustStorePassword, trustStoreKeyPassword) return SslConfiguration.mutual(keyStore, trustStore) } @@ -61,18 +66,20 @@ class CertificateStoreStubs { companion object { const val DEFAULT_STORE_FILE_NAME = "sslkeystore.jks" - const val DEFAULT_STORE_PASSWORD = "cordacadevpass" + const val DEFAULT_STORE_PASSWORD = DEV_CA_KEY_STORE_PASS @JvmStatic - fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, keyPassword: String = password, + certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { - return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password, keyPassword) } @JvmStatic - fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, keyPassword: String = password, + certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { - return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password, keyPassword) } } } @@ -82,18 +89,21 @@ class CertificateStoreStubs { companion object { const val DEFAULT_STORE_FILE_NAME = "truststore.jks" - const val DEFAULT_STORE_PASSWORD = "trustpass" + const val DEFAULT_STORE_PASSWORD = DEV_CA_TRUST_STORE_PASS + const val DEFAULT_KEY_PASSWORD = DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS @JvmStatic - fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, + keyPassword: String = DEFAULT_KEY_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { - return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password, keyPassword) } @JvmStatic - fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, keyPassword: String = DEFAULT_KEY_PASSWORD, + certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { - return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password, keyPassword) } } } From e0d8ea8a5836cfce714f9b7cff1fcc246a8e50a0 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Mon, 22 Oct 2018 10:26:10 +0100 Subject: [PATCH 70/83] =?UTF-8?q?CORDA-535:=20Move=20implementation=20spec?= =?UTF-8?q?ific=20configuration=20values=20out=20of=20n=E2=80=A6=20(#4058)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configuration objects for specific notary implementations have been replaced by a single untyped "extraConfig" Config object that is left to the notary service itself to parse. * Remove the raft bootstrapping command from node, we'll need a different mechanism for that. * Remove pre-generated identity config value. * Split up obtainIdentity() in AbstractNode to make it easier to read. * A temporary workaround for the bootstrapper tool to support BFT notaries. * Update docs * Add upgrade notes * Fix rebase issue * Add a config diff for the bft notary as well --- docs/source/changelog.rst | 23 ++++++ docs/source/corda-configuration-file.rst | 25 ++----- .../resources/notary-config-update-bft.png | Bin 0 -> 131365 bytes .../source/resources/notary-config-update.png | Bin 0 -> 86107 bytes docs/source/running-a-node.rst | 2 - .../corda/notary/bftsmart/BFTSMaRtConfig.kt | 18 +++++ .../notary/bftsmart/BftSmartNotaryService.kt | 11 +-- .../notary/bftsmart/BFTNotaryServiceTests.kt | 13 ++-- .../net/corda/notary/raft/RaftConfig.kt | 18 +++++ .../corda/notary/raft/RaftNotaryService.kt | 10 ++- .../notary/raft/RaftUniquenessProvider.kt | 1 - .../internal/network/NetworkBootstrapper.kt | 10 ++- .../net/corda/node/NodeCmdLineOptions.kt | 6 -- .../net/corda/node/internal/AbstractNode.kt | 67 +++++++++--------- .../net/corda/node/internal/NodeStartup.kt | 15 +--- .../node/services/config/NodeConfiguration.kt | 53 ++++---------- .../net.corda.node.internal.NodeStartup.yml | 5 -- .../corda/node/internal/NodeStartupTest.kt | 1 - .../net/corda/node/services/TimedFlowTests.kt | 7 +- samples/notary-demo/build.gradle | 14 ++-- .../testing/node/internal/DriverDSLImpl.kt | 25 +++---- 21 files changed, 167 insertions(+), 157 deletions(-) create mode 100644 docs/source/resources/notary-config-update-bft.png create mode 100644 docs/source/resources/notary-config-update.png create mode 100644 experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftConfig.kt diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 51330fffd3..eb81172b4a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -16,6 +16,29 @@ Unreleased * Vault storage of contract state constraints metadata and associated vault query functions to retrieve and sort by constraint type. +* UPGRADE REQUIRED: changes have been made to how notary implementations are configured and loaded. + No upgrade steps are required for the single-node notary (both validating and non-validating variants). + Other notary implementations have been moved out of the Corda node into individual Cordapps, and require configuration + file updates. + + To run a notary you will now need to include the appropriate notary CorDapp in the ``cordapps/`` directory: + + * ``corda-notary-raft`` for the Raft notary. + * ``corda-notary-bft-smart`` for the BFT-Smart notary. + + It is now required to specify the fully qualified notary service class name, ``className``, and the legal name of + the notary service in case of distributed notaries: ``serviceLegalName``. + + Implementation-specific configuration values have been moved to the ``extraConfig`` configuration block. + + Example configuration changes for the Raft notary: + + .. image:: resources/notary-config-update.png + + Example configuration changes for the BFT-Smart notary: + + .. image:: resources/notary-config-update-bft.png + * New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call. * Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index e5afa2f27b..6f5fcf17b5 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -132,34 +132,17 @@ absolute path to the node's base directory. :security: Contains various nested fields controlling user authentication/authorization, in particular for RPC accesses. See :doc:`clientrpc` for details. -:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt - cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. +:notary: Optional configuration object which if present configures the node to run as a notary. :validating: Boolean to determine whether the notary is a validating or non-validating one. :serviceLegalName: If the node is part of a distributed cluster, specify the legal name of the cluster. At runtime, Corda checks whether this name matches the name of the certificate of the notary cluster. - :raft: If part of a distributed Raft cluster specify this config object, with the following settings: + :className: The fully qualified class name of the notary service to run. The class is expected to be loaded from + a notary CorDapp. Defaults to run the ``SimpleNotaryService``, which is built in. - :nodeAddress: The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a - separate transport layer for communication that does not integrate with ArtemisMQ messaging services. - - :clusterAddresses: Must list the addresses of all the members in the cluster. At least one of the members must - be active and be able to communicate with the cluster leader for the node to join the cluster. If empty, a - new cluster will be bootstrapped. - - :bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings: - - :replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id. - - :clusterAddresses: Must list the addresses of all the members in the cluster. At least one of the members must - be active and be able to communicate with the cluster leader for the node to join the cluster. If empty, a - new cluster will be bootstrapped. - - :custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`. - - Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified. + :extraConfig: an optional configuration block for providing notary implementation-specific values. :rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the following fields: diff --git a/docs/source/resources/notary-config-update-bft.png b/docs/source/resources/notary-config-update-bft.png new file mode 100644 index 0000000000000000000000000000000000000000..207d87cdefcc58fbf19dda1f983791608edfd168 GIT binary patch literal 131365 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 literal 0 HcmV?d00001 diff --git a/docs/source/resources/notary-config-update.png b/docs/source/resources/notary-config-update.png new file mode 100644 index 0000000000000000000000000000000000000000..345c3854feac0fd60ec8c8f4cf4f3155ad25dc9d GIT binary patch literal 86107 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

    vRRMAAR9@WgKHdWZn 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ȋi&>ld%0 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, + val debug: Boolean = false, + /** Used for testing purposes only. */ + val exposeRaces: Boolean = false +) { + init { + require(replicaId >= 0) { "replicaId cannot be negative" } + } +} + /** * BFT SMaRt can only be configured via files in a configHome directory. * Each instance of this class creates such a configHome, accessible via [path]. diff --git a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt index 391b92a630..905a8fe569 100644 --- a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt @@ -20,9 +20,9 @@ import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.api.ServiceHubInternal -import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import java.security.PublicKey import javax.persistence.Entity @@ -54,10 +54,13 @@ class BftSmartNotaryService( } private val notaryConfig = services.configuration.notary - ?: throw IllegalArgumentException("Failed to register ${this::class.java}: notary configuration not present") + ?: throw IllegalArgumentException("Failed to register ${BftSmartNotaryService::class.java}: notary configuration not present") - private val bftSMaRtConfig = notaryConfig.bftSMaRt - ?: throw IllegalArgumentException("Failed to register ${this::class.java}: raft configuration not present") + private val bftSMaRtConfig = try { + notaryConfig.extraConfig!!.parseAs() + } catch (e: Exception) { + throw IllegalArgumentException("Failed to register ${BftSmartNotaryService::class.java}: BFT-Smart configuration not present") + } private val cluster: BFTSMaRt.Cluster = makeBFTCluster(notaryIdentityKey, bftSMaRtConfig) diff --git a/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTNotaryServiceTests.kt b/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTNotaryServiceTests.kt index 1924470588..b69255d24a 100644 --- a/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTNotaryServiceTests.kt +++ b/experimental/notary-bft-smart/src/test/kotlin/net/corda/notary/bftsmart/BFTNotaryServiceTests.kt @@ -21,9 +21,9 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.Try import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds -import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract @@ -55,7 +55,7 @@ class BFTNotaryServiceTests { @BeforeClass @JvmStatic fun before() { - mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts", "net.corda.notary.bftsmart")) val clusterSize = minClusterSize(1) val started = startBftClusterAndNode(clusterSize, mockNet) notary = started.first @@ -71,10 +71,10 @@ class BFTNotaryServiceTests { fun startBftClusterAndNode(clusterSize: Int, mockNet: InternalMockNetwork, exposeRaces: Boolean = false): Pair { (Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) - + val serviceLegalName = CordaX500Name("BFT", "Zurich", "CH") val notaryIdentity = DevIdentityGenerator.generateDistributedNotaryCompositeIdentity( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, - CordaX500Name("BFT", "Zurich", "CH")) + serviceLegalName) val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryIdentity, false)))) @@ -84,8 +84,9 @@ class BFTNotaryServiceTests { mockNet.createUnstartedNode(InternalMockNodeParameters(configOverrides = { val notary = NotaryConfig( validating = false, - bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces), - className = "net.corda.notary.bftsmart.BftSmartNotaryService" + extraConfig = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces).toConfig(), + className = "net.corda.notary.bftsmart.BftSmartNotaryService", + serviceLegalName = serviceLegalName ) doReturn(notary).whenever(it).notary })) diff --git a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftConfig.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftConfig.kt new file mode 100644 index 0000000000..75996a6ebf --- /dev/null +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftConfig.kt @@ -0,0 +1,18 @@ +package net.corda.notary.raft + +import net.corda.core.utilities.NetworkHostAndPort + +/** Configuration properties specific to the RaftNotaryService. */ +data class RaftConfig( + /** + * The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a + * separate transport layer for communication that does not integrate with ArtemisMQ messaging services. + */ + val nodeAddress: NetworkHostAndPort, + /** + * Must list the addresses of all the members in the cluster. At least one of the members mustbe active and + * be able to communicate with the cluster leader for the node to join the cluster. If empty, a new cluster + * will be bootstrapped. + */ + val clusterAddresses: List +) \ No newline at end of file diff --git a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt index 3913551802..97843cb13c 100644 --- a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftNotaryService.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.transactions.NonValidatingNotaryFlow import net.corda.node.services.transactions.ValidatingNotaryFlow +import net.corda.nodeapi.internal.config.parseAs import java.security.PublicKey /** A highly available notary service using the Raft algorithm to achieve consensus. */ @@ -14,11 +15,14 @@ class RaftNotaryService( override val notaryIdentityKey: PublicKey ) : TrustedAuthorityNotaryService() { private val notaryConfig = services.configuration.notary - ?: throw IllegalArgumentException("Failed to register ${this::class.java}: notary configuration not present") + ?: throw IllegalArgumentException("Failed to register ${RaftNotaryService::class.java}: notary configuration not present") override val uniquenessProvider = with(services) { - val raftConfig = notaryConfig.raft - ?: throw IllegalArgumentException("Failed to register ${this::class.java}: raft configuration not present") + val raftConfig = try { + notaryConfig.extraConfig!!.parseAs() + } catch (e: Exception) { + throw IllegalArgumentException("Failed to register ${RaftNotaryService::class.java}: raft configuration not present") + } RaftUniquenessProvider( configuration.baseDirectory, configuration.p2pSslOptions, diff --git a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt index e9aac5a99e..f3b7671a3e 100644 --- a/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt +++ b/experimental/notary-raft/src/main/kotlin/net/corda/notary/raft/RaftUniquenessProvider.kt @@ -26,7 +26,6 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug -import net.corda.node.services.config.RaftConfig import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 1747cc2333..c581577c57 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -126,8 +126,8 @@ internal constructor(private val initSerEnv: Boolean, } return clusteredNotaries.groupBy { it.first }.map { (k, vs) -> val cs = vs.map { it.second.config } - if (cs.any { it.hasPath("notary.bftSMaRt") }) { - require(cs.all { it.hasPath("notary.bftSMaRt") }) { "Mix of BFT and non-BFT notaries with service name $k" } + if (cs.any { isBFTNotary(it) }) { + require(cs.all { isBFTNotary(it) }) { "Mix of BFT and non-BFT notaries with service name $k" } NotaryCluster.BFT(k) to vs.map { it.second.directory } } else { NotaryCluster.CFT(k) to vs.map { it.second.directory } @@ -135,6 +135,12 @@ internal constructor(private val initSerEnv: Boolean, }.toMap() } + private fun isBFTNotary(config: Config): Boolean { + // TODO: pass a commandline parameter to the bootstrapper instead. Better yet, a notary config map + // specifying the notary identities and the type (single-node, CFT, BFT) of each notary to set up. + return config.getString ("notary.className").contains("BFT", true) + } + private fun generateServiceIdentitiesForNotaryClusters(configs: Map) { notaryClusters(configs).forEach { (cluster, directories) -> when (cluster) { diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt index 6d04567d9d..4f4ba98c1a 100644 --- a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -88,12 +88,6 @@ class NodeCmdLineOptions { ) var justGenerateRpcSslCerts: Boolean = false - @Option( - names = ["--bootstrap-raft-cluster"], - description = ["Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer addresses), acting as a seed for other nodes to join the cluster."] - ) - var bootstrapRaftCluster: Boolean = false - @Option( names = ["-c", "--clear-network-map-cache"], description = ["Clears local copy of network map, on node startup it will be restored from server or file system."] diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 72eb7e1bc1..a8ba165e1f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -268,7 +268,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") val trustRoot = initKeyStores() - val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) + val (identity, identityKeyPair) = obtainIdentity() startDatabase() val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) @@ -323,7 +323,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkMapCache.start(netParams.notaries) startDatabase() - val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) + val (identity, identityKeyPair) = obtainIdentity() identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) val (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction { @@ -400,8 +400,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val keyPairs = mutableSetOf(identityKeyPair) val myNotaryIdentity = configuration.notary?.let { - if (it.isClusterConfig) { - val (notaryIdentity, notaryIdentityKeyPair) = obtainIdentity(it) + if (it.serviceLegalName != null) { + val (notaryIdentity, notaryIdentityKeyPair) = loadNotaryClusterIdentity(it.serviceLegalName) keyPairs += notaryIdentityKeyPair notaryIdentity } else { @@ -840,24 +840,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration, myNotaryIdentity: PartyAndCertificate?, networkParameters: NetworkParameters) - private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair { + /** Loads or generates the node's legal identity and key-pair. */ + private fun obtainIdentity(): Pair { val keyStore = configuration.signingCertificateStore.get() + val legalName = configuration.myLegalName - val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { - // Node's main identity or if it's a single node notary. - Pair(NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) - } else { - // The node is part of a distributed notary whose identity must already be generated beforehand. - Pair(DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) - } // TODO: Integrate with Key management service? - val privateKeyAlias = "$id-private-key" - + val privateKeyAlias = "$NODE_IDENTITY_ALIAS_PREFIX-private-key" if (privateKeyAlias !in keyStore) { - // We shouldn't have a distributed notary at this stage, so singleName should NOT be null. - requireNotNull(singleName) { - "Unable to find in the key store the identity of the distributed notary the node is part of" - } log.info("$privateKeyAlias not found in key store, generating fresh key!") keyStore.storeLegalIdentity(privateKeyAlias, generateKeyPair()) } @@ -865,30 +855,41 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias, keyStore.entryPassword) } // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. - val compositeKeyAlias = "$id-composite-key" + val certificates = keyStore.query { getCertificateChain(privateKeyAlias) } + check(certificates.first() == x509Cert) { + "Certificates from key store do not line up!" + } + + val subject = CordaX500Name.build(certificates.first().subjectX500Principal) + if (subject != legalName) { + throw ConfigurationException("The name '$legalName' for $NODE_IDENTITY_ALIAS_PREFIX doesn't match what's in the key store: $subject") + } + + val certPath = X509Utilities.buildCertPath(certificates) + return Pair(PartyAndCertificate(certPath), keyPair) + } + + /** Loads pre-generated notary service cluster identity. */ + private fun loadNotaryClusterIdentity(serviceLegalName: CordaX500Name): Pair { + val keyStore = configuration.signingCertificateStore.get() + + val privateKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key" + val keyPair = keyStore.query { getCertificateAndKeyPair(privateKeyAlias) }.keyPair + + val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key" val certificates = if (compositeKeyAlias in keyStore) { - // Use composite key instead if it exists. val certificate = keyStore[compositeKeyAlias] // We have to create the certificate chain for the composite key manually, this is because we don't have a keystore // provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate + // the tail of the private key certificates, as they are both signed by the same certificate chain. listOf(certificate) + keyStore.query { getCertificateChain(privateKeyAlias) }.drop(1) - } else { - keyStore.query { getCertificateChain(privateKeyAlias) }.let { - check(it[0] == x509Cert) { "Certificates from key store do not line up!" } - it - } - } + } else throw IllegalStateException("The identity public key for the notary service $serviceLegalName was not found in the key store.") - val subject = CordaX500Name.build(certificates[0].subjectX500Principal) - if (singleName != null && subject != singleName) { - throw ConfigurationException("The name '$singleName' for $id doesn't match what's in the key store: $subject") - } else if (notaryConfig != null && notaryConfig.isClusterConfig && notaryConfig.serviceLegalName != null && subject != notaryConfig.serviceLegalName) { - // Note that we're not checking if `notaryConfig.serviceLegalName` is not present for backwards compatibility. - throw ConfigurationException("The name of the notary service '${notaryConfig.serviceLegalName}' for $id doesn't " + + val subject = CordaX500Name.build(certificates.first().subjectX500Principal) + if (subject != serviceLegalName) { + throw ConfigurationException("The name of the notary service '$serviceLegalName' for $DISTRIBUTED_NOTARY_ALIAS_PREFIX doesn't " + "match what's in the key store: $subject. You might need to adjust the configuration of `notary.serviceLegalName`.") } - val certPath = X509Utilities.buildCertPath(certificates) return Pair(PartyAndCertificate(certPath), keyPair) } 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 10fc55f7e5..f26af3380a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -18,7 +18,6 @@ import net.corda.node.* import net.corda.node.internal.Node.Companion.isValidJavaVersion import net.corda.node.internal.cordapp.MultipleCordappsForFlowException import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.NodeConfigurationImpl import net.corda.node.services.config.shouldStartLocalShell import net.corda.node.services.config.shouldStartSSHDaemon import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate @@ -27,7 +26,6 @@ import net.corda.node.utilities.registration.NodeRegistrationException import net.corda.node.utilities.registration.NodeRegistrationHelper import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore -import net.corda.core.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException @@ -189,14 +187,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { if (cmdLineOptions.devMode == true) { println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") } - val configuration = configurationResult.getOrThrow() - return if (cmdLineOptions.bootstrapRaftCluster) { - println("Bootstrapping raft cluster (starting up as seed node).") - // Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining. - (configuration as NodeConfigurationImpl).copy(notary = configuration.notary?.copy(raft = configuration.notary?.raft?.copy(clusterAddresses = emptyList()))) - } else { - configuration - } + return configurationResult.getOrThrow() } private fun checkRegistrationMode(): Boolean { @@ -298,12 +289,12 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { val console: Console? = System.console() when (console) { - // In this case, the JVM is not connected to the console so we need to exit. + // In this case, the JVM is not connected to the console so we need to exit. null -> { println("Not connected to console. Exiting") exitProcess(1) } - // Otherwise we can proceed normally. + // Otherwise we can proceed normally. else -> { while (true) { val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ") diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index ecf0c407f9..eab762dcdd 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -11,12 +11,7 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier -import net.corda.nodeapi.internal.config.SslConfiguration -import net.corda.nodeapi.internal.config.MutualSslConfiguration -import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy -import net.corda.nodeapi.internal.config.User -import net.corda.nodeapi.internal.config.parseAs +import net.corda.nodeapi.internal.config.* import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.tools.shell.SSHDConfiguration import org.slf4j.Logger @@ -73,7 +68,7 @@ interface NodeConfiguration { val flowMonitorPeriodMillis: Duration get() = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS val flowMonitorSuspensionLoggingThresholdMillis: Duration get() = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS val crlCheckSoftFail: Boolean - val jmxReporterType : JmxReporterType? get() = defaultJmxReporterType + val jmxReporterType: JmxReporterType? get() = defaultJmxReporterType val baseDirectory: Path val certificatesDirectory: Path @@ -119,34 +114,16 @@ fun NodeConfiguration.shouldStartSSHDaemon() = this.sshd != null fun NodeConfiguration.shouldStartLocalShell() = !this.noLocalShell && System.console() != null && this.devMode fun NodeConfiguration.shouldInitCrashShell() = shouldStartLocalShell() || shouldStartSSHDaemon() -data class NotaryConfig(val validating: Boolean, - val raft: RaftConfig? = null, - val bftSMaRt: BFTSMaRtConfiguration? = null, - val serviceLegalName: CordaX500Name? = null, - val className: String = "net.corda.node.services.transactions.SimpleNotaryService" -) { - init { - require(raft == null || bftSMaRt == null) { - "raft and bftSMaRt configs cannot be specified together" - } - } - - val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null -} - -data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List) - -/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ -data class BFTSMaRtConfiguration( - val replicaId: Int, - val clusterAddresses: List, - val debug: Boolean = false, - val exposeRaces: Boolean = false -) { - init { - require(replicaId >= 0) { "replicaId cannot be negative" } - } -} +data class NotaryConfig( + /** Specifies whether the notary validates transactions or not. */ + val validating: Boolean, + /** The legal name of cluster in case of a distributed notary service. */ + val serviceLegalName: CordaX500Name? = null, + /** The name of the notary service class to load. */ + val className: String = "net.corda.node.services.transactions.SimpleNotaryService", + /** Notary implementation-specific configuration parameters. */ + val extraConfig: Config? = null +) /** * Used as an alternative to the older compatibilityZoneURL to allow the doorman and network map @@ -167,7 +144,7 @@ data class NetworkServicesConfig( val doormanURL: URL, val networkMapURL: URL, val pnm: UUID? = null, - val inferred : Boolean = false + val inferred: Boolean = false ) /** @@ -360,7 +337,7 @@ data class NodeConfigurationImpl( override val effectiveH2Settings: NodeH2Settings? get() = when { - h2port != null -> NodeH2Settings(address = NetworkHostAndPort(host="localhost", port=h2port)) + h2port != null -> NodeH2Settings(address = NetworkHostAndPort(host = "localhost", port = h2port)) else -> h2Settings } @@ -372,7 +349,7 @@ data class NodeConfigurationImpl( "Cannot specify both 'rpcUsers' and 'security' in configuration" } @Suppress("DEPRECATION") - if(certificateChainCheckPolicies.isNotEmpty()) { + if (certificateChainCheckPolicies.isNotEmpty()) { logger.warn("""You are configuring certificateChainCheckPolicies. This is a setting that is not used, and will be removed in a future version. |Please contact the R3 team on the public slack to discuss your use case. """.trimMargin()) diff --git a/node/src/main/resources/net.corda.node.internal.NodeStartup.yml b/node/src/main/resources/net.corda.node.internal.NodeStartup.yml index a67e7cee5a..28f0651a78 100644 --- a/node/src/main/resources/net.corda.node.internal.NodeStartup.yml +++ b/node/src/main/resources/net.corda.node.internal.NodeStartup.yml @@ -6,11 +6,6 @@ required: false multiParam: true acceptableValues: [] - - parameterName: "--bootstrap-raft-cluster" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "--clear-network-map-cache" parameterType: "boolean" required: false diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt index 947c6f0e73..ec1da7a226 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt @@ -35,7 +35,6 @@ class NodeStartupTest { assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false) assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false) assertThat(startup.cmdLineOptions.justGenerateRpcSslCerts).isEqualTo(false) - assertThat(startup.cmdLineOptions.bootstrapRaftCluster).isEqualTo(false) assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL) assertThat(startup.cmdLineOptions.devMode).isEqualTo(null) assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false) diff --git a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt index 3dd2d51152..86c6283f62 100644 --- a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt @@ -15,9 +15,7 @@ import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.internal.notary.UniquenessProvider -import net.corda.core.node.AppServiceHub import net.corda.core.node.NotaryInfo -import net.corda.core.node.services.CordaService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker @@ -82,13 +80,14 @@ class TimedFlowTests { private fun startClusterAndNode(mockNet: InternalMockNetwork): Pair { val replicaIds = (0 until CLUSTER_SIZE) + val serviceLegalName = CordaX500Name("Custom Notary", "Zurich", "CH") val notaryIdentity = DevIdentityGenerator.generateDistributedNotaryCompositeIdentity( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, - CordaX500Name("Custom Notary", "Zurich", "CH")) + serviceLegalName) val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryIdentity, true)))) val notaryConfig = mock { - whenever(it.isClusterConfig).thenReturn(true) + whenever(it.serviceLegalName).thenReturn(serviceLegalName) whenever(it.validating).thenReturn(true) whenever(it.className).thenReturn(TestNotaryService::class.java.name) } diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index ad7f9110e1..d5b6a07a89 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -112,7 +112,7 @@ task deployNodesRaft(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { notary = [ validating: true, serviceLegalName: "O=Raft,L=Zurich,C=CH", - raft: [ + extraConfig: [ nodeAddress: "localhost:10008" ], className: className @@ -128,7 +128,7 @@ task deployNodesRaft(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { notary = [ validating: true, serviceLegalName: "O=Raft,L=Zurich,C=CH", - raft: [ + extraConfig: [ nodeAddress: "localhost:10012", clusterAddresses: ["localhost:10008"] ], @@ -145,7 +145,7 @@ task deployNodesRaft(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { notary = [ validating: true, serviceLegalName: "O=Raft,L=Zurich,C=CH", - raft: [ + extraConfig: [ nodeAddress: "localhost:10016", clusterAddresses: ["localhost:10008"] ], @@ -181,7 +181,7 @@ task deployNodesBFT(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { notary = [ validating: false, serviceLegalName: "O=BFT,L=Zurich,C=CH", - bftSMaRt: [ + extraConfig: [ replicaId: 0, clusterAddresses: clusterAddresses ], @@ -198,7 +198,7 @@ task deployNodesBFT(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { notary = [ validating: false, serviceLegalName: "O=BFT,L=Zurich,C=CH", - bftSMaRt: [ + extraConfig: [ replicaId: 1, clusterAddresses: clusterAddresses ], @@ -215,7 +215,7 @@ task deployNodesBFT(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { notary = [ validating: false, serviceLegalName: "O=BFT,L=Zurich,C=CH", - bftSMaRt: [ + extraConfig: [ replicaId: 2, clusterAddresses: clusterAddresses ], @@ -232,7 +232,7 @@ task deployNodesBFT(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { notary = [ validating: false, serviceLegalName: "O=BFT,L=Zurich,C=CH", - bftSMaRt: [ + extraConfig: [ replicaId: 3, clusterAddresses: clusterAddresses ], diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index cd46caf2fb..c722058ba3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -163,7 +163,7 @@ class DriverDSLImpl( private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { val rpcAddress = config.corda.rpcOptions.address - val clientRpcSslOptions = clientSslOptionsCompatibleWith(config.corda.rpcOptions) + val clientRpcSslOptions = clientSslOptionsCompatibleWith(config.corda.rpcOptions) val client = createCordaRPCClientWithSslAndClassLoader(rpcAddress, sslConfiguration = clientRpcSslOptions) val connectionFuture = poll(executorService, "RPC connection") { try { @@ -481,29 +481,30 @@ class DriverDSLImpl( } } - // TODO This mapping is done is several places including the gradle plugin. In general we need a better way of - // generating the configs for the nodes, probably making use of Any.toConfig() - private fun NotaryConfig.toConfigMap(): Map = mapOf("notary" to toConfig().root().unwrapped()) - private fun startSingleNotary(spec: NotarySpec, localNetworkMap: LocalNetworkMap?, customOverrides: Map): CordaFuture> { + val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating)) return startRegisteredNode( spec.name, localNetworkMap, spec.rpcUsers, spec.verifierType, - customOverrides = NotaryConfig(spec.validating).toConfigMap() + customOverrides + customOverrides = notaryConfig + customOverrides ).map { listOf(it) } } private fun startRaftNotaryCluster(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture> { fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() - val config = NotaryConfig( - validating = spec.validating, - serviceLegalName = spec.name, - className = "net.corda.notary.raft.RaftNotaryService", - raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) - return config.toConfigMap() + val config = configOf("notary" to mapOf( + "validating" to spec.validating, + "serviceLegalName" to spec.name.toString(), + "className" to "net.corda.notary.raft.RaftNotaryService", + "extraConfig" to mapOf( + "nodeAddress" to nodeAddress.toString(), + "clusterAddresses" to clusterAddresses.map { it.toString() } + )) + ) + return config.root().unwrapped() } val nodeNames = generateNodeNames(spec) From ba7727a4e1c750901df12c1e4a22fd60221de056 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 22 Oct 2018 12:24:05 +0100 Subject: [PATCH 71/83] ENT-2610: Resolve conflicted changes (#4102) --- node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a8ba165e1f..b16ed8698c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -874,7 +874,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val keyStore = configuration.signingCertificateStore.get() val privateKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key" - val keyPair = keyStore.query { getCertificateAndKeyPair(privateKeyAlias) }.keyPair + val keyPair = keyStore.query { getCertificateAndKeyPair(privateKeyAlias, keyStore.entryPassword) }.keyPair val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key" val certificates = if (compositeKeyAlias in keyStore) { From 391c6bf66fc31f2609f787298ef1ce77ffe7fb73 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Mon, 22 Oct 2018 15:00:08 +0100 Subject: [PATCH 72/83] Feature/corda 1947/add package ownership (#4097) * Upgrade hibernate and fix tests CORDA-1947 Address code review changes CORDA-1947 Address code review changes (cherry picked from commit ab98c03d1ab15479c106b89f8b85bec185a7f9fa) * ENT-2506 Changes signers field type ENT-2506 Clean up some docs ENT-2506 Fix tests and api ENT-2506 Fix compilation error ENT-2506 Fix compilation error (cherry picked from commit 32f279a24372e31b07cfddac53edf805175fc971) * CORDA-1947 added packageOwnership parameter CORDA-1947 add signers field to DbAttachment. Add check when importing attachments CORDA-1947 add signers field to DbAttachment. Add check when importing attachments CORDA-1947 add tests CORDA-1947 fix comment CORDA-1947 Fix test CORDA-1947 fix serialiser CORDA-1947 fix tests CORDA-1947 fix tests CORDA-1947 fix serialiser CORDA-1947 Address code review changes CORDA-1947 Address code review changes CORDA-1947 Revert test fixes CORDA-1947 address code review comments CORDA-1947 move verification logic to LedgerTransaction.verify CORDA-1947 fix test CORDA-1947 fix tests CORDA-1947 fix tests CORDA-1947 address code review comments CORDA-1947 address code review comments (cherry picked from commit 86bc0d9606922d48a30d395af2a21d6ce7dfc03b) CORDA-1947 fix merge --- .ci/api-current.txt | 4 +- build.gradle | 2 +- .../client/jfx/model/NodeMonitorModel.kt | 8 +- .../deterministic/contracts/AttachmentTest.kt | 4 +- .../verifier/MockContractAttachment.kt | 4 +- .../net/corda/core/contracts/Attachment.kt | 5 +- .../core/contracts/AttachmentConstraint.kt | 2 +- .../core/contracts/ContractAttachment.kt | 8 +- .../TransactionVerificationException.kt | 9 ++ .../corda/core/internal/AbstractAttachment.kt | 6 +- .../core/internal/JarSignatureCollector.kt | 28 +++-- .../net/corda/core/node/NetworkParameters.kt | 107 ++++++++++++++-- .../ContractUpgradeTransactions.kt | 2 +- .../core/transactions/LedgerTransaction.kt | 54 ++++++++ .../core/transactions/TransactionBuilder.kt | 4 +- .../net/corda/core/JarSignatureTestUtils.kt | 45 +++++++ .../PackageOwnershipVerificationTests.kt | 91 ++++++++++++++ .../internal/JarSignatureCollectorTest.kt | 116 ++++++------------ .../transactions/TransactionBuilderTest.kt | 26 ++-- .../core/transactions/TransactionTests.kt | 4 +- docs/source/network-map.rst | 9 +- .../persistence/HibernateStatistics.kt | 27 ++-- .../net/corda/node/internal/AbstractNode.kt | 6 +- .../kryo/DefaultKryoCustomizer.kt | 10 +- .../persistence/NodeAttachmentService.kt | 34 ++++- .../persistence/PublicKeyToTextConverter.kt | 18 +++ .../node/services/vault/NodeVaultService.kt | 3 +- .../migration/node-core.changelog-v8.xml | 13 ++ .../node/internal/NetworkParametersTest.kt | 62 +++++++++- .../persistence/NodeAttachmentServiceTest.kt | 107 ++++++++++++++-- .../custom/ContractAttachmentSerializer.kt | 7 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 7 +- .../testing/dsl/TransactionDSLInterpreter.kt | 19 +++ .../testing/internal/MockCordappProvider.kt | 22 ++-- .../testing/services/MockAttachmentStorage.kt | 15 ++- 35 files changed, 707 insertions(+), 181 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt create mode 100644 core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 6aecda8fa6..7c34ea623b 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -429,7 +429,7 @@ public static final class net.corda.core.contracts.AmountTransfer$Companion exte public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash public void extractFile(String, java.io.OutputStream) @NotNull - public abstract java.util.List getSigners() + public abstract java.util.List getSigners() public abstract int getSize() @NotNull public abstract java.io.InputStream open() @@ -537,7 +537,7 @@ public final class net.corda.core.contracts.ContractAttachment extends java.lang @NotNull public net.corda.core.crypto.SecureHash getId() @NotNull - public java.util.List getSigners() + public java.util.List getSigners() public int getSize() @Nullable public final String getUploader() diff --git a/build.gradle b/build.gradle index 500dc25229..b5c9156435 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { ext.hamkrest_version = '1.4.2.2' ext.jopt_simple_version = '5.0.2' ext.jansi_version = '1.14' - ext.hibernate_version = '5.2.6.Final' + ext.hibernate_version = '5.3.6.Final' ext.h2_version = '1.4.197' // Update docs if renamed or removed. ext.postgresql_version = '42.1.4' ext.rxjava_version = '1.3.8' diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index eb8e78898f..550344ae35 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -191,10 +191,10 @@ class NodeMonitorModel : AutoCloseable { val retryInterval = 5.seconds val client = CordaRPCClient( - nodeHostAndPort, - CordaRPCClientConfiguration.DEFAULT.copy( - connectionMaxRetryInterval = retryInterval - ) + nodeHostAndPort, + CordaRPCClientConfiguration.DEFAULT.copy( + connectionMaxRetryInterval = retryInterval + ) ) do { val connection = try { diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt index 21ad6a3e45..01f7751905 100644 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt @@ -42,8 +42,8 @@ class AttachmentTest { attachment = object : Attachment { override val id: SecureHash get() = SecureHash.allOnesHash - override val signers: List - get() = listOf(ALICE) + override val signers: List + get() = listOf(ALICE_KEY) override val size: Int get() = jarData.size diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt index 0e28c9647e..f7e90ce2cc 100644 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt @@ -3,13 +3,13 @@ package net.corda.deterministic.verifier import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import java.io.ByteArrayInputStream import java.io.InputStream +import java.security.PublicKey @CordaSerializable -class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List = ArrayList()) : Attachment { +class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List = emptyList()) : Attachment { override fun open(): InputStream = ByteArrayInputStream(id.bytes) override val size = id.size } diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index 8cf0ed5839..d17d053e1f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -7,6 +7,7 @@ import net.corda.core.serialization.CordaSerializable import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream +import java.security.PublicKey import java.util.jar.JarInputStream /** @@ -51,10 +52,10 @@ interface Attachment : NamedByHash { fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) } /** - * The parties that have correctly signed the whole attachment. + * The keys that have correctly signed the whole attachment. * Can be empty, for example non-contract attachments won't be necessarily be signed. */ - val signers: List + val signers: List /** * Attachment size in bytes. diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index b4fbe24ef4..76ffc8a866 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -82,5 +82,5 @@ data class SignatureAttachmentConstraint( val key: PublicKey ) : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean = - key.isFulfilledBy(attachment.signers.map { it.owningKey }) + key.isFulfilledBy(attachment.signers.map { it }) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt index 7a19128c32..4b1c25fed8 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -2,6 +2,7 @@ package net.corda.core.contracts import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable +import java.security.PublicKey /** * Wrap an attachment in this if it is to be used as an executable contract attachment @@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable */ @KeepForDJVM @CordaSerializable -class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set = emptySet(), val uploader: String? = null) : Attachment by attachment { +class ContractAttachment @JvmOverloads constructor( + val attachment: Attachment, + val contract: ContractClassName, + val additionalContracts: Set = emptySet(), + val uploader: String? = null, + override val signers: List = emptyList()) : Attachment by attachment { val allContracts: Set get() = additionalContracts + contract diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 980589e9cb..a6392a01a9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.identity.Party +import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.NonEmptySet import java.security.PublicKey @@ -169,4 +170,12 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str @DeleteForDJVM class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null) + + /** + * Thrown to indicate that a contract attachment is not signed by the network-wide package owner. + */ + class ContractAttachmentNotSignedByPackageOwnerException(txId: SecureHash, val attachmentHash: AttachmentId, val contractClass: String) : TransactionVerificationException(txId, + """The Contract attachment JAR: $attachmentHash containing the contract: $contractClass is not signed by the owner specified in the network parameters. + Please check the source of this attachment and if it is malicious contact your zone operator to report this incident. + For details see: https://docs.corda.net/network-map.html#network-parameters""".trimIndent(), null) } diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index 3c72e5d0ef..c509abdd4f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -1,4 +1,5 @@ @file:KeepForDJVM + package net.corda.core.internal import net.corda.core.DeleteForDJVM @@ -11,6 +12,7 @@ import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.security.PublicKey import java.util.jar.JarInputStream const val DEPLOYED_CORDAPP_UPLOADER = "app" @@ -40,8 +42,8 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { override val size: Int get() = attachmentData.size override fun open(): InputStream = attachmentData.inputStream() - override val signers by lazy { - openAsJAR().use(JarSignatureCollector::collectSigningParties) + override val signers: List by lazy { + openAsJAR().use(JarSignatureCollector::collectSigners) } override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 78d5a15517..963623e474 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import net.corda.core.identity.Party import java.security.CodeSigner +import java.security.PublicKey import java.security.cert.X509Certificate import java.util.jar.JarEntry import java.util.jar.JarInputStream @@ -23,22 +24,25 @@ object JarSignatureCollector { * @param jar The open [JarInputStream] to collect signing parties from. * @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other. */ - fun collectSigningParties(jar: JarInputStream): List { + fun collectSigners(jar: JarInputStream): List = getSigners(jar).toOrderedPublicKeys() + + fun collectSigningParties(jar: JarInputStream): List = getSigners(jar).toPartiesOrderedByName() + + private fun getSigners(jar: JarInputStream): Set { val signerSets = jar.fileSignerSets - if (signerSets.isEmpty()) return emptyList() + if (signerSets.isEmpty()) return emptySet() val (firstFile, firstSignerSet) = signerSets.first() for ((otherFile, otherSignerSet) in signerSets.subList(1, signerSets.size)) { if (otherSignerSet != firstSignerSet) throw InvalidJarSignersException( - """ - Mismatch between signers ${firstSignerSet.toPartiesOrderedByName()} for file $firstFile - and signers ${otherSignerSet.toPartiesOrderedByName()} for file ${otherFile}. - See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the - constraints applied to attachment signatures. - """.trimIndent().replace('\n', ' ')) + """ + Mismatch between signers ${firstSignerSet.toOrderedPublicKeys()} for file $firstFile + and signers ${otherSignerSet.toOrderedPublicKeys()} for file ${otherFile}. + See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the + constraints applied to attachment signatures. + """.trimIndent().replace('\n', ' ')) } - - return firstSignerSet.toPartiesOrderedByName() + return firstSignerSet } private val JarInputStream.fileSignerSets: List>> get() = @@ -63,6 +67,10 @@ object JarSignatureCollector { Party(it.signerCertPath.certificates[0] as X509Certificate) }.sortedBy { it.name.toString() } // Sorted for determinism. + private fun Set.toOrderedPublicKeys(): List = map { + (it.signerCertPath.certificates[0] as X509Certificate).publicKey + }.sortedBy { it.hash} // Sorted for determinism. + private val JarInputStream.entries get(): Sequence = generateSequence(nextJarEntry) { nextJarEntry } } diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 7127177c71..f707a8e308 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -7,6 +7,7 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.utilities.days +import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -22,6 +23,7 @@ import java.time.Instant * of parameters. * @property whitelistedContractImplementations List of whitelisted jars containing contract code for each contract class. * This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html). + * @property packageOwnership List of the network-wide java packages that were successfully claimed by their owners. Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner. * @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen * during this period */ @@ -35,7 +37,8 @@ data class NetworkParameters( val modifiedTime: Instant, val epoch: Int, val whitelistedContractImplementations: Map>, - val eventHorizon: Duration + val eventHorizon: Duration, + val packageOwnership: Map ) { @DeprecatedConstructorForDeserialization(1) constructor (minimumPlatformVersion: Int, @@ -52,7 +55,28 @@ data class NetworkParameters( modifiedTime, epoch, whitelistedContractImplementations, - Int.MAX_VALUE.days + Int.MAX_VALUE.days, + emptyMap() + ) + + @DeprecatedConstructorForDeserialization(2) + constructor (minimumPlatformVersion: Int, + notaries: List, + maxMessageSize: Int, + maxTransactionSize: Int, + modifiedTime: Instant, + epoch: Int, + whitelistedContractImplementations: Map>, + eventHorizon: Duration + ) : this(minimumPlatformVersion, + notaries, + maxMessageSize, + maxTransactionSize, + modifiedTime, + epoch, + whitelistedContractImplementations, + eventHorizon, + emptyMap() ) init { @@ -63,6 +87,7 @@ data class NetworkParameters( require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" } require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" } require(!eventHorizon.isNegative) { "eventHorizon must be positive value" } + require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." } } fun copy(minimumPlatformVersion: Int, @@ -83,20 +108,47 @@ data class NetworkParameters( eventHorizon = eventHorizon) } + fun copy(minimumPlatformVersion: Int, + notaries: List, + maxMessageSize: Int, + maxTransactionSize: Int, + modifiedTime: Instant, + epoch: Int, + whitelistedContractImplementations: Map>, + eventHorizon: Duration + ): NetworkParameters { + return copy(minimumPlatformVersion = minimumPlatformVersion, + notaries = notaries, + maxMessageSize = maxMessageSize, + maxTransactionSize = maxTransactionSize, + modifiedTime = modifiedTime, + epoch = epoch, + whitelistedContractImplementations = whitelistedContractImplementations, + eventHorizon = eventHorizon) + } + override fun toString(): String { return """NetworkParameters { - minimumPlatformVersion=$minimumPlatformVersion - notaries=$notaries - maxMessageSize=$maxMessageSize - maxTransactionSize=$maxTransactionSize - whitelistedContractImplementations { - ${whitelistedContractImplementations.entries.joinToString("\n ")} - } - eventHorizon=$eventHorizon - modifiedTime=$modifiedTime - epoch=$epoch -}""" + minimumPlatformVersion=$minimumPlatformVersion + notaries=$notaries + maxMessageSize=$maxMessageSize + maxTransactionSize=$maxTransactionSize + whitelistedContractImplementations { + ${whitelistedContractImplementations.entries.joinToString("\n ")} + } + eventHorizon=$eventHorizon + modifiedTime=$modifiedTime + epoch=$epoch, + packageOwnership= { + ${packageOwnership.keys.joinToString()}} + } + }""" } + + /** + * Returns the public key of the package owner of the [contractClassName], or null if not owned. + */ + fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull() } /** @@ -113,3 +165,32 @@ data class NotaryInfo(val identity: Party, val validating: Boolean) * version. */ class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message) + +/** + * A wrapper for a legal java package. Used by the network parameters to store package ownership. + */ +@CordaSerializable +data class JavaPackageName(val name: String) { + init { + require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" } + } + + /** + * Returns true if the [fullClassName] is in a subpackage of the current package. + * E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken" + * + * Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp. + * By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails. + */ + fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true) +} + +// Check if a string is a legal Java package name. +private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token -> + Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) } +} + +// Make sure that packages don't overlap so that ownership is clear. +private fun noOverlap(packages: Collection) = packages.all { currentPackage -> + packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") } +} diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 55be2f424f..ddadaefc0f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -197,7 +197,7 @@ data class ContractUpgradeLedgerTransaction( private fun verifyConstraints() { val attachmentForConstraintVerification = AttachmentWithContext( legacyContractAttachment as? ContractAttachment - ?: ContractAttachment(legacyContractAttachment, legacyContractClassName), + ?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signers = legacyContractAttachment.signers), upgradedContract.legacyContract, networkParameters.whitelistedContractImplementations ) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 5c97af83a4..ebce06de33 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -3,6 +3,7 @@ package net.corda.core.transactions import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.isFulfilledBy import net.corda.core.identity.Party import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.castIfPossible @@ -74,6 +75,9 @@ data class LedgerTransaction @JvmOverloads constructor( val inputStates: List get() = inputs.map { it.state.data } val referenceStates: List get() = references.map { it.state.data } + private val inputAndOutputStates = inputs.map { it.state } + outputs + private val allStates = inputAndOutputStates + references.map { it.state } + /** * Returns the typed input StateAndRef at the specified index * @param index The index into the inputs. @@ -88,10 +92,37 @@ data class LedgerTransaction @JvmOverloads constructor( */ @Throws(TransactionVerificationException::class) fun verify() { + val contractAttachmentsByContract: Map = getUniqueContractAttachmentsByContract() + + validatePackageOwnership(contractAttachmentsByContract) verifyConstraints() verifyContracts() } + /** + * Verify that package ownership is respected. + * + * TODO - revisit once transaction contains network parameters. + */ + private fun validatePackageOwnership(contractAttachmentsByContract: Map) { + // This should never happen once we have network parameters in the transaction. + if (networkParameters == null) { + return + } + + val contractsAndOwners = allStates.mapNotNull { transactionState -> + val contractClassName = transactionState.contract + networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it } + }.toMap() + + contractsAndOwners.forEach { contract, owner -> + val attachment = contractAttachmentsByContract[contract]!! + if (!owner.isFulfilledBy(attachment.signers)) { + throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract) + } + } + } + /** * Verify that all contract constraints are valid for each state before running any contract code * @@ -123,6 +154,29 @@ data class LedgerTransaction @JvmOverloads constructor( } } + private fun getUniqueContractAttachmentsByContract(): Map { + val result = mutableMapOf() + + for (attachment in attachments) { + if (attachment !is ContractAttachment) continue + + for (contract in attachment.allContracts) { + result.compute(contract) { _, previousAttachment -> + when { + previousAttachment == null -> attachment + attachment.id == previousAttachment.id -> previousAttachment + // In case multiple attachments have been added for the same contract, fail because this + // transaction will not be able to be verified because it will break the no-overlap rule + // that we have implemented in our Classloaders + else -> throw TransactionVerificationException.ConflictingAttachmentsRejection(id, contract) + } + } + } + } + + return result + } + /** * Check the transaction is contract-valid by running the verify() for each input and output state contract. * If any contract fails to verify, the whole transaction is considered to be invalid. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 9b54509064..99c069de88 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -158,8 +158,8 @@ open class TransactionBuilder @JvmOverloads constructor( } } - private fun makeSignatureAttachmentConstraint(attachmentSigners: List) = - SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it.owningKey }).build()) + private fun makeSignatureAttachmentConstraint(attachmentSigners: List) = + SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build()) private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys diff --git a/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt b/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt new file mode 100644 index 0000000000..d5d9bed118 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt @@ -0,0 +1,45 @@ +package net.corda.core + +import net.corda.core.internal.JarSignatureCollector +import net.corda.core.internal.div +import net.corda.nodeapi.internal.crypto.loadKeyStore +import java.io.FileInputStream +import java.nio.file.Path +import java.nio.file.Paths +import java.security.PublicKey +import java.util.jar.JarInputStream +import kotlin.test.assertEquals + +object JarSignatureTestUtils { + val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin" + + fun Path.executeProcess(vararg command: String) { + val shredder = (this / "_shredder").toFile() // No need to delete after each test. + assertEquals(0, ProcessBuilder() + .inheritIO() + .redirectOutput(shredder) + .redirectError(shredder) + .directory(this.toFile()) + .command((bin / command[0]).toString(), *command.sliceArray(1 until command.size)) + .start() + .waitFor()) + } + + fun Path.generateKey(alias: String, password: String, name: String) = + executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name) + + fun Path.createJar(fileName: String, vararg contents: String) = + executeProcess(*(arrayOf("jar", "cvf", fileName) + contents)) + + fun Path.updateJar(fileName: String, vararg contents: String) = + executeProcess(*(arrayOf("jar", "uvf", fileName) + contents)) + + fun Path.signJar(fileName: String, alias: String, password: String): PublicKey { + executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias) + val ks = loadKeyStore(this.resolve("_teststore"), "storepass") + return ks.getCertificate(alias).publicKey + } + + fun Path.getJarSigners(fileName: String) = + JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners) +} diff --git a/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt new file mode 100644 index 0000000000..98fe6683bd --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt @@ -0,0 +1,91 @@ +package net.corda.core.contracts + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.JavaPackageName +import net.corda.core.transactions.LedgerTransaction +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockServices +import net.corda.testing.node.ledger +import org.junit.Rule +import org.junit.Test + +class PackageOwnershipVerificationTests { + + private companion object { + val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB")) + val ALICE_PARTY get() = ALICE.party + val ALICE_PUBKEY get() = ALICE.publicKey + val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB")) + val BOB_PARTY get() = BOB.party + val BOB_PUBKEY get() = BOB.publicKey + val dummyContract = "net.corda.core.contracts.DummyContract" + val OWNER_KEY_PAIR = Crypto.generateKeyPair() + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val ledgerServices = MockServices( + cordappPackages = listOf("net.corda.finance.contracts.asset"), + initialIdentity = ALICE, + identityService = rigorousMock().also { + doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY) + doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY) + }, + networkParameters = testNetworkParameters() + .copy(packageOwnership = mapOf(JavaPackageName("net.corda.core.contracts") to OWNER_KEY_PAIR.public)) + ) + + @Test + fun `Happy path - Transaction validates when package signed by owner`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(dummyContract, SecureHash.allOnesHash, listOf(OWNER_KEY_PAIR.public)) + output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState()) + command(ALICE_PUBKEY, DummyIssue()) + verifies() + } + } + } + + @Test + fun `Transaction validation fails when the selected attachment is not signed by the owner`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(dummyContract, SecureHash.allOnesHash, listOf(ALICE_PUBKEY)) + output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState()) + command(ALICE_PUBKEY, DummyIssue()) + failsWith("is not signed by the owner specified in the network parameters") + } + } + } + +} + +class DummyContractState : ContractState { + override val participants: List + get() = emptyList() +} + +class DummyContract : Contract { + interface Commands : CommandData + class Create : Commands + + override fun verify(tx: LedgerTransaction) { + //do nothing + } +} + +class DummyIssue : TypeOnlyCommandData() \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt index fed904c1fa..7f83f23a47 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt @@ -1,6 +1,10 @@ package net.corda.core.internal -import net.corda.core.identity.CordaX500Name +import net.corda.core.JarSignatureTestUtils.createJar +import net.corda.core.JarSignatureTestUtils.generateKey +import net.corda.core.JarSignatureTestUtils.getJarSigners +import net.corda.core.JarSignatureTestUtils.signJar +import net.corda.core.JarSignatureTestUtils.updateJar import net.corda.core.identity.Party import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -10,29 +14,14 @@ import org.junit.After import org.junit.AfterClass import org.junit.BeforeClass import org.junit.Test -import java.io.FileInputStream import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths -import java.util.jar.JarInputStream import kotlin.test.assertEquals import kotlin.test.assertFailsWith class JarSignatureCollectorTest { companion object { private val dir = Files.createTempDirectory(JarSignatureCollectorTest::class.simpleName) - private val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin" - private val shredder = (dir / "_shredder").toFile() // No need to delete after each test. - - fun execute(vararg command: String) { - assertEquals(0, ProcessBuilder() - .inheritIO() - .redirectOutput(shredder) - .directory(dir.toFile()) - .command((bin / command[0]).toString(), *command.sliceArray(1 until command.size)) - .start() - .waitFor()) - } private const val FILENAME = "attachment.jar" private const val ALICE = "alice" @@ -42,15 +31,11 @@ class JarSignatureCollectorTest { private const val CHARLIE = "Charlie" private const val CHARLIE_PASS = "charliepass" - private fun generateKey(alias: String, password: String, name: CordaX500Name, keyalg: String = "RSA") = - execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name.toString()) - @BeforeClass @JvmStatic fun beforeClass() { - generateKey(ALICE, ALICE_PASS, ALICE_NAME) - generateKey(BOB, BOB_PASS, BOB_NAME) - generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME, "EC") + dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString()) + dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString()) (dir / "_signable1").writeLines(listOf("signable1")) (dir / "_signable2").writeLines(listOf("signable2")) @@ -64,7 +49,7 @@ class JarSignatureCollectorTest { } } - private val List.names get() = map { it.name } + private val List.keys get() = map { it.owningKey } @After fun tearDown() { @@ -77,101 +62,74 @@ class JarSignatureCollectorTest { @Test fun `empty jar has no signers`() { (dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this. - createJar("META-INF") - assertEquals(emptyList(), getJarSigners()) + dir.createJar(FILENAME, "META-INF") + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) signAsAlice() - assertEquals(emptyList(), getJarSigners()) // There needs to have been a file for ALICE to sign. + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) // There needs to have been a file for ALICE to sign. } @Test fun `unsigned jar has no signers`() { - createJar("_signable1") - assertEquals(emptyList(), getJarSigners()) + dir.createJar(FILENAME, "_signable1") + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) - updateJar("_signable2") - assertEquals(emptyList(), getJarSigners()) + dir.updateJar(FILENAME, "_signable2") + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) } @Test fun `one signer`() { - createJar("_signable1", "_signable2") - signAsAlice() - assertEquals(listOf(ALICE_NAME), getJarSigners().names) // We only reused ALICE's distinguished name, so the keys will be different. + dir.createJar(FILENAME, "_signable1", "_signable2") + val key = signAsAlice() + assertEquals(listOf(key), dir.getJarSigners(FILENAME)) (dir / "my-dir").createDirectory() - updateJar("my-dir") - assertEquals(listOf(ALICE_NAME), getJarSigners().names) // Unsigned directory is irrelevant. + dir.updateJar(FILENAME, "my-dir") + assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // Unsigned directory is irrelevant. } @Test fun `two signers`() { - createJar("_signable1", "_signable2") - signAsAlice() - signAsBob() + dir.createJar(FILENAME, "_signable1", "_signable2") + val key1 = signAsAlice() + val key2 = signAsBob() - assertEquals(listOf(ALICE_NAME, BOB_NAME), getJarSigners().names) + assertEquals(setOf(key1, key2), dir.getJarSigners(FILENAME).toSet()) } @Test fun `all files must be signed by the same set of signers`() { - createJar("_signable1") - signAsAlice() - assertEquals(listOf(ALICE_NAME), getJarSigners().names) + dir.createJar(FILENAME, "_signable1") + val key1 = signAsAlice() + assertEquals(listOf(key1), dir.getJarSigners(FILENAME)) - updateJar("_signable2") + dir.updateJar(FILENAME, "_signable2") signAsBob() assertFailsWith( - """ + """ Mismatch between signers [O=Alice Corp, L=Madrid, C=ES, O=Bob Plc, L=Rome, C=IT] for file _signable1 and signers [O=Bob Plc, L=Rome, C=IT] for file _signable2. See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the constraints applied to attachment signatures. """.trimIndent().replace('\n', ' ') - ) { getJarSigners() } + ) { dir.getJarSigners(FILENAME) } } @Test fun `bad signature is caught even if the party would not qualify as a signer`() { (dir / "volatile").writeLines(listOf("volatile")) - createJar("volatile") - signAsAlice() - assertEquals(listOf(ALICE_NAME), getJarSigners().names) + dir.createJar(FILENAME, "volatile") + val key1 = signAsAlice() + assertEquals(listOf(key1), dir.getJarSigners(FILENAME)) (dir / "volatile").writeLines(listOf("garbage")) - updateJar("volatile", "_signable1") // ALICE's signature on volatile is now bad. + dir.updateJar(FILENAME, "volatile", "_signable1") // ALICE's signature on volatile is now bad. signAsBob() // The JDK doesn't care that BOB has correctly signed the whole thing, it won't let us process the entry with ALICE's bad signature: - assertFailsWith { getJarSigners() } + assertFailsWith { dir.getJarSigners(FILENAME) } } - // Signing using EC algorithm produced JAR File spec incompatible signature block (META-INF/*.EC) which is anyway accepted by jarsiner, see [JarSignatureCollector] - @Test - fun `one signer with EC sign algorithm`() { - createJar("_signable1", "_signable2") - signJar(CHARLIE, CHARLIE_PASS) - assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // We only reused CHARLIE's distinguished name, so the keys will be different. - - (dir / "my-dir").createDirectory() - updateJar("my-dir") - assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // Unsigned directory is irrelevant. - } - - //region Helper functions - private fun createJar(vararg contents: String) = - execute(*(arrayOf("jar", "cvf", FILENAME) + contents)) - - private fun updateJar(vararg contents: String) = - execute(*(arrayOf("jar", "uvf", FILENAME) + contents)) - - private fun signJar(alias: String, password: String) = - execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias) - - private fun signAsAlice() = signJar(ALICE, ALICE_PASS) - private fun signAsBob() = signJar(BOB, BOB_PASS) - - private fun getJarSigners() = - JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigningParties) - //endregion - + private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS) + private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS) } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt index f779c85678..cd9456cdc3 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -23,6 +23,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test +import java.security.PublicKey class TransactionBuilderTest { @Rule @@ -40,7 +41,15 @@ class TransactionBuilderTest { doReturn(cordappProvider).whenever(services).cordappProvider doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) doReturn(testNetworkParameters()).whenever(services).networkParameters - doReturn(attachments).whenever(services).attachments + + val attachmentStorage = rigorousMock() + doReturn(attachmentStorage).whenever(services).attachments + val attachment = rigorousMock() + doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId) + doReturn(contractAttachmentId).whenever(attachment).id + doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts + doReturn("app").whenever(attachment).uploader + doReturn(emptyList()).whenever(attachment).signers } @Test @@ -103,6 +112,7 @@ class TransactionBuilderTest { assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment)) assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment)) + doReturn(attachments).whenever(services).attachments doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId) val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) @@ -112,19 +122,17 @@ class TransactionBuilderTest { val wtx = builder.toWireTransaction(services) assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) - } - - private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) { + private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) { override val id: SecureHash get() = throw UnsupportedOperationException() - override val signers: List get() = emptyList() - } + override val signers: List get() = emptyList() + }, DummyContract.PROGRAM_ID) - private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) { + private fun signedAttachment(vararg parties: Party) = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) { override val id: SecureHash get() = throw UnsupportedOperationException() - override val signers: List get() = parties.toList() - } + override val signers: List get() = parties.map { it.owningKey } + }, DummyContract.PROGRAM_ID, signers = parties.map { it.owningKey }) } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index 3f6f40bf51..32f9105a1c 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* import net.corda.testing.internal.createWireTransaction @@ -129,7 +130,8 @@ class TransactionTests { id, null, timeWindow, - privacySalt + privacySalt, + testNetworkParameters() ) transaction.verify() diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 3b5e4e5893..3af6b5d6ce 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -125,7 +125,14 @@ The current set of network parameters: :eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their ``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node. -More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6 +:packageOwnership: List of the network-wide java packages that were successfully claimed by their owners. + Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner. + This ensures that when a node encounters an owned contract it can uniquely identify it and knows that all other nodes can do the same. + Encountering an owned contract in a JAR that is not signed by the rightful owner is most likely a sign of malicious behaviour, and should be reported. + The transaction verification logic will throw an exception when this happens. + Read more about *Package ownership* here :doc:`design/data-model-upgrades/package-namespace-ownership`. + +More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6 connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on. Network parameters update process diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt index 2c08d3e77f..c902f57f27 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt @@ -1,14 +1,8 @@ package net.corda.nodeapi.internal.persistence +import org.hibernate.stat.* import javax.management.MXBean -import org.hibernate.stat.Statistics -import org.hibernate.stat.SecondLevelCacheStatistics -import org.hibernate.stat.QueryStatistics -import org.hibernate.stat.NaturalIdCacheStatistics -import org.hibernate.stat.EntityStatistics -import org.hibernate.stat.CollectionStatistics - /** * Exposes Hibernate [Statistics] contract as JMX resource. */ @@ -20,6 +14,25 @@ interface StatisticsService : Statistics * session factory. */ class DelegatingStatisticsService(private val delegate: Statistics) : StatisticsService { + override fun getNaturalIdStatistics(entityName: String?): NaturalIdStatistics { + return delegate.getNaturalIdStatistics(entityName) + } + + override fun getDomainDataRegionStatistics(regionName: String?): CacheRegionStatistics { + return delegate.getDomainDataRegionStatistics(regionName) + } + + override fun getQueryRegionStatistics(regionName: String?): CacheRegionStatistics { + return delegate.getQueryRegionStatistics(regionName) + } + + override fun getNaturalIdQueryExecutionMaxTimeEntity(): String { + return delegate.getNaturalIdQueryExecutionMaxTimeEntity() + } + + override fun getCacheRegionStatistics(regionName: String?): CacheRegionStatistics { + return delegate.getCacheRegionStatistics(regionName) + } override fun clear() { delegate.clear() diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index b16ed8698c..4bee70d8c6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -170,7 +170,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize() @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() - val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage) + val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also { + attachments.servicesForResolution = it + } @Suppress("LeakingThis") val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize() val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory) @@ -1036,7 +1038,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, // so we end up providing both descriptor and converter. We should re-examine this in later versions to see if // either Hibernate can be convinced to stop warning, use the descriptor by default, or something else. JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) - val attributeConverters = listOf(AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) + val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, cacheFactory, attributeConverters) } diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt index 0fd8fa7432..37bd1acd33 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt @@ -209,15 +209,17 @@ object DefaultKryoCustomizer { output.writeString(obj.contract) kryo.writeClassAndObject(output, obj.additionalContracts) output.writeString(obj.uploader) + kryo.writeClassAndObject(output, obj.signers) } + @Suppress("UNCHECKED_CAST") override fun read(kryo: Kryo, input: Input, type: Class): ContractAttachment { if (kryo.serializationContext() != null) { val attachmentHash = SecureHash.SHA256(input.readBytes(32)) val contract = input.readString() - @Suppress("UNCHECKED_CAST") val additionalContracts = kryo.readClassAndObject(input) as Set val uploader = input.readString() + val signers = kryo.readClassAndObject(input) as List val context = kryo.serializationContext()!! val attachmentStorage = context.serviceHub.attachments @@ -229,14 +231,14 @@ object DefaultKryoCustomizer { override val id = attachmentHash } - return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader) + return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers) } else { val attachment = GeneratedAttachment(input.readBytesWithLength()) val contract = input.readString() - @Suppress("UNCHECKED_CAST") val additionalContracts = kryo.readClassAndObject(input) as Set val uploader = input.readString() - return ContractAttachment(attachment, contract, additionalContracts, uploader) + val signers = kryo.readClassAndObject(input) as List + return ContractAttachment(attachment, contract, additionalContracts, uploader, signers) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 31f937f62b..1ea12ffb15 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -6,13 +6,16 @@ import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream +import net.corda.core.ClientRelevantError import net.corda.core.CordaRuntimeException import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.sha256 import net.corda.core.internal.* +import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentSort @@ -30,6 +33,7 @@ import java.io.FilterInputStream import java.io.IOException import java.io.InputStream import java.nio.file.Paths +import java.security.PublicKey import java.time.Instant import java.util.* import java.util.jar.JarInputStream @@ -45,6 +49,10 @@ class NodeAttachmentService( cacheFactory: NamedCacheFactory, private val database: CordaPersistence ) : AttachmentStorageInternal, SingletonSerializeAsToken() { + + // This is to break the circular dependency. + lateinit var servicesForResolution: ServicesForResolution + companion object { private val log = contextLogger() @@ -94,7 +102,13 @@ class NodeAttachmentService( @Column(name = "contract_class_name", nullable = false) @CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))], foreignKey = ForeignKey(name = "FK__ctr_class__attachments")) - var contractClassNames: List? = null + var contractClassNames: List? = null, + + @ElementCollection(targetClass = PublicKey::class, fetch = FetchType.EAGER) + @Column(name = "signer", nullable = false) + @CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_signers", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))], + foreignKey = ForeignKey(name = "FK__signers__attachments")) + var signers: List? = null ) @VisibleForTesting @@ -212,11 +226,13 @@ class NodeAttachmentService( private fun loadAttachmentContent(id: SecureHash): Pair? { return database.transaction { - val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) ?: return@transaction null + val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + ?: return@transaction null val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let { val contracts = attachment.contractClassNames if (contracts != null && contracts.isNotEmpty()) { - ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader) + ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers + ?: emptyList()) } else { it } @@ -290,14 +306,19 @@ class NodeAttachmentService( val id = bytes.sha256() if (!hasAttachment(id)) { checkIsAValidJAR(bytes.inputStream()) + + val jarSigners = getSigners(bytes) + val session = currentDBSession() val attachment = NodeAttachmentService.DBAttachment( attId = id.toString(), content = bytes, uploader = uploader, filename = filename, - contractClassNames = contractClassNames + contractClassNames = contractClassNames, + signers = jarSigners ) + session.save(attachment) attachmentCount.inc() log.info("Stored new attachment $id") @@ -309,6 +330,9 @@ class NodeAttachmentService( } } + private fun getSigners(attachmentBytes: ByteArray) = + JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream())) + @Suppress("OverridingDeprecatedMember") override fun importOrGetAttachment(jar: InputStream): AttachmentId { return try { @@ -339,4 +363,4 @@ class NodeAttachmentService( query.resultList.map { AttachmentId.parse(it.attId) } } } -} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt new file mode 100644 index 0000000000..4ee5a15e57 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt @@ -0,0 +1,18 @@ +package net.corda.node.services.persistence + +import net.corda.core.crypto.Crypto +import net.corda.core.utilities.hexToByteArray +import net.corda.core.utilities.toHex +import java.security.PublicKey +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +/** + * Converts to and from a Public key into a hex encoded string. + * Used by JPA to automatically map a [PublicKey] to a text column + */ +@Converter(autoApply = true) +class PublicKeyToTextConverter : AttributeConverter { + override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex() + override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index c893472a6f..915ff0bcca 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -505,7 +505,8 @@ class NodeVaultService( // Even if we set the default pageNumber to be 1 instead, that may not cover the non-default cases. // So the floor may be necessary anyway. query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize) - query.maxResults = paging.pageSize + 1 // detection too many results + val pageSize = paging.pageSize + 1 + query.maxResults = if (pageSize > 0) pageSize else Integer.MAX_VALUE // detection too many results, protected against overflow // execution val results = query.resultList diff --git a/node/src/main/resources/migration/node-core.changelog-v8.xml b/node/src/main/resources/migration/node-core.changelog-v8.xml index 380faf518d..11c55db5ee 100644 --- a/node/src/main/resources/migration/node-core.changelog-v8.xml +++ b/node/src/main/resources/migration/node-core.changelog-v8.xml @@ -14,4 +14,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index b5b14d93ad..0716d4e394 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -2,6 +2,8 @@ package net.corda.node.internal import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.generateKeyPair +import net.corda.core.node.JavaPackageName import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS @@ -10,6 +12,7 @@ import net.corda.node.services.config.NotaryConfig import net.corda.core.node.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.core.node.NotaryInfo +import net.corda.core.utilities.days import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME @@ -65,7 +68,8 @@ class NetworkParametersTest { fun `choosing notary not specified in network parameters will fail`() { val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = { val notary = NotaryConfig(false) - doReturn(notary).whenever(it).notary})) + doReturn(notary).whenever(it).notary + })) val fakeNotaryId = fakeNotary.info.singleIdentity() val alice = mockNet.createPartyNode(ALICE_NAME) assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId) @@ -87,6 +91,62 @@ class NetworkParametersTest { }.withMessage("maxTransactionSize cannot be bigger than maxMessageSize") } + @Test + fun `package ownership checks are correct`() { + val key1 = generateKeyPair().public + val key2 = generateKeyPair().public + + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + NetworkParameters(1, + emptyList(), + 2001, + 2000, + Instant.now(), + 1, + emptyMap(), + Int.MAX_VALUE.days, + mapOf( + JavaPackageName("com.!example.stuff") to key2 + ) + ) + }.withMessageContaining("Attempting to whitelist illegal java package") + + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + NetworkParameters(1, + emptyList(), + 2001, + 2000, + Instant.now(), + 1, + emptyMap(), + Int.MAX_VALUE.days, + mapOf( + JavaPackageName("com.example") to key1, + JavaPackageName("com.example.stuff") to key2 + ) + ) + }.withMessage("multiple packages added to the packageOwnership overlap.") + + NetworkParameters(1, + emptyList(), + 2001, + 2000, + Instant.now(), + 1, + emptyMap(), + Int.MAX_VALUE.days, + mapOf( + JavaPackageName("com.example") to key1, + JavaPackageName("com.examplestuff") to key2 + ) + ) + + assert(JavaPackageName("com.example").owns("com.example.something.MyClass")) + assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass")) + assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass")) + + } + // Helpers private fun dropParametersToDir(dir: Path, params: NetworkParameters) { NetworkParametersCopier(params).install(dir) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 255f0c323f..e87d60de3c 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.MetricRegistry import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.JarSignatureTestUtils.createJar +import net.corda.core.JarSignatureTestUtils.generateKey +import net.corda.core.JarSignatureTestUtils.signJar import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.flows.FlowLogic import net.corda.core.internal.* +import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.Builder @@ -17,33 +23,40 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.ALICE_NAME import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.configureDatabase import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow import org.assertj.core.api.Assertions.assertThatIllegalArgumentException -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test +import org.junit.* import java.io.ByteArrayOutputStream import java.io.OutputStream +import java.net.URI import java.nio.charset.StandardCharsets -import java.nio.file.FileAlreadyExistsException -import java.nio.file.FileSystem -import java.nio.file.Path +import java.nio.file.* +import java.security.PublicKey import java.util.jar.JarEntry import java.util.jar.JarOutputStream +import javax.tools.JavaFileObject +import javax.tools.SimpleJavaFileObject +import javax.tools.StandardLocation +import javax.tools.ToolProvider import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull + class NodeAttachmentServiceTest { + // Use an in memory file system for testing attachment storage. private lateinit var fs: FileSystem private lateinit var database: CordaPersistence private lateinit var storage: NodeAttachmentService + private val services = rigorousMock() @Before fun setUp() { @@ -52,18 +65,40 @@ class NodeAttachmentServiceTest { val dataSourceProperties = makeTestDataSourceProperties() database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null }) fs = Jimfs.newFileSystem(Configuration.unix()) + + doReturn(testNetworkParameters()).whenever(services).networkParameters + storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also { database.transaction { it.start() } } + storage.servicesForResolution = services } @After fun tearDown() { + dir.list { subdir -> + subdir.forEach(Path::deleteRecursively) + } database.close() } + @Test + fun `importing a signed jar saves the signers to the storage`() { + val jarAndSigner = makeTestSignedContractJar("com.example.MyContract") + val signedJar = jarAndSigner.first + val attachmentId = storage.importAttachment(signedJar.inputStream(), "test", null) + assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash }) + } + + @Test + fun `importing a non-signed jar will save no signers`() { + val jarName = makeTestContractJar("com.example.MyContract") + val attachmentId = storage.importAttachment(dir.resolve(jarName).inputStream(), "test", null) + assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size) + } + @Test fun `insert and retrieve`() { val (testJar, expectedHash) = makeTestJar() @@ -289,7 +324,20 @@ class NodeAttachmentServiceTest { return Pair(file, file.readAll().sha256()) } - private companion object { + companion object { + private val dir = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName) + + @BeforeClass + @JvmStatic + fun beforeClass() { + } + + @AfterClass + @JvmStatic + fun afterClass() { + dir.deleteRecursively() + } + private fun makeTestJar(output: OutputStream, extraEntries: List> = emptyList()) { output.use { val jar = JarOutputStream(it) @@ -305,5 +353,48 @@ class NodeAttachmentServiceTest { jar.closeEntry() } } + + private fun makeTestSignedContractJar(contractName: String): Pair { + val alias = "testAlias" + val pwd = "testPassword" + dir.generateKey(alias, pwd, ALICE_NAME.toString()) + val jarName = makeTestContractJar(contractName) + val signer = dir.signJar(jarName, alias, pwd) + return dir.resolve(jarName) to signer + } + + private fun makeTestContractJar(contractName: String): String { + val packages = contractName.split(".") + val jarName = "testattachment.jar" + val className = packages.last() + createTestClass(className, packages.subList(0, packages.size - 1)) + dir.createJar(jarName, "${contractName.replace(".", "/")}.class") + return jarName + } + + private fun createTestClass(className: String, packages: List): Path { + val newClass = """package ${packages.joinToString(".")}; + import net.corda.core.contracts.*; + import net.corda.core.transactions.*; + + public class $className implements Contract { + @Override + public void verify(LedgerTransaction tx) throws IllegalArgumentException { + } + } + """.trimIndent() + val compiler = ToolProvider.getSystemJavaCompiler() + val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/${className}.java"), JavaFileObject.Kind.SOURCE) { + override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence { + return newClass + } + } + val fileManager = compiler.getStandardFileManager(null, null, null) + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(dir.toFile())) + + val compile = compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call() + return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name) + } } + } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt index 05fe559ac7..f487840f25 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt @@ -9,6 +9,7 @@ import net.corda.core.serialization.MissingAttachmentsException import net.corda.serialization.internal.GeneratedAttachment import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory +import java.security.PublicKey /** * A serializer for [ContractAttachment] that uses a proxy object to write out the full attachment eagerly. @@ -23,13 +24,13 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize } catch (e: Exception) { throw MissingAttachmentsException(listOf(obj.id)) } - return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader) + return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signers) } override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment { - return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader) + return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers) } @KeepForDJVM - data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set, val uploader: String?) + data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set, val uploader: String?, val signers: List) } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 19f282dac1..e174e0545b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.UNKNOWN_UPLOADER import net.corda.core.internal.uncheckedCast import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.AttachmentId import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -147,7 +148,11 @@ data class TestTransactionDSLInterpreter private constructor( override fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl() override fun _attachment(contractClassName: ContractClassName) { - (services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage) + attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage)) + } + + override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List){ + attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt index ecee16308c..00eac984b2 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt @@ -1,9 +1,18 @@ package net.corda.testing.dsl import net.corda.core.DoNotImplement +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentConstraint +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party +import net.corda.core.node.services.AttachmentId import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds import java.security.PublicKey @@ -80,6 +89,13 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * @param contractClassName The contract class to attach */ fun _attachment(contractClassName: ContractClassName) + + /** + * Attaches an attachment containing the named contract to the transaction + * @param contractClassName The contract class to attach + * @param attachmentId The attachment + */ + fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List) } /** @@ -186,5 +202,8 @@ class TransactionDSL(interpreter: T, private */ fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName) + fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List) = _attachment(contractClassName, attachmentId, signers) + fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId) = _attachment(contractClassName, attachmentId, emptyList()) + fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 9874799f6d..a6847b8fa2 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -3,6 +3,7 @@ package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId @@ -11,6 +12,7 @@ import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.services.MockAttachmentStorage import java.nio.file.Paths +import java.security.PublicKey import java.util.* class MockCordappProvider( @@ -21,7 +23,7 @@ class MockCordappProvider( private val cordappRegistry = mutableListOf>() - fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { + fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List = emptyList()): AttachmentId { val cordapp = CordappImpl( contractClassNames = listOf(contractClassName), initiatedFlows = emptyList(), @@ -36,23 +38,23 @@ class MockCordappProvider( info = CordappImpl.Info.UNKNOWN, allFlows = emptyList(), jarHash = SecureHash.allOnesHash) - if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { - cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments))) + if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) { + cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments, contractHash, signers))) } + return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!! } - override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? { - return cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) - } + override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second + ?: super.getContractAttachmentID(contractClassName) - private fun findOrImportAttachment(contractClassNames: List, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId { - val existingAttachment = attachments.files.filter { - Arrays.equals(it.value.second, data) + private fun findOrImportAttachment(contractClassNames: List, data: ByteArray, attachments: MockAttachmentStorage, contractHash: AttachmentId?, signers: List): AttachmentId { + val existingAttachment = attachments.files.filter { (attachmentId, content) -> + contractHash == attachmentId } return if (!existingAttachment.isEmpty()) { existingAttachment.keys.first() } else { - attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream()) + attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers) } } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index b113dda1b8..35a59c37b2 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.internal.AbstractAttachment +import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.UNKNOWN_UPLOADER import net.corda.core.internal.readFully import net.corda.core.node.services.AttachmentId @@ -15,6 +16,7 @@ import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.nodeapi.internal.withContractsInJar import java.io.InputStream +import java.security.PublicKey import java.util.* import java.util.jar.JarInputStream @@ -53,22 +55,23 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { } } - fun importContractAttachment(contractClassNames: List, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames) + @JvmOverloads + fun importContractAttachment(contractClassNames: List, uploader: String, jar: InputStream, attachmentId: AttachmentId? = null, signers: List = emptyList()): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames, attachmentId, signers) fun getAttachmentIdAndBytes(jar: InputStream): Pair = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) } - private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader) + private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signers: List) : AbstractAttachment(dataLoader) - private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List? = null): AttachmentId { + private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List? = null, attachmentId: AttachmentId? = null, signers: List = emptyList()): AttachmentId { // JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here. require(jar !is JarInputStream) val bytes = jar.readFully() - val sha256 = bytes.sha256() + val sha256 = attachmentId ?: bytes.sha256() if (sha256 !in files.keys) { - val baseAttachment = MockAttachment({ bytes }, sha256) - val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader) + val baseAttachment = MockAttachment({ bytes }, sha256, signers) + val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers) _files[sha256] = Pair(attachment, bytes) } return sha256 From 6c315d6adfb332c3048b39b45c43c9ba804434cf Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 22 Oct 2018 15:53:09 +0100 Subject: [PATCH 73/83] CORDA-2111: Upgrade to Kotlin 1.2.71. (#4083) --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 5bc1a09cd5..8fde90cd02 100644 --- a/constants.properties +++ b/constants.properties @@ -1,5 +1,5 @@ gradlePluginsVersion=4.0.32 -kotlinVersion=1.2.51 +kotlinVersion=1.2.71 # ***************************************************************# # When incrementing platformVersion make sure to update # # net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. # From c15b693f06edcecba1597fba289de1b95eee662e Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Mon, 22 Oct 2018 17:17:41 +0100 Subject: [PATCH 74/83] Early Discussion Of 'Maximus' Scope and Provisional High Level Design (#4055) * Move Lightning DRB to Markdown and PR. * Wrap text in raw source --- docs/source/design/maximus/design.md | 146 ++++++++++++++++++ .../design/maximus/images/current_state.png | Bin 0 -> 49636 bytes .../design/maximus/images/maximus_final.png | Bin 0 -> 101087 bytes .../design/maximus/images/maximus_phase1.png | Bin 0 -> 88344 bytes .../design/maximus/images/maximus_poc.png | Bin 0 -> 74048 bytes .../maximus/images/shared_bridge_float.png | Bin 0 -> 55740 bytes 6 files changed, 146 insertions(+) create mode 100644 docs/source/design/maximus/design.md create mode 100644 docs/source/design/maximus/images/current_state.png create mode 100644 docs/source/design/maximus/images/maximus_final.png create mode 100644 docs/source/design/maximus/images/maximus_phase1.png create mode 100644 docs/source/design/maximus/images/maximus_poc.png create mode 100644 docs/source/design/maximus/images/shared_bridge_float.png diff --git a/docs/source/design/maximus/design.md b/docs/source/design/maximus/design.md new file mode 100644 index 0000000000..4eee260a29 --- /dev/null +++ b/docs/source/design/maximus/design.md @@ -0,0 +1,146 @@ +# Validation of Maximus Scope and Future Work Proposal + +## Introduction + +The intent of this document is to ensure that the Tech Leads and Product Management are comfortable with the proposed +direction of HA team future work. The term Maximus has been used widely across R3 and we wish to ensure that the scope +is clearly understood and in alignment with wider delivery expectations. + +I hope to explain the successes and failures of our rapid POC work, so it is clearer what guides our decision making in +this. + +Also, it will hopefully inform other teams of changes that may cross into their area. + +## What is Maximus? + +Mike’s original proposal for Maximus, made at CordaCon Tokyo 2018, was to use some automation to start and stop node +VM’s using some sort of automation to reduce runtime cost. In Mike’s words this would allow ‘huge numbers of +identities’, perhaps ‘thousands’. + +The HA team and Andrey Brozhko have tried to stay close to this original definition that Maximus is for managing +100’s-1000’s Enterprise Nodes and that the goal of the project is to better manage costs, especially in cloud +deployments and with low overall flow rates. However, this leads to the following assumptions: + +1. The overall rate of flows is low and users will accept some latency. The additional sharing of identities on a +reduced physical footprint will inevitably reduce throughput compared to dedicated nodes, but should not be a problem. + +2. At least in the earlier phases it is acceptable to statically manage identity keys/certificates for each individual +identity. This will be scripted but will incur some effort/procedures/checking on the doorman side. + +3. Every identity has an associated ‘DB schema’, which might be on a shared database server, but the separation is +managed at that level. This database is a fixed runtime cost per identity and will not be shared in the earlier phases +of Maximus. It might be optionally shareable in future, but this is not a hard requirement for Corda 5 as it needs +significant help from core to change the DB schemas. Also, our understanding is that the isolation is a positive feature +in some deployments. + +4. Maximus may share infrastructure and possibly JVM memory between identities without breaking some customer +requirement for isolation. In other words we are virtualizing the ‘node’, but CorDapps and peer nodes will be unaware of +any changes. + +## What Maximus is not + +1. Maximus is not designed to handle millions of identities. That is firmly Marco Polo and possibly handled completely +differently. + +2. Maximus should be not priced such as to undercut our own high-performance Enterprise nodes, or allow customers to run +arbitrary numbers of nodes for free. + +3. Maximus is not a ‘wallet’ based solution. The nodes in Maximus are fully equivalent to the current Enterprise +offering and have first class identities. There is also no remoting of the signing operations. + +## The POC technologies we have tried + +The HA team has looked at several elements of the solution. Some approaches look promising, some do not. + +1. We have already started the work to share a common P2P Artemis between multiple nodes and common bridge/float. This +is the ‘SNI header’ work which has been DRB’s recently. This should be functionally complete soon and available in Corda +4.0 This work will reduce platform cost and simplify deployment of multiple nodes. For Maximus the main effect is that it +should make the configuration much more consistent between nodes and it means that where a node runs is immaterial as +the shared broker distributes messages and the Corda firewall handles the public communication. + +2. I looked at flattening the flow state machine, so that we could map Corda operations into combining state and +messages in the style of a Map-Reduce pattern. Unfortunately, the work involved is extreme and not compatible with the +Corda API. Therefore a pure ‘flow worker’ approach does not look viable any time soon and in general full hot-hot is +still a way off. + +3. Chris looked at reducing the essential service set in the node to those needed to support the public flow API and the +StateMachine. Then we attached a simple start flow messaging interface. This simple ‘FlowRunner’ class allowed +exploration of several options in a gaffer taped state. + + 1. We created a simple messaging interface between an RPC runner and a Flow Runner and showed that we can run + standard flows. + + 2. We were able to POC combining two identities running side-by-side in a Flow Runner, which is in fact quite similar + to many of our integration tests. We must address static variable leakage but should be feasible. + + 3. We were able to create an RPC worker that could handle several identities at once and start flows on the + same/different flow runner harnesses. + +4. We then pushed forward looking into flow sharding. Here we made some progress, but the task started to get more and more + complicated. It also highlighted that we don’t have suitable headers on our messages and that the message header + whitelist will make this difficult to change whilst maintaining wire compatibility. The conclusion from this is that + hot-hot flow sharding will have to wait. + +8. We have been looking at resource/cost management technologies. The almost immediate conclusion is that whilst cloud +providers do have automated VM/container as service they are not standardized. Instead, the only standardized approach +is Kubernetes+docker, which will charge dynamically according to active use levels. + +9. Looking at resource management in Kubernetes we can dynamically scale relatively homogeneous pods, but the metrics +approach cannot easily cope with identity injection. Instead we can scale the number of running pods, but they will have +to self-organize the work balancing amongst themselves. + +## Maximus Work Proposal + +#### Current State + +![Current Enterprise State](./images/current_state.png) + +The current enterprise node solution in GA 3.1 is as above. This has dynamic HA failover available for the bridge/float +using ZooKeeper as leader elector, but the node has to be hot-cold. There is some sharing support for the ZooKeeper +cluster, but otherwise all this infrastructure has to be replicated per identity. In addition, all elements of this have +to have at least one resident instance to ensure that messages are captured and RPC clients have an endpoint to talk to. + +#### Corda 4.0 Agreed Target with SNI Shared Corda Firewalls + +![Corda 4.0 Enterprise State](./images/shared_bridge_float.png) + +Here by sharing the P2P Artemis externally and work on the messaging protocol it should be possible to reuse the corda +firewall for multiple nodes. This means that the externally advertised address will be stable for the whole cluster +independent of the deployed identities. Also, the durable messaging is outside nodes, which means that we can +theoretically schedule running the nodes only if a few times a day if they only act in response to external peer +messages. Mostly this is a prelude to greater sharing in the future Maximus state. + +#### Intermediate State Explored during POC + +![Maximus POC](./images/maximus_poc.png) + +During the POC we explore the model above, although none of the components were completed to a production standard. The +key feature here is that the RPC side has been split out of the node and has API support for multiple identities built +in. The flow and P2P elements of the node have been split out too, which means that the ‘FlowWorker’ start-up code can +be simpler than the current AbstractNode as it doesn’t have to support the same testing framework. The actual service +implementations are unchanged in this. + +The principal communication between the RPC and FlowWorker is about starting flows and completed work is broadcast as +events. A message protocol will be defined to allow re-attachment and status querying if the RPC client is restarted. +The vault RPC api will continue to the database directly in the RpcWorker and not involve the FlowWorker. The scheduler +service will live in the RPC service as potentially the FlowWorkers will not yet be running when the due time occurs. + +#### Proposed Maximus Phase 1 State + +![Maximus Phase 1](./images/maximus_phase1.png) + +The productionised version of the above POC will introduce ‘Max Nodes’ that can load FlowWorkers on demand. We still +require only one runs at once, but for this we will use ZooKeeper to ensure that FlowWorkers with capacity compete to +process the work and only one wins. Based on trials we can safely run a couple of identities at one inside the same Max +Node assuming load is manageable. Idle identities will be dropped trivially, since the Hibernate, Artemis connections +and thread pools will be owned by the Max Node not the flow workers. At this stage there is no dynamic management of the +physical resources, but some sort of scheduler could control how many Max Nodes are running at once. + +#### Final State Maximus with Dynamic Resource Management + +![Maximus Final](./images/maximus_final.png) + +The final evolution is to add dynamic cost control to the system. As the Max Nodes are homogeneous the RpcWorker can +monitor the load and signal metrics available to Kubernetes. This means that Max Nodes can be added and removed as +required and potentially cost zero. Ideally, separate work would begin in parallel to combine database data into a +single schema, but that is possibly not required. \ No newline at end of file diff --git a/docs/source/design/maximus/images/current_state.png b/docs/source/design/maximus/images/current_state.png new file mode 100644 index 0000000000000000000000000000000000000000..e8dd93aa31fe626a766c9c5d6a4ace1c7dc4a21a GIT binary patch literal 49636 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

    IV6acM?{sVcNvX&xrXE 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≤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 Q8VUT96q6Gz64vwie^1qxTL1t6 literal 0 HcmV?d00001 diff --git a/docs/source/design/maximus/images/maximus_poc.png b/docs/source/design/maximus/images/maximus_poc.png new file mode 100644 index 0000000000000000000000000000000000000000..906a45dba4dc357eb9d35c458ef4c4505ce7a437 GIT binary patch literal 74048 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 Date: Mon, 22 Oct 2018 18:56:30 +0100 Subject: [PATCH 75/83] CORDA-1621: The finance CorDapp uses the app config feature rather than the node's config (#4100) --- docs/source/testnet-explorer-corda.rst | 45 ++++++----- .../node/configuration/Configuration.kt | 5 +- .../configuration/CordappConfiguration.kt | 2 + .../configuration/CurrencyConfiguration.kt | 4 + .../corda/behave/scenarios/helpers/Cash.kt | 2 +- .../flows/test/CashConfigDataFlowTest.kt | 24 ------ .../corda/finance/flows/CashConfigDataFlow.kt | 77 ------------------- .../finance/internal/CashConfigDataFlow.kt | 48 ++++++++++++ .../internal/CashConfigDataFlowTest.kt | 29 +++++++ ...owCheckpointVersionNodeStartupCheckTest.kt | 9 +-- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../cordapp/CordappConfigFileProvider.kt | 24 +++--- .../cordapp/CordappConfigFileProviderTests.kt | 6 +- samples/bank-of-corda-demo/build.gradle | 4 +- .../net/corda/testing/node/TestCordapp.kt | 11 ++- .../node/internal/TestCordappDirectories.kt | 24 +++--- .../testing/node/internal/TestCordappImpl.kt | 3 + .../net/corda/demobench/explorer/Explorer.kt | 2 +- .../net/corda/demobench/model/NodeConfig.kt | 31 ++++---- .../corda/demobench/model/NodeController.kt | 12 +-- .../demobench/plugin/CordappController.kt | 13 ++-- .../demobench/profile/ProfileController.kt | 2 +- .../corda/demobench/model/NodeConfigTest.kt | 7 +- .../net/corda/explorer/ExplorerSimulation.kt | 28 +++++-- .../net/corda/explorer/model/IssuerModel.kt | 2 +- 25 files changed, 213 insertions(+), 203 deletions(-) delete mode 100644 finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt delete mode 100644 finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt create mode 100644 finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt create mode 100644 finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt diff --git a/docs/source/testnet-explorer-corda.rst b/docs/source/testnet-explorer-corda.rst index c9217dfca2..e8902a3b3c 100644 --- a/docs/source/testnet-explorer-corda.rst +++ b/docs/source/testnet-explorer-corda.rst @@ -20,17 +20,15 @@ Get the testing tools To run the tests and make sure your node is connecting correctly to the network you will need to download and install a couple of resources. -1. Log into your Cloud VM via SSH. +#. Log into your Cloud VM via SSH. - -2. Stop the Corda node(s) running on your cloud instance. +#. Stop the Corda node(s) running on your cloud instance. .. code:: bash ps aux | grep corda.jar | awk '{ print $2 }' | xargs sudo kill - -3. Download the finance CorDapp +#. Download the finance CorDapp In the terminal on your cloud instance run: @@ -38,32 +36,32 @@ couple of resources. wget https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-finance/-corda/corda-finance--corda.jar - This is required to run some flows to check your connections, and to issue/transfer cash to counterparties. Copy it to the Corda installation location: + This is required to run some flows to check your connections, and to issue/transfer cash to counterparties. Copy it to + the Corda installation location: .. code:: bash sudo cp /home//corda-finance--corda.jar /opt/corda/cordapps/ -4. Add the following line to the bottom of your ``node.conf``: +#. Run the following to create a config file for the finance CorDapp: .. code:: bash - issuableCurrencies : [ USD ] + echo "issuableCurrencies : [ USD ]" > /opt/corda/cordapps/config/corda-finance--corda.conf - .. note:: Make sure that the config file is in the correct format, e.g., by ensuring that there's a comma at the end of the line prior to the added config. - -4. Restart the Corda node: +#. Restart the Corda node: .. code:: bash cd /opt/corda sudo ./run-corda.sh - Your node is now running the Finance Cordapp. + Your node is now running the finance Cordapp. - .. note:: You can double-check that the CorDapp is loaded in the log file ``/opt/corda/logs/node-.log``. This file will list installed apps at startup. Search for ``Loaded CorDapps`` in the logs. + .. note:: You can double-check that the CorDapp is loaded in the log file ``/opt/corda/logs/node-.log``. This + file will list installed apps at startup. Search for ``Loaded CorDapps`` in the logs. -6. Now download the Node Explorer to your **LOCAL** machine: +#. Now download the Node Explorer to your **LOCAL** machine: .. note:: Node Explorer is a JavaFX GUI which connects to the node over the RPC interface and allows you to send transactions. @@ -73,9 +71,10 @@ couple of resources. http://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-tools-explorer/-corda/corda-tools-explorer--corda.jar - .. warning:: This Node Explorer is incompatible with the Corda Enterprise distribution and vice versa as they currently use different serialisation schemes (Kryo vs AMQP). + .. warning:: This Node Explorer is incompatible with the Corda Enterprise distribution and vice versa as they currently + use different serialisation schemes (Kryo vs AMQP). -7. Run the Node Explorer tool on your **LOCAL** machine. +#. Run the Node Explorer tool on your **LOCAL** machine. .. code:: bash @@ -90,8 +89,10 @@ Connect to the node To connect to the node you will need: * The IP address of your node (the public IP of your cloud instance). You can find this in the instance page of your cloud console. -* The port number of the RPC interface to the node, specified in ``/opt/corda/node.conf`` in the ``rpcSettings`` section, (by default this is 10003 on Testnet). -* The username and password of the RPC interface of the node, also in the ``node.conf`` in the ``rpcUsers`` section, (by default the username is ``cordazoneservice`` on Testnet). +* The port number of the RPC interface to the node, specified in ``/opt/corda/node.conf`` in the ``rpcSettings`` section, + (by default this is 10003 on Testnet). +* The username and password of the RPC interface of the node, also in the ``node.conf`` in the ``rpcUsers`` section, + (by default the username is ``cordazoneservice`` on Testnet). Click on ``Connect`` to log into the node. @@ -102,7 +103,8 @@ Once Explorer has logged in to your node over RPC click on the ``Network`` tab i .. image:: resources/explorer-network.png -If your Corda node is correctly configured and connected to the Testnet then you should be able to see the identities of your node, the Testnet notary and the network map listing all the counterparties currently on the network. +If your Corda node is correctly configured and connected to the Testnet then you should be able to see the identities of +your node, the Testnet notary and the network map listing all the counterparties currently on the network. Test issuance transaction @@ -120,8 +122,9 @@ Click ``Execute`` and the transaction will start. .. image:: resources/explorer-cash-issue3.png -Click on the red X to close the notification window and click on ``Transactions`` tab to see the transaction in progress, or wait for a success message to be displayed: +Click on the red X to close the notification window and click on ``Transactions`` tab to see the transaction in progress, +or wait for a success message to be displayed: .. image:: resources/explorer-transactions.png -Congratulations! You have now successfully installed a CorDapp and executed a transaction on the Corda Testnet. \ No newline at end of file +Congratulations! You have now successfully installed a CorDapp and executed a transaction on the Corda Testnet. diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt index 6ec510c75b..de6450a6ca 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt @@ -21,6 +21,7 @@ class Configuration( nodeInterface.dbPort, password = DEFAULT_PASSWORD ), + // TODO This is not being used when it could be. The call-site is using configElements instead. val notary: NotaryConfiguration = NotaryConfiguration(), val cordapps: CordappConfiguration = CordappConfiguration(), vararg configElements: ConfigurationTemplate @@ -28,9 +29,7 @@ class Configuration( private val developerMode = true - val cordaX500Name: CordaX500Name by lazy({ - CordaX500Name(name, location, country) - }) + val cordaX500Name: CordaX500Name = CordaX500Name(name, location, country) private val basicConfig = """ |myLegalName="C=$country,L=$location,O=$name" diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt index 57a1f96e6b..b288cb9514 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt @@ -1,5 +1,7 @@ package net.corda.behave.node.configuration +// TODO This is a ConfigurationTemplate but is never used as one. Therefore the private "applications" list is never used +// and thus includeFinance isn't necessary either. Something is amiss. class CordappConfiguration(var apps: List = emptyList(), val includeFinance: Boolean = false) : ConfigurationTemplate() { private val applications = apps + if (includeFinance) { diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt index 16252650d2..301aad7d24 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt @@ -7,6 +7,10 @@ class CurrencyConfiguration(private val issuableCurrencies: List) : Conf if (issuableCurrencies.isEmpty()) { "" } else { + // TODO This is no longer correct. issuableCurrencies is a config of the finance app and belongs + // in a separate .conf file for the app (in the config sub-directory, with a filename matching the CorDapp + // jar filename). It is no longer read in from the node conf file. There seem to be pieces missing in the + // behave framework to allow one to do this. """ |custom : { | issuableCurrencies : [ diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt index 6930331f10..fbaec51f64 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt @@ -7,9 +7,9 @@ import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.finance.flows.CashConfigDataFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.internal.CashConfigDataFlow import java.util.* import java.util.concurrent.TimeUnit diff --git a/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt deleted file mode 100644 index 77316c148e..0000000000 --- a/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.finance.flows.test - -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.finance.EUR -import net.corda.finance.USD -import net.corda.finance.flows.CashConfigDataFlow -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test - -class CashConfigDataFlowTest { - @Test - fun `issuable currencies are read in from node config`() { - driver(DriverParameters( - extraCordappPackagesToScan = listOf("net.corda.finance.flows"), - notarySpecs = emptyList())) { - val node = startNode(customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("EUR", "USD")))).getOrThrow() - val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow() - assertThat(config.issuableCurrencies).containsExactly(EUR, USD) - } - } -} diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt deleted file mode 100644 index ac890bd517..0000000000 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.corda.finance.flows - -import co.paralleluniverse.fibers.Suspendable -import com.typesafe.config.ConfigFactory -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.internal.declaredField -import net.corda.core.node.AppServiceHub -import net.corda.core.node.services.CordaService -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.finance.CHF -import net.corda.finance.EUR -import net.corda.finance.GBP -import net.corda.finance.USD -import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies -import java.io.IOException -import java.io.InputStream -import java.nio.file.Files -import java.nio.file.OpenOption -import java.nio.file.Path -import java.nio.file.Paths -import java.util.* - -// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf -// again to get our config and store it here for access by our flow -@CordaService -class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() { - companion object { - val supportedCurrencies = listOf(USD, GBP, CHF, EUR) - - // TODO: In future releases, the Finance app should be fully decoupled from internal APIs in Core. - private operator fun Path.div(other: String): Path = resolve(other) - private operator fun String.div(other: String): Path = Paths.get(this) / other - private fun Path.inputStream(vararg options: OpenOption): InputStream = Files.newInputStream(this, *options) - private inline fun Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = inputStream(*options).use(block) - } - - val issuableCurrencies: List - - init { - // Warning!! You are about to see a major hack! - val baseDirectory = services.declaredField("serviceHub").value - .let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) } - .let { it.javaClass.getMethod("getBaseDirectory").apply { isAccessible = true }.invoke(it) } - .let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String } - - var issuableCurrenciesValue: List - try { - val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) } - if (config.hasPath("custom.issuableCurrencies")) { - issuableCurrenciesValue = config.getStringList("custom.issuableCurrencies").map { Currency.getInstance(it) } - require(supportedCurrencies.containsAll(issuableCurrenciesValue)) - } else { - issuableCurrenciesValue = emptyList() - } - } catch (e: IOException) { - issuableCurrenciesValue = emptyList() - } - issuableCurrencies = issuableCurrenciesValue - } -} - -/** - * Flow to obtain cash cordapp app configuration. - */ -@StartableByRPC -class CashConfigDataFlow : FlowLogic() { - @Suspendable - override fun call(): CashConfiguration { - val configHolder = serviceHub.cordaService(ConfigHolder::class.java) - return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies) - } -} - -@CordaSerializable -data class CashConfiguration(val issuableCurrencies: List, val supportedCurrencies: List) diff --git a/finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt new file mode 100644 index 0000000000..3c2ab3c988 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt @@ -0,0 +1,48 @@ +package net.corda.finance.internal + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.uncheckedCast +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.finance.CHF +import net.corda.finance.EUR +import net.corda.finance.GBP +import net.corda.finance.USD +import net.corda.finance.internal.ConfigHolder.Companion.supportedCurrencies +import java.util.* + +@CordaService +class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() { + companion object { + val supportedCurrencies = listOf(USD, GBP, CHF, EUR) + } + + val issuableCurrencies: List + + init { + val issuableCurrenciesStringList: List = uncheckedCast(services.getAppContext().config.get("issuableCurrencies")) + issuableCurrencies = issuableCurrenciesStringList.map(Currency::getInstance) + (issuableCurrencies - supportedCurrencies).let { + require(it.isEmpty()) { "$it are not supported currencies" } + } + } +} + +/** + * Flow to obtain cash cordapp app configuration. + */ +@StartableByRPC +class CashConfigDataFlow : FlowLogic() { + @Suspendable + override fun call(): CashConfiguration { + val configHolder = serviceHub.cordaService(ConfigHolder::class.java) + return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies) + } +} + +@CordaSerializable +data class CashConfiguration(val issuableCurrencies: List, val supportedCurrencies: List) diff --git a/finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt b/finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt new file mode 100644 index 0000000000..669f1a2c9a --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt @@ -0,0 +1,29 @@ +package net.corda.finance.internal + +import net.corda.core.internal.packageName +import net.corda.core.utilities.getOrThrow +import net.corda.finance.EUR +import net.corda.finance.USD +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetworkParameters +import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.internal.cordappForPackages +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Test + +class CashConfigDataFlowTest { + private val mockNet = MockNetwork(emptyList(), MockNetworkParameters(threadPerNode = true)) + + @After + fun cleanUp() = mockNet.stopNodes() + + @Test + fun `issuable currencies read in from cordapp config`() { + val node = mockNet.createNode(MockNodeParameters(additionalCordapps = listOf( + cordappForPackages(javaClass.packageName).withConfig(mapOf("issuableCurrencies" to listOf("EUR", "USD"))) + ))) + val config = node.startFlow(CashConfigDataFlow()).getOrThrow() + assertThat(config.issuableCurrencies).containsExactly(EUR, USD) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt index 169ba207ce..40e84e57fb 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt @@ -69,12 +69,11 @@ class FlowCheckpointVersionNodeStartupCheckTest { // Create the CorDapp jar file manually first to get hold of the directory that will contain it so that we can // rename the filename later. The cordappDir, which acts as pointer to the jar file, does not get renamed. val cordappDir = TestCordappDirectories.getJarDirectory(cordapp) - val cordappJar = cordappDir.list().single() + val cordappJar = cordappDir.list().single { it.toString().endsWith(".jar") } createSuspendedFlowInBob(setOf(cordapp)) - // Rename the jar file. TestCordappDirectories caches the location of the jar file but the use of the random - // UUID in the name means there's zero chance of contaminating another test. + // Rename the jar file. cordappJar.moveTo(cordappDir / "renamed-${cordappJar.fileName}") assertBobFailsToStartWithLogMessage( @@ -88,13 +87,13 @@ class FlowCheckpointVersionNodeStartupCheckTest { fun `restart node with incompatible version of suspended flow due to different jar hash`() { driver(parametersForRestartingNodes()) { val originalCordapp = defaultCordapp.withName("different-jar-hash-test-${UUID.randomUUID()}") - val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single() + val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single { it.toString().endsWith(".jar") } createSuspendedFlowInBob(setOf(originalCordapp)) // The vendor is part of the MANIFEST so changing it is sufficient to change the jar hash val modifiedCordapp = originalCordapp.withVendor("${originalCordapp.vendor}-modified") - val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single() + val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single { it.toString().endsWith(".jar") } modifiedCordappJar.moveTo(originalCordappJar, REPLACE_EXISTING) assertBobFailsToStartWithLogMessage( diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 4bee70d8c6..d8b9aa15a6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -167,7 +167,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize() - val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize() + val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt index dbf4daf657..5a6e8377b6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt @@ -5,30 +5,26 @@ import com.typesafe.config.ConfigFactory import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists -import net.corda.core.internal.isDirectory +import net.corda.core.internal.noneOrSingle import net.corda.core.utilities.contextLogger import java.nio.file.Path -import java.nio.file.Paths -class CordappConfigFileProvider(private val configDir: Path = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider { +class CordappConfigFileProvider(cordappDirectories: List) : CordappConfigProvider { companion object { - val DEFAULT_CORDAPP_CONFIG_DIR = Paths.get("cordapps") / "config" - const val CONFIG_EXT = ".conf" - val logger = contextLogger() + private val logger = contextLogger() } - init { - configDir.createDirectories() - } + private val configDirectories = cordappDirectories.map { (it / "config").createDirectories() } override fun getConfigByName(name: String): Config { - val configFile = configDir / "$name$CONFIG_EXT" - return if (configFile.exists()) { - check(!configFile.isDirectory()) { "${configFile.toAbsolutePath()} is a directory, expected a config file" } - logger.info("Found config for cordapp $name in ${configFile.toAbsolutePath()}") + // TODO There's nothing stopping the same CorDapp jar from occuring in different directories and thus causing + // conflicts. The cordappDirectories list config option should just be a single cordappDirectory + val configFile = configDirectories.map { it / "$name.conf" }.noneOrSingle { it.exists() } + return if (configFile != null) { + logger.info("Found config for cordapp $name in $configFile") ConfigFactory.parseFile(configFile.toFile()) } else { - logger.info("No config found for cordapp $name in ${configFile.toAbsolutePath()}") + logger.info("No config found for cordapp $name in $configDirectories") ConfigFactory.empty() } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt index f3ea03a72e..73829d5a8c 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt @@ -12,16 +12,16 @@ import java.nio.file.Paths class CordappConfigFileProviderTests { private companion object { - val cordappConfDir = Paths.get("build") / "tmp" / "cordapps" / "config" + val cordappDir = Paths.get("build") / "tmp" / "cordapps" const val cordappName = "test" - val cordappConfFile = cordappConfDir / "$cordappName.conf" + val cordappConfFile = cordappDir / "config" / "$cordappName.conf" val validConfig: Config = ConfigFactory.parseString("key=value") val alternateValidConfig: Config = ConfigFactory.parseString("key=alternateValue") const val invalidConfig = "Invalid" } - private val provider = CordappConfigFileProvider(cordappConfDir) + private val provider = CordappConfigFileProvider(listOf(cordappDir)) @Test fun `test that config can be loaded`() { diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 0990cc5987..135a51c921 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -56,9 +56,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webPort 10007 rpcUsers = [[user: "bankUser", password: "test", permissions: ["ALL"]]] extraConfig = [ - custom : [issuableCurrencies: ["USD"]], h2Settings: [address: "localhost:10017"] ] + cordapp(project(':finance')) { + config "issuableCurrencies = [ USD ]" + } } node { name "O=BigCorporation,L=New York,C=US" diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt index 6f881660c1..fed1f14e22 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt @@ -19,12 +19,15 @@ interface TestCordapp { /** Returns the version string, defaults to "1.0" if not specified. */ val version: String - /** Returns the vendor string, defaults to "Corda" if not specified. */ + /** Returns the vendor string, defaults to "test-vendor" if not specified. */ val vendor: String /** Returns the target platform version, defaults to the current platform version if not specified. */ val targetVersion: Int + /** Returns the config for this CorDapp, defaults to empty if not specified. */ + val config: Map + /** Returns the set of package names scanned for this test CorDapp. */ val packages: Set @@ -43,6 +46,9 @@ interface TestCordapp { /** Return a copy of this [TestCordapp] but with the specified target platform version. */ fun withTargetVersion(targetVersion: Int): TestCordapp + /** Returns a copy of this [TestCordapp] but with the specified CorDapp config. */ + fun withConfig(config: Map): TestCordapp + class Factory { companion object { @JvmStatic @@ -57,9 +63,10 @@ interface TestCordapp { return TestCordappImpl( name = "test-cordapp", version = "1.0", - vendor = "Corda", + vendor = "test-vendor", title = "test-title", targetVersion = PLATFORM_VERSION, + config = emptyMap(), packages = simplifyScanPackages(packageNames), classes = emptySet() ) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt index 3cd553de39..354aebe183 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt @@ -1,9 +1,11 @@ package net.corda.testing.node.internal +import com.typesafe.config.ConfigValueFactory import net.corda.core.crypto.sha256 import net.corda.core.internal.createDirectories import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div +import net.corda.core.internal.writeText import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.testing.node.TestCordapp @@ -22,24 +24,28 @@ object TestCordappDirectories { fun getJarDirectory(cordapp: TestCordapp, cordappsDirectory: Path = defaultCordappsDirectory): Path { cordapp as TestCordappImpl return testCordappsCache.computeIfAbsent(cordapp) { - val cordappDir = (cordappsDirectory / UUID.randomUUID().toString()).createDirectories() - val uniqueScanString = if (cordapp.packages.size == 1 && cordapp.classes.isEmpty()) { - cordapp.packages.first() - } else { - "${cordapp.packages}${cordapp.classes.joinToString { it.name }}".toByteArray().sha256().toString() + val configString = ConfigValueFactory.fromMap(cordapp.config).toConfig().root().render() + val filename = cordapp.run { + val uniqueScanString = if (packages.size == 1 && classes.isEmpty() && config.isEmpty()) { + packages.first() + } else { + "$packages$classes$configString".toByteArray().sha256().toString() + } + "${name}_${vendor}_${title}_${version}_${targetVersion}_$uniqueScanString".replace(whitespace, "-") } - val jarFileName = cordapp.run { "${name}_${vendor}_${title}_${version}_${targetVersion}_$uniqueScanString.jar".replace(whitespace, "-") } - val jarFile = cordappDir / jarFileName + val cordappDir = cordappsDirectory / UUID.randomUUID().toString() + val configDir = (cordappDir / "config").createDirectories() + val jarFile = cordappDir / "$filename.jar" cordapp.packageAsJar(jarFile) + (configDir / "$filename.conf").writeText(configString) logger.debug { "$cordapp packaged into $jarFile" } cordappDir } } private val defaultCordappsDirectory: Path by lazy { - val cordappsDirectory = (Paths.get("build") / "tmp" / getTimestampAsDirectoryName() / "generated-test-cordapps").toAbsolutePath() + val cordappsDirectory = Paths.get("build").toAbsolutePath() / "generated-test-cordapps" / getTimestampAsDirectoryName() logger.info("Initialising generated test CorDapps directory in $cordappsDirectory") - cordappsDirectory.toFile().deleteOnExit() cordappsDirectory.deleteRecursively() cordappsDirectory.createDirectories() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt index 831198320c..c8e7ab626b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt @@ -7,6 +7,7 @@ data class TestCordappImpl(override val name: String, override val vendor: String, override val title: String, override val targetVersion: Int, + override val config: Map, override val packages: Set, val classes: Set>) : TestCordapp { @@ -20,6 +21,8 @@ data class TestCordappImpl(override val name: String, override fun withTargetVersion(targetVersion: Int): TestCordappImpl = copy(targetVersion = targetVersion) + override fun withConfig(config: Map): TestCordappImpl = copy(config = config) + fun withClasses(vararg classes: Class<*>): TestCordappImpl { return copy(classes = classes.filter { clazz -> packages.none { clazz.name.startsWith("$it.") } }.toSet()) } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index 4606621280..7baabdb8fe 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -85,7 +85,7 @@ class Explorer internal constructor(private val explorerController: ExplorerCont // Note: does not copy dependencies because we should soon be making all apps fat jars and dependencies implicit. // // TODO: Remove this code when serialisation has been upgraded. - val cordappsDir = config.explorerDir / NodeConfig.cordappDirName + val cordappsDir = config.explorerDir / NodeConfig.CORDAPP_DIR_NAME cordappsDir.createDirectories() config.cordappsDir.list { it.forEachOrdered { path -> diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 9f63d8f38b..5ae40f6eb4 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -3,6 +3,7 @@ package net.corda.demobench.model import com.typesafe.config.* import com.typesafe.config.ConfigFactory.empty import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.copyToDirectory import net.corda.core.internal.createDirectories import net.corda.core.internal.div @@ -11,7 +12,7 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.toConfig import java.nio.file.Path import java.nio.file.StandardCopyOption -import java.util.Properties +import java.util.* /** * This is a subset of FullNodeConfiguration, containing only those configs which we need. The node uses reference.conf @@ -38,33 +39,33 @@ data class NodeConfig( companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) val defaultUser = user("guest") - const val cordappDirName = "cordapps" + const val CORDAPP_DIR_NAME = "cordapps" } - fun nodeConf(): Config { + @VisibleForTesting + internal fun nodeConf(): Config { val rpcSettings: ConfigObject = empty() .withValue("address", valueFor(rpcSettings.address.toString())) .withValue("adminAddress", valueFor(rpcSettings.adminAddress.toString())) .root() - val customMap: Map = HashMap().also { - if (issuableCurrencies.isNotEmpty()) { - it["issuableCurrencies"] = issuableCurrencies - } - } - val custom: ConfigObject = ConfigFactory.parseMap(customMap).root() return NodeConfigurationData(myLegalName, p2pAddress, this.rpcSettings.address, notary, h2port, rpcUsers, useTestClock, detectPublicIp, devMode) .toConfig() .withoutPath("rpcAddress") .withoutPath("rpcAdminAddress") .withValue("rpcSettings", rpcSettings) - .withOptionalValue("custom", custom) } - fun webServerConf() = WebServerConfigurationData(myLegalName, rpcSettings.address, webAddress, rpcUsers).asConfig() + @VisibleForTesting + internal fun webServerConf() = WebServerConfigurationData(myLegalName, rpcSettings.address, webAddress, rpcUsers).asConfig() - fun toNodeConfText() = nodeConf().render() + fun toNodeConfText(): String = nodeConf().render() - fun toWebServerConfText() = webServerConf().render() + fun toWebServerConfText(): String = webServerConf().render() + + @VisibleForTesting + internal fun financeConf() = FinanceConfData(issuableCurrencies).toConfig() + + fun toFinanceConfText(): String = financeConf().render() fun serialiseAsString(): String = toConfig().render() @@ -92,6 +93,8 @@ private data class WebServerConfigurationData( fun asConfig() = toConfig() } +private data class FinanceConfData(val issuableCurrencies: List) + /** * This is a subset of NotaryConfig. It implements [ExtraService] to avoid unnecessary copying. */ @@ -104,7 +107,7 @@ data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : Ha val key: String = nodeConfig.myLegalName.organisation.toKey() val nodeDir: Path = baseDir / key val explorerDir: Path = baseDir / "$key-explorer" - override val cordappsDir: Path = nodeDir / NodeConfig.cordappDirName + override val cordappsDir: Path = nodeDir / NodeConfig.CORDAPP_DIR_NAME var state: NodeState = NodeState.STARTING fun install(cordapps: Collection) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 05e1bdc3e3..09d12ebf03 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -112,18 +112,14 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { try { // Notary can be removed and then added again, that's why we need to perform this check. require((config.nodeConfig.notary != null).xor(notaryIdentity != null)) { "There must be exactly one notary in the network" } - config.nodeDir.createDirectories() + val cordappConfigDir = (config.cordappsDir / "config").createDirectories() // Install any built-in plugins into the working directory. cordappController.populate(config) - // Write this node's configuration file into its working directory. - val confFile = config.nodeDir / "node.conf" - confFile.writeText(config.nodeConfig.toNodeConfText()) - - // Write this node's configuration file into its working directory. - val webConfFile = config.nodeDir / "web-server.conf" - webConfFile.writeText(config.nodeConfig.toWebServerConfText()) + (config.nodeDir / "node.conf").writeText(config.nodeConfig.toNodeConfText()) + (config.nodeDir / "web-server.conf").writeText(config.nodeConfig.toWebServerConfText()) + (cordappConfigDir / "${CordappController.FINANCE_CORDAPP_FILENAME}.conf").writeText(config.nodeConfig.toFinanceConfText()) // Execute the Corda node val cordaEnv = System.getenv().toMutableMap().apply { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt index 66f844e885..b82d9c7779 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt @@ -12,10 +12,13 @@ import java.nio.file.StandardCopyOption import kotlin.streams.toList class CordappController : Controller() { + companion object { + const val FINANCE_CORDAPP_FILENAME = "corda-finance" + } private val jvm by inject() - private val cordappDir: Path = jvm.applicationDir.resolve(NodeConfig.cordappDirName) - private val finance: Path = cordappDir.resolve("corda-finance.jar") + private val cordappDir: Path = jvm.applicationDir / NodeConfig.CORDAPP_DIR_NAME + private val financeCordappJar: Path = cordappDir / "$FINANCE_CORDAPP_FILENAME.jar" /** * Install any built-in cordapps that this node requires. @@ -25,8 +28,8 @@ class CordappController : Controller() { if (!config.cordappsDir.exists()) { config.cordappsDir.createDirectories() } - if (finance.exists()) { - finance.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) + if (financeCordappJar.exists()) { + financeCordappJar.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) log.info("Installed 'Finance' cordapp") } } @@ -39,7 +42,7 @@ class CordappController : Controller() { if (!config.cordappsDir.isDirectory()) return emptyList() return config.cordappsDir.walk(1) { paths -> paths.filter(Path::isCordapp) - .filter { !finance.endsWith(it.fileName) } + .filter { !financeCordappJar.endsWith(it.fileName) } .toList() } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt index 7c034983b8..4c8bfeb02d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -60,7 +60,7 @@ class ProfileController : Controller() { log.info("Wrote: $file") // Write all of the non-built-in cordapps. - val cordappDir = (nodeDir / NodeConfig.cordappDirName).createDirectory() + val cordappDir = (nodeDir / NodeConfig.CORDAPP_DIR_NAME).createDirectory() cordappController.useCordappsFor(config).forEach { val cordapp = it.copyToDirectory(cordappDir) log.info("Wrote: $cordapp") diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index d044c63f14..e07f18331b 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -65,12 +65,7 @@ class NodeConfigTest { issuableCurrencies = listOf("GBP") ) - val nodeConfig = config.nodeConf() - .withValue("baseDirectory", valueFor(baseDir.toString())) - .withFallback(ConfigFactory.parseResources("reference.conf")) - .resolve() - val custom = nodeConfig.getConfig("custom") - assertEquals(listOf("GBP"), custom.getAnyRefList("issuableCurrencies")) + assertEquals(listOf("GBP"), config.financeConf().getStringList("issuableCurrencies")) } @Test diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index b44c2b7af4..fd4c43fcf9 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -18,14 +18,19 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.GBP import net.corda.finance.USD import net.corda.finance.contracts.asset.Cash -import net.corda.finance.flows.* +import net.corda.finance.flows.AbstractCashFlow +import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashExitFlow.ExitRequest +import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest +import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.internal.CashConfigDataFlow import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.driver.* import net.corda.testing.node.User +import net.corda.testing.node.internal.FINANCE_CORDAPP import java.time.Instant import java.util.* @@ -63,16 +68,27 @@ class ExplorerSimulation(private val options: OptionSet) { private fun startDemoNodes() { val portAllocation = PortAllocation.Incremental(20000) - driver(DriverParameters(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true))) { + driver(DriverParameters( + portAllocation = portAllocation, + cordappsForAllNodes = listOf(FINANCE_CORDAPP), + waitForAllNodesToFinish = true, + jmxPolicy = JmxPolicy(true) + )) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)) val bob = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)) val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") - val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager), - customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("GBP")))) - val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager), - customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD")))) + val issuerGBP = startNode( + providedName = ukBankName, + rpcUsers = listOf(manager), + additionalCordapps = listOf(FINANCE_CORDAPP.withConfig(mapOf("issuableCurrencies" to listOf("GBP")))) + ) + val issuerUSD = startNode( + providedName = usaBankName, + rpcUsers = listOf(manager), + additionalCordapps = listOf(FINANCE_CORDAPP.withConfig(mapOf("issuableCurrencies" to listOf("USD")))) + ) notaryNode = defaultNotaryNode.get() aliceNode = alice.get() diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt index 56049a35ee..3226466630 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt @@ -7,7 +7,7 @@ import net.corda.client.jfx.utils.ChosenList import net.corda.client.jfx.utils.map import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.finance.flows.CashConfigDataFlow +import net.corda.finance.internal.CashConfigDataFlow import tornadofx.* import java.util.* From 268b544b4b94fec0f9a9f2231afeedf0ded46a40 Mon Sep 17 00:00:00 2001 From: Lamar Thomas <38670842+r3ltsupport@users.noreply.github.com> Date: Fri, 19 Oct 2018 15:23:48 -0400 Subject: [PATCH 76/83] fixed order of repudiation and information disclos --- docs/source/design/threat-model/corda-threat-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/design/threat-model/corda-threat-model.md b/docs/source/design/threat-model/corda-threat-model.md index 83e5a1da1f..cbec1a1769 100644 --- a/docs/source/design/threat-model/corda-threat-model.md +++ b/docs/source/design/threat-model/corda-threat-model.md @@ -46,8 +46,8 @@ threats is the [STRIDE](https://en.wikipedia.org/wiki/STRIDE_(security)) framewo - Spoofing - Tampering -- Information Disclosure - Repudiation +- Information Disclosure - Denial of Service - Elevation of Privilege From ce9f95ca86b613d462c4b77541e55ef92d83d21b Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 23 Oct 2018 11:08:30 +0100 Subject: [PATCH 77/83] ENT-2610: Correct `withBaseDirectory` method (#4106) Even though it is not used in OS, it is better to keep it in sync with Ent. --- .../corda/testing/internal/stubs/CertificateStoreStubs.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt index dfe21245f1..c93aeaeebe 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt @@ -55,9 +55,12 @@ class CertificateStoreStubs { } @JvmStatic - fun withBaseDirectory(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { + fun withBaseDirectory(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, + keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, + keyPassword: String = keyStorePassword, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, + trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { - return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStoreFileName, keyStorePassword, trustStoreFileName, trustStorePassword) + return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStoreFileName, keyStorePassword, keyPassword, trustStoreFileName, trustStorePassword) } } From b9aa23d3ac44eedbe1987d4a60002a5534592362 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Tue, 23 Oct 2018 11:12:29 +0100 Subject: [PATCH 78/83] ENT-2431 Move some changes from enterprise to OS (#4101) --- .../main/kotlin/net/corda/node/utilities/NodeNamedCache.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index 5c963ac80d..5c2b9a1241 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -27,13 +27,13 @@ interface BindableNamedCacheFactory : NamedCacheFactory, SerializeAsToken { fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory } -open class DefaultNamedCacheFactory private constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : BindableNamedCacheFactory, SingletonSerializeAsToken() { +open class DefaultNamedCacheFactory protected constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : BindableNamedCacheFactory, SingletonSerializeAsToken() { constructor() : this(null, null) override fun bindWithMetrics(metricRegistry: MetricRegistry): BindableNamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration) override fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration) - protected fun configuredForNamed(caffeine: Caffeine, name: String): Caffeine { + open protected fun configuredForNamed(caffeine: Caffeine, name: String): Caffeine { return with(nodeConfiguration!!) { when { name.startsWith("RPCSecurityManagerShiroCache_") -> with(security?.authService?.options?.cache!!) { caffeine.maximumSize(maxEntries).expireAfterWrite(expireAfterSecs, TimeUnit.SECONDS) } @@ -77,5 +77,5 @@ open class DefaultNamedCacheFactory private constructor(private val metricRegist return configuredForNamed(caffeine, name).build(loader) } - protected val defaultCacheSize = 1024L + open protected val defaultCacheSize = 1024L } \ No newline at end of file From f8ac35df25459651d47ef768ba18f945573ad4ad Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 23 Oct 2018 16:39:48 +0200 Subject: [PATCH 79/83] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 31cdf7d71c..aa4befbc67 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -180,7 +180,7 @@ see changes to this list. * Scott James * Sean Zhang (Wells Fargo) * Shams Asari (R3) -* Shivan Sawant (Persistent Systems Limited) +* Shivan Sawant * Siddhartha Sengupta (Tradewind Markets) * Simon Taylor (Barclays) * Sofus Mortensen (Digital Asset Holdings) From 0919b01271b7b26c204879fe89429058358dc1de Mon Sep 17 00:00:00 2001 From: Stefano Franz Date: Tue, 23 Oct 2018 16:45:07 +0100 Subject: [PATCH 80/83] ENT-2509 - Make @InitiatedBy flows overridable via node config (#3960) * first attempt at a flowManager fix test breakages add testing around registering subclasses make flowManager a param of MockNode extract interface rename methods more work around overriding flows more test fixes add sample project showing how to use flowOverrides rebase * make smallest possible changes to AttachmentSerializationTest and ReceiveAllFlowTests * add some comments about how flow manager weights flows * address review comments add documentation * address more review comments --- constants.properties | 2 +- .../net/corda/core/contracts/Attachment.kt | 1 - .../core/contracts/AttachmentConstraint.kt | 1 - .../core/internal/cordapp/CordappImpl.kt | 2 +- .../core/transactions/TransactionBuilder.kt | 5 +- .../net/corda/core/flows/FlowTestsUtils.kt | 36 +-- .../corda/core/flows/ReceiveAllFlowTests.kt | 17 +- .../internal/JarSignatureCollectorTest.kt | 1 - .../AttachmentSerializationTest.kt | 15 +- docs/source/flow-overriding.rst | 141 +++++++++++ .../net/corda/node/flows/FlowOverrideTests.kt | 85 +++++++ .../statemachine/FlowVersioningTest.kt | 11 +- .../net/corda/node/internal/AbstractNode.kt | 106 ++------- .../net/corda/node/internal/FlowManager.kt | 222 ++++++++++++++++++ .../node/internal/InitiatedFlowFactory.kt | 2 + .../kotlin/net/corda/node/internal/Node.kt | 19 +- .../cordapp/JarScanningCordappLoader.kt | 24 +- .../node/services/config/NodeConfiguration.kt | 7 +- .../node/internal/FlowRegistrationTest.kt | 42 +++- .../node/internal/NodeFlowManagerTest.kt | 110 +++++++++ .../net/corda/node/internal/NodeTest.kt | 5 +- .../cordapp/JarScanningCordappLoaderTest.kt | 4 +- .../config/NodeConfigurationImplTest.kt | 7 +- .../statemachine/FlowFrameworkTests.kt | 90 ++++--- .../vault/VaultSoftLockManagerTest.kt | 11 +- samples/trader-demo/build.gradle | 14 ++ .../net/corda/traderdemo/flow/BuyerFlow.kt | 36 +-- .../corda/traderdemo/flow/LoggingBuyerFlow.kt | 48 ++++ .../kotlin/net/corda/testing/driver/Driver.kt | 3 +- .../net/corda/testing/driver/DriverDSL.kt | 26 +- .../testing/driver/internal/DriverInternal.kt | 7 +- .../net/corda/testing/node/MockNetwork.kt | 34 ++- .../testing/node/internal/DriverDSLImpl.kt | 26 +- .../node/internal/InternalMockNetwork.kt | 80 +++++-- .../testing/node/internal/NodeBasedTest.kt | 12 +- .../testing/internal/MockCordappProvider.kt | 2 - 36 files changed, 930 insertions(+), 324 deletions(-) create mode 100644 docs/source/flow-overriding.rst create mode 100644 node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/FlowManager.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt create mode 100644 samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt diff --git a/constants.properties b/constants.properties index 8fde90cd02..9ed2417171 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=4.0.32 +gradlePluginsVersion=4.0.33 kotlinVersion=1.2.71 # ***************************************************************# # When incrementing platformVersion make sure to update # diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index d17d053e1f..0535f051e6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -1,7 +1,6 @@ package net.corda.core.contracts import net.corda.core.KeepForDJVM -import net.corda.core.identity.Party import net.corda.core.internal.extractFile import net.corda.core.serialization.CordaSerializable import java.io.FileNotFoundException diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 76ffc8a866..55be99325b 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -3,7 +3,6 @@ package net.corda.core.contracts import net.corda.core.DoNotImplement import net.corda.core.KeepForDJVM import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy import net.corda.core.internal.AttachmentWithContext diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 8ab16a5190..2d6075a5e3 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -43,7 +43,7 @@ data class CordappImpl( */ override val cordappClasses: List = run { val classList = rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass } + notaryService - classList.mapNotNull { it?.name } + contractClassNames + classList.mapNotNull { it?.name } + contractClassNames } // TODO Why a seperate Info class and not just have the fields directly in CordappImpl? diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 99c069de88..e52bda5456 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -5,7 +5,10 @@ import net.corda.core.CordaInternal import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.* +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.ensureMinimumPlatformVersion diff --git a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt index fbb2d3bad2..ea3b44a98b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt @@ -1,10 +1,13 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.core.concurrent.CordaFuture +import net.corda.core.toFuture import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap import net.corda.node.internal.InitiatedFlowFactory import net.corda.testing.node.internal.TestStartedNode +import rx.Observable import kotlin.reflect.KClass /** @@ -34,20 +37,6 @@ class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic() { override fun call() = closure() } -/** - * Allows to register a flow of type [R] against an initiating flow of type [I]. - */ -inline fun , reified R : FlowLogic<*>> TestStartedNode.registerInitiatedFlow(initiatingFlowType: KClass, crossinline construct: (session: FlowSession) -> R) { - registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true) -} - -/** - * Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R]. - */ -inline fun , reified R : Any> TestStartedNode.registerAnswer(initiatingFlowType: KClass, value: R) { - registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true) -} - /** * Extracts data from a [Map[FlowSession, UntrustworthyData]] without performing checks and casting to [R]. */ @@ -112,4 +101,23 @@ inline fun FlowLogic<*>.receiveAll(session: FlowSession, varar private fun Array>>.enforceNoDuplicates() { require(this.size == this.toSet().size) { "A flow session can only appear once as argument." } +} + +inline fun > TestStartedNode.registerCordappFlowFactory( + initiatingFlowClass: KClass>, + initiatedFlowVersion: Int = 1, + noinline flowFactory: (FlowSession) -> P): CordaFuture

    { + + val observable = internals.registerInitiatedFlowFactory( + initiatingFlowClass.java, + P::class.java, + InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory), + track = true) + return observable.toFuture() +} + +fun > TestStartedNode.registerCoreFlowFactory(initiatingFlowClass: Class>, + initiatedFlowClass: Class, + flowFactory: (FlowSession) -> T , track: Boolean): Observable { + return this.internals.registerInitiatedFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory), track) } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt index c546a06c20..ec56bb1237 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -2,16 +2,18 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import com.natpryce.hamkrest.assertion.assert -import net.corda.testing.internal.matchers.flow.willReturn import net.corda.core.flows.mixins.WithMockNet import net.corda.core.identity.Party import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap import net.corda.testing.core.singleIdentity +import net.corda.testing.internal.matchers.flow.willReturn import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.TestStartedNode import org.assertj.core.api.Assertions.assertThat import org.junit.AfterClass import org.junit.Test +import kotlin.reflect.KClass class ReceiveMultipleFlowTests : WithMockNet { @@ -43,7 +45,7 @@ class ReceiveMultipleFlowTests : WithMockNet { } } - nodes[1].registerInitiatedFlow(initiatingFlow::class) { session -> + nodes[1].registerCordappFlowFactory(initiatingFlow::class) { session -> object : FlowLogic() { @Suspendable override fun call() { @@ -123,4 +125,15 @@ class ReceiveMultipleFlowTests : WithMockNet { return double * string.length } } +} + +private inline fun TestStartedNode.registerAnswer(kClass: KClass>, value1: T) { + this.registerCordappFlowFactory(kClass) { session -> + object : FlowLogic() { + @Suspendable + override fun call() { + session.send(value1!!) + } + } + } } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt index 7f83f23a47..1a379a9f09 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt @@ -8,7 +8,6 @@ import net.corda.core.JarSignatureTestUtils.updateJar import net.corda.core.identity.Party import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.CHARLIE_NAME import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.AfterClass diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 3ba769ce88..c89e1d0c17 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -3,16 +3,12 @@ package net.corda.core.serialization import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.TestNoSecurityDataVendingFlow +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchDataFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.persistence.NodeAttachmentService import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.testing.core.ALICE_NAME @@ -151,11 +147,10 @@ class AttachmentSerializationTest { } private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) { - server.registerFlowFactory( - ClientLogic::class.java, - InitiatedFlowFactory.Core { ServerLogic(it, sendData) }, - ServerLogic::class.java, - track = false) + server.registerCordappFlowFactory( + ClientLogic::class, + 1 + ) { ServerLogic(it, sendData) } client.services.startFlow(clientLogic) mockNet.runNetwork(rounds) } diff --git a/docs/source/flow-overriding.rst b/docs/source/flow-overriding.rst new file mode 100644 index 0000000000..273ff7fa03 --- /dev/null +++ b/docs/source/flow-overriding.rst @@ -0,0 +1,141 @@ +Configuring Responder Flows +=========================== + +A flow can be a fairly complex thing that interacts with many backend systems, and so it is likely that different users +of a specific CordApp will require differences in how flows interact with their specific infrastructure. + +Corda supports this functionality by providing two mechanisms to modify the behaviour of apps in your node. + +Subclassing a Flow +------------------ + +If you have a workflow which is mostly common, but also requires slight alterations in specific situations, most developers would be familiar +with refactoring into `Base` and `Sub` classes. A simple example is shown below. + +java +~~~~ + + .. code-block:: java + + @InitiatingFlow + public class Initiator extends FlowLogic { + private final Party otherSide; + + public Initiator(Party otherSide) { + this.otherSide = otherSide; + } + + @Override + public String call() throws FlowException { + return initiateFlow(otherSide).receive(String.class).unwrap((it) -> it); + } + } + + @InitiatedBy(Initiator.class) + public class BaseResponder extends FlowLogic { + private FlowSession counterpartySession; + + public BaseResponder(FlowSession counterpartySession) { + super(); + this.counterpartySession = counterpartySession; + } + + @Override + public Void call() throws FlowException { + counterpartySession.send(getMessage()); + return Void; + } + + + protected String getMessage() { + return "This Is the Legacy Responder"; + } + } + + public class SubResponder extends BaseResponder { + + public SubResponder(FlowSession counterpartySession) { + super(counterpartySession); + } + + @Override + protected String getMessage() { + return "This is the sub responder"; + } + } + + + +kotlin +~~~~~~ + + .. code-block:: kotlin + + @InitiatedBy(Initiator::class) + open class BaseResponder(internal val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + otherSideSession.send(getMessage()) + } + protected open fun getMessage() = "This Is the Legacy Responder" + } + + @InitiatedBy(Initiator::class) + class SubResponder(otherSideSession: FlowSession) : BaseResponder(otherSideSession) { + override fun getMessage(): String { + return "This is the sub responder" + } + } + + + + + +Corda would detect that both ``BaseResponder`` and ``SubResponder`` are configured for responding to ``Initiator``. +Corda will then calculate the hops to ``FlowLogic`` and select the implementation which is furthest distance, ie: the most subclassed implementation. +In the above example, ``SubResponder`` would be selected as the default responder for ``Initiator`` + +.. note:: The flows do not need to be within the same CordApp, or package, therefore to customise a shared app you obtained from a third party, you'd write your own CorDapp that subclasses the first." + +Overriding a flow via node configuration +---------------------------------------- + +Whilst the subclassing approach is likely to be useful for most applications, there is another mechanism to override this behaviour. +This would be useful if for example, a specific CordApp user requires such a different responder that subclassing an existing flow +would not be a good solution. In this case, it's possible to specify a hardcoded flow via the node configuration. + +The configuration section is named ``flowOverrides`` and it accepts an array of ``overrides`` + +.. container:: codeset + + .. code-block:: json + + flowOverrides { + overrides=[ + { + initiator="net.corda.Initiator" + responder="net.corda.BaseResponder" + } + ] + } + +The cordform plugin also provides a ``flowOverride`` method within the ``deployNodes`` block which can be used to override a flow. In the below example, we will override +the ``SubResponder`` with ``BaseResponder`` + +.. container:: codeset + + .. code-block:: groovy + + node { + name "O=Bank,L=London,C=GB" + p2pPort 10025 + rpcUsers = ext.rpcUsers + rpcSettings { + address "localhost:10026" + adminAddress "localhost:10027" + } + extraConfig = ['h2Settings.address' : 'localhost:10035'] + flowOverride("net.corda.Initiator", "net.corda.BaseResponder") + } + +This will generate the corresponding ``flowOverrides`` section and place it in the configuration for that node. \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt new file mode 100644 index 0000000000..881daf18cf --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt @@ -0,0 +1,85 @@ +package net.corda.node.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.internal.cordappForClasses +import org.hamcrest.CoreMatchers.`is` +import org.junit.Assert +import org.junit.Test + +class FlowOverrideTests { + + @StartableByRPC + @InitiatingFlow + class Ping(private val pongParty: Party) : FlowLogic() { + @Suspendable + override fun call(): String { + val pongSession = initiateFlow(pongParty) + return pongSession.sendAndReceive("PING").unwrap { it } + } + } + + @InitiatedBy(Ping::class) + open class Pong(private val pingSession: FlowSession) : FlowLogic() { + companion object { + val PONG = "PONG" + } + + @Suspendable + override fun call() { + pingSession.send(PONG) + } + } + + @InitiatedBy(Ping::class) + class Pong2(private val pingSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + pingSession.send("PONGPONG") + } + } + + @InitiatedBy(Ping::class) + class Pongiest(private val pingSession: FlowSession) : Pong(pingSession) { + + companion object { + val GORGONZOLA = "Gorgonzola" + } + + @Suspendable + override fun call() { + pingSession.send(GORGONZOLA) + } + } + + private val nodeAClasses = setOf(Ping::class.java, + Pong::class.java, Pongiest::class.java) + private val nodeBClasses = setOf(Ping::class.java, Pong::class.java) + + @Test + fun `should use the most "specific" implementation of a responding flow`() { + driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) { + val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow() + val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow() + Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pongiest.GORGONZOLA)) + } + } + + @Test + fun `should use the overriden implementation of a responding flow`() { + val flowOverrides = mapOf(Ping::class.java to Pong::class.java) + driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) { + val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray())), flowOverrides = flowOverrides).getOrThrow() + val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow() + Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pong.PONG)) + } + } + +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt index f4b3531b13..1248d75b4f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt @@ -7,10 +7,11 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.testing.core.singleIdentity -import net.corda.testing.node.internal.NodeBasedTest +import net.corda.node.internal.NodeFlowManager import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.internal.startFlow import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -18,9 +19,10 @@ import org.junit.Test class FlowVersioningTest : NodeBasedTest() { @Test fun `getFlowContext returns the platform version for core flows`() { + val bobFlowManager = NodeFlowManager() val alice = startNode(ALICE_NAME, platformVersion = 2) - val bob = startNode(BOB_NAME, platformVersion = 3) - bob.node.installCoreFlow(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow) + val bob = startNode(BOB_NAME, platformVersion = 3, flowManager = bobFlowManager) + bobFlowManager.registerInitiatedCoreFlowFactory(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow) val (alicePlatformVersionAccordingToBob, bobPlatformVersionAccordingToAlice) = alice.services.startFlow( PretendInitiatingCoreFlow(bob.info.singleIdentity())).resultFuture.getOrThrow() assertThat(alicePlatformVersionAccordingToBob).isEqualTo(2) @@ -45,4 +47,5 @@ class FlowVersioningTest : NodeBasedTest() { @Suspendable override fun call() = otherSideSession.send(otherSideSession.getCounterpartyFlowInfo().flowVersion) } + } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index d8b9aa15a6..2d12f73a9e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -30,7 +30,10 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.* +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.days +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.minutes import net.corda.node.CordaClock import net.corda.node.SerialFilter import net.corda.node.VersionInfo @@ -99,13 +102,10 @@ import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException import java.util.* -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS -import kotlin.collections.set -import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair /** @@ -120,11 +120,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val platformClock: CordaClock, cacheFactoryPrototype: BindableNamedCacheFactory, protected val versionInfo: VersionInfo, + protected val flowManager: FlowManager, protected val serverThread: AffinityExecutor.ServiceAffinityExecutor, private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { protected abstract val log: Logger - @Suppress("LeakingThis") private var tokenizableServices: MutableList? = mutableListOf(platformClock, this) @@ -211,7 +211,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ).tokenize().closeOnStop() private val cordappServices = MutableClassToInstanceMap.create() - private val flowFactories = ConcurrentHashMap>, InitiatedFlowFactory<*>>() private val shutdownExecutor = Executors.newSingleThreadExecutor() protected abstract val transactionVerifierWorkerCount: Int @@ -237,7 +236,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private var _started: S? = null private fun T.tokenize(): T { - tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised") + tokenizableServices?.add(this) + ?: throw IllegalStateException("The tokenisable services list has already been finalised") return this } @@ -607,91 +607,27 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } private fun registerCordappFlows() { - cordappLoader.cordapps.flatMap { it.initiatedFlows } - .forEach { + cordappLoader.cordapps.forEach { cordapp -> + cordapp.initiatedFlows.groupBy { it.requireAnnotation().value.java }.forEach { initiator, responders -> + responders.forEach { responder -> try { - registerInitiatedFlowInternal(smm, it, track = false) + flowManager.registerInitiatedFlow(initiator, responder) } catch (e: NoSuchMethodException) { - log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " + + log.error("${responder.name}, as an initiated flow, must have a constructor with a single parameter " + "of type ${Party::class.java.name}") - } catch (e: Exception) { - log.error("Unable to register initiated flow ${it.name}", e) + throw e } } - } - - fun > registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class): Observable { - return registerInitiatedFlowInternal(smm, initiatedFlowClass, track = true) - } - - // TODO remove once not needed - private fun deprecatedFlowConstructorMessage(flowClass: Class<*>): String { - return "Installing flow factory for $flowClass accepting a ${Party::class.java.simpleName}, which is deprecated. " + - "It should accept a ${FlowSession::class.java.simpleName} instead" - } - - private fun > registerInitiatedFlowInternal(smm: StateMachineManager, initiatedFlow: Class, track: Boolean): Observable { - val constructors = initiatedFlow.declaredConstructors.associateBy { it.parameterTypes.toList() } - val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true } - val ctor: (FlowSession) -> F = if (flowSessionCtor == null) { - // Try to fallback to a Party constructor - val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true } - if (partyCtor == null) { - throw IllegalArgumentException("$initiatedFlow must have a constructor accepting a ${FlowSession::class.java.name}") - } else { - log.warn(deprecatedFlowConstructorMessage(initiatedFlow)) } - { flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) } - } else { - { flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) } } - val initiatingFlow = initiatedFlow.requireAnnotation().value.java - val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass - require(classWithAnnotation == initiatingFlow) { - "${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiatingFlow.name}" - } - val flowFactory = InitiatedFlowFactory.CorDapp(version, initiatedFlow.appName, ctor) - val observable = internalRegisterFlowFactory(smm, initiatingFlow, flowFactory, initiatedFlow, track) - log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)") - return observable - } - - protected fun > internalRegisterFlowFactory(smm: StateMachineManager, - initiatingFlowClass: Class>, - flowFactory: InitiatedFlowFactory, - initiatedFlowClass: Class, - track: Boolean): Observable { - val observable = if (track) { - smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) - } else { - Observable.empty() - } - check(initiatingFlowClass !in flowFactories.keys) { - "$initiatingFlowClass is attempting to register multiple initiated flows" - } - flowFactories[initiatingFlowClass] = flowFactory - return observable - } - - /** - * Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using - * [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards - * compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party. - */ - @VisibleForTesting - fun installCoreFlow(clientFlowClass: KClass>, flowFactory: (FlowSession) -> FlowLogic<*>) { - require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) { - "${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version" - } - flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory) - log.debug { "Installed core flow ${clientFlowClass.java.name}" } + flowManager.validateRegistrations() } private fun installCoreFlows() { - installCoreFlow(FinalityFlow::class, ::FinalityHandler) - installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler) - installCoreFlow(ContractUpgradeFlow.Initiate::class, ::ContractUpgradeHandler) - installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler) + flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class, ::FinalityHandler) + flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler) + flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler) + flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler) } protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { @@ -781,7 +717,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, service.run { tokenize() runOnStop += ::stop - installCoreFlow(NotaryFlow.Client::class, ::createServiceFlow) + flowManager.registerInitiatedCoreFlowFactory(NotaryFlow.Client::class, ::createServiceFlow) start() } return service @@ -961,7 +897,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { - return flowFactories[initiatingFlowClass] + return flowManager.getFlowFactoryForInitiatingFlow(initiatingFlowClass) } override fun jdbcSession(): Connection = database.createSession() @@ -1066,4 +1002,4 @@ fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSsl } // Here we're using the node's RPC key store as the RPC client's trust store. return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword) -} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/FlowManager.kt b/node/src/main/kotlin/net/corda/node/internal/FlowManager.kt new file mode 100644 index 0000000000..68aa8ff056 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/FlowManager.kt @@ -0,0 +1,222 @@ +package net.corda.node.internal + +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.internal.uncheckedCast +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.services.config.FlowOverrideConfig +import net.corda.node.services.statemachine.appName +import net.corda.node.services.statemachine.flowVersionAndInitiatingClass +import javax.annotation.concurrent.ThreadSafe +import kotlin.reflect.KClass + +/** + * + * This class is responsible for organising which flow should respond to a specific @InitiatingFlow + * + * There are two main ways to modify the behaviour of a cordapp with regards to responding with a different flow + * + * 1.) implementing a new subclass. For example, if we have a ResponderFlow similar to @InitiatedBy(Sender) MyBaseResponder : FlowLogic + * If we subclassed a new Flow with specific logic for DB2, it would be similar to IBMB2Responder() : MyBaseResponder + * When these two flows are encountered by the classpath scan for @InitiatedBy, they will both be selected for responding to Sender + * This implementation will sort them for responding in order of their "depth" from FlowLogic - see: FlowWeightComparator + * So IBMB2Responder would win and it would be selected for responding + * + * 2.) It is possible to specify a flowOverride key in the node configuration. Say we configure a node to have + * flowOverrides{ + * "Sender" = "MyBaseResponder" + * } + * In this case, FlowWeightComparator would detect that there is an override in action, and it will assign MyBaseResponder a maximum weight + * This will result in MyBaseResponder being selected for responding to Sender + * + * + */ +interface FlowManager { + + fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, flowFactory: (FlowSession) -> FlowLogic<*>) + fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: (FlowSession) -> FlowLogic<*>) + fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: InitiatedFlowFactory.Core>) + + fun > registerInitiatedFlow(initiator: Class>, responder: Class) + fun > registerInitiatedFlow(responder: Class) + + fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class>): InitiatedFlowFactory<*>? + + fun validateRegistrations() +} + +@ThreadSafe +open class NodeFlowManager(flowOverrides: FlowOverrideConfig? = null) : FlowManager { + + private val flowFactories = HashMap>, MutableList>() + private val flowOverrides = (flowOverrides + ?: FlowOverrideConfig()).overrides.map { it.initiator to it.responder }.toMutableMap() + + companion object { + private val log = contextLogger() + + } + + @Synchronized + override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class>): InitiatedFlowFactory<*>? { + return flowFactories[initiatedFlowClass]?.firstOrNull()?.flowFactory + } + + @Synchronized + override fun > registerInitiatedFlow(responder: Class) { + return registerInitiatedFlow(responder.requireAnnotation().value.java, responder) + } + + @Synchronized + override fun > registerInitiatedFlow(initiator: Class>, responder: Class) { + val constructors = responder.declaredConstructors.associateBy { it.parameterTypes.toList() } + val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true } + val ctor: (FlowSession) -> F = if (flowSessionCtor == null) { + // Try to fallback to a Party constructor + val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true } + if (partyCtor == null) { + throw IllegalArgumentException("$responder must have a constructor accepting a ${FlowSession::class.java.name}") + } else { + log.warn("Installing flow factory for $responder accepting a ${Party::class.java.simpleName}, which is deprecated. " + + "It should accept a ${FlowSession::class.java.simpleName} instead") + } + { flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) } + } else { + { flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) } + } + val (version, classWithAnnotation) = initiator.flowVersionAndInitiatingClass + require(classWithAnnotation == initiator) { + "${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiator.name}" + } + val flowFactory = InitiatedFlowFactory.CorDapp(version, responder.appName, ctor) + registerInitiatedFlowFactory(initiator, flowFactory, responder) + log.info("Registered ${initiator.name} to initiate ${responder.name} (version $version)") + } + + private fun > registerInitiatedFlowFactory(initiatingFlowClass: Class>, + flowFactory: InitiatedFlowFactory, + initiatedFlowClass: Class?) { + + check(flowFactory !is InitiatedFlowFactory.Core) { "This should only be used for Cordapp flows" } + val listOfFlowsForInitiator = flowFactories.computeIfAbsent(initiatingFlowClass) { mutableListOf() } + if (listOfFlowsForInitiator.isNotEmpty() && listOfFlowsForInitiator.first().type == FlowType.CORE) { + throw IllegalStateException("Attempting to register over an existing platform flow: $initiatingFlowClass") + } + synchronized(listOfFlowsForInitiator) { + val flowToAdd = RegisteredFlowContainer(initiatingFlowClass, initiatedFlowClass, flowFactory, FlowType.CORDAPP) + val flowWeightComparator = FlowWeightComparator(initiatingFlowClass, flowOverrides) + listOfFlowsForInitiator.add(flowToAdd) + listOfFlowsForInitiator.sortWith(flowWeightComparator) + if (listOfFlowsForInitiator.size > 1) { + log.warn("Multiple flows are registered for InitiatingFlow: $initiatingFlowClass, currently using: ${listOfFlowsForInitiator.first().initiatedFlowClass}") + } + } + + } + + // TODO Harmonise use of these methods - 99% of invocations come from tests. + @Synchronized + override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: (FlowSession) -> FlowLogic<*>) { + registerInitiatedCoreFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory)) + } + + @Synchronized + override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, flowFactory: (FlowSession) -> FlowLogic<*>) { + registerInitiatedCoreFlowFactory(initiatingFlowClass, null, InitiatedFlowFactory.Core(flowFactory)) + } + + @Synchronized + override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: InitiatedFlowFactory.Core>) { + require(initiatingFlowClass.java.flowVersionAndInitiatingClass.first == 1) { + "${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version" + } + flowFactories.computeIfAbsent(initiatingFlowClass.java) { mutableListOf() }.add( + RegisteredFlowContainer( + initiatingFlowClass.java, + initiatedFlowClass?.java, + flowFactory, + FlowType.CORE) + ) + log.debug { "Installed core flow ${initiatingFlowClass.java.name}" } + } + + // To verify the integrity of the current state, it is important that the tip of the responders is a unique weight + // if there are multiple flows with the same weight as the tip, it means that it is impossible to reliably pick one as the responder + private fun validateInvariants(toValidate: List) { + val currentTip = toValidate.first() + val flowWeightComparator = FlowWeightComparator(currentTip.initiatingFlowClass, flowOverrides) + val equalWeightAsCurrentTip = toValidate.map { flowWeightComparator.compare(currentTip, it) to it }.filter { it.first == 0 }.map { it.second } + if (equalWeightAsCurrentTip.size > 1) { + val message = "Unable to determine which flow to use when responding to: ${currentTip.initiatingFlowClass.canonicalName}. ${equalWeightAsCurrentTip.map { it.initiatedFlowClass!!.canonicalName }} are all registered with equal weight." + throw IllegalStateException(message) + } + } + + @Synchronized + override fun validateRegistrations() { + flowFactories.values.forEach { + validateInvariants(it) + } + } + + private enum class FlowType { + CORE, CORDAPP + } + + private data class RegisteredFlowContainer(val initiatingFlowClass: Class>, + val initiatedFlowClass: Class>?, + val flowFactory: InitiatedFlowFactory>, + val type: FlowType) + + // this is used to sort the responding flows in order of "importance" + // the logic is as follows + // IF responder is a specific lambda (like for notary implementations / testing code) always return that responder + // ELSE IF responder is present in the overrides list, always return that responder + // ELSE compare responding flows by their depth from FlowLogic, always return the flow which is most specific (IE, has the most hops to FlowLogic) + private open class FlowWeightComparator(val initiatingFlowClass: Class>, val flowOverrides: Map) : Comparator { + + override fun compare(o1: NodeFlowManager.RegisteredFlowContainer, o2: NodeFlowManager.RegisteredFlowContainer): Int { + if (o1.initiatedFlowClass == null && o2.initiatedFlowClass != null) { + return Int.MIN_VALUE + } + if (o1.initiatedFlowClass != null && o2.initiatedFlowClass == null) { + return Int.MAX_VALUE + } + + if (o1.initiatedFlowClass == null && o2.initiatedFlowClass == null) { + return 0 + } + + val hopsTo1 = calculateHopsToFlowLogic(initiatingFlowClass, o1.initiatedFlowClass!!) + val hopsTo2 = calculateHopsToFlowLogic(initiatingFlowClass, o2.initiatedFlowClass!!) + return hopsTo1.compareTo(hopsTo2) * -1 + } + + private fun calculateHopsToFlowLogic(initiatingFlowClass: Class>, + initiatedFlowClass: Class>): Int { + + val overriddenClassName = flowOverrides[initiatingFlowClass.canonicalName] + return if (overriddenClassName == initiatedFlowClass.canonicalName) { + Int.MAX_VALUE + } else { + var currentClass: Class<*> = initiatedFlowClass + var count = 0 + while (currentClass != FlowLogic::class.java) { + currentClass = currentClass.superclass + count++ + } + count; + } + } + + } +} + +private fun Iterable>.toMutableMap(): MutableMap { + return this.toMap(HashMap()) +} diff --git a/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt index 3b86147c4e..9d00e83a28 100644 --- a/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt +++ b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt @@ -4,10 +4,12 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession sealed class InitiatedFlowFactory> { + protected abstract val factory: (FlowSession) -> F fun createFlow(initiatingFlowSession: FlowSession): F = factory(initiatingFlowSession) data class Core>(override val factory: (FlowSession) -> F) : InitiatedFlowFactory() + data class CorDapp>(val flowVersion: Int, val appName: String, override val factory: (FlowSession) -> F) : InitiatedFlowFactory() diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index c6cad69913..27523d9ccb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -43,6 +43,7 @@ import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.* import net.corda.node.services.messaging.* import net.corda.node.services.rpc.ArtemisRpcBroker +import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.utilities.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER import net.corda.nodeapi.internal.ShutdownHook @@ -56,7 +57,6 @@ import org.apache.commons.lang.SystemUtils import org.h2.jdbc.JdbcSQLException import org.slf4j.Logger import org.slf4j.LoggerFactory -import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers import java.net.BindException @@ -72,8 +72,7 @@ import kotlin.system.exitProcess class NodeWithInfo(val node: Node, val info: NodeInfo) { val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {} fun dispose() = node.stop() - fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable = - node.registerInitiatedFlow(node.smm, initiatedFlowClass) + fun > registerInitiatedFlow(initiatedFlowClass: Class) = node.registerInitiatedFlow(node.smm, initiatedFlowClass) } /** @@ -85,12 +84,14 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) { open class Node(configuration: NodeConfiguration, versionInfo: VersionInfo, private val initialiseSerialization: Boolean = true, + flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides), cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory() ) : AbstractNode( configuration, createClock(configuration), cacheFactoryPrototype, versionInfo, + flowManager, // Under normal (non-test execution) it will always be "1" AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1) ) { @@ -202,7 +203,8 @@ open class Node(configuration: NodeConfiguration, return P2PMessagingClient( config = configuration, versionInfo = versionInfo, - serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port), + serverAddress = configuration.messagingServerAddress + ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port), nodeExecutor = serverThread, database = database, networkMap = networkMapCache, @@ -228,7 +230,8 @@ open class Node(configuration: NodeConfiguration, } val messageBroker = if (!configuration.messagingServerExternal) { - val brokerBindAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port) + val brokerBindAddress = configuration.messagingServerAddress + ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port) ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize) } else { null @@ -442,7 +445,7 @@ open class Node(configuration: NodeConfiguration, }.build().start() } - private fun registerNewRelicReporter (registry: MetricRegistry) { + private fun registerNewRelicReporter(registry: MetricRegistry) { log.info("Registering New Relic JMX Reporter:") val reporter = NewRelicReporter.forRegistry(registry) .name("New Relic Reporter") @@ -504,4 +507,8 @@ open class Node(configuration: NodeConfiguration, log.info("Shutdown complete") } + + fun > registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class) { + this.flowManager.registerInitiatedFlow(initiatedFlowClass) + } } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index ac1badeafe..b1ba5f9f29 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -19,7 +19,6 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader -import net.corda.node.internal.classloading.requireAnnotation import net.corda.nodeapi.internal.coreContractClasses import net.corda.serialization.internal.DefaultWhitelist import org.apache.commons.collections4.map.LRUMap @@ -148,17 +147,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun findInitiatedFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) - // First group by the initiating flow class in case there are multiple mappings - .groupBy { it.requireAnnotation().value.java } - .map { (initiatingFlow, initiatedFlows) -> - val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow)) - if (sorted.size > 1) { - logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " + - "in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " + - "specific sub-type for registration: ${sorted[0].name}.") - } - sorted[0] - } } private fun Class>.isUserInvokable(): Boolean { @@ -209,17 +197,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } } - private class FlowTypeHierarchyComparator(val initiatingFlow: Class>) : Comparator>> { - override fun compare(o1: Class>, o2: Class>): Int { - return when { - o1 == o2 -> 0 - o1.isAssignableFrom(o2) -> 1 - o2.isAssignableFrom(o1) -> -1 - else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " + - "both ${o1.name} and ${o2.name}") - } - } - } + private fun loadClass(className: String, type: KClass): Class? { return try { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index eab762dcdd..3073d7574a 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -76,6 +76,7 @@ interface NodeConfiguration { val p2pSslOptions: MutualSslConfiguration val cordappDirectories: List + val flowOverrides: FlowOverrideConfig? fun validate(): List @@ -97,6 +98,9 @@ interface NodeConfiguration { } } +data class FlowOverrideConfig(val overrides: List = listOf()) +data class FlowOverride(val initiator: String, val responder: String) + /** * Currently registered JMX Reporters */ @@ -210,7 +214,8 @@ data class NodeConfigurationImpl( override val flowMonitorPeriodMillis: Duration = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS, override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS, override val cordappDirectories: List = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT), - override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA + override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA, + override val flowOverrides: FlowOverrideConfig? ) : NodeConfiguration { companion object { private val logger = loggerFor() diff --git a/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt b/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt index e6a9b5fd3e..8b59037248 100644 --- a/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt @@ -12,7 +12,6 @@ import net.corda.testing.core.singleIdentity import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.StartedMockNode -import org.assertj.core.api.Assertions.assertThatIllegalStateException import org.junit.After import org.junit.Before import org.junit.Test @@ -39,15 +38,15 @@ class FlowRegistrationTest { } @Test - fun `startup fails when two flows initiated by the same flow are registered`() { + fun `succeeds when a subclass of a flow initiated by the same flow is registered`() { // register the same flow twice to invoke the error without causing errors in other tests - responder.registerInitiatedFlow(Responder::class.java) - assertThatIllegalStateException().isThrownBy { responder.registerInitiatedFlow(Responder::class.java) } + responder.registerInitiatedFlow(Responder1::class.java) + responder.registerInitiatedFlow(Responder1Subclassed::class.java) } @Test fun `a single initiated flow can be registered without error`() { - responder.registerInitiatedFlow(Responder::class.java) + responder.registerInitiatedFlow(Responder1::class.java) val result = initiator.startFlow(Initiator(responder.info.singleIdentity())) mockNetwork.runNetwork() assertNotNull(result.get()) @@ -63,7 +62,38 @@ class Initiator(val party: Party) : FlowLogic() { } @InitiatedBy(Initiator::class) -private class Responder(val session: FlowSession) : FlowLogic() { +private open class Responder1(val session: FlowSession) : FlowLogic() { + open fun getPayload(): String { + return "whats up" + } + + @Suspendable + override fun call() { + session.receive().unwrap { it } + session.send("What's up") + } +} + +@InitiatedBy(Initiator::class) +private open class Responder2(val session: FlowSession) : FlowLogic() { + open fun getPayload(): String { + return "whats up" + } + + @Suspendable + override fun call() { + session.receive().unwrap { it } + session.send("What's up") + } +} + +@InitiatedBy(Initiator::class) +private class Responder1Subclassed(session: FlowSession) : Responder1(session) { + + override fun getPayload(): String { + return "im subclassed! that's what's up!" + } + @Suspendable override fun call() { session.receive().unwrap { it } diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt new file mode 100644 index 0000000000..25722ef295 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt @@ -0,0 +1,110 @@ +package net.corda.node.internal + +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.node.services.config.FlowOverride +import net.corda.node.services.config.FlowOverrideConfig +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.instanceOf +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import java.lang.IllegalStateException + +private val marker = "This is a special marker" + +class NodeFlowManagerTest { + + @InitiatingFlow + class Init : FlowLogic() { + override fun call() { + TODO("not implemented") + } + } + + @InitiatedBy(Init::class) + open class Resp(val otherSesh: FlowSession) : FlowLogic() { + override fun call() { + TODO("not implemented") + } + + } + + @InitiatedBy(Init::class) + class Resp2(val otherSesh: FlowSession) : FlowLogic() { + override fun call() { + TODO("not implemented") + } + + } + + @InitiatedBy(Init::class) + open class RespSub(sesh: FlowSession) : Resp(sesh) { + override fun call() { + TODO("not implemented") + } + + } + + @InitiatedBy(Init::class) + class RespSubSub(sesh: FlowSession) : RespSub(sesh) { + override fun call() { + TODO("not implemented") + } + + } + + + @Test(expected = IllegalStateException::class) + fun `should fail to validate if more than one registration with equal weight`() { + val nodeFlowManager = NodeFlowManager() + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java) + nodeFlowManager.validateRegistrations() + } + + @Test() + fun `should allow registration of flows with different weights`() { + val nodeFlowManager = NodeFlowManager() + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java) + nodeFlowManager.validateRegistrations() + val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + val flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java))) + } + + @Test() + fun `should allow updating of registered responder at runtime`() { + val nodeFlowManager = NodeFlowManager() + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java) + nodeFlowManager.validateRegistrations() + var factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + var flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java))) + // update + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java) + nodeFlowManager.validateRegistrations() + + factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + Assert.assertThat(flow, `is`(instanceOf(RespSubSub::class.java))) + } + + @Test + fun `should allow an override to be specified`() { + val nodeFlowManager = NodeFlowManager(FlowOverrideConfig(listOf(FlowOverride(Init::class.qualifiedName!!, Resp::class.qualifiedName!!)))) + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java) + nodeFlowManager.validateRegistrations() + + val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + val flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + + Assert.assertThat(flow, `is`(instanceOf(Resp::class.java))) + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt index 48a266ce31..fd2e1db83f 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt @@ -149,7 +149,7 @@ class NodeTest { } } - private fun createConfig(nodeName: CordaX500Name): NodeConfiguration { + private fun createConfig(nodeName: CordaX500Name): NodeConfigurationImpl { val fakeAddress = NetworkHostAndPort("0.1.2.3", 456) return NodeConfigurationImpl( baseDirectory = temporaryFolder.root.toPath(), @@ -167,7 +167,8 @@ class NodeTest { flowTimeout = FlowTimeoutConfiguration(timeout = Duration.ZERO, backoffBase = 1.0, maxRestartCount = 1), rpcSettings = NodeRpcSettings(address = fakeAddress, adminAddress = null, ssl = null), messagingServerAddress = null, - notary = null + notary = null, + flowOverrides = FlowOverrideConfig(listOf()) ) } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 176bbfb5d8..9eba3d64ef 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -56,7 +56,7 @@ class JarScanningCordappLoaderTest { val actualCordapp = loader.cordapps.single() assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) - assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") + assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") assertThat(actualCordapp.rpcFlows).isEmpty() assertThat(actualCordapp.schedulableFlows).isEmpty() assertThat(actualCordapp.services).isEmpty() @@ -74,7 +74,7 @@ class JarScanningCordappLoaderTest { assertThat(loader.cordapps).isNotEmpty val actualCordapp = loader.cordapps.single { !it.initiatedFlows.isEmpty() } - assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java) + assertThat(actualCordapp.initiatedFlows.first()).hasSameClassAs(DummyFlow::class.java) assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java) assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java) } diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index 27247b8eb5..a9e7da8a92 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -172,8 +172,8 @@ class NodeConfigurationImplTest { val errors = configuration.validate() - assertThat(errors).hasOnlyOneElementSatisfying { - error -> error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously") + assertThat(errors).hasOnlyOneElementSatisfying { error -> + error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously") } } @@ -268,7 +268,8 @@ class NodeConfigurationImplTest { noLocalShell = false, rpcSettings = rpcSettings, crlCheckSoftFail = true, - tlsCertCrlDistPoint = null + tlsCertCrlDistPoint = null, + flowOverrides = FlowOverrideConfig(listOf()) ) } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 075d324e9f..d1517dc318 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -26,7 +26,6 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Change import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.persistence.checkpoints import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState @@ -116,7 +115,7 @@ class FlowFrameworkTests { @Test fun `exception while fiber suspended`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } val flow = ReceiveFlow(bob) val fiber = aliceNode.services.startFlow(flow) as FlowStateMachineImpl // Before the flow runs change the suspend action to throw an exception @@ -134,7 +133,7 @@ class FlowFrameworkTests { @Test fun `both sides do a send as their first IO request`() { - bobNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } + bobNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } aliceNode.services.startFlow(PingPongFlow(bob, 10L)) mockNet.runNetwork() @@ -151,7 +150,7 @@ class FlowFrameworkTests { @Test fun `other side ends before doing expected send`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { NoOpFlow() } val resultFuture = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { @@ -161,7 +160,7 @@ class FlowFrameworkTests { @Test fun `receiving unexpected session end before entering sendAndReceive`() { - bobNode.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } + bobNode.registerCordappFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } val sessionEndReceived = Semaphore(0) receivedSessionMessagesObservable().filter { it.message is ExistingSessionMessage && it.message.payload === EndSessionMessage @@ -176,7 +175,7 @@ class FlowFrameworkTests { @Test fun `FlowException thrown on other side`() { - val erroringFlow = bobNode.registerFlowFactory(ReceiveFlow::class) { + val erroringFlow = bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps } @@ -240,7 +239,7 @@ class FlowFrameworkTests { } } - bobNode.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } + bobNode.registerCordappFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } val resultFuture = aliceNode.services.startFlow(RetryOnExceptionFlow(bob)).resultFuture mockNet.runNetwork() assertThat(resultFuture.getOrThrow()).isEqualTo("Hello") @@ -248,7 +247,7 @@ class FlowFrameworkTests { @Test fun `serialisation issue in counterparty`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { @@ -258,7 +257,7 @@ class FlowFrameworkTests { @Test fun `FlowException has non-serialisable object`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) } } val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture @@ -275,7 +274,7 @@ class FlowFrameworkTests { .addCommand(dummyCommand(alice.owningKey)) val stx = aliceNode.services.signInitialTransaction(ptx) - val committerFiber = aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { + val committerFiber = aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) }.map { it.stateMachine }.map { uncheckedCast, FlowStateMachine>(it) } val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture @@ -290,7 +289,7 @@ class FlowFrameworkTests { .addCommand(dummyCommand()) val stx = aliceNode.services.signInitialTransaction(ptx) - aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { + aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) { throw Exception("Error") } } val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture @@ -307,7 +306,7 @@ class FlowFrameworkTests { .addCommand(dummyCommand(alice.owningKey)) val stx = aliceNode.services.signInitialTransaction(ptx) - aliceNode.registerFlowFactory(VaultQueryFlow::class) { + aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) { WaitingFlows.Committer(it) } val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture @@ -318,7 +317,7 @@ class FlowFrameworkTests { @Test fun `customised client flow`() { - val receiveFlowFuture = bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } + val receiveFlowFuture = bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } aliceNode.services.startFlow(CustomSendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello") @@ -333,7 +332,7 @@ class FlowFrameworkTests { @Test fun `upgraded initiating flow`() { - bobNode.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } + bobNode.registerCordappFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } val result = aliceNode.services.startFlow(UpgradedFlow(bob)).resultFuture mockNet.runNetwork() assertThat(receivedSessionMessages).startsWith( @@ -347,7 +346,7 @@ class FlowFrameworkTests { @Test fun `upgraded initiated flow`() { - bobNode.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } + bobNode.registerCordappFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } val initiatingFlow = SendFlow("Old initiating", bob) val flowInfo = aliceNode.services.startFlow(initiatingFlow).resultFuture mockNet.runNetwork() @@ -387,7 +386,7 @@ class FlowFrameworkTests { @Test fun `single inlined sub-flow`() { - bobNode.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } + bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") @@ -395,7 +394,7 @@ class FlowFrameworkTests { @Test fun `double inlined sub-flow`() { - bobNode.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } + bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") @@ -403,7 +402,7 @@ class FlowFrameworkTests { @Test fun `non-FlowException thrown on other side`() { - val erroringFlowFuture = bobNode.registerFlowFactory(ReceiveFlow::class) { + val erroringFlowFuture = bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } } val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps } @@ -507,8 +506,8 @@ class FlowFrameworkTripartyTests { @Test fun `sending to multiple parties`() { - bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - charlieNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } val payload = "Hello World" aliceNode.services.startFlow(SendFlow(payload, bob, charlie)) mockNet.runNetwork() @@ -538,8 +537,8 @@ class FlowFrameworkTripartyTests { fun `receiving from multiple parties`() { val bobPayload = "Test 1" val charliePayload = "Test 2" - bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } - charlieNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating() aliceNode.services.startFlow(multiReceiveFlow) aliceNode.internals.acceptableLiveFiberCountOnStop = 1 @@ -564,8 +563,8 @@ class FlowFrameworkTripartyTests { @Test fun `FlowException only propagated to parent`() { - charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } - bobNode.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java) @@ -577,9 +576,9 @@ class FlowFrameworkTripartyTests { // Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move // onto Charlie which will throw the exception val node2Fiber = bobNode - .registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } + .registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } .map { it.stateMachine } - charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl mockNet.runNetwork() @@ -630,6 +629,8 @@ class FlowFrameworkPersistenceTests { private lateinit var notaryIdentity: Party private lateinit var alice: Party private lateinit var bob: Party + private lateinit var aliceFlowManager: MockNodeFlowManager + private lateinit var bobFlowManager: MockNodeFlowManager @Before fun start() { @@ -637,8 +638,11 @@ class FlowFrameworkPersistenceTests { cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), servicePeerAllocationStrategy = RoundRobin() ) - aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) - bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) + aliceFlowManager = MockNodeFlowManager() + bobFlowManager = MockNodeFlowManager() + + aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager)) + bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager)) receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } @@ -664,7 +668,7 @@ class FlowFrameworkPersistenceTests { @Test fun `flow restarted just after receiving payload`() { - bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } aliceNode.services.startFlow(SendFlow("Hello", bob)) // We push through just enough messages to get only the payload sent @@ -679,7 +683,7 @@ class FlowFrameworkPersistenceTests { @Test fun `flow loaded from checkpoint will respond to messages from before start`() { - aliceNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow val restoredFlow = bobNode.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") @@ -694,7 +698,7 @@ class FlowFrameworkPersistenceTests { var sentCount = 0 mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) - val secondFlow = charlieNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } + val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } mockNet.runNetwork() val charlie = charlieNode.info.singleIdentity() @@ -802,23 +806,14 @@ private infix fun TestStartedNode.sent(message: SessionMessage): Pair.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress) private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) { - val isPayloadTransfer: Boolean get() = - message is ExistingSessionMessage && message.payload is DataSessionMessage || - message is InitialSessionMessage && message.firstPayload != null + val isPayloadTransfer: Boolean + get() = + message is ExistingSessionMessage && message.payload is DataSessionMessage || + message is InitialSessionMessage && message.firstPayload != null + override fun toString(): String = "$from sent $message to $to" } -private inline fun > TestStartedNode.registerFlowFactory( - initiatingFlowClass: KClass>, - initiatedFlowVersion: Int = 1, - noinline flowFactory: (FlowSession) -> P): CordaFuture

    { - val observable = registerFlowFactory( - initiatingFlowClass.java, - InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory), - P::class.java, - track = true) - return observable.toFuture() -} private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage { return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize()) @@ -1061,7 +1056,8 @@ private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val ot constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession) @Suspendable - override fun call(): Any = (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } + override fun call(): Any = (otherPartySession + ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } } private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic() { @@ -1098,4 +1094,4 @@ private class ExceptionFlow(val exception: () -> E) : FlowLogic communicate(clientLogic: AbstractClientLogic, rebootClient: Boolean): FlowStateMachine { - server.registerFlowFactory(AbstractClientLogic::class.java, InitiatedFlowFactory.Core { ServerLogic(it, serverRunning) }, ServerLogic::class.java, false) + server.registerCoreFlowFactory(AbstractClientLogic::class.java, ServerLogic::class.java, { ServerLogic(it, serverRunning) }, false) client.services.startFlow(clientLogic) while (!serverRunning.get()) mockNet.runNetwork(1) if (rebootClient) { diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index f8ffd4edf3..291f15d988 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -89,6 +89,20 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, } extraConfig = ['h2Settings.address' : 'localhost:10017'] } + + //All other nodes should be using LoggingBuyerFlow as it is a subclass of BuyerFlow + node { + name "O=LoggingBank,L=London,C=GB" + p2pPort 10025 + cordapps = ["$project.group:finance:$corda_release_version"] + rpcUsers = ext.rpcUsers + rpcSettings { + address "localhost:10026" + adminAddress "localhost:10027" + } + extraConfig = ['h2Settings.address' : 'localhost:10035'] + flowOverride("net.corda.traderdemo.flow.SellerFlow", "net.corda.traderdemo.flow.BuyerFlow") + } } task integrationTest(type: Test, dependsOn: []) { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt index 70cee50154..52a5a7c982 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt @@ -11,20 +11,17 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.finance.contracts.CommercialPaper -import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.TwoPartyTradeFlow -import net.corda.traderdemo.TransactionGraphSearch import java.util.* @InitiatedBy(SellerFlow::class) -class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic() { +open class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic() { object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset") - override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY) @Suspendable - override fun call() { + override fun call(): SignedTransaction { progressTracker.currentStep = STARTING_BUY // Receive the offered amount and automatically agree to it (in reality this would be a longer negotiation) @@ -43,33 +40,6 @@ class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic() { println("Purchase complete - we are a happy customer! Final transaction is: " + "\n\n${Emoji.renderIfSupported(tradeTX.tx)}") - logIssuanceAttachment(tradeTX) - logBalance() - } - - private fun logBalance() { - val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" } - println("Remaining balance: ${balances.joinToString()}") - } - - private fun logIssuanceAttachment(tradeTX: SignedTransaction) { - // Find the original CP issuance. - // TODO: This is potentially very expensive, and requires transaction details we may no longer have once - // SGX is enabled. Should be replaced with including the attachment on all transactions involving - // the state. - val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx), - TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, - followInputsOfType = CommercialPaper.State::class.java)) - val cpIssuance = search.call().single() - - // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. - - cpIssuance.attachments.first().let { - println(""" - -The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar - -${Emoji.renderIfSupported(cpIssuance)}""") - } + return tradeTX } } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt new file mode 100644 index 0000000000..841b96f594 --- /dev/null +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt @@ -0,0 +1,48 @@ +package net.corda.traderdemo.flow + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.internal.Emoji +import net.corda.core.transactions.SignedTransaction +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.contracts.getCashBalances +import net.corda.traderdemo.TransactionGraphSearch + +@InitiatedBy(SellerFlow::class) +class LoggingBuyerFlow(private val otherSideSession: FlowSession) : BuyerFlow(otherSideSession) { + + @Suspendable + override fun call(): SignedTransaction { + val tradeTX = super.call() + logIssuanceAttachment(tradeTX) + logBalance() + return tradeTX + } + + private fun logBalance() { + val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" } + println("Remaining balance: ${balances.joinToString()}") + } + + private fun logIssuanceAttachment(tradeTX: SignedTransaction) { + // Find the original CP issuance. + // TODO: This is potentially very expensive, and requires transaction details we may no longer have once + // SGX is enabled. Should be replaced with including the attachment on all transactions involving + // the state. + val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx), + TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, + followInputsOfType = CommercialPaper.State::class.java)) + val cpIssuance = search.call().single() + + // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. + + cpIssuance.attachments.first().let { + println(""" + +The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar + +${Emoji.renderIfSupported(cpIssuance)}""") + } + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 793c6fd8bb..14d7037398 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -148,7 +148,8 @@ data class NodeParameters( val maximumHeapSize: String = "512m", val logLevel: String? = null, val additionalCordapps: Collection = emptySet(), - val regenerateCordappsOnStart: Boolean = false + val regenerateCordappsOnStart: Boolean = false, + val flowOverrides: Map>, Class>> = emptyMap() ) { /** * Helper builder for configuring a [Node] from Java. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt index bd1bcb464a..6eb2e7a71a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt @@ -2,6 +2,7 @@ package net.corda.testing.driver import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture +import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.map @@ -27,13 +28,14 @@ interface DriverDSL { * Returns the [NotaryHandle] for the single notary on the network. Throws if there are none or more than one. * @see notaryHandles */ - val defaultNotaryHandle: NotaryHandle get() { - return when (notaryHandles.size) { - 0 -> throw IllegalStateException("There are no notaries defined on the network") - 1 -> notaryHandles[0] - else -> throw IllegalStateException("There is more than one notary defined on the network") + val defaultNotaryHandle: NotaryHandle + get() { + return when (notaryHandles.size) { + 0 -> throw IllegalStateException("There are no notaries defined on the network") + 1 -> notaryHandles[0] + else -> throw IllegalStateException("There is more than one notary defined on the network") + } } - } /** * Returns the identity of the single notary on the network. Throws if there are none or more than one. @@ -47,11 +49,12 @@ interface DriverDSL { * @see defaultNotaryHandle * @see notaryHandles */ - val defaultNotaryNode: CordaFuture get() { - return defaultNotaryHandle.nodeHandles.map { - it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node") + val defaultNotaryNode: CordaFuture + get() { + return defaultNotaryHandle.nodeHandles.map { + it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node") + } } - } /** * Start a node. @@ -110,7 +113,8 @@ interface DriverDSL { startInSameProcess: Boolean? = defaultParameters.startInSameProcess, maximumHeapSize: String = defaultParameters.maximumHeapSize, additionalCordapps: Collection = defaultParameters.additionalCordapps, - regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart + regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart, + flowOverrides: Map>, Class>> = defaultParameters.flowOverrides ): CordaFuture /** diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt index cf85d45e15..927314827a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt @@ -14,6 +14,7 @@ import net.corda.testing.driver.OutOfProcess import net.corda.testing.node.User import rx.Observable import java.nio.file.Path +import javax.validation.constraints.NotNull interface NodeHandleInternal : NodeHandle { val configuration: NodeConfiguration @@ -70,7 +71,11 @@ data class InProcessImpl( } override fun close() = stop() - override fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable = node.registerInitiatedFlow(initiatedFlowClass) + @NotNull + override fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable { + node.registerInitiatedFlow(initiatedFlowClass) + return Observable.empty() + } } val InProcess.internalServices: StartedNodeServices get() = services as StartedNodeServices diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 3c3ba57aa5..b3731f27ec 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -206,11 +206,8 @@ class StartedMockNode private constructor(private val node: TestStartedNode) { fun > registerResponderFlow(initiatingFlowClass: Class>, flowFactory: ResponderFlowFactory, responderFlowClass: Class): CordaFuture = - node.registerFlowFactory( - initiatingFlowClass, - InitiatedFlowFactory.CorDapp(flowVersion = 0, appName = "", factory = flowFactory::invoke), - responderFlowClass, true) - .toFuture() + + node.registerInitiatedFlow(initiatingFlowClass, responderFlowClass).toFuture() } /** @@ -240,13 +237,12 @@ interface ResponderFlowFactory> { */ inline fun > StartedMockNode.registerResponderFlow( initiatingFlowClass: Class>, - noinline flowFactory: (FlowSession) -> F): Future = - registerResponderFlow( - initiatingFlowClass, - object : ResponderFlowFactory { - override fun invoke(flowSession: FlowSession) = flowFactory(flowSession) - }, - F::class.java) + noinline flowFactory: (FlowSession) -> F): Future = registerResponderFlow( + initiatingFlowClass, + object : ResponderFlowFactory { + override fun invoke(flowSession: FlowSession) = flowFactory(flowSession) + }, + F::class.java) /** * A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing. @@ -302,13 +298,13 @@ open class MockNetwork( constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) constructor( - cordappPackages: List, - defaultParameters: MockNetworkParameters = MockNetworkParameters(), - networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, - threadPerNode: Boolean = defaultParameters.threadPerNode, - servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - notarySpecs: List = defaultParameters.notarySpecs, - networkParameters: NetworkParameters = defaultParameters.networkParameters + cordappPackages: List, + defaultParameters: MockNetworkParameters = MockNetworkParameters(), + networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, + threadPerNode: Boolean = defaultParameters.threadPerNode, + servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, + notarySpecs: List = defaultParameters.notarySpecs, + networkParameters: NetworkParameters = defaultParameters.networkParameters ) : this(emptyList(), defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs, networkParameters, cordappsForAllNodes = cordappsForPackages(cordappPackages)) private val internalMockNetwork: InternalMockNetwork = InternalMockNetwork(defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs, networkParameters = networkParameters, cordappsForAllNodes = cordappsForAllNodes) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index c722058ba3..ad2173c181 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -8,6 +8,7 @@ import com.typesafe.config.ConfigValueFactory import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf +import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.internal.concurrent.* @@ -37,7 +38,6 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme -import net.corda.testing.node.TestCordapp import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME @@ -50,6 +50,7 @@ import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec +import net.corda.testing.node.TestCordapp import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages import okhttp3.OkHttpClient @@ -213,7 +214,8 @@ class DriverDSLImpl( startInSameProcess: Boolean?, maximumHeapSize: String, additionalCordapps: Collection, - regenerateCordappsOnStart: Boolean + regenerateCordappsOnStart: Boolean, + flowOverrides: Map>, Class>> ): CordaFuture { val p2pAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name @@ -230,7 +232,7 @@ class DriverDSLImpl( return registrationFuture.flatMap { networkMapAvailability.flatMap { // But starting the node proper does require the network map - startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress, additionalCordapps, regenerateCordappsOnStart) + startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress, additionalCordapps, regenerateCordappsOnStart, flowOverrides) } } } @@ -244,7 +246,8 @@ class DriverDSLImpl( maximumHeapSize: String = "512m", p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort(), additionalCordapps: Collection = emptySet(), - regenerateCordappsOnStart: Boolean = false): CordaFuture { + regenerateCordappsOnStart: Boolean = false, + flowOverrides: Map>, Class>> = emptyMap()): CordaFuture { val rpcAddress = portAllocation.nextHostAndPort() val rpcAdminAddress = portAllocation.nextHostAndPort() val webAddress = portAllocation.nextHostAndPort() @@ -258,14 +261,16 @@ class DriverDSLImpl( "networkServices.networkMapURL" to compatibilityZone.networkMapURL().toString()) } + val flowOverrideConfig = flowOverrides.entries.map { FlowOverride(it.key.canonicalName, it.value.canonicalName) }.let { FlowOverrideConfig(it) } val overrides = configOf( - "myLegalName" to name.toString(), - "p2pAddress" to p2pAddress.toString(), + NodeConfiguration::myLegalName.name to name.toString(), + NodeConfiguration::p2pAddress.name to p2pAddress.toString(), "rpcSettings.address" to rpcAddress.toString(), "rpcSettings.adminAddress" to rpcAdminAddress.toString(), - "useTestClock" to useTestClock, - "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, - "verifierType" to verifierType.name + NodeConfiguration::useTestClock.name to useTestClock, + NodeConfiguration::rpcUsers.name to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, + NodeConfiguration::verifierType.name to verifierType.name, + NodeConfiguration::flowOverrides.name to flowOverrideConfig.toConfig().root().unwrapped() ) + czUrlConfig + customOverrides val config = NodeConfig(ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), @@ -516,8 +521,7 @@ class DriverDSLImpl( localNetworkMap, spec.rpcUsers, spec.verifierType, - customOverrides = notaryConfig(clusterAddress) - ) + customOverrides = notaryConfig(clusterAddress)) // All other nodes will join the cluster val restNodeFutures = nodeNames.drop(1).map { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index d60f9f6ee1..f280cee72d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -30,6 +30,8 @@ import net.corda.core.utilities.seconds import net.corda.node.VersionInfo import net.corda.node.internal.AbstractNode import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.NodeFlowManager +import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StartedNodeServices @@ -51,7 +53,6 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.node.TestCordapp import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.setGlobalSerialization @@ -79,7 +80,8 @@ data class MockNodeArgs( val network: InternalMockNetwork, val id: Int, val entropyRoot: BigInteger, - val version: VersionInfo = MOCK_VERSION_INFO + val version: VersionInfo = MOCK_VERSION_INFO, + val flowManager: MockNodeFlowManager = MockNodeFlowManager() ) // TODO We don't need a parameters object as this is internal only @@ -89,7 +91,8 @@ data class InternalMockNodeParameters( val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), val configOverrides: (NodeConfiguration) -> Any? = {}, val version: VersionInfo = MOCK_VERSION_INFO, - val additionalCordapps: Collection? = null) { + val additionalCordapps: Collection? = null, + val flowManager: MockNodeFlowManager = MockNodeFlowManager()) { constructor(mockNodeParameters: MockNodeParameters) : this( mockNodeParameters.forcedID, mockNodeParameters.legalName, @@ -132,12 +135,10 @@ interface TestStartedNode { * starts up for all [FlowLogic] classes it finds which are annotated with [InitiatedBy]. * @return An [Observable] of the initiated flows started by counterparties. */ - fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable + fun > registerInitiatedFlow(initiatedFlowClass: Class, track: Boolean = false): Observable + + fun > registerInitiatedFlow(initiatingFlowClass: Class>, initiatedFlowClass: Class, track: Boolean = false): Observable - fun > registerFlowFactory(initiatingFlowClass: Class>, - flowFactory: InitiatedFlowFactory, - initiatedFlowClass: Class, - track: Boolean): Observable } open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParameters(), @@ -202,7 +203,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe */ val defaultNotaryIdentity: Party get() { - return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + return defaultNotaryNode.info.legalIdentities.singleOrNull() + ?: throw IllegalStateException("Default notary has multiple identities") } /** @@ -270,11 +272,12 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } } - open class MockNode(args: MockNodeArgs) : AbstractNode( + open class MockNode(args: MockNodeArgs, private val mockFlowManager: MockNodeFlowManager = args.flowManager) : AbstractNode( args.config, TestClock(Clock.systemUTC()), DefaultNamedCacheFactory(), args.version, + mockFlowManager, args.network.getServerThread(args.id), args.network.busyLatch ) { @@ -294,24 +297,28 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe override val rpcOps: CordaRPCOps, override val notaryService: NotaryService?) : TestStartedNode { - override fun > registerFlowFactory( - initiatingFlowClass: Class>, - flowFactory: InitiatedFlowFactory, - initiatedFlowClass: Class, - track: Boolean): Observable = - internals.internalRegisterFlowFactory(smm, initiatingFlowClass, flowFactory, initiatedFlowClass, track) - override fun dispose() = internals.stop() - override fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable = - internals.registerInitiatedFlow(smm, initiatedFlowClass) + override fun > registerInitiatedFlow(initiatedFlowClass: Class, track: Boolean): Observable { + internals.flowManager.registerInitiatedFlow(initiatedFlowClass) + return smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) + } + + override fun > registerInitiatedFlow(initiatingFlowClass: Class>, initiatedFlowClass: Class, track: Boolean): Observable { + internals.flowManager.registerInitiatedFlow(initiatingFlowClass, initiatedFlowClass) + return smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) + } + + } val mockNet = args.network val id = args.id + init { require(id >= 0) { "Node ID must be zero or positive, was passed: $id" } } + private val entropyRoot = args.entropyRoot var counter = entropyRoot override val log get() = staticLog @@ -333,7 +340,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe this, attachments, network as MockNodeMessagingService, - object : StartedNodeServices, ServiceHubInternal by services, FlowStarter by flowStarter { }, + object : StartedNodeServices, ServiceHubInternal by services, FlowStarter by flowStarter {}, nodeInfo, smm, database, @@ -417,8 +424,19 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe var acceptableLiveFiberCountOnStop: Int = 0 override fun acceptableLiveFiberCountOnStop(): Int = acceptableLiveFiberCountOnStop + + fun > registerInitiatedFlowFactory(initiatingFlowClass: Class>, initiatedFlowClass: Class, factory: InitiatedFlowFactory, track: Boolean): Observable { + mockFlowManager.registerTestingFactory(initiatingFlowClass, factory) + return if (track) { + smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) + } else { + Observable.empty() + } + } } + + fun createUnstartedNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters()): MockNode { return createUnstartedNode(parameters, defaultFactory) } @@ -453,7 +471,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe val cordappDirectories = cordapps.map { TestCordappDirectories.getJarDirectory(it) }.distinct() doReturn(cordappDirectories).whenever(config).cordappDirectories - val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version)) + val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version, flowManager = parameters.flowManager)) _nodes += node if (start) { node.start() @@ -482,8 +500,10 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe */ @JvmOverloads fun runNetwork(rounds: Int = -1) { - check(!networkSendManuallyPumped) { "MockNetwork.runNetwork() should only be used when networkSendManuallyPumped == false. " + - "You can use MockNetwork.waitQuiescent() to wait for all the nodes to process all the messages on their queues instead." } + check(!networkSendManuallyPumped) { + "MockNetwork.runNetwork() should only be used when networkSendManuallyPumped == false. " + + "You can use MockNetwork.waitQuiescent() to wait for all the nodes to process all the messages on their queues instead." + } fun pumpAll() = messagingNetwork.endpoints.map { it.pumpReceive(false) } if (rounds == -1) { @@ -572,3 +592,17 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio doReturn(null).whenever(it).devModeOptions } } + +class MockNodeFlowManager : NodeFlowManager() { + val testingRegistrations = HashMap>, InitiatedFlowFactory<*>>() + override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class>): InitiatedFlowFactory<*>? { + if (initiatedFlowClass in testingRegistrations) { + return testingRegistrations.get(initiatedFlowClass) + } + return super.getFlowFactoryForInitiatingFlow(initiatedFlowClass) + } + + fun registerTestingFactory(initiator: Class>, factory: InitiatedFlowFactory<*>) { + testingRegistrations.put(initiator, factory) + } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index dae33f8c4b..a86abe6b9d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -10,7 +10,9 @@ import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo +import net.corda.node.internal.FlowManager import net.corda.node.internal.Node +import net.corda.node.internal.NodeFlowManager import net.corda.node.internal.NodeWithInfo import net.corda.node.services.config.* import net.corda.nodeapi.internal.config.toConfig @@ -87,7 +89,8 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi fun startNode(legalName: CordaX500Name, platformVersion: Int = PLATFORM_VERSION, rpcUsers: List = emptyList(), - configOverrides: Map = emptyMap()): NodeWithInfo { + configOverrides: Map = emptyMap(), + flowManager: FlowManager = NodeFlowManager(FlowOverrideConfig())): NodeWithInfo { val baseDirectory = baseDirectory(legalName).createDirectories() val p2pAddress = configOverrides["p2pAddress"] ?: portAllocation.nextHostAndPort().toString() val config = ConfigHelper.loadConfig( @@ -103,7 +106,8 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi ) + configOverrides ) - val cordapps = cordappsForPackages(getCallerPackage(NodeBasedTest::class)?.let { cordappPackages + it } ?: cordappPackages) + val cordapps = cordappsForPackages(getCallerPackage(NodeBasedTest::class)?.let { cordappPackages + it } + ?: cordappPackages) val existingCorDappDirectoriesOption = if (config.hasPath(NodeConfiguration.cordappDirectoriesKey)) config.getStringList(NodeConfiguration.cordappDirectoriesKey) else emptyList() @@ -119,7 +123,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi } defaultNetworkParameters.install(baseDirectory) - val node = InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion)) + val node = InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), flowManager = flowManager) val nodeInfo = node.start() val nodeWithInfo = NodeWithInfo(node, nodeInfo) nodes += nodeWithInfo @@ -145,7 +149,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi } } -class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo) : Node(configuration, versionInfo, false) { +class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)) : Node(configuration, versionInfo, false, flowManager = flowManager) { override fun start() : NodeInfo { check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index a6847b8fa2..1494a38715 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -3,7 +3,6 @@ package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId @@ -13,7 +12,6 @@ import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.services.MockAttachmentStorage import java.nio.file.Paths import java.security.PublicKey -import java.util.* class MockCordappProvider( cordappLoader: CordappLoader, From 7e3aa7f30c01ba83de9dc48c04c8adafdc776eed Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 24 Oct 2018 10:53:39 +0100 Subject: [PATCH 81/83] CORDA-1915 node rejects CorDapps signed by our dev keys in prod mode (#4041) Related to CORDA-1915 Signing CorDapp JARs - Corda node rejects CorDapps signed by our development keys when running in production mode. This prevents Cordapps signed by our dev key (by default) running in production (node devMode=false). --- .../core/internal/JarSignatureCollector.kt | 12 ++++++++ .../nodeapi/internal/KeyStoreConfigHelpers.kt | 2 ++ .../net/corda/node/internal/AbstractNode.kt | 5 +++- .../cordapp/JarScanningCordappLoader.kt | 27 ++++++++++++++---- .../cordapp/JarScanningCordappLoaderTest.kt | 22 ++++++++++++++ .../cordapp/signed/signed-by-dev-key.jar | Bin 0 -> 22358 bytes .../cordapp/signed/signed-by-two-keys.jar | Bin 0 -> 24454 bytes 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar create mode 100644 node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 963623e474..f7bece5b01 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -28,6 +28,14 @@ object JarSignatureCollector { fun collectSigningParties(jar: JarInputStream): List = getSigners(jar).toPartiesOrderedByName() + /** + * Returns an ordered list of every [X509Certificate] which has signed every signable item in the given [JarInputStream]. + * + * @param jar The open [JarInputStream] to collect signing parties from. + * @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other. + */ + fun collectCertificates(jar: JarInputStream): List = getSigners(jar).toCertificates() + private fun getSigners(jar: JarInputStream): Set { val signerSets = jar.fileSignerSets if (signerSets.isEmpty()) return emptySet() @@ -71,6 +79,10 @@ object JarSignatureCollector { (it.signerCertPath.certificates[0] as X509Certificate).publicKey }.sortedBy { it.hash} // Sorted for determinism. + private fun Set.toCertificates(): List = map { + it.signerCertPath.certificates[0] as X509Certificate + }.sortedBy { it.toString() } // Sorted for determinism. + private val JarInputStream.entries get(): Sequence = generateSequence(nextJarEntry) { nextJarEntry } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index 03caed3ca2..d6cb0f5877 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -99,6 +99,8 @@ const val DEV_CA_TRUST_STORE_FILE: String = "cordatruststore.jks" const val DEV_CA_TRUST_STORE_PASS: String = "trustpass" const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass" +val DEV_CERTIFICATES: List get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate) + // We need a class so that we can get hold of the class loader internal object DevCaHelper { fun loadDevCa(alias: String): CertificateAndKeyPair { diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2d12f73a9e..99e9f50778 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -72,6 +72,7 @@ import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.* +import net.corda.nodeapi.internal.DEV_CERTIFICATES import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.CertificateStore @@ -513,10 +514,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // CorDapp will be generated. generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo) } + val blacklistedCerts = if (configuration.devMode) emptyList() else DEV_CERTIFICATES return JarScanningCordappLoader.fromDirectories( configuration.cordappDirectories, versionInfo, - extraCordapps = generatedCordapps + extraCordapps = generatedCordapps, + blacklistedCerts = blacklistedCerts ) } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index b1ba5f9f29..9bd47c564a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -26,6 +26,7 @@ import java.lang.reflect.Modifier import java.net.URL import java.net.URLClassLoader import java.nio.file.Path +import java.security.cert.X509Certificate import java.util.* import java.util.jar.JarInputStream import kotlin.reflect.KClass @@ -38,7 +39,8 @@ import kotlin.streams.toList */ class JarScanningCordappLoader private constructor(private val cordappJarPaths: List, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List) : CordappLoaderTemplate() { + extraCordapps: List, + private val blacklistedCordappSigners: List = emptyList()) : CordappLoaderTemplate() { override val cordapps: List by lazy { loadCordapps() + extraCordapps @@ -64,10 +66,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: */ fun fromDirectories(cordappDirs: Collection, versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List = emptyList()): JarScanningCordappLoader { + extraCordapps: List = emptyList(), + blacklistedCerts: List = emptyList()): JarScanningCordappLoader { logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) } /** @@ -75,9 +78,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. */ - fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList()): JarScanningCordappLoader { + fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), blacklistedCerts: List = emptyList()): JarScanningCordappLoader { val paths = scanJars.map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) } private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) @@ -106,6 +109,20 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: true } } + .filter { + if (blacklistedCordappSigners.isEmpty()) { + true //Nothing blacklisted, no need to check + } else { + val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) + if (certificates.isEmpty() || (certificates - blacklistedCordappSigners).isNotEmpty()) + true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate + else { + logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " + + "${certificates.intersect(blacklistedCordappSigners).map { it.publicKey }}.") + false + } + } + } cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) } return cordapps } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 9eba3d64ef..feb4e7b476 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.packageName import net.corda.node.VersionInfo import net.corda.testing.node.internal.TestCordappDirectories import net.corda.testing.node.internal.cordappForPackages +import net.corda.nodeapi.internal.DEV_CERTIFICATES import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Paths @@ -142,4 +143,25 @@ class JarScanningCordappLoaderTest { val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) assertThat(loader.cordapps).hasSize(1) } + + @Test + fun `cordapp classloader loads app signed by allowed certificate`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = emptyList()) + assertThat(loader.cordapps).hasSize(1) + } + + @Test + fun `cordapp classloader does not load app signed by blacklisted certificate`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES) + assertThat(loader.cordapps).hasSize(0) + } + + @Test + fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES) + assertThat(loader.cordapps).hasSize(1) + } } diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar new file mode 100644 index 0000000000000000000000000000000000000000..beb401a992b0a44b69c7487d199ed19b43cf885f GIT binary patch literal 22358 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 literal 0 HcmV?d00001 diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar new file mode 100644 index 0000000000000000000000000000000000000000..c5360a057637c1f66da5139e7e76c94ce5e4b9a2 GIT binary patch literal 24454 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 Date: Wed, 24 Oct 2018 13:58:19 +0100 Subject: [PATCH 82/83] CORDA-1838: Add subcommands to node (#4091) * Tidy up * Add install-shell-extensions command * Make cli tests use same version of picocli as everything else * Remove initLogging from NodeStartup, it is ran earlier by CordaCLIWrapper * Use picocli snapshot for testing * Use RunLast() parser to invoke correct subcommands * Deprecate old clear-network-map-cache parameter * Restructure NodeStartup for commands * Get rid of -c option since the flag method has been deprecated and that didn't exist in last release * Update documentation * Update backwards compatibility test * Get all subcommands working * Refactor sub commands into seperate classes * Update docs and fix some tests * Docs changes * Fix merge conflicts with master * Fix renamed parameters * Fix test failure * Fix compatibility tests * Add missing compatibility test for blob inspector * Remove blob inspector compatibility test as there are import conflicts * Assorted doc fixes * Addressing review comments * More review comments * Couple more bits * Fix broken tests * Fix compilation error * More merge conflicts * Make startup logging function a bit more sensible * Fix broken shell extensions * Make shell extensions work with subcommands * Make sure parameters for deprecated options are carried through * More review comments * Adding some s's * One last go * Fix compilation error on Windows * Revert logging changes * Revert docs back to their original imperatively moody state --- build.gradle | 2 +- docs/source/blob-inspector.rst | 14 +- docs/source/changelog.rst | 6 +- .../cli-application-shell-extensions.rst | 4 +- docs/source/cli-ux-guidelines.rst | 7 +- docs/source/corda-configuration-file.rst | 2 +- docs/source/network-bootstrapper.rst | 13 +- docs/source/network-map.rst | 4 +- docs/source/running-a-node.rst | 31 +- docs/source/shell.rst | 20 +- .../internal/network/NetworkBootstrapper.kt | 2 +- .../net/corda/node/flows/FlowOverrideTests.kt | 2 +- node/src/main/kotlin/net/corda/node/Corda.kt | 4 +- .../net/corda/node/NodeCmdLineOptions.kt | 141 +++--- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../kotlin/net/corda/node/internal/Node.kt | 13 +- .../net/corda/node/internal/NodeStartup.kt | 462 +++++++----------- .../subcommands/ClearNetworkCacheCli.kt | 14 + .../subcommands/GenerateNodeInfoCli.kt | 16 + .../subcommands/GenerateRpcSslCertsCli.kt | 103 ++++ .../subcommands/InitialRegistrationCli.kt | 110 +++++ ...et.corda.node.internal.NodeStartupCli.yml} | 10 - .../internal/NodeStartupCompatibilityTest.kt | 2 +- .../corda/node/internal/NodeStartupTest.kt | 9 +- .../testing/node/internal/DriverDSLImpl.kt | 4 +- .../testing/node/internal/NodeBasedTest.kt | 4 +- testing/test-cli/build.gradle | 2 +- tools/blobinspector/build.gradle | 1 - .../net/corda/blobinspector/BlobInspector.kt | 2 +- .../net.corda.blobinspector.BlobInspector.yml | 58 +++ .../kotlin/net/corda/bootstrapper/Main.kt | 2 +- ...bootstrapper.NetworkBootstrapperRunner.yml | 5 - .../net/corda/cliutils/CordaCliWrapper.kt | 86 ++-- .../cliutils/InstallShellExtensionsParser.kt | 52 +- .../bootstrapper/notaries/NotaryCopier.kt | 2 +- .../net.corda.tools.shell.StandaloneShell.yml | 5 - 36 files changed, 716 insertions(+), 500 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt rename node/src/main/resources/{net.corda.node.internal.NodeStartup.yml => net.corda.node.internal.NodeStartupCli.yml} (92%) create mode 100644 tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml diff --git a/build.gradle b/build.gradle index b5c9156435..41f3f98e69 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ buildscript { ext.snappy_version = '0.4' ext.class_graph_version = '4.2.12' ext.jcabi_manifests_version = '1.1' - ext.picocli_version = '3.5.2' + ext.picocli_version = '3.6.1' // Name of the IntelliJ SDK created for the deterministic Java rt.jar. // ext.deterministic_idea_sdk = '1.8 (Deterministic)' diff --git a/docs/source/blob-inspector.rst b/docs/source/blob-inspector.rst index fe58b63a3e..cffa40118b 100644 --- a/docs/source/blob-inspector.rst +++ b/docs/source/blob-inspector.rst @@ -98,9 +98,9 @@ The blob inspector can be started with the following command-line options: .. code-block:: shell - blob-inspector [-hvV] [--full-parties] [--install-shell-extensions] [--schema] - [--format=type] [--input-format=type] - [--logging-level=] [SOURCE] + blob-inspector [-hvV] [--full-parties] [--schema] [--format=type] + [--input-format=type] [--logging-level=] SOURCE + [COMMAND] * ``--format=type``: Output format. Possible values: [YAML, JSON]. Default: YAML. * ``--input-format=type``: Input format. If the file can't be decoded with the given value it's auto-detected, so you should @@ -109,6 +109,10 @@ The blob inspector can be started with the following command-line options: * ``--schema``: Print the blob's schema first. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. -* ``--version``, ``-V``: Print version information and exit. \ No newline at end of file +* ``--version``, ``-V``: Print version information and exit. + +Sub-commands +^^^^^^^^^^^^ + +``install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index eb81172b4a..48aca71439 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1548,9 +1548,9 @@ New features in this release: * Testnet - * Permissioning infrastructure phase one is built out. The node now has a notion of developer mode vs normal - mode. In developer mode it works like M3 and the SSL certificates used by nodes running on your local - machine all self-sign using a developer key included in the source tree. When developer mode is not active, + * Permissioning infrastructure phase one is built out. The node now has a notion of development mode vs normal + mode. In development mode it works like M3 and the SSL certificates used by nodes running on your local + machine all self-sign using a developer key included in the source tree. When development mode is not active, the node won't start until it has a signed certificate. Such a certificate can be obtained by simply running an included command line utility which generates a CSR and submits it to a permissioning service, then waits for the signed certificate to be returned. Note that currently there is no public Corda testnet, so we are diff --git a/docs/source/cli-application-shell-extensions.rst b/docs/source/cli-application-shell-extensions.rst index 4c81ea6c07..ba765d73ed 100644 --- a/docs/source/cli-application-shell-extensions.rst +++ b/docs/source/cli-application-shell-extensions.rst @@ -10,7 +10,7 @@ Users of ``bash`` or ``zsh`` can install an alias and auto-completion for Corda .. code-block:: shell - java -jar .jar --install-shell-extensions + java -jar .jar install-shell-extensions Then, either restart your shell, or for ``bash`` users run: @@ -34,7 +34,7 @@ For example, for the Corda node, install the shell extensions using .. code-block:: shell - java -jar corda-.jar --install-shell-extensions + java -jar corda-.jar install-shell-extensions And then run the node by running: diff --git a/docs/source/cli-ux-guidelines.rst b/docs/source/cli-ux-guidelines.rst index a4917b1620..41b3ac81df 100644 --- a/docs/source/cli-ux-guidelines.rst +++ b/docs/source/cli-ux-guidelines.rst @@ -50,8 +50,11 @@ Standard options * A ``--logging-level`` option should be provided which specifies the logging level to be used in any logging files. Acceptable values should be ``DEBUG``, ``TRACE``, ``INFO``, ``WARN`` and ``ERROR``. * ``--verbose`` and ``--log-to-console`` options should be provided (both equivalent) which specifies that logging output should be displayed in the console. A ``-v`` short option should also be provided. -* A ``--install-shell-extensions`` option should be provided that creates and installs a bash completion file. +Standard subcommands +~~~~~~~~~~~~~~~~~~~~ + +* An ``install-shell-extensions`` subcommand should be provided that creates and installs a bash completion file. Defaults ~~~~~~~~ @@ -94,7 +97,7 @@ In order to use it, create a class containing your command line options using th } class UsefulUtility : CordaCliWrapper( - "useful-utility", // the alias to be used for this utility in bash. When --install-shell-extensions is run + "useful-utility", // the alias to be used for this utility in bash. When install-shell-extensions is run // you will be able to invoke this command by running from the command line "A command line utility that is super useful!" // A description of this utility to be displayed when --help is run ) { diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 6f5fcf17b5..399b0bae78 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -125,7 +125,7 @@ absolute path to the node's base directory. .. note:: The RPC SSL certificate is used by RPC clients to authenticate the connection. The Node operator must provide RPC clients with a truststore containing the certificate they can trust. We advise Node operators to not use the P2P keystore for RPC. - The node ships with a command line argument "--just-generate-rpc-ssl-settings", which generates a secure keystore + The node can be run with the "generate-rpc-ssl-settings" command, which generates a secure keystore and truststore that can be used to secure the RPC connection. You can use this if you have no special requirements. diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index 4cc6ec559d..d8568e7ca9 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -255,14 +255,19 @@ The network bootstrapper can be started with the following command-line options: .. code-block:: shell - bootstrapper [-hvV] [--install-shell-extensions] [--no-copy] [--dir=

    ] - [--logging-level=] + bootstrapper [-hvV] [--no-copy] [--dir=] [--logging-level=] + [--minimum-platform-version=] [COMMAND] * ``--dir=``: Root directory containing the node configuration files and CorDapp JARs that will form the test network. It may also contain existing node directories. Defaults to the current directory. * ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. -* ``--version``, ``-V``: Print version information and exit. \ No newline at end of file +* ``--version``, ``-V``: Print version information and exit. +* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters. + +Sub-commands +^^^^^^^^^^^^ + +``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. \ No newline at end of file diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 3af6b5d6ce..2bda2ed9a8 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -63,7 +63,7 @@ be used to supplement or replace the HTTP network map. If the same node is adver latest one is taken. On startup the node generates its own signed node info file, filename of the format ``nodeInfo-${hash}``. It can also be -generated using the ``--just-generate-node-info`` command line flag without starting the node. To create a simple network +generated using the ``generate-node-info`` sub-command without starting the node. To create a simple network without the HTTP network map service simply place this file in the ``additional-node-infos`` directory of every node that's part of this network. For example, a simple way to do this is to use rsync. @@ -192,7 +192,7 @@ you either need to run from the command line: .. code-block:: shell - java -jar corda.jar --clear-network-map-cache + java -jar corda.jar clear-network-cache or call RPC method `clearNetworkMapCache` (it can be invoked through the node's shell as `run clearNetworkMapCache`, for more information on how to log into node's shell see :doc:`shell`). As we are testing and hardening the implementation this step shouldn't be required. diff --git a/docs/source/running-a-node.rst b/docs/source/running-a-node.rst index 4a214b6845..849139bde9 100644 --- a/docs/source/running-a-node.rst +++ b/docs/source/running-a-node.rst @@ -48,25 +48,38 @@ Command-line options The node can optionally be started with the following command-line options: * ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``). -* ``--clear-network-map-cache``, ``-c``: Clears local copy of network map, on node startup it will be restored from server or file system. * ``--config-file``, ``-f``: The path to the config file. Defaults to ``node.conf``. -* ``--dev-mode``, ``-d``: Runs the node in developer mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise. -* ``--initial-registration``: Start initial node registration with the compatibility zone to obtain a certificate from the Doorman. -* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then - quit. -* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection. -* ``--network-root-truststore``, ``-t``: Network root trust store obtained from network operator. -* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator. +* ``--dev-mode``, ``-d``: Runs the node in development mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise. * ``--no-local-shell``, ``-n``: Do not start the embedded shell locally. * ``--on-unknown-config-keys <[FAIL,WARN,INFO]>``: How to behave on unknown node configuration. Defaults to FAIL. * ``--sshd``: Enables SSH server for node administration. * ``--sshd-port``: Sets the port for the SSH server. If not supplied and SSH server is enabled, the port defaults to 2222. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. * ``--version``, ``-V``: Print version information and exit. +Sub-commands +^^^^^^^^^^^^ + +``bootstrap-raft-cluster``: Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer +addresses), acting as a seed for other nodes to join the cluster. + +``clear-network-cache``: Clears local copy of network map, on node startup it will be restored from server or file system. + +``initial-registration``: Starts initial node registration with the compatibility zone to obtain a certificate from the Doorman. + +Parameters: + +* ``--network-root-truststore``, ``-t`` **required**: Network root trust store obtained from network operator. +* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator. + +``generate-node-info``: Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits. + +``generate-rpc-ssl-settings``: Generates the SSL keystore and truststore for a secure RPC connection. + +``install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. + .. _enabling-remote-debugging: Enabling remote debugging diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 43fc87aae5..ee570a48ba 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -105,28 +105,15 @@ Starting the standalone shell Run the following command from the terminal: -Linux and MacOS -^^^^^^^^^^^^^^^ - .. code:: bash - java -jar corda-tools-shell-cli-VERSION_NUMBER.jar [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT - --user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD - --keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help] - -Windows -^^^^^^^ - -.. code:: bash - - corda-shell [-hvV] [--install-shell-extensions] - [--logging-level=] [--password=] + corda-shell [-hvV] [--logging-level=] [--password=] [--sshd-hostkey-directory=] [--sshd-port=] [--truststore-file=] [--truststore-password=] [--truststore-type=] [--user=] [-a=] [-c=] [-f=] [-o=] - [-p=] + [-p=] [COMMAND] Where: @@ -144,10 +131,11 @@ Where: * ``--truststore-type=``: The type of the TrustStore (e.g. JKS). * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. * ``--version``, ``-V``: Print version information and exit. +Additionally, the ``install-shell-extensions`` subcommand can be used to install the ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. + The format of ``config-file``: .. code:: bash diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index c581577c57..f9ed9e0f88 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -65,7 +65,7 @@ internal constructor(private val initSerEnv: Boolean, "java", "-jar", "corda.jar", - "--just-generate-node-info" + "generate-node-info" ) private const val LOGS_DIR_NAME = "logs" diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt index 881daf18cf..b432de8a53 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt @@ -64,7 +64,7 @@ class FlowOverrideTests { private val nodeBClasses = setOf(Ping::class.java, Pong::class.java) @Test - fun `should use the most "specific" implementation of a responding flow`() { + fun `should use the most specific implementation of a responding flow`() { driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) { val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow() val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow() diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index 62252de5a5..07940ee422 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -4,11 +4,11 @@ package net.corda.node import net.corda.cliutils.start -import net.corda.node.internal.NodeStartup +import net.corda.node.internal.NodeStartupCli fun main(args: Array) { // Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass. // It will exit the process in case of startup failure and is not intended to be used by embedders. If you want // to embed Node in your own container, instantiate it directly and set up the configuration objects yourself. - NodeStartup().start(args) + NodeStartupCli().start(args) } diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt index 4f4ba98c1a..66be04a34a 100644 --- a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -2,18 +2,18 @@ package net.corda.node import com.typesafe.config.Config import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigRenderOptions import net.corda.core.internal.div -import net.corda.core.internal.exists -import net.corda.core.utilities.Try import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NodeConfigurationImpl import net.corda.node.services.config.parseAsNodeConfiguration import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import picocli.CommandLine.Option import java.nio.file.Path import java.nio.file.Paths -class NodeCmdLineOptions { +open class SharedNodeCmdLineOptions { @Option( names = ["-b", "--base-directory"], description = ["The node working directory where all the files are kept."] @@ -27,6 +27,53 @@ class NodeCmdLineOptions { private var _configFile: Path? = null val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf") + @Option( + names = ["--on-unknown-config-keys"], + description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"] + ) + var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL + + @Option( + names = ["-d", "--dev-mode"], + description = ["Runs the node in development mode. Unsafe for production."] + ) + var devMode: Boolean? = null + + open fun loadConfig(): NodeConfiguration { + return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle) + } + + protected fun getRawConfig(): Config { + val rawConfig = ConfigHelper.loadConfig( + baseDirectory, + configFile + ) + if (devMode == true) { + println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") + } + return rawConfig + } + + fun copyFrom(other: SharedNodeCmdLineOptions) { + baseDirectory = other.baseDirectory + _configFile = other._configFile + unknownConfigKeysPolicy= other.unknownConfigKeysPolicy + devMode = other.devMode + } +} + +class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() { + override fun loadConfig(): NodeConfiguration { + return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> + require(!config.devMode) { "Registration cannot occur in development mode" } + require(config.compatibilityZoneURL != null || config.networkServices != null) { + "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." + } + } + } +} + +open class NodeCmdLineOptions : SharedNodeCmdLineOptions() { @Option( names = ["--sshd"], description = ["If set, enables SSH server for node administration."] @@ -45,84 +92,66 @@ class NodeCmdLineOptions { ) var noLocalShell: Boolean = false - @Option( - names = ["--initial-registration"], - description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."] - ) - var isRegistration: Boolean = false - - @Option( - names = ["-t", "--network-root-truststore"], - description = ["Network root trust store obtained from network operator."] - ) - private var _networkRootTrustStorePath: Path? = null - val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks" - - @Option( - names = ["-p", "--network-root-truststore-password"], - description = ["Network root trust store password obtained from network operator."] - ) - var networkRootTrustStorePassword: String? = null - - @Option( - names = ["--on-unknown-config-keys"], - description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"] - ) - var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL - - @Option( - names = ["-d", "--dev-mode"], - description = ["Run the node in developer mode. Unsafe for production."] - ) - var devMode: Boolean? = null - @Option( names = ["--just-generate-node-info"], - description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"] + description = ["DEPRECATED. Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits."], + hidden = true ) var justGenerateNodeInfo: Boolean = false @Option( names = ["--just-generate-rpc-ssl-settings"], - description = ["Generate the SSL key and trust stores for a secure RPC connection."] + description = ["DEPRECATED. Generates the SSL key and trust stores for a secure RPC connection."], + hidden = true ) var justGenerateRpcSslCerts: Boolean = false @Option( - names = ["-c", "--clear-network-map-cache"], - description = ["Clears local copy of network map, on node startup it will be restored from server or file system."] + names = ["--clear-network-map-cache"], + description = ["DEPRECATED. Clears local copy of network map, on node startup it will be restored from server or file system."], + hidden = true ) var clearNetworkMapCache: Boolean = false - val nodeRegistrationOption: NodeRegistrationOption? by lazy { - if (isRegistration) { - requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." } - require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" } - NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!) - } else { - null - } - } + @Option( + names = ["--initial-registration"], + description = ["DEPRECATED. Starts initial node registration with Corda network to obtain certificate from the permissioning server."], + hidden = true + ) + var isRegistration: Boolean = false - fun loadConfig(): Pair> { + @Option( + names = ["-t", "--network-root-truststore"], + description = ["DEPRECATED. Network root trust store obtained from network operator."], + hidden = true + ) + var networkRootTrustStorePathParameter: Path? = null + + @Option( + names = ["-p", "--network-root-truststore-password"], + description = ["DEPRECATED. Network root trust store password obtained from network operator."], + hidden = true + ) + var networkRootTrustStorePassword: String? = null + + override fun loadConfig(): NodeConfiguration { val rawConfig = ConfigHelper.loadConfig( baseDirectory, configFile, configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) + if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap() + - if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap()) + if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap()) ) - return rawConfig to Try.on { - rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> - if (nodeRegistrationOption != null) { - require(!config.devMode) { "Registration cannot occur in devMode" } - require(config.compatibilityZoneURL != null || config.networkServices != null) { - "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." - } + return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> + if (isRegistration) { + require(!config.devMode) { "Registration cannot occur in development mode" } + require(config.compatibilityZoneURL != null || config.networkServices != null) { + "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." } } } } } + data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 99e9f50778..aa6b5a16b8 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -661,7 +661,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, requireNotNull(getCertificateStores()) { "One or more keyStores (identity or TLS) or trustStore not found. " + "Please either copy your existing keys and certificates from another node, " + - "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + + "or if you don't have one yet, fill out the config file and run corda.jar initial-registration. " + "Read more at: https://docs.corda.net/permissioning.html" } } catch (e: KeyStoreException) { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 27523d9ccb..1794cc135c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -6,6 +6,7 @@ import com.codahale.metrics.MetricRegistry import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter import com.palominolabs.metrics.newrelic.NewRelicReporter import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme +import net.corda.cliutils.ShellConstants import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name @@ -110,9 +111,13 @@ open class Node(configuration: NodeConfiguration, LoggerFactory.getLogger(loggerName).info(msg) } + fun printInRed(message: String) { + println("${ShellConstants.RED}$message${ShellConstants.RESET}") + } + fun printWarning(message: String) { Emoji.renderIfSupported { - println("${Emoji.warningSign} ATTENTION: $message") + printInRed("${Emoji.warningSign} ATTENTION: $message") } staticLog.warn(message) } @@ -132,13 +137,13 @@ open class Node(configuration: NodeConfiguration, // TODO: make this configurable. const val MAX_RPC_MESSAGE_SIZE = 10485760 - fun isValidJavaVersion(): Boolean { + fun isInvalidJavaVersion(): Boolean { if (!hasMinimumJavaVersion()) { println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.") println("Corda will now exit...") - return false + return true } - return true + return false } private fun hasMinimumJavaVersion(): Boolean { 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 f26af3380a..e74de238ca 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -1,31 +1,24 @@ package net.corda.node.internal -import com.typesafe.config.Config import com.typesafe.config.ConfigException -import com.typesafe.config.ConfigRenderOptions import io.netty.channel.unix.Errors -import net.corda.cliutils.CordaCliWrapper -import net.corda.cliutils.CordaVersionProvider -import net.corda.cliutils.ExitCodes +import net.corda.cliutils.* import net.corda.core.crypto.Crypto import net.corda.core.internal.* import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.errors.AddressBindingException import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor import net.corda.node.* -import net.corda.node.internal.Node.Companion.isValidJavaVersion +import net.corda.node.internal.Node.Companion.isInvalidJavaVersion import net.corda.node.internal.cordapp.MultipleCordappsForFlowException +import net.corda.node.internal.subcommands.* import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.shouldStartLocalShell import net.corda.node.services.config.shouldStartSSHDaemon -import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate -import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NodeRegistrationException -import net.corda.node.utilities.registration.NodeRegistrationHelper -import net.corda.node.utilities.saveToKeyStore -import net.corda.node.utilities.saveToTrustStore import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException @@ -33,10 +26,8 @@ import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.tools.shell.InteractiveShell import org.fusesource.jansi.Ansi import org.slf4j.bridge.SLF4JBridgeHandler -import picocli.CommandLine.Mixin +import picocli.CommandLine.* import sun.misc.VMSupport -import java.io.Console -import java.io.File import java.io.IOException import java.io.RandomAccessFile import java.lang.management.ManagementFactory @@ -45,201 +36,145 @@ import java.nio.file.Path import java.time.DayOfWeek import java.time.ZonedDateTime import java.util.* -import kotlin.system.exitProcess -/** This class is responsible for starting a Node from command line arguments. */ -open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { +/** An interface that can be implemented to tell the node what to do once it's intitiated. */ +interface RunAfterNodeInitialisation { + fun run(node: Node) +} + +/** Base class for subcommands to derive from that initialises the logs and provides standard options. */ +abstract class NodeCliCommand(alias: String, description: String, val startup: NodeStartup) : CliWrapperBase(alias, description), NodeStartupLogging { companion object { - private val logger by lazy { loggerFor() } // I guess this is lazy to allow for logging init, but why Node? const val LOGS_DIRECTORY_NAME = "logs" - const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" - private const val INITIAL_REGISTRATION_MARKER = ".initialregistration" + } + + override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory) + + @Mixin + val cmdLineOptions = SharedNodeCmdLineOptions() +} + +/** Main corda entry point. */ +open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") { + val startup = NodeStartup() + private val networkCacheCli = ClearNetworkCacheCli(startup) + private val justGenerateNodeInfoCli = GenerateNodeInfoCli(startup) + private val justGenerateRpcSslCertsCli = GenerateRpcSslCertsCli(startup) + private val initialRegistrationCli = InitialRegistrationCli(startup) + + override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory) + + override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli) + + override fun runProgram(): Int { + return when { + InitialRegistration.checkRegistrationMode(cmdLineOptions.baseDirectory) -> { + println("Node was started before in `initial-registration` mode, but the registration was not completed.\nResuming registration.") + initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions) + initialRegistrationCli.runProgram() + } + //deal with legacy flags and redirect to subcommands + cmdLineOptions.isRegistration -> { + Node.printWarning("The --initial-registration flag has been deprecated and will be removed in a future version. Use the initial-registration command instead.") + requireNotNull(cmdLineOptions.networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." } + initialRegistrationCli.networkRootTrustStorePassword = cmdLineOptions.networkRootTrustStorePassword!! + initialRegistrationCli.networkRootTrustStorePathParameter = cmdLineOptions.networkRootTrustStorePathParameter + initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions) + initialRegistrationCli.runProgram() + } + cmdLineOptions.clearNetworkMapCache -> { + Node.printWarning("The --clear-network-map-cache flag has been deprecated and will be removed in a future version. Use the clear-network-cache command instead.") + networkCacheCli.cmdLineOptions.copyFrom(cmdLineOptions) + networkCacheCli.runProgram() + } + cmdLineOptions.justGenerateNodeInfo -> { + Node.printWarning("The --just-generate-node-info flag has been deprecated and will be removed in a future version. Use the generate-node-info command instead.") + justGenerateNodeInfoCli.cmdLineOptions.copyFrom(cmdLineOptions) + justGenerateNodeInfoCli.runProgram() + } + cmdLineOptions.justGenerateRpcSslCerts -> { + Node.printWarning("The --just-generate-rpc-ssl-settings flag has been deprecated and will be removed in a future version. Use the generate-rpc-ssl-settings command instead.") + justGenerateRpcSslCertsCli.cmdLineOptions.copyFrom(cmdLineOptions) + justGenerateRpcSslCertsCli.runProgram() + } + else -> startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation { + val startupTime = System.currentTimeMillis() + override fun run(node: Node) = startup.startNode(node, startupTime) + }) + } } @Mixin val cmdLineOptions = NodeCmdLineOptions() +} + +/** This class provides a common set of functionality for starting a Node from command line arguments. */ +open class NodeStartup : NodeStartupLogging { + companion object { + private val logger by lazy { loggerFor() } // I guess this is lazy to allow for logging init, but why Node? + const val LOGS_DIRECTORY_NAME = "logs" + const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" + } + + lateinit var cmdLineOptions: SharedNodeCmdLineOptions + + fun initialiseAndRun(cmdLineOptions: SharedNodeCmdLineOptions, afterNodeInitialisation: RunAfterNodeInitialisation): Int { + this.cmdLineOptions = cmdLineOptions - /** - * @return exit code based on the success of the node startup. This value is intended to be the exit code of the process. - */ - override fun runProgram(): Int { - val startTime = System.currentTimeMillis() // Step 1. Check for supported Java version. - if (!isValidJavaVersion()) return ExitCodes.FAILURE + if (isInvalidJavaVersion()) return ExitCodes.FAILURE // Step 2. We do the single node check before we initialise logging so that in case of a double-node start it // doesn't mess with the running node's logs. enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory) - // Step 3. Initialise logging. - initLogging() - - // Step 4. Register all cryptography [Provider]s. + // Step 3. Register all cryptography [Provider]s. // Required to install our [SecureRandom] before e.g., UUID asks for one. - // This needs to go after initLogging(netty clashes with our logging). + // This needs to go after initLogging(netty clashes with our logging) Crypto.registerProviders() - // Step 5. Print banner and basic node info. + // Step 4. Print banner and basic node info. val versionInfo = getVersionInfo() drawBanner(versionInfo) Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) - // Step 6. Load and validate node configuration. - val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success::value) ?: return ExitCodes.FAILURE + // Step 5. Load and validate node configuration. + val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success::value) + ?: return ExitCodes.FAILURE val errors = configuration.validate() if (errors.isNotEmpty()) { logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") return ExitCodes.FAILURE } - // Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization. - attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE + // Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization. + attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success + ?: return ExitCodes.FAILURE - // Step 8. Any actions required before starting up the Corda network layer. - attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE + // Step 7. Any actions required before starting up the Corda network layer. + attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success + ?: return ExitCodes.FAILURE - // Step 9. Check if in registration mode. - checkAndRunRegistrationMode(configuration, versionInfo)?.let { - return if (it) ExitCodes.SUCCESS - else ExitCodes.FAILURE - } - - // Step 10. Log startup info. + // Step 8. Log startup info. logStartupInfo(versionInfo, configuration) - // Step 11. Start node: create the node, check for other command-line options, add extra logging etc. - attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE + // Step 9. Start node: create the node, check for other command-line options, add extra logging etc. + attempt { + cmdLineOptions.baseDirectory.createDirectories() + afterNodeInitialisation.run(createNode(configuration, versionInfo)) + }.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE return ExitCodes.SUCCESS } - private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? { - checkUnfinishedRegistration() - cmdLineOptions.nodeRegistrationOption?.let { - // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] - attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success - ?: return false - // At this point the node registration was successful. We can delete the marker file. - deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory) - return true - } - return null - } - - // TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration. - // There might be cases where the node user should investigate what went wrong before registering again. - private fun checkUnfinishedRegistration() { - if (checkRegistrationMode() && !cmdLineOptions.isRegistration) { - println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.") - // Pretend that the node was started with `--initial-registration` to help prevent user error. - cmdLineOptions.isRegistration = true - } - } - - private fun attempt(action: () -> RESULT): Try = Try.on(action) - - private fun Exception.isExpectedWhenStartingNode() = startNodeExpectedErrors.any { error -> error.isInstance(this) } - - private val startNodeExpectedErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class) - - private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message) - - private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error) - - private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true - - private val handleRegistrationError = { error: Exception -> - when (error) { - is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}") - else -> error.logAsUnexpected("Exception during node registration") - } - } - - private val handleStartError = { error: Exception -> - when { - error.isExpectedWhenStartingNode() -> error.logAsExpected() - error is CouldNotCreateDataSourceException -> error.logAsUnexpected() - error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.") - error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.") - else -> error.logAsUnexpected("Exception during node startup") - } - } - - private fun handleConfigurationLoadingError(configFile: Path) = { error: Exception -> - when (error) { - is UnknownConfigurationKeysException -> error.logAsExpected() - is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println) - else -> error.logAsUnexpected("Unexpected error whilst reading node configuration") - } - } - - private fun configFileNotFoundMessage(configFile: Path): String { - return """ - Unable to load the node config file from '$configFile'. - - Try setting the --base-directory flag to change which directory the node - is looking in, or use the --config-file flag to specify it explicitly. - """.trimIndent() - } - - private fun loadConfiguration(): NodeConfiguration { - val (rawConfig, configurationResult) = loadConfigFile() - if (cmdLineOptions.devMode == true) { - println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") - } - return configurationResult.getOrThrow() - } - - private fun checkRegistrationMode(): Boolean { - // If the node was started with `--initial-registration`, create marker file. - // We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails. - val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER - if (!cmdLineOptions.isRegistration && !marker.exists()) { - return false - } - try { - marker.createFile() - } catch (e: Exception) { - logger.warn("Could not create marker file for `--initial-registration`.", e) - } - return true - } - - private fun deleteNodeRegistrationMarker(baseDir: Path) { - try { - val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri()) - if (marker.exists()) { - marker.delete() - } - } catch (e: Exception) { - e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn) - } - } - protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit - protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) + open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) - protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) { - cmdLineOptions.baseDirectory.createDirectories() - val node = createNode(conf, versionInfo) - if (cmdLineOptions.clearNetworkMapCache) { - node.clearNetworkMapCache() - return - } - if (cmdLineOptions.justGenerateNodeInfo) { - // Perform the minimum required start-up logic to be able to write a nodeInfo to disk - node.generateAndSaveNodeInfo() - return - } - if (cmdLineOptions.justGenerateRpcSslCerts) { - generateRpcSslCertificates(conf) - return - } - - if (conf.devMode) { + fun startNode(node: Node, startTime: Long) { + if (node.configuration.devMode) { Emoji.renderIfSupported { - Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.") + Node.printWarning("This node is running in development mode! ${Emoji.developer} This is not safe for production deployment.") } } else { logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.") @@ -256,7 +191,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") // Don't start the shell if there's no console attached. - if (conf.shouldStartLocalShell()) { + if (node.configuration.shouldStartLocalShell()) { node.startupComplete.then { try { InteractiveShell.runLocalShell(node::stop) @@ -265,8 +200,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { } } } - if (conf.shouldStartSSHDaemon()) { - Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString()) + if (node.configuration.shouldStartSSHDaemon()) { + Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString()) } }, { th -> @@ -275,82 +210,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { node.run() } - private fun generateRpcSslCertificates(conf: NodeConfiguration) { - val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal) - - val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks" - val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks" - - if (keyStorePath.exists() || trustStorePath.exists()) { - println("Found existing RPC SSL keystores. Command was already run. Exiting..") - exitProcess(0) - } - - val console: Console? = System.console() - - when (console) { - // In this case, the JVM is not connected to the console so we need to exit. - null -> { - println("Not connected to console. Exiting") - exitProcess(1) - } - // Otherwise we can proceed normally. - else -> { - while (true) { - val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ") - // TODO: consider adding a password strength policy. - if (keystorePassword1.isEmpty()) { - println("The RPC keystore password cannot be an empty String.") - continue - } - - val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ") - if (!keystorePassword1.contentEquals(keystorePassword2)) { - println("The RPC keystore passwords don't match.") - continue - } - - saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl") - println("The RPC keystore was saved to: $keyStorePath .") - break - } - - while (true) { - val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ") - // TODO: consider adding a password strength policy. - if (trustStorePassword1.isEmpty()) { - println("The RPC truststore password cannot be an empty String.") - continue - } - - val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ") - if (!trustStorePassword1.contentEquals(trustStorePassword2)) { - println("The RPC truststore passwords don't match.") - continue - } - - saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl") - println("The RPC truststore was saved to: $trustStorePath .") - println("You need to distribute this file along with the password in a secure way to all RPC clients.") - break - } - - val dollar = '$' - println(""" - | - |The SSL certificates for RPC were generated successfully. - | - |Add this snippet to the "rpcSettings" section of your node.conf: - | useSsl=true - | ssl { - | keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks - | keyStorePassword=the_above_password - | } - |""".trimMargin()) - } - } - } - protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) { logger.info("Vendor: ${versionInfo.vendor}") logger.info("Release: ${versionInfo.releaseVersion}") @@ -376,40 +235,12 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { logger.info(nodeStartedMessage) } - protected open fun registerWithNetwork( - conf: NodeConfiguration, - versionInfo: VersionInfo, - nodeRegistrationConfig: NodeRegistrationOption - ) { - println("\n" + - "******************************************************************\n" + - "* *\n" + - "* Registering as a new participant with a Corda network *\n" + - "* *\n" + - "******************************************************************\n") - - NodeRegistrationHelper(conf, - HTTPNetworkRegistrationService( - requireNotNull(conf.networkServices), - versionInfo), - nodeRegistrationConfig).buildKeystore() - - // Minimal changes to make registration tool create node identity. - // TODO: Move node identity generation logic from node to registration helper. - createNode(conf, getVersionInfo()).generateAndSaveNodeInfo() - - println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.") - println("Corda node will now terminate.") - } - - protected open fun loadConfigFile(): Pair> = cmdLineOptions.loadConfig() - protected open fun banJavaSerialisation(conf: NodeConfiguration) { // Note that in dev mode this filter can be overridden by a notary service implementation. SerialFilter.install(::defaultSerialFilter) } - protected open fun getVersionInfo(): VersionInfo { + open fun getVersionInfo(): VersionInfo { return VersionInfo( PLATFORM_VERSION, CordaVersionProvider.releaseVersion, @@ -462,18 +293,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { } } - override fun initLogging() { - val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) - System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. - if (verbose) { - System.setProperty("consoleLogLevel", loggingLevel) - Node.renderBasicInfoToConsole = false - } - System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString()) - SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. - SLF4JBridgeHandler.install() - } - private fun lookupMachineNameAndMaybeWarn(): String { val start = System.currentTimeMillis() val hostName: String = InetAddress.getLocalHost().hostName @@ -577,3 +396,66 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { } } +/** Provide some common logging methods for node startup commands. */ +interface NodeStartupLogging { + companion object { + val logger by lazy { contextLogger() } + val startupErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class) + } + + fun attempt(action: () -> RESULT): Try = Try.on(action) + + fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message) + + fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error) + + fun handleRegistrationError(error: Exception) { + when (error) { + is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}") + else -> error.logAsUnexpected("Exception during node registration") + } + } + + fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true + + fun Exception.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) } + + fun handleStartError(error: Exception) { + when { + error.isExpectedWhenStartingNode() -> error.logAsExpected() + error is CouldNotCreateDataSourceException -> error.logAsUnexpected() + error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.") + error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.") + else -> error.logAsUnexpected("Exception during node startup") + } + } + + fun handleConfigurationLoadingError(configFile: Path) = { error: Exception -> + when (error) { + is UnknownConfigurationKeysException -> error.logAsExpected() + is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println) + else -> error.logAsUnexpected("Unexpected error whilst reading node configuration") + } + } + + private fun configFileNotFoundMessage(configFile: Path): String { + return """ + Unable to load the node config file from '$configFile'. + + Try setting the --base-directory flag to change which directory the node + is looking in, or use the --config-file flag to specify it explicitly. + """.trimIndent() + } +} + +fun CliWrapperBase.initLogging(baseDirectory: Path) { + val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) + System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. + if (verbose) { + System.setProperty("consoleLogLevel", loggingLevel) + Node.renderBasicInfoToConsole = false + } + System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString()) + SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. + SLF4JBridgeHandler.install() +} diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt new file mode 100644 index 0000000000..224697d410 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt @@ -0,0 +1,14 @@ +package net.corda.node.internal.subcommands + +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation + +class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation { + override fun run(node: Node) = node.clearNetworkMapCache() + }) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt new file mode 100644 index 0000000000..dc4b240bd8 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt @@ -0,0 +1,16 @@ +package net.corda.node.internal.subcommands + +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation + +class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation { + override fun run(node: Node) { + node.generateAndSaveNodeInfo() + } + }) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt new file mode 100644 index 0000000000..6174a2df2e --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt @@ -0,0 +1,103 @@ +package net.corda.node.internal.subcommands + +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate +import net.corda.node.utilities.saveToKeyStore +import net.corda.node.utilities.saveToTrustStore +import java.io.Console +import kotlin.system.exitProcess + +class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts()) + } +} + +class GenerateRpcSslCerts: RunAfterNodeInitialisation { + override fun run(node: Node) { + generateRpcSslCertificates(node.configuration) + } + + private fun generateRpcSslCertificates(conf: NodeConfiguration) { + val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal) + + val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks" + val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks" + + if (keyStorePath.exists() || trustStorePath.exists()) { + println("Found existing RPC SSL keystores. Command was already run. Exiting.") + exitProcess(0) + } + + val console: Console? = System.console() + + when (console) { + // In this case, the JVM is not connected to the console so we need to exit. + null -> { + println("Not connected to console. Exiting.") + exitProcess(1) + } + // Otherwise we can proceed normally. + else -> { + while (true) { + val keystorePassword1 = console.readPassword("Enter the RPC keystore password:") + // TODO: consider adding a password strength policy. + if (keystorePassword1.isEmpty()) { + println("The RPC keystore password cannot be an empty String.") + continue + } + + val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password:") + if (!keystorePassword1.contentEquals(keystorePassword2)) { + println("The RPC keystore passwords don't match.") + continue + } + + saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl") + println("The RPC keystore was saved to: $keyStorePath .") + break + } + + while (true) { + val trustStorePassword1 = console.readPassword("Enter the RPC truststore password:") + // TODO: consider adding a password strength policy. + if (trustStorePassword1.isEmpty()) { + println("The RPC truststore password cannot be an empty string.") + continue + } + + val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password:") + if (!trustStorePassword1.contentEquals(trustStorePassword2)) { + println("The RPC truststore passwords don't match.") + continue + } + + saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl") + println("The RPC truststore was saved to: $trustStorePath.") + println("You need to distribute this file along with the password in a secure way to all RPC clients.") + break + } + + val dollar = '$' + println(""" + | + |The SSL certificates for RPC were generated successfully. + | + |Add this snippet to the "rpcSettings" section of your node.conf: + | useSsl=true + | ssl { + | keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks + | keyStorePassword=the_above_password + | } + |""".trimMargin()) + } + } + } + +} diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt new file mode 100644 index 0000000000..33c9ac048c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt @@ -0,0 +1,110 @@ +package net.corda.node.internal.subcommands + +import net.corda.cliutils.CliWrapperBase +import net.corda.core.internal.createFile +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.utilities.Try +import net.corda.node.InitialRegistrationCmdLineOptions +import net.corda.node.NodeRegistrationOption +import net.corda.node.internal.* +import net.corda.node.internal.NodeStartupLogging.Companion.logger +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.utilities.registration.HTTPNetworkRegistrationService +import net.corda.node.utilities.registration.NodeRegistrationHelper +import picocli.CommandLine.Mixin +import picocli.CommandLine.Option +import java.io.File +import java.nio.file.Path + +class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") { + @Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."]) + var networkRootTrustStorePathParameter: Path? = null + + @Option(names = ["-p", "--network-root-truststore-password"], description = ["Network root trust store password obtained from network operator."], required = true) + var networkRootTrustStorePassword: String = "" + + override fun runProgram() : Int { + val networkRootTrustStorePath: Path = networkRootTrustStorePathParameter ?: cmdLineOptions.baseDirectory / "certificates" / "network-root-truststore.jks" + return startup.initialiseAndRun(cmdLineOptions, InitialRegistration(cmdLineOptions.baseDirectory, networkRootTrustStorePath, networkRootTrustStorePassword, startup)) + } + + override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory) + + @Mixin + val cmdLineOptions = InitialRegistrationCmdLineOptions() +} + +class InitialRegistration(val baseDirectory: Path, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, private val startup: NodeStartup) : RunAfterNodeInitialisation, NodeStartupLogging { + companion object { + private const val INITIAL_REGISTRATION_MARKER = ".initialregistration" + + fun checkRegistrationMode(baseDirectory: Path): Boolean { + // If the node was started with `--initial-registration`, create marker file. + // We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails. + val marker = baseDirectory / INITIAL_REGISTRATION_MARKER + if (!marker.exists()) { + return false + } + try { + marker.createFile() + } catch (e: Exception) { + logger.warn("Could not create marker file for `initial-registration`.", e) + } + return true + } + } + + private val nodeRegistration = NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword) + + private fun registerWithNetwork(conf: NodeConfiguration) { + val versionInfo = startup.getVersionInfo() + + println("\n" + + "******************************************************************\n" + + "* *\n" + + "* Registering as a new participant with a Corda network *\n" + + "* *\n" + + "******************************************************************\n") + + NodeRegistrationHelper(conf, + HTTPNetworkRegistrationService( + requireNotNull(conf.networkServices), + versionInfo), + nodeRegistration).buildKeystore() + + // Minimal changes to make registration tool create node identity. + // TODO: Move node identity generation logic from node to registration helper. + startup.createNode(conf, versionInfo).generateAndSaveNodeInfo() + + println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.") + println("Corda node will now terminate.") + } + + private fun initialRegistration(config: NodeConfiguration) { + // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] + attempt { registerWithNetwork(config) }.doOnException(this::handleRegistrationError) as? Try.Success + // At this point the node registration was successful. We can delete the marker file. + deleteNodeRegistrationMarker(baseDirectory) + } + + private fun deleteNodeRegistrationMarker(baseDir: Path) { + try { + val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri()) + if (marker.exists()) { + marker.delete() + } + } catch (e: Exception) { + e.logAsUnexpected( "Could not delete the marker file that was created for `initial-registration`.", print = logger::warn) + } + } + + override fun run(node: Node) { + require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" } + if (checkRegistrationMode(baseDirectory)) { + println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.") + } + initialRegistration(node.configuration) + } +} + diff --git a/node/src/main/resources/net.corda.node.internal.NodeStartup.yml b/node/src/main/resources/net.corda.node.internal.NodeStartupCli.yml similarity index 92% rename from node/src/main/resources/net.corda.node.internal.NodeStartup.yml rename to node/src/main/resources/net.corda.node.internal.NodeStartupCli.yml index 28f0651a78..ea59505694 100644 --- a/node/src/main/resources/net.corda.node.internal.NodeStartup.yml +++ b/node/src/main/resources/net.corda.node.internal.NodeStartupCli.yml @@ -26,11 +26,6 @@ required: false multiParam: false acceptableValues: [] - - parameterName: "--install-shell-extensions" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "--just-generate-node-info" parameterType: "boolean" required: false @@ -99,11 +94,6 @@ required: false multiParam: true acceptableValues: [] - - parameterName: "-c" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "-d" parameterType: "java.lang.Boolean" required: false diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt index 283814b936..d7766ecb76 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt @@ -2,4 +2,4 @@ package net.corda.node.internal import net.corda.testing.CliBackwardsCompatibleTest -class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java) \ No newline at end of file +class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartupCli::class.java) \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt index ec1da7a226..976c1fec79 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt @@ -1,6 +1,8 @@ package net.corda.node.internal import net.corda.core.internal.div +import net.corda.node.InitialRegistrationCmdLineOptions +import net.corda.node.internal.subcommands.InitialRegistrationCli import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import org.assertj.core.api.Assertions.assertThat import org.junit.BeforeClass @@ -11,7 +13,7 @@ import java.nio.file.Path import java.nio.file.Paths class NodeStartupTest { - private val startup = NodeStartup() + private val startup = NodeStartupCli() companion object { private lateinit var workingDirectory: Path @@ -30,7 +32,6 @@ class NodeStartupTest { assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf") assertThat(startup.verbose).isEqualTo(false) assertThat(startup.loggingLevel).isEqualTo(Level.INFO) - assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null) assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false) assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false) assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false) @@ -38,7 +39,7 @@ class NodeStartupTest { assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL) assertThat(startup.cmdLineOptions.devMode).isEqualTo(null) assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false) - assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks") + assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null) } @Test @@ -46,6 +47,6 @@ class NodeStartupTest { CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString()) assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir") assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf") - assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks") + assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index ad2173c181..18bc7da028 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -322,7 +322,7 @@ class DriverDSLImpl( } else { startOutOfProcessMiniNode( config, - "--initial-registration", + "initial-registration", "--network-root-truststore=${rootTruststorePath.toAbsolutePath()}", "--network-root-truststore-password=$rootTruststorePassword" ).map { config } @@ -457,7 +457,7 @@ class DriverDSLImpl( } else { // TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper // This causes two node info files to be generated. - startOutOfProcessMiniNode(config, "--just-generate-node-info").map { + startOutOfProcessMiniNode(config, "generate-node-info").map { // Once done we have to read the signed node info file that's been generated val nodeInfoFile = config.corda.baseDirectory.list { paths -> paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index a86abe6b9d..21988565aa 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -32,6 +32,7 @@ import rx.internal.schedulers.CachedThreadScheduler import java.nio.file.Path import java.util.concurrent.Executors import kotlin.concurrent.thread +import kotlin.test.assertFalse // TODO Some of the logic here duplicates what's in the driver - the reason why it's not straightforward to replace it by // using DriverDSLImpl in `init()` and `stopAllNodes()` is because of the platform version passed to nodes (driver doesn't @@ -150,9 +151,8 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi } class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)) : Node(configuration, versionInfo, false, flowManager = flowManager) { - override fun start() : NodeInfo { - check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." } + assertFalse(isInvalidJavaVersion(), "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." ) return super.start() } diff --git a/testing/test-cli/build.gradle b/testing/test-cli/build.gradle index 462499bf64..3d32512119 100644 --- a/testing/test-cli/build.gradle +++ b/testing/test-cli/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'java' apply plugin: 'kotlin' dependencies { - compile group: 'info.picocli', name: 'picocli', version: '3.0.1' + compile "info.picocli:picocli:$picocli_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0" compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0" diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle index a1f6f865ef..8c0045035d 100644 --- a/tools/blobinspector/build.gradle +++ b/tools/blobinspector/build.gradle @@ -14,7 +14,6 @@ dependencies { exclude module: 'node-api' exclude module: 'finance' } - testCompile project(':test-utils') } jar { diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index 8670989fd1..8d8e762ed7 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -47,7 +47,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised description = ["Display the owningKey and certPath properties of Party and PartyAndReference objects respectively"]) private var fullParties: Boolean = false - @Option(names = ["--schema"], description = ["Print the blob's schema first"]) + @Option(names = ["--schema"], description = ["Prints the blob's schema first"]) private var schema: Boolean = false override fun runProgram() = run(System.out) diff --git a/tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml b/tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml new file mode 100644 index 0000000000..66b94ae7de --- /dev/null +++ b/tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml @@ -0,0 +1,58 @@ +- commandName: "
    " + positionalParams: + - parameterName: "0" + parameterType: "java.net.URL" + required: true + multiParam: false + acceptableValues: [] + params: + - parameterName: "--format" + parameterType: "net.corda.blobinspector.OutputFormatType" + required: false + multiParam: false + acceptableValues: + - "YAML" + - "JSON" + - parameterName: "--full-parties" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--input-format" + parameterType: "net.corda.blobinspector.InputFormatType" + required: false + multiParam: false + acceptableValues: + - "BINARY" + - "HEX" + - "BASE64" + - parameterName: "--log-to-console" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--logging-level" + parameterType: "org.slf4j.event.Level" + required: false + multiParam: false + acceptableValues: + - "ERROR" + - "WARN" + - "INFO" + - "DEBUG" + - "TRACE" + - parameterName: "--schema" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--verbose" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-v" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] \ No newline at end of file diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index e3a1967e2e..3436bdfaf3 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -25,7 +25,7 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) private var noCopy: Boolean = false - @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters"]) + @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."]) private var minimumPlatformVersion = PLATFORM_VERSION override fun runProgram(): Int { diff --git a/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml index de9379b953..f8e50ac29b 100644 --- a/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml +++ b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml @@ -6,11 +6,6 @@ required: false multiParam: true acceptableValues: [] - - parameterName: "--install-shell-extensions" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "--log-to-console" parameterType: "boolean" required: false diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index 5a35be8089..60fa089ff4 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -69,57 +69,43 @@ fun CordaCliWrapper.start(args: Array) { cmd.registerConverter(Path::class.java) { Paths.get(it).toAbsolutePath().normalize() } cmd.commandSpec.name(alias) cmd.commandSpec.usageMessage().description(description) - cmd.commandSpec.parser().collectErrors(true) + this.subCommands().forEach { + val subCommand = CommandLine(it) + it.args = args + subCommand.commandSpec.usageMessage().description(it.description) + cmd.commandSpec.addSubcommand(it.alias, subCommand) + } + try { val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { Help.Ansi.ON } else { Help.Ansi.AUTO } - - val results = cmd.parse(*args) - val app = cmd.getCommand() - if (cmd.isUsageHelpRequested) { - cmd.usage(System.out, defaultAnsiMode) - exitProcess(ExitCodes.SUCCESS) - } - if (cmd.isVersionHelpRequested) { - cmd.printVersionHelp(System.out, defaultAnsiMode) - exitProcess(ExitCodes.SUCCESS) - } - if (app.installShellExtensionsParser.installShellExtensions) { - System.out.println("Install shell extensions: ${app.installShellExtensionsParser.installShellExtensions}") - // ignore any parsing errors and run the program - exitProcess(app.call()) - } - val allErrors = results.flatMap { it.parseResult?.errors() ?: emptyList() } - if (allErrors.any()) { - val parameterExceptions = allErrors.asSequence().filter { it is ParameterException } - if (parameterExceptions.any()) { - System.err.println("${ShellConstants.RED}${parameterExceptions.map{ it.message }.joinToString()}${ShellConstants.RESET}") - parameterExceptions.filter { it is UnmatchedArgumentException}.forEach { (it as UnmatchedArgumentException).printSuggestions(System.out) } - usage(cmd, System.out, defaultAnsiMode) + val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), + DefaultExceptionHandler>().useErr(System.err).useAnsi(defaultAnsiMode), + *args) + // If an error code has been returned, use this and exit + results?.firstOrNull()?.let { + if (it is Int) { + exitProcess(it) + } else { exitProcess(ExitCodes.FAILURE) } - throw allErrors.first() } - exitProcess(app.call()) - } catch (e: Exception) { + // If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully + exitProcess(ExitCodes.SUCCESS) + } catch (e: ExecutionException) { val throwable = e.cause ?: e if (this.verbose) { throwable.printStackTrace() } else { - System.err.println("${ShellConstants.RED}${throwable.rootMessage ?: "Use --verbose for more details"}${ShellConstants.RESET}") + System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}") } exitProcess(ExitCodes.FAILURE) } } -/** - * Simple base class for handling help, version, verbose and logging-level commands. - * As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node) - * Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations - */ @Command(mixinStandardHelpOptions = true, versionProvider = CordaVersionProvider::class, sortOptions = false, @@ -129,9 +115,9 @@ fun CordaCliWrapper.start(args: Array) { parameterListHeading = "%n@|bold,underline Parameters|@:%n%n", optionListHeading = "%n@|bold,underline Options|@:%n%n", commandListHeading = "%n@|bold,underline Commands|@:%n%n") -abstract class CordaCliWrapper(val alias: String, val description: String) : Callable { +abstract class CliWrapperBase(val alias: String, val description: String) : Callable { companion object { - private val logger by lazy { loggerFor() } + private val logger by lazy { contextLogger() } } // Raw args are provided for use in logging - this is a lateinit var rather than a constructor parameter as the class @@ -148,9 +134,6 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal ) var loggingLevel: Level = Level.INFO - @Mixin - lateinit var installShellExtensionsParser: InstallShellExtensionsParser - // This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before). // Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used. open fun initLogging() { @@ -169,7 +152,32 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal override fun call(): Int { initLogging() logger.info("Application Args: ${args.joinToString(" ")}") - installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name) + return runProgram() + } +} + +/** + * Simple base class for handling help, version, verbose and logging-level commands. + * As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node) + * Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations + */ +abstract class CordaCliWrapper(alias: String, description: String) : CliWrapperBase(alias, description) { + companion object { + private val logger by lazy { contextLogger() } + } + + private val installShellExtensionsParser = InstallShellExtensionsParser(this) + + protected open fun additionalSubCommands(): Set = emptySet() + + fun subCommands(): Set { + return additionalSubCommands() + installShellExtensionsParser + } + + override fun call(): Int { + initLogging() + logger.info("Application Args: ${args.joinToString(" ")}") + installShellExtensionsParser.updateShellExtensions() return runProgram() } } diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt index cd271e1014..a9be0cbae0 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt @@ -2,13 +2,13 @@ package net.corda.cliutils import net.corda.core.internal.* import picocli.CommandLine +import picocli.CommandLine.Command import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardCopyOption import java.util.* -import kotlin.system.exitProcess -private class ShellExtensionsGenerator(val alias: String, val className: String) { +private class ShellExtensionsGenerator(val parent: CordaCliWrapper) { private class SettingsFile(val filePath: Path) { private val lines: MutableList by lazy { getFileLines() } var fileModified: Boolean = false @@ -68,25 +68,27 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}" private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias - private fun generateAutoCompleteFile(alias: String, className: String) { + private fun generateAutoCompleteFile(alias: String) { println("Generating $alias auto completion file") val autoCompleteFile = getAutoCompleteFileLocation(alias) autoCompleteFile.parent.createDirectories() - picocli.AutoComplete.main("-f", "-n", alias, className, "-o", autoCompleteFile.toStringWithDeWindowsfication()) + val hierarchy = CommandLine(parent) + parent.subCommands().forEach { hierarchy.addSubcommand(it.alias, it)} - // Append hash of file to autocomplete file - autoCompleteFile.toFile().appendText(jarVersion(alias)) + val builder = StringBuilder(picocli.AutoComplete.bash(alias, hierarchy)) + builder.append(jarVersion(alias)) + autoCompleteFile.writeText(builder.toString()) } fun installShellExtensions() { // Get jar location and generate alias command - val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'" - generateAutoCompleteFile(alias, className) + val command = "alias ${parent.alias}='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'" + generateAutoCompleteFile(parent.alias) // Get bash settings file val bashSettingsFile = SettingsFile(userHome / ".bashrc") // Replace any existing alias. There can be only one. - bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command) + bashSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command) val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done" bashSettingsFile.addIfNotExists(completionFileCommand) bashSettingsFile.updateAndBackupIfNecessary() @@ -95,17 +97,17 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) val zshSettingsFile = SettingsFile(userHome / ".zshrc") zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit") zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit") - zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command) + zshSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command) zshSettingsFile.addIfNotExists(completionFileCommand) zshSettingsFile.updateAndBackupIfNecessary() - println("Installation complete, $alias is available in bash with autocompletion. ") - println("Type `$alias ` from the commandline.") + println("Installation complete, ${parent.alias} is available in bash with autocompletion. ") + println("Type `${parent.alias} ` from the commandline.") println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now") } fun checkForAutoCompleteUpdate() { - val autoCompleteFile = getAutoCompleteFileLocation(alias) + val autoCompleteFile = getAutoCompleteFileLocation(parent.alias) // If no autocomplete file, it hasn't been installed, so don't do anything if (!autoCompleteFile.exists()) return @@ -113,25 +115,21 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) var lastLine = "" autoCompleteFile.toFile().forEachLine { lastLine = it } - if (lastLine != jarVersion(alias)) { + if (lastLine != jarVersion(parent.alias)) { println("Old auto completion file detected... regenerating") - generateAutoCompleteFile(alias, className) + generateAutoCompleteFile(parent.alias) println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now") } } } -class InstallShellExtensionsParser { - @CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"]) - var installShellExtensions: Boolean = false - - fun installOrUpdateShellExtensions(alias: String, className: String) { - val generator = ShellExtensionsGenerator(alias, className) - if (installShellExtensions) { - generator.installShellExtensions() - exitProcess(0) - } else { - generator.checkForAutoCompleteUpdate() - } +@Command(helpCommand = true) +class InstallShellExtensionsParser(private val cliWrapper: CordaCliWrapper) : CliWrapperBase("install-shell-extensions", "Install alias and autocompletion for bash and zsh") { + private val generator = ShellExtensionsGenerator(cliWrapper) + override fun runProgram(): Int { + generator.installShellExtensions() + return ExitCodes.SUCCESS } + + fun updateShellExtensions() = generator.checkForAutoCompleteUpdate() } \ No newline at end of file diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt index e3b51b8e85..92af11ef76 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt @@ -25,7 +25,7 @@ class NotaryCopier(val cacheDir: File) : NodeCopier(cacheDir) { fun generateNodeInfo(dirToGenerateFrom: File): File { val nodeInfoGeneratorProcess = ProcessBuilder() - .command(listOf("java", "-jar", "corda.jar", "--just-generate-node-info")) + .command(listOf("java", "-jar", "corda.jar", "generate-node-info")) .directory(dirToGenerateFrom) .inheritIO() .start() diff --git a/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml b/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml index ce5ec0bab1..20a9fde8fb 100644 --- a/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml +++ b/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml @@ -21,11 +21,6 @@ required: false multiParam: false acceptableValues: [] - - parameterName: "--install-shell-extensions" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "--log-to-console" parameterType: "boolean" required: false From 16453ebfcc15ba4013216f6f4ad80bf8426a4d72 Mon Sep 17 00:00:00 2001 From: bpaunescu Date: Wed, 24 Oct 2018 14:36:01 +0100 Subject: [PATCH 83/83] update OWASP to latest version after kotlin update (#4109) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 41f3f98e69..7cfd603ad1 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ buildscript { ext.rxjava_version = '1.3.8' ext.dokka_version = '0.9.17' ext.eddsa_version = '0.2.0' - ext.dependency_checker_version = '3.1.0' + ext.dependency_checker_version = '3.3.2' ext.commons_collections_version = '4.1' ext.beanutils_version = '1.9.3' ext.crash_version = 'cadb53544fbb3c0fb901445da614998a6a419488'

    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