Repository: omisego/elixir-omg Branch: master Commit: 2c68973d8f29 Files: 737 Total size: 2.7 MB Directory structure: gitextract_2b551nix/ ├── .circleci/ │ ├── ci_increase_chart_version.sh │ ├── ci_publish.sh │ ├── config.yml │ ├── status.sh │ └── test_runner.py ├── .formatter.exs ├── .githooks/ │ └── pre-commit ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── auto-merge-pr.yml │ ├── auto-pr-for-branch-syncing.yml │ └── enforce-changelog-labels.yml ├── .github_changelog_generator ├── .gitignore ├── .gitmodules ├── .releaserc.yaml ├── .tool-versions ├── AUTHORS ├── CHANGELOG.md ├── CODEOWNERS ├── Dockerfile.watcher ├── Dockerfile.watcher_info ├── LICENSE ├── Makefile ├── README.md ├── apps/ │ ├── omg_bus/ │ │ ├── lib/ │ │ │ ├── omg_bus/ │ │ │ │ ├── application.ex │ │ │ │ ├── event.ex │ │ │ │ ├── pubsub.ex │ │ │ │ └── supervisor.ex │ │ │ └── omg_bus.ex │ │ ├── mix.exs │ │ └── test/ │ │ ├── omg_bus/ │ │ │ └── event_test.exs │ │ └── test_helper.exs │ ├── omg_conformance/ │ │ ├── mix.exs │ │ └── test/ │ │ ├── omg_conformance/ │ │ │ └── conformance/ │ │ │ ├── merkle_proof_property_test.exs │ │ │ ├── merkle_proof_test.exs │ │ │ ├── signature_property_test.exs │ │ │ └── signature_test.exs │ │ ├── support/ │ │ │ └── conformance/ │ │ │ ├── merkle_proof_context.ex │ │ │ ├── merkle_proofs.ex │ │ │ ├── property.ex │ │ │ ├── signatures_hashes.ex │ │ │ └── signatures_hashes_case.ex │ │ └── test_helper.exs │ ├── omg_db/ │ │ ├── lib/ │ │ │ ├── db.ex │ │ │ └── omg_db/ │ │ │ ├── application.ex │ │ │ ├── measure.ex │ │ │ ├── models/ │ │ │ │ └── payment_exit_info.ex │ │ │ ├── release_tasks/ │ │ │ │ ├── init_key_value_db.ex │ │ │ │ ├── init_keys_with_values.ex │ │ │ │ └── set_key_value_db.ex │ │ │ ├── rocks_db.ex │ │ │ └── rocksdb/ │ │ │ ├── core.ex │ │ │ └── server.ex │ │ ├── mix.exs │ │ └── test/ │ │ ├── fixtures.exs │ │ ├── omg_db/ │ │ │ ├── application_test.exs │ │ │ ├── db_test.exs │ │ │ ├── models/ │ │ │ │ └── payment_exit_info_test.exs │ │ │ ├── release_tasks/ │ │ │ │ ├── init_key_value_db_test.exs │ │ │ │ ├── init_keys_with_values_test.exs │ │ │ │ └── set_key_value_db_test.exs │ │ │ └── rocks_db_test.exs │ │ ├── support/ │ │ │ └── rocks_db_case.ex │ │ └── test_helper.exs │ ├── omg_eth/ │ │ ├── lib/ │ │ │ ├── eth.ex │ │ │ └── omg_eth/ │ │ │ ├── application.ex │ │ │ ├── blockchain/ │ │ │ │ ├── bit_helper.ex │ │ │ │ ├── private_key.ex │ │ │ │ ├── transaction/ │ │ │ │ │ ├── hash.ex │ │ │ │ │ └── signature.ex │ │ │ │ └── transaction.ex │ │ │ ├── client.ex │ │ │ ├── configuration.ex │ │ │ ├── encoding/ │ │ │ │ └── contract_constructor.ex │ │ │ ├── encoding.ex │ │ │ ├── ethereum_height.ex │ │ │ ├── ethereum_height_monitor/ │ │ │ │ └── alarm_handler.ex │ │ │ ├── ethereum_height_monitor.ex │ │ │ ├── metric/ │ │ │ │ └── ethereumex.ex │ │ │ ├── release_tasks/ │ │ │ │ ├── set_contract.ex │ │ │ │ ├── set_ethereum_block_time.ex │ │ │ │ ├── set_ethereum_client.ex │ │ │ │ ├── set_ethereum_events_check_interval.ex │ │ │ │ └── set_ethereum_stalled_sync_threshold.ex │ │ │ ├── root_chain/ │ │ │ │ ├── abi.ex │ │ │ │ ├── abi_event_selector.ex │ │ │ │ ├── abi_function_selector.ex │ │ │ │ ├── event.ex │ │ │ │ ├── fields.ex │ │ │ │ ├── rpc.ex │ │ │ │ └── submit_block.ex │ │ │ ├── root_chain.ex │ │ │ ├── supervisor.ex │ │ │ └── transaction.ex │ │ ├── mix.exs │ │ └── test/ │ │ ├── fixtures.exs │ │ ├── omg_eth/ │ │ │ ├── application_test.exs │ │ │ ├── blockchain/ │ │ │ │ ├── bit_helper_test.exs │ │ │ │ ├── transaction/ │ │ │ │ │ ├── hash_test.exs │ │ │ │ │ └── signature_test.exs │ │ │ │ └── transaction_test.exs │ │ │ ├── client_test.exs │ │ │ ├── encoding/ │ │ │ │ └── contract_constructor_test.exs │ │ │ ├── encoding_test.exs │ │ │ ├── eth_test.exs │ │ │ ├── ethereum_height_monitor_test.exs │ │ │ ├── release_tasks/ │ │ │ │ ├── set_contract_test.exs │ │ │ │ ├── set_ethereum_block_time_test.exs │ │ │ │ ├── set_ethereum_client_test.exs │ │ │ │ ├── set_ethereum_events_check_interval_test.exs │ │ │ │ └── set_ethereum_stalled_sync_threshold_test.exs │ │ │ ├── root_chain/ │ │ │ │ ├── abi_test.exs │ │ │ │ └── event_test.exs │ │ │ └── root_chain_test.exs │ │ ├── support/ │ │ │ ├── defaults.ex │ │ │ ├── dev_geth.ex │ │ │ ├── dev_helper.ex │ │ │ ├── dev_node.ex │ │ │ ├── root_chain_helper.ex │ │ │ ├── snapshot_contracts.ex │ │ │ ├── token.ex │ │ │ ├── transaction_helper.ex │ │ │ └── wait_for.ex │ │ └── test_helper.exs │ ├── omg_status/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── omg_status/ │ │ │ │ ├── alert/ │ │ │ │ │ ├── alarm.ex │ │ │ │ │ ├── alarm_handler.ex │ │ │ │ │ └── alarm_printer.ex │ │ │ │ ├── application.ex │ │ │ │ ├── configuration.ex │ │ │ │ ├── datadog_event/ │ │ │ │ │ ├── alarm_consumer.ex │ │ │ │ │ └── alarm_handler.ex │ │ │ │ ├── metric/ │ │ │ │ │ ├── datadog.ex │ │ │ │ │ ├── event.ex │ │ │ │ │ ├── statix.ex │ │ │ │ │ ├── telemetry.ex │ │ │ │ │ ├── tracer.ex │ │ │ │ │ └── vmstats_sink.ex │ │ │ │ ├── monitor/ │ │ │ │ │ ├── memory_monitor.ex │ │ │ │ │ └── statsd_monitor.ex │ │ │ │ ├── release_tasks/ │ │ │ │ │ ├── set_application.ex │ │ │ │ │ ├── set_logger.ex │ │ │ │ │ ├── set_sentry.ex │ │ │ │ │ └── set_tracer.ex │ │ │ │ └── sentry_filter.ex │ │ │ └── status.ex │ │ ├── mix.exs │ │ └── test/ │ │ ├── omg_status/ │ │ │ ├── alert/ │ │ │ │ └── alarm_printer_test.exs │ │ │ ├── datadog_event/ │ │ │ │ └── alarm_consumer_test.exs │ │ │ ├── integration/ │ │ │ │ └── alarms_test.exs │ │ │ ├── metric/ │ │ │ │ └── datadog_test.exs │ │ │ ├── monitor/ │ │ │ │ ├── memory_monitor_test.exs │ │ │ │ └── statsd_monitor_test.exs │ │ │ └── release_tasks/ │ │ │ ├── set_logger_test.exs │ │ │ ├── set_sentry_test.exs │ │ │ └── set_tracer_test.exs │ │ ├── sentry_filter_test.exs │ │ └── test_helper.exs │ ├── omg_utils/ │ │ ├── lib/ │ │ │ ├── omg_utils/ │ │ │ │ ├── app_version.ex │ │ │ │ ├── http_rpc/ │ │ │ │ │ ├── encoding.ex │ │ │ │ │ ├── error.ex │ │ │ │ │ ├── response.ex │ │ │ │ │ └── validators/ │ │ │ │ │ └── base.ex │ │ │ │ ├── paginator.ex │ │ │ │ └── remote_ip.ex │ │ │ └── utils.ex │ │ ├── mix.exs │ │ └── test/ │ │ ├── omg_utils/ │ │ │ ├── app_version_tet.exs │ │ │ ├── http_rpc/ │ │ │ │ ├── encoding_test.exs │ │ │ │ ├── response_test.exs │ │ │ │ └── validators/ │ │ │ │ └── base_test.exs │ │ │ └── remote_ip_test.exs │ │ └── test_helper.exs │ ├── omg_watcher/ │ │ ├── lib/ │ │ │ ├── omg_watcher/ │ │ │ │ ├── api/ │ │ │ │ │ ├── account.ex │ │ │ │ │ ├── alarm.ex │ │ │ │ │ ├── configuration.ex │ │ │ │ │ ├── in_flight_exit.ex │ │ │ │ │ ├── status.ex │ │ │ │ │ ├── status_cache/ │ │ │ │ │ │ ├── external.ex │ │ │ │ │ │ └── storage.ex │ │ │ │ │ ├── status_cache.ex │ │ │ │ │ ├── transaction.ex │ │ │ │ │ └── utxo.ex │ │ │ │ ├── application.ex │ │ │ │ ├── block.ex │ │ │ │ ├── block_getter/ │ │ │ │ │ ├── block_application.ex │ │ │ │ │ ├── core.ex │ │ │ │ │ ├── measure.ex │ │ │ │ │ ├── status.ex │ │ │ │ │ └── supervisor.ex │ │ │ │ ├── block_getter.ex │ │ │ │ ├── block_validator.ex │ │ │ │ ├── child_manager.ex │ │ │ │ ├── configuration.ex │ │ │ │ ├── coordinator_setup.ex │ │ │ │ ├── crypto.ex │ │ │ │ ├── datadog_event/ │ │ │ │ │ ├── contract_event_consumer.ex │ │ │ │ │ └── encode.ex │ │ │ │ ├── ethereum_event_aggregator.ex │ │ │ │ ├── ethereum_event_listener/ │ │ │ │ │ ├── core.ex │ │ │ │ │ └── measure.ex │ │ │ │ ├── ethereum_event_listener.ex │ │ │ │ ├── event.ex │ │ │ │ ├── exit_processor/ │ │ │ │ │ ├── canonicity.ex │ │ │ │ │ ├── competitor_info.ex │ │ │ │ │ ├── core.ex │ │ │ │ │ ├── double_spend.ex │ │ │ │ │ ├── exit_info.ex │ │ │ │ │ ├── finalizations.ex │ │ │ │ │ ├── in_flight_exit_info.ex │ │ │ │ │ ├── known_tx.ex │ │ │ │ │ ├── measure.ex │ │ │ │ │ ├── piggyback.ex │ │ │ │ │ ├── request.ex │ │ │ │ │ ├── standard_exit.ex │ │ │ │ │ ├── tools.ex │ │ │ │ │ └── tx_appendix.ex │ │ │ │ ├── exit_processor.ex │ │ │ │ ├── fees/ │ │ │ │ │ └── fee_filter.ex │ │ │ │ ├── fees.ex │ │ │ │ ├── http_rpc/ │ │ │ │ │ ├── adapter.ex │ │ │ │ │ └── client.ex │ │ │ │ ├── merge_transaction_validator.ex │ │ │ │ ├── merkle.ex │ │ │ │ ├── monitor.ex │ │ │ │ ├── output.ex │ │ │ │ ├── raw_data.ex │ │ │ │ ├── release_tasks/ │ │ │ │ │ ├── set_application.ex │ │ │ │ │ ├── set_ethereum_events_check_interval.ex │ │ │ │ │ ├── set_exit_processor_sla_margin.ex │ │ │ │ │ └── set_tracer.ex │ │ │ │ ├── root_chain_coordinator/ │ │ │ │ │ ├── core.ex │ │ │ │ │ ├── measure.ex │ │ │ │ │ └── service.ex │ │ │ │ ├── root_chain_coordinator.ex │ │ │ │ ├── signature.ex │ │ │ │ ├── state/ │ │ │ │ │ ├── core.ex │ │ │ │ │ ├── measure.ex │ │ │ │ │ ├── measurement_calculation.ex │ │ │ │ │ ├── transaction/ │ │ │ │ │ │ ├── fee.ex │ │ │ │ │ │ ├── payment.ex │ │ │ │ │ │ ├── recovered.ex │ │ │ │ │ │ ├── signed.ex │ │ │ │ │ │ ├── validator/ │ │ │ │ │ │ │ ├── fee_claim.ex │ │ │ │ │ │ │ └── payment.ex │ │ │ │ │ │ ├── validator.ex │ │ │ │ │ │ └── witness.ex │ │ │ │ │ ├── transaction.ex │ │ │ │ │ └── utxo_set.ex │ │ │ │ ├── state.ex │ │ │ │ ├── supervisor.ex │ │ │ │ ├── sync_supervisor.ex │ │ │ │ ├── tracer.ex │ │ │ │ ├── typed_data_hash/ │ │ │ │ │ ├── config.ex │ │ │ │ │ ├── tools.ex │ │ │ │ │ └── types.ex │ │ │ │ ├── typed_data_hash.ex │ │ │ │ ├── utxo/ │ │ │ │ │ └── position.ex │ │ │ │ ├── utxo.ex │ │ │ │ ├── utxo_exit/ │ │ │ │ │ └── core.ex │ │ │ │ └── wire_format_types.ex │ │ │ └── omg_watcher.ex │ │ ├── mix.exs │ │ └── test/ │ │ ├── fixtures.exs │ │ ├── omg_watcher/ │ │ │ ├── api/ │ │ │ │ ├── account_test.exs │ │ │ │ ├── alarm_test.exs │ │ │ │ └── status_cache_test.exs │ │ │ ├── block_getter/ │ │ │ │ └── core_test.exs │ │ │ ├── block_test.exs │ │ │ ├── block_validator_test.exs │ │ │ ├── child_manager_test.exs │ │ │ ├── crypto_test.exs │ │ │ ├── datadog_event/ │ │ │ │ ├── contract_event_consumer_test.exs │ │ │ │ └── encode_test.exs │ │ │ ├── ethereum_event_aggregator_test.exs │ │ │ ├── ethereum_event_listener/ │ │ │ │ └── core_test.exs │ │ │ ├── exit_processor/ │ │ │ │ ├── canonicity_test.exs │ │ │ │ ├── core/ │ │ │ │ │ └── state_interaction_test.exs │ │ │ │ ├── core_test.exs │ │ │ │ ├── exit_info_test.exs │ │ │ │ ├── finalizations_test.exs │ │ │ │ ├── in_flight_exit_info_test.exs │ │ │ │ ├── persistence_test.exs │ │ │ │ ├── piggyback_test.exs │ │ │ │ ├── standard_exit_test.exs │ │ │ │ └── tools_test.exs │ │ │ ├── fees/ │ │ │ │ └── fee_filter_test.exs │ │ │ ├── fees_test.exs │ │ │ ├── http_rpc/ │ │ │ │ └── adapter_test.exs │ │ │ ├── integration/ │ │ │ │ ├── block_getter_1_test.exs │ │ │ │ ├── block_getter_2_test.exs │ │ │ │ ├── block_getter_3_test.exs │ │ │ │ ├── block_getter_4_test.exs │ │ │ │ ├── block_getter_test.exs │ │ │ │ ├── in_flight_exit_test.exs │ │ │ │ ├── in_flight_exit_test_1_test.exs │ │ │ │ ├── in_flight_exit_test_2_test.exs │ │ │ │ ├── in_flight_exit_test_3_test.exs │ │ │ │ ├── in_flight_exit_test_4_test.exs │ │ │ │ ├── invalid_exit_1_test.exs │ │ │ │ ├── invalid_exit_2_test.exs │ │ │ │ ├── monitor_test.exs │ │ │ │ ├── root_chain_coordinator_test.exs │ │ │ │ └── test_server_test.exs │ │ │ ├── merge_transaction_validator_test.exs │ │ │ ├── merkle_test.exs │ │ │ ├── output_test.exs │ │ │ ├── raw_data_test.exs │ │ │ ├── release_tasks/ │ │ │ │ ├── set_ethereum_events_check_interval_test.exs │ │ │ │ ├── set_exit_processor_sla_margin_test.exs │ │ │ │ └── set_tracer_test.exs │ │ │ ├── root_chain_coordinator/ │ │ │ │ └── core_test.exs │ │ │ ├── signature_test.exs │ │ │ ├── state/ │ │ │ │ ├── core_test.exs │ │ │ │ ├── measurement_calculation_test.exs │ │ │ │ ├── persistence_test.exs │ │ │ │ ├── transaction/ │ │ │ │ │ ├── fee_test.exs │ │ │ │ │ ├── recovered_test.exs │ │ │ │ │ └── witness_test.exs │ │ │ │ ├── transaction_test.exs │ │ │ │ └── utxo_set_test.exs │ │ │ ├── state_test.exs │ │ │ ├── supervisor_test.exs │ │ │ ├── typed_data_hash_test.exs │ │ │ ├── utxo/ │ │ │ │ └── position_test.exs │ │ │ ├── utxo_exit/ │ │ │ │ └── core_test.exs │ │ │ ├── utxo_test.exs │ │ │ └── wire_format_types_test.exs │ │ ├── support/ │ │ │ ├── dev_crypto.ex │ │ │ ├── exit_processor/ │ │ │ │ ├── case.ex │ │ │ │ └── test_helper.ex │ │ │ ├── integration/ │ │ │ │ ├── bad_child_chain_server.ex │ │ │ │ ├── deposit_helper.ex │ │ │ │ ├── fixtures.exs │ │ │ │ ├── test_helper.ex │ │ │ │ └── test_server.ex │ │ │ ├── signature_helper.ex │ │ │ ├── test_helper.ex │ │ │ └── watcher_helper.ex │ │ └── test_helper.exs │ ├── omg_watcher_info/ │ │ ├── lib/ │ │ │ ├── omg_watcher_info/ │ │ │ │ ├── api/ │ │ │ │ │ ├── account.ex │ │ │ │ │ ├── block.ex │ │ │ │ │ ├── deposit.ex │ │ │ │ │ ├── stats.ex │ │ │ │ │ └── transaction.ex │ │ │ │ ├── application.ex │ │ │ │ ├── block_applicator.ex │ │ │ │ ├── db/ │ │ │ │ │ ├── block.ex │ │ │ │ │ ├── eth_event.ex │ │ │ │ │ ├── eth_event_txoutput.ex │ │ │ │ │ ├── repo.ex │ │ │ │ │ ├── transaction.ex │ │ │ │ │ ├── txoutput.ex │ │ │ │ │ └── types/ │ │ │ │ │ ├── atom_type.ex │ │ │ │ │ ├── block/ │ │ │ │ │ │ └── chunk.ex │ │ │ │ │ └── integer_type.ex │ │ │ │ ├── http_rpc/ │ │ │ │ │ ├── adapter.ex │ │ │ │ │ └── client.ex │ │ │ │ ├── measure.ex │ │ │ │ ├── order_fee_fetcher.ex │ │ │ │ ├── release_tasks/ │ │ │ │ │ ├── init_postgresql_db.ex │ │ │ │ │ └── set_tracer.ex │ │ │ │ ├── supervisor.ex │ │ │ │ ├── tracer.ex │ │ │ │ ├── transaction.ex │ │ │ │ └── utxo_selection.ex │ │ │ └── watcher_info.ex │ │ ├── mix.exs │ │ ├── priv/ │ │ │ └── repo/ │ │ │ └── migrations/ │ │ │ ├── 20180813131000_create_block_table.exs │ │ │ ├── 20180813131706_create_transaction_table.exs │ │ │ ├── 20180813133000_create_ethevent_table.exs │ │ │ ├── 20180813143343_create_txoutput_table.exs │ │ │ ├── 20190314105410_alter_transactions_table_add_metadata_field.exs │ │ │ ├── 20190315095855_alter_transactions_table_add_partitial_index.exs │ │ │ ├── 20190408131000_add_missing_indices_to_txoutputs.exs │ │ │ ├── 20190806111817_alter_txoutputs_ethevents_make_many_to_many_relation.exs │ │ │ ├── 20190917165912_set_inserted_at_updated_at_to_epoc.exs │ │ │ ├── 20200129051756_index_block_timestamp.exs │ │ │ ├── 20200211064454_add_txtype_to_transaction_and_output.exs │ │ │ ├── 20200214132000_add_and_fix_timestamps.exs │ │ │ ├── 20200514115919_add_eth_height_to_eth_events.exs │ │ │ └── 20200529085008_create_pending_block_table.exs │ │ └── test/ │ │ ├── fixtures.exs │ │ ├── omg_watcher_info/ │ │ │ ├── api/ │ │ │ │ ├── block_test.exs │ │ │ │ ├── deposit_test.exs │ │ │ │ ├── stats_test.exs │ │ │ │ └── transaction_test.exs │ │ │ ├── block_applicator_test.exs │ │ │ ├── db/ │ │ │ │ ├── block/ │ │ │ │ │ └── chunk_test.exs │ │ │ │ ├── block_test.exs │ │ │ │ ├── eth_event_test.exs │ │ │ │ ├── transaction_test.exs │ │ │ │ └── txoutput_test.exs │ │ │ ├── http_rpc/ │ │ │ │ └── adapter_test.exs │ │ │ ├── order_fee_fetcher_test.exs │ │ │ ├── release_tasks/ │ │ │ │ └── set_tracer_test.exs │ │ │ ├── transaction_test.exs │ │ │ └── utxo_selection_test.exs │ │ ├── support/ │ │ │ ├── factories/ │ │ │ │ ├── block_factory.ex │ │ │ │ ├── data_helper.ex │ │ │ │ ├── eth_event_factory.ex │ │ │ │ ├── transaction_factory.ex │ │ │ │ └── txoutput_factory.ex │ │ │ ├── factory.ex │ │ │ └── test_server.ex │ │ └── test_helper.exs │ ├── omg_watcher_rpc/ │ │ ├── lib/ │ │ │ ├── application.ex │ │ │ ├── configuration.ex │ │ │ ├── release_tasks/ │ │ │ │ ├── set_api_mode.ex │ │ │ │ ├── set_endpoint.ex │ │ │ │ └── set_tracer.ex │ │ │ ├── tracer.ex │ │ │ ├── web/ │ │ │ │ ├── controllers/ │ │ │ │ │ ├── account.ex │ │ │ │ │ ├── alarm.ex │ │ │ │ │ ├── block.ex │ │ │ │ │ ├── challenge.ex │ │ │ │ │ ├── configuration.ex │ │ │ │ │ ├── deposit.ex │ │ │ │ │ ├── fallback.ex │ │ │ │ │ ├── fee.ex │ │ │ │ │ ├── in_flight_exit.ex │ │ │ │ │ ├── stats.ex │ │ │ │ │ ├── status.ex │ │ │ │ │ ├── transaction.ex │ │ │ │ │ └── utxo.ex │ │ │ │ ├── endpoint.ex │ │ │ │ ├── plugs/ │ │ │ │ │ ├── health.ex │ │ │ │ │ ├── method_param_filter.ex │ │ │ │ │ └── supported_watcher_modes.ex │ │ │ │ ├── response.ex │ │ │ │ ├── router.ex │ │ │ │ ├── serializers/ │ │ │ │ │ └── base.ex │ │ │ │ ├── sockets/ │ │ │ │ │ └── socket.ex │ │ │ │ ├── validators/ │ │ │ │ │ ├── account_constraints.ex │ │ │ │ │ ├── block_constraints.ex │ │ │ │ │ ├── deposit_constraints.ex │ │ │ │ │ ├── helpers.ex │ │ │ │ │ ├── merge_constraints.ex │ │ │ │ │ ├── order.ex │ │ │ │ │ ├── transaction_constraints.ex │ │ │ │ │ └── typed_data_signed.ex │ │ │ │ └── views/ │ │ │ │ ├── account.ex │ │ │ │ ├── alarm.ex │ │ │ │ ├── block.ex │ │ │ │ ├── challenge.ex │ │ │ │ ├── configuration.ex │ │ │ │ ├── deposit.ex │ │ │ │ ├── error.ex │ │ │ │ ├── fee.ex │ │ │ │ ├── in_flight_exit.ex │ │ │ │ ├── stats.ex │ │ │ │ ├── status.ex │ │ │ │ ├── transaction.ex │ │ │ │ └── utxo.ex │ │ │ └── web.ex │ │ ├── mix.exs │ │ ├── priv/ │ │ │ └── swagger/ │ │ │ ├── info_api_specs/ │ │ │ │ ├── account/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── alarm/ │ │ │ │ │ ├── alarms_schema.yml │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── batch_transaction/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── block/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── configuration/ │ │ │ │ │ ├── configuration_schema.yml │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── deposit/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── fees/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── response_schemas.yaml │ │ │ │ ├── responses.yaml │ │ │ │ ├── stats/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── swagger.yaml │ │ │ │ └── transaction/ │ │ │ │ ├── paths.yaml │ │ │ │ ├── request_bodies.yaml │ │ │ │ ├── response_schemas.yaml │ │ │ │ ├── responses.yaml │ │ │ │ └── schemas.yaml │ │ │ ├── info_api_specs.yaml │ │ │ ├── security_critical_api_specs/ │ │ │ │ ├── account/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── alarm/ │ │ │ │ │ ├── alarms_schema.yml │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── batch_transaction/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── block/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── configuration/ │ │ │ │ │ ├── configuration_schema.yml │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── in_flight_exit/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── response_schemas.yaml │ │ │ │ ├── responses.yaml │ │ │ │ ├── status/ │ │ │ │ │ ├── byzantine_events_schema.yml │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ ├── swagger.yaml │ │ │ │ ├── transaction/ │ │ │ │ │ ├── paths.yaml │ │ │ │ │ ├── request_bodies.yaml │ │ │ │ │ ├── response_schemas.yaml │ │ │ │ │ ├── responses.yaml │ │ │ │ │ └── schemas.yaml │ │ │ │ └── utxo/ │ │ │ │ ├── paths.yaml │ │ │ │ ├── request_bodies.yaml │ │ │ │ ├── response_schemas.yaml │ │ │ │ ├── responses.yaml │ │ │ │ └── schemas.yaml │ │ │ ├── security_critical_api_specs.yaml │ │ │ ├── shared/ │ │ │ │ ├── paths.yaml │ │ │ │ ├── request_bodies.yaml │ │ │ │ └── schemas.yaml │ │ │ └── swagger.md │ │ └── test/ │ │ ├── omg_watcher_rpc/ │ │ │ ├── release_tasks/ │ │ │ │ ├── set_endpoint_test.exs │ │ │ │ └── set_tracer_test.exs │ │ │ ├── tracer_test.exs │ │ │ └── web/ │ │ │ ├── conn_case.ex │ │ │ ├── controllers/ │ │ │ │ ├── account_test.exs │ │ │ │ ├── alarm_test.exs │ │ │ │ ├── block_test.exs │ │ │ │ ├── challenge_test.exs │ │ │ │ ├── deposit_test.exs │ │ │ │ ├── enforce_content_plug_test.exs │ │ │ │ ├── fallback_test.exs │ │ │ │ ├── fee_test.exs │ │ │ │ ├── in_flight_exit_test.exs │ │ │ │ ├── stats_test.exs │ │ │ │ ├── status_test.exs │ │ │ │ ├── transaction_test.exs │ │ │ │ └── utxo_test.exs │ │ │ ├── data_case.ex │ │ │ ├── plugs/ │ │ │ │ ├── method_param_filter_test.exs │ │ │ │ └── supported_watcher_modes_test.exs │ │ │ ├── response_test.exs │ │ │ ├── router_test.exs │ │ │ ├── validators/ │ │ │ │ ├── account_contraints_test.exs │ │ │ │ ├── block_constraints_test.exs │ │ │ │ ├── merge_constraints_test.exs │ │ │ │ ├── transaction_constraints_test.exs │ │ │ │ └── typed_data_signed_test.exs │ │ │ ├── view_case.ex │ │ │ └── views/ │ │ │ └── transaction_test.exs │ │ └── test_helper.exs │ └── xomg_tasks/ │ ├── lib/ │ │ ├── mix/ │ │ │ └── tasks/ │ │ │ ├── watcher.ex │ │ │ └── watcher_info.ex │ │ └── utils.ex │ ├── mix.exs │ └── test/ │ └── test_helper.exs ├── bin/ │ ├── generate-localchain-env │ ├── revert │ ├── rocksdb │ ├── setup │ ├── variables │ └── variables_test_barebone ├── config/ │ ├── .credo.exs │ ├── config.exs │ ├── credo/ │ │ ├── license_header.ex │ │ └── require_parentheses_on_zero_arity_defs.ex │ ├── dev.exs │ ├── prod.exs │ ├── releases.exs │ └── test.exs ├── contract_addresses_template.env ├── coveralls.json ├── dialyzer.ignore-warnings ├── docker/ │ ├── create_databases.sql │ ├── geth/ │ │ ├── command │ │ └── geth-blank-password │ ├── nginx/ │ │ ├── geth_nginx.conf │ │ ├── nginx.conf │ │ └── nginx.reorg.conf │ └── static_feefeed/ │ └── file.json ├── docker-compose-infura.yml ├── docker-compose-watcher.yml ├── docker-compose.datadog.yml ├── docker-compose.dev.yml ├── docker-compose.feefeed.yml ├── docker-compose.reorg.yml ├── docker-compose.specs.yml ├── docker-compose.yml ├── docs/ │ ├── api_specs/ │ │ ├── errors.md │ │ ├── index.html.md │ │ └── status_events_specs.md │ ├── architecture.md │ ├── branching.md │ ├── deployment_configuration.md │ ├── details.md │ ├── dex_design.md │ ├── exit_validation.md │ ├── fee_design.md │ ├── in_flight_exit_scenarios.md │ ├── install.md │ ├── morevp.md │ ├── perf_test_result_dumps.md │ ├── run_local_watcher.md │ ├── source_consumption_log.md │ ├── stack_architecture.md │ ├── standard_vs_in_flight_exits_interaction.md │ ├── tesuji_blockchain_design.md │ ├── transaction_validation.md │ ├── unified_api.md │ └── watcher_db_design.md ├── dummy ├── fees_setup.env ├── mix.exs ├── priv/ │ ├── dev-artifacts/ │ │ ├── README.md │ │ ├── fee_specs.dev.json │ │ └── fee_specs.test.json │ └── perf/ │ ├── .formatter.exs │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── apps/ │ │ └── load_test/ │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── application.ex │ │ │ ├── child_chain/ │ │ │ │ ├── abi/ │ │ │ │ │ ├── abi_event_selector.ex │ │ │ │ │ ├── abi_function_selector.ex │ │ │ │ │ └── fields.ex │ │ │ │ ├── abi.ex │ │ │ │ ├── deposit.ex │ │ │ │ ├── exit.ex │ │ │ │ ├── transaction.ex │ │ │ │ ├── utxos.ex │ │ │ │ └── watcher_sync.ex │ │ │ ├── connection/ │ │ │ │ ├── child_chain.ex │ │ │ │ ├── connection_defaults.ex │ │ │ │ ├── watcher_info.ex │ │ │ │ └── watcher_security.ex │ │ │ ├── ethereum/ │ │ │ │ ├── account.ex │ │ │ │ ├── bit_helper.ex │ │ │ │ ├── crypto.ex │ │ │ │ ├── ethereum.ex │ │ │ │ ├── hash.ex │ │ │ │ ├── nonce_tracker.ex │ │ │ │ └── transaction/ │ │ │ │ ├── signature.ex │ │ │ │ └── transaction.ex │ │ │ ├── performance.ex │ │ │ ├── runner/ │ │ │ │ ├── childchain.ex │ │ │ │ ├── deposits.ex │ │ │ │ ├── smoke.ex │ │ │ │ ├── standard_exits.ex │ │ │ │ ├── transactions.ex │ │ │ │ ├── utxos_load.ex │ │ │ │ └── watcher_info.ex │ │ │ ├── scenario/ │ │ │ │ ├── account_transactions.ex │ │ │ │ ├── create_utxos.ex │ │ │ │ ├── deposits.ex │ │ │ │ ├── fund_account.ex │ │ │ │ ├── many_standard_exits.ex │ │ │ │ ├── smoke.ex │ │ │ │ ├── spend_eth_utxo.ex │ │ │ │ ├── start_standard_exit.ex │ │ │ │ ├── transactions.ex │ │ │ │ └── watcher_status.ex │ │ │ ├── service/ │ │ │ │ ├── datadog/ │ │ │ │ │ ├── api.ex │ │ │ │ │ └── dummy_statix.ex │ │ │ │ ├── datadog.ex │ │ │ │ ├── faucet.ex │ │ │ │ ├── metrics.ex │ │ │ │ ├── sleeper.ex │ │ │ │ └── sync.ex │ │ │ ├── test_runner/ │ │ │ │ ├── config.ex │ │ │ │ └── help.ex │ │ │ ├── test_runner.ex │ │ │ ├── utils/ │ │ │ │ └── encoding.ex │ │ │ └── watcher_info/ │ │ │ ├── balance.ex │ │ │ ├── client.ex │ │ │ ├── transaction.ex │ │ │ └── utxo.ex │ │ ├── mix.exs │ │ └── test/ │ │ ├── load_test/ │ │ │ ├── runner/ │ │ │ │ ├── childchain_test.exs │ │ │ │ ├── smoke_test.exs │ │ │ │ ├── standard_exit_test.exs │ │ │ │ ├── utxos_load_test.exs │ │ │ │ └── watcher_info_test.exs │ │ │ └── service/ │ │ │ └── datadog/ │ │ │ └── api_test.exs │ │ └── test_helper.exs │ ├── config/ │ │ ├── .credo.exs │ │ ├── config.exs │ │ ├── dev.exs │ │ ├── stress.exs │ │ └── test.exs │ ├── mix.exs │ └── scripts/ │ └── generate_api_client.sh ├── rel/ │ └── env.sh.eex ├── rootfs/ │ ├── watcher_entrypoint │ └── watcher_info_entrypoint ├── snapshot_reorg.env └── snapshots.env ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/ci_increase_chart_version.sh ================================================ #!/bin/sh """ This is the script that would send a dispatch event to the helm chart repo to auto increase chart version. You can set the UPDATE_DEV flag to decide whether to update to dev too. For master, we increase the chart version and update dev together. The app version should be short git sha with length 7. For release, we increase the chart version only. The app version should be semver. (eg. 1.0.3-pre.0) Required env vars: - CHART_NAME (eg. childchain, watcher, watcher-info) - APP_VERSION (eg. 3d75118 or 1.0.3-pre.0) - HELM_CHART_REPO (eg. helm-devlopement) - UPDATE_DEV (true/false) - GITHUB_API_TOKEN """ set -ex [ -z "$CHART_NAME" ] && echo "CHART_NAME should be set" && exit 1 [ -z "$APP_VERSION" ] && echo "APP_VERSION should be set" && exit 1 [ -z "$HELM_CHART_REPO" ] && echo "HELM_CHART_REPO should be set" && exit 1 [ -z "$UPDATE_DEV" ] && echo "HELM_CHART_REPO should be set" && exit 1 [ -z "$GITHUB_API_TOKEN" ] && echo "GITHUB_API_TOKEN should be set" && exit 1 echo "increase chart version: chart [${CHART_NAME}], appVersion: [${APP_VERSION}], update_dev: [${UPDATE_DEV}]" curl --location --request POST "https://api.github.com/repos/omgnetwork/${HELM_CHART_REPO}/dispatches" \ --header "Accept: application/vnd.github.v3+json" \ --header "authorization: token ${GITHUB_API_TOKEN}" \ --header "Content-Type: application/json" \ --data-raw " { \ \"event_type\": \"increase-chart-version\", \ \"client_payload\": { \ \"chart_name\": \"${CHART_NAME}\", \ \"app_version\": \"${APP_VERSION}\", \ \"update_dev\": \"${UPDATE_DEV}\" \ } \ }" ================================================ FILE: .circleci/ci_publish.sh ================================================ #!/bin/sh set -e echo_info() { printf "\\033[0;34m%s\\033[0;0m\\n" "$1" } echo_warn() { printf "\\033[0;33m%s\\033[0;0m\\n" "$1" } ## Sanity check ## if [ -z "$DOCKER_PASS" ] || [ -z "$DOCKER_USER" ]; then echo_warn "Docker credentials is not present, skipping publish." exit 0 fi if [ -z "$IMAGE_NAME" ]; then echo_warn "IMAGE_NAME not present, failing." exit 1 fi ## Generate tags ## if [ -n "$CIRCLE_SHA1" ]; then _image_tag="$(printf "%s" "$CIRCLE_SHA1" | head -c 7)" fi if [ -n "$CIRCLE_TAG" ]; then _ver="${CIRCLE_TAG#*v}" # Given a v1.0.0-pre.1 tag, this will generate: # - 1.0 # - 1.0.0-pre # - 1.0.0-pre.1 while true; do case "$_ver" in *.* ) _image_tag="$_ver $_image_tag" _ver="${_ver%.*}" ;; * ) break;; esac done # In case the commit is HEAD of latest version branch, also tag stable. if [ -n "$CIRCLE_REPOSITORY_URL" ] && [ -n "$CIRCLE_SHA1" ]; then _stable_head="$( git ls-remote --heads "$CIRCLE_REPOSITORY_URL" "v*" | awk '/refs\/heads\/v[0-9]+\.[0-9]+$/ { LH=$1 } END { print LH }' )" if [ "$CIRCLE_SHA1" = "$_stable_head" ]; then _image_tag="$_image_tag stable" fi fi else case "$CIRCLE_BRANCH" in master ) _image_tag="$_image_tag latest";; v* ) _image_tag="$_image_tag ${CIRCLE_BRANCH#*v}-dev";; * ) ;; esac fi ## Publishing ## if [ -f "$HOME/caches/docker-layers.tar" ]; then docker load -i "$HOME/caches/docker-layers.tar" fi printf "%s\\n" "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin for tag in $_image_tag; do echo_info "Publishing Docker image as $IMAGE_NAME:$tag" docker tag "$IMAGE_NAME" "$IMAGE_NAME:$tag" docker push "$IMAGE_NAME:$tag" done ================================================ FILE: .circleci/config.yml ================================================ version: 2.1 executors: metal: docker: - image: circleci/elixir:1.10.2 - image: circleci/postgres:9.6 environment: MIX_ENV: test POSTGRES_USER: omisego_dev POSTGRES_PASSWORD: omisego_dev POSTGRES_DB: omisego_test CIRLCECI: true working_directory: ~/src metal_macos: macos: xcode: "11.0.0" builder: docker: - image: omisegoimages/elixir-omg-builder:stable-20201207 working_directory: ~/src builder_pg: docker: - image: omisegoimages/elixir-omg-builder:stable-20201207 - image: circleci/postgres:9.6-alpine environment: POSTGRES_USER: omisego_dev POSTGRES_PASSWORD: omisego_dev POSTGRES_DB: omisego_test working_directory: ~/src builder_pg_geth: docker: - image: omisegoimages/elixir-omg-tester:stable-20201207 - image: circleci/postgres:9.6-alpine environment: POSTGRES_USER: omisego_dev POSTGRES_PASSWORD: omisego_dev POSTGRES_DB: omisego_test working_directory: ~/src deployer: docker: - image: omisegoimages/elixir-omg-deploy:stable-20201207 working_directory: ~/src commands: add_rust_to_path: description: "Add path to PATH env var" steps: - run: name: Add rust to PATH env command: echo 'export PATH=~/.cargo/bin/:$PATH' >> $BASH_ENV install_rust: description: "Install Rust" steps: - run: name: Install Rust command: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - add_rust_to_path setup_elixir-omg_workspace: description: "Setup workspace" steps: - attach_workspace: name: Attach workspace at: . docker_login: description: login to dockerhub for private repo access steps: - run: printf "%s\\n" "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin make_docker_images: description: Builds docker images steps: - run: make docker-watcher - run: make docker-watcher_info check_docker_status: description: Installs elixir and checks if docker is healthy steps: - run: name: Print docker states command: | docker image ls docker-compose ps setup_childchain: description: "Setups Child chain for watcher tests" steps: # otherwise docker compose down errors with ERROR: Couldn't find env file - run: touch localchain_contract_addresses.env - run: docker-compose down - run: sudo rm -rf data/ - run: name: Setup data dir command: | [ -d data ] || mkdir data && chmod 777 data - run: name: Pull down snapshot command: SNAPSHOT=SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_20 make init_test - run: | echo -e "FEE_SPECS_FILE_PATH=/dev-artifacts/fee_specs.test.json\n$(cat fees_setup.env)" > fees_setup.env echo "FEE_SPECS_FILE_PATH=/dev-artifacts/fee_specs.test.json" >> fees_setup.env cat fees_setup.env - run: name: Standup Geth and Child Chain command: docker-compose up geth childchain postgres background: true - run: name: Has Childchain started? command: | attempt_counter=0 max_attempts=25 until $(curl --output /dev/null --silent --head --fail http://localhost:9656/alarm.get); do if [ ${attempt_counter} -eq ${max_attempts} ];then echo "Max attempts reached" exit 1 fi printf '.' attempt_counter=$(($attempt_counter+1)) sleep 5 done run_test_in_docker: description: "Quick test runs" parameters: test_command: type: string test_name: type: string steps: - run: name: "Docker run <>" command: | docker run --rm -it --network=chain_net -e DOCKER=true -e CHILD_CHAIN_URL=http://172.27.0.108:9656/ -e ETHEREUM_RPC_URL=http://172.27.0.108:80 -e DOCKER_GETH=true -e TEST_DATABASE_URL=postgresql://omisego_dev:omisego_dev@172.27.0.107:5432/omisego_test -e SHELL=/bin/sh -v $(pwd):/app --entrypoint /bin/sh omisegoimages/elixir-omg-builder:stable-20201207 -c "cd /app && mix deps.get && <>" install_elixir: description: Installs elixir and checks if docker is healthy steps: - restore_cache: key: v2-asdf-install - run: name: Install Erlang and Elixir command: | [ -d ~/.asdf-vm ] || git clone https://github.com/asdf-vm/asdf.git ~/.asdf-vm --branch v0.8.0 echo 'source ~/.asdf-vm/asdf.sh' >> $BASH_ENV source $BASH_ENV asdf plugin-add erlang || asdf plugin-update erlang asdf plugin-add elixir || asdf plugin-update elixir asdf plugin-add rust || asdf plugin-update rust asdf install no_output_timeout: 2400 - install_rust - save_cache: key: v2-asdf-install paths: - ~/.asdf - ~/.asdf-vm - run: make install-hex-rebar - restore_cache: key: v2-mix-specs-cache-{{ .Branch }}-{{ checksum "mix.lock" }} install_deps: description: Install linux dependencies steps: - run: name: Install deps command: | set -e sudo killall dpkg || true && sudo rm /var/lib/dpkg/lock || true && sudo rm /var/cache/apt/archives/lock || true && sudo dpkg --configure -a || true && sudo apt-get update && ./bin/setup no_output_timeout: 2400 install_and_setup_gcloud: description: Installs and sets up gcloud to fetch feefeed steps: - run: | export LD_LIBRARY_PATH=/usr/local/lib export CLOUDSDK_PYTHON=/usr/bin/python wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-323.0.0-linux-x86_64.tar.gz -O gcloud-sdk.tar.gz tar zxf gcloud-sdk.tar.gz google-cloud-sdk mv google-cloud-sdk ~/.google-cloud-sdk ~/.google-cloud-sdk/install.sh --quiet echo $GCP_KEY_FILE | gcloud auth activate-service-account $GCP_SERVICE_EMAIL --key-file=- gcloud --quiet config set project ${GCP_PROJECT} gcloud --quiet config set compute/zone ${GCP_ZONE} gcloud --quiet auth configure-docker jobs: barebuild: executor: metal environment: MIX_ENV: test steps: - checkout - run: make install-hex-rebar - run: echo 'export PATH=~/.cargo/bin:$PATH' >> $BASH_ENV - run: command: ./bin/setup no_output_timeout: 2400 - run: make deps-elixir-omg - run: ERLANG_ROCKSDB_BUILDOPTS='-j 2' make build-test - run: mix test - run: name: Integration Tests command: | # Slow, serial integration test, run nightly. Here to make sure the standard `mix test --only integration ` works export SHELL=/bin/bash mix test --only integration no_output_timeout: 30m barebuild_macos: executor: metal_macos environment: MIX_ENV: test steps: - checkout - run: echo 'export PATH=~/.cargo/bin:$PATH' >> $BASH_ENV - run: | brew install postgres initdb /usr/local/var/postgres/data pg_ctl -D /usr/local/var/postgres/data -l /tmp/postgresql.log start psql template1 \<> ~/.bash_profile echo -e '\n. $HOME/.asdf/completions/asdf.bash' >> ~/.bash_profile source ~/.bash_profile asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git asdf plugin-add rust || asdf plugin-update rust asdf install - run: make init_test - install_rust - run: command: ./bin/setup no_output_timeout: 2400 - run: make deps-elixir-omg - run: ERLANG_ROCKSDB_BUILDOPTS='-j 2' make build-test - run: mix test build: executor: builder environment: MIX_ENV: test steps: - checkout - restore_cache: key: v1-rocksdb-cache-{{ checksum "mix.lock" }} - run: make init_test - run: make deps-elixir-omg - run: ERLANG_ROCKSDB_BUILDOPTS='-j 2' make build-test - save_cache: key: v2-mix-cache-test-compile-{{ checksum "mix.lock" }}-{{ .Branch }}-{{ .Revision }} paths: "_build_docker" - save_cache: key: v1-rocksdb-cache-{{ checksum "mix.lock" }} paths: - "deps_docker/" - "deps_docker/rocksdb" - "_build_docker/test/lib/rocksdb/" - "_build_docker/test/dev/rocksdb/" - "deps/" - "_build/test/lib/rocksdb/" - "_build/test/dev/rocksdb/" - persist_to_workspace: name: Persist workspace root: ~/src paths: - .circleci - dialyzer.ignore-warnings - .formatter.exs - _build_docker - .credo.exs - apps - bin - config - deps_docker - doc - mix.exs - mix.lock - deploy_and_populate.sh - launcher.py - docker-compose.yml - rel/ - VERSION - .git - Makefile - priv - data - snapshots.env - snapshot_reorg.env - nginx.conf - contract_addresses_template.env - localchain_contract_addresses.env audit_deps: executor: builder environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - run: mix deps.audit lint: executor: builder environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - run: make install-hex-rebar - run: mix do compile --warnings-as-errors --force, credo --ignore-checks Credo.Check.Readability.SinglePipe, format --check-formatted --dry-run - run: command: | export SHELL=/bin/bash set +eo pipefail _counter=$(mix credo --only Credo.Check.Readability.SinglePipe | grep -c "Use a function call when a pipeline is only one function long") echo "Current Credo.Check.Readability.SinglePipe occurrences:" echo $_counter if [ $_counter -gt 273 ]; then echo "Have you been naughty or nice? Find out if Santa knows." exit 1 fi sobelow: executor: builder_pg environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - run: mix archive.install hex sobelow --force - run: mix sobelow --exit --skip --ignore Config.HTTPS -r . - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_bus - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_db - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_eth - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_status - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_utils - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_watcher - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_watcher_info - run: mix sobelow --exit --skip --ignore Config.HTTPS -r apps/omg_watcher_rpc --router apps/omg_watcher_rpc/lib/web/router.ex watcher_coveralls_and_integration_tests: executor: builder_pg_geth environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - restore_cache: keys: - v2-mix-cache-test-compile-{{ checksum "mix.lock" }}-{{ .Branch }}-{{ .Revision }} - run: name: Compile command: mix compile - run: name: Integration Tests & Coveralls Part Watcher command: | # Don't submit coverage report for forks, but let the build succeed export SHELL=/bin/bash if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then mix coveralls.html --parallel --umbrella --include watcher --exclude watcher_info --exclude common --exclude test else mix coveralls.circle --parallel --umbrella --include watcher --exclude watcher_info --exclude common --exclude test || # if mix failed, then coveralls_report won't run, so signal done here and return original exit status (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && exit $retval) fi watcher_mix_based_childchain: machine: image: ubuntu-2004:202010-01 environment: MIX_ENV: test steps: - checkout - restore_cache: keys: - v2-mix-cache-test-compile-watcher_mix_based_childchain-{{ checksum "mix.lock" }}-{{ .Branch }} - run: rm -rf _build_docker/test/lib/omg* - run: name: Setup dirs command: | [ -d _build_docker ] || mkdir _build_docker && sudo chmod -R 777 _build_docker - run: name: Setup dirs deps_docker command: | [ -d deps_docker ] || mkdir deps_docker && sudo chmod -R 777 deps_docker - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/invalid_exit_1_test.exs --include mix_based_child_chain" test_name: "invalid_exit_1_test.exs" - save_cache: key: v2-mix-cache-test-compile-watcher_mix_based_childchain-{{ checksum "mix.lock" }}-{{ .Branch }} paths: - deps_docker - _build_docker - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/in_flight_exit_test_3_test.exs --include mix_based_child_chain" test_name: "in_flight_exit_test_3_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/in_flight_exit_test_2_test.exs --include mix_based_child_chain" test_name: "in_flight_exit_test_2_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/in_flight_exit_test_1_test.exs --include mix_based_child_chain" test_name: "in_flight_exit_test_1_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/in_flight_exit_test_4_test.exs --include mix_based_child_chain" test_name: "in_flight_exit_test_4_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/in_flight_exit_test.exs --include mix_based_child_chain" test_name: "in_flight_exit_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/invalid_exit_2_test.exs --include mix_based_child_chain" test_name: "invalid_exit_2_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/block_getter_1_test.exs --include mix_based_child_chain" test_name: "block_getter_1_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/block_getter_2_test.exs --include mix_based_child_chain" test_name: "block_getter_2_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/block_getter_3_test.exs --include mix_based_child_chain" test_name: "block_getter_3_test.exs" - setup_childchain - run_test_in_docker: test_command: "mix test test/omg_watcher/integration/block_getter_4_test.exs --include mix_based_child_chain" test_name: "block_getter_4_test.exs" watcher_info_coveralls_and_integration_tests: executor: builder_pg_geth environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - restore_cache: keys: - v2-mix-cache-test-compile-{{ checksum "mix.lock" }}-{{ .Branch }}-{{ .Revision }} - run: name: Compile command: mix compile - run: name: Integration Tests & Coveralls Part Watcher command: | # Don't submit coverage report for forks, but let the build succeed export SHELL=/bin/bash if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then mix coveralls.html --parallel --umbrella --include watcher_info --exclude watcher --exclude common --exclude test else mix coveralls.circle --parallel --umbrella --include watcher_info --exclude watche --exclude common --exclude test || # if mix failed, then coveralls_report won't run, so signal done here and return original exit status (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && exit $retval) fi common_coveralls_and_integration_tests: executor: builder_pg_geth environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - restore_cache: keys: - v2-mix-cache-test-compile-{{ checksum "mix.lock" }}-{{ .Branch }}-{{ .Revision }} - run: name: Compile command: mix compile - run: name: Integration Tests & Coveralls Part Common command: | # Don't submit coverage report for forks, but let the build succeed export SHELL=/bin/bash if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then mix coveralls.html --parallel --umbrella --include common --exclude watcher --exclude watcher_info --exclude test else mix coveralls.circle --parallel --umbrella --include common --exclude watcher --exclude watcher_info --exclude test || # if mix failed, then coveralls_report won't run, so signal done here and return original exit status (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && exit $retval) fi test: executor: builder_pg environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - restore_cache: keys: - v2-mix-cache-test-compile-{{ checksum "mix.lock" }}-{{ .Branch }}-{{ .Revision }} - run: name: Compile command: mix compile - run: name: Test command: | # Don't submit coverage report for forks, but let the build succeed export SHELL=/bin/bash if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then mix coveralls.html --parallel --umbrella --exclude common --exclude watcher --exclude watcher_info else mix coveralls.circle --parallel --umbrella --exclude common --exclude watcher --exclude watcher_info || # if mix failed, then coveralls_report won't run, so signal done here and return original exit status (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && exit $retval) fi property_tests: executor: builder_pg_geth environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - restore_cache: keys: - v2-mix-cache-test-compile-{{ checksum "mix.lock" }}-{{ .Branch }}-{{ .Revision }} - run: name: Compile command: mix compile - run: name: Property Test command: | export SHELL=/bin/bash # no coverage calculation, coverage is on the other tests mix test --only property integration_tests: executor: builder_pg_geth environment: MIX_ENV: test steps: - setup_elixir-omg_workspace - restore_cache: keys: - v2-mix-cache-test-compile-{{ checksum "mix.lock" }}-{{ .Branch }}-{{ .Revision }} - run: name: Compile command: mix compile - install_rust - run: name: Integration Tests command: | # Slow, serial integration test, run nightly. Here to make sure the standard `mix test --only integration` works export SHELL=/bin/bash mix test --only integration dialyzer: executor: builder_pg steps: - setup_elixir-omg_workspace - restore_cache: keys: - v3-plt-cache-{{ ".tool-versions" }}-{{ checksum "mix.lock" }} - v3-plt-cache-{{ ".tool-versions" }}-{{ checksum "mix.exs" }} - v3-plt-cache-{{ ".tool-versions" }} - run: name: Unpack PLT cache command: | mkdir -p _build_docker/test cp plts/dialyxir*.plt _build_docker/test/ || true mkdir -p ~/.mix cp plts/dialyxir*.plt ~/.mix/ || true - run: mix dialyzer --plt - run: name: Pack PLT cache command: | mkdir -p plts cp _build_docker/test/dialyxir*.plt plts/ cp ~/.mix/dialyxir*.plt plts/ - save_cache: key: v3-plt-cache-{{ ".tool-versions" }}-{{ checksum "mix.lock" }} paths: - plts - save_cache: key: v3-plt-cache-{{ ".tool-versions" }}-{{ checksum "mix.exs" }} paths: - plts - save_cache: key: v3-plt-cache-{{ ".tool-versions" }} paths: - plts - run: mix dialyzer --format short test_docker_compose_release: machine: image: ubuntu-2004:202010-01 environment: SNAPSHOT: SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120 LD_LIBRARY_PATH: /usr/local/lib CLOUDSDK_PYTHON: /usr/bin/python CHILD_CHAIN_URL: "http://localhost:9656" FEE_CLAIMER_ADDRESS: "0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837" parallelism: 5 steps: - checkout - run: name: "Pull Submodules" command: | git submodule init git submodule update --remote - run: name: Setup data dir command: | [ -d data ] || mkdir data && chmod 777 data - docker_login - make_docker_images - install_and_setup_gcloud - run: name: Start daemon services command: make cabbage-start-services - run: name: Log daemon services command: make cabbage-logs background: true - check_docker_status - install_elixir - run: sh .circleci/status.sh - run: name: Run specs command: | cd priv/cabbage make install make generate_api_code mix deps.get - run: name: Run specs working_directory: ~/project/priv/cabbage environment: MIX_ENV: test command: | mix compile - run: name: Run specs working_directory: ~/project/priv/cabbage command: | TESTFILES=$(circleci tests glob "apps/itest/test/itest/*_test.exs" | circleci tests split --split-by=timings --show-counts) echo ${TESTFILES} mix test ${TESTFILES} --trace - store_test_results: path: ~/project/priv/cabbage/_build/test/lib/itest/ test_docker_compose_performance: description: "These are not actually performance tests, we're checking if the scripts work" machine: image: ubuntu-2004:202010-01 environment: PERF_IMAGE_NAME: "omisego/perf:latest" STATIX_TAG: "env:perf_circleci" LD_LIBRARY_PATH: /usr/local/lib CLOUDSDK_PYTHON: /usr/bin/python steps: - checkout - run: name: Setup data dir command: | [ -d data ] || mkdir data && chmod 777 data - docker_login - make_docker_images - install_and_setup_gcloud - run: name: Build perf docker image command: make docker-perf IMAGE_NAME=$PERF_IMAGE_NAME - install_elixir - run: name: Start daemon services command: | cd priv/perf make start-services - run: name: docker services logs background: true command: | cd priv/perf make log-services - run: sh .circleci/status.sh - run: name: Run load test command: | cd priv/perf make init export $(cat ../../localchain_contract_addresses.env | xargs) make test - run: name: Show help information command: docker run -it $PERF_IMAGE_NAME mix run -e "LoadTest.TestRunner.run()" -- help - run: name: Run perf smoke test (deposits) command: | docker run -it --env-file ./localchain_contract_addresses.env -e FEE_AMOUNT=1 --env DD_API_KEY --env DD_APP_KEY --env STATIX_TAG --network host $PERF_IMAGE_NAME mix run -e "LoadTest.TestRunner.run()" -- "deposits" 1 200 - run: name: Run perf smoke test (transactions) command: | docker run -it --env-file ./localchain_contract_addresses.env -e FEE_AMOUNT=1 --env DD_API_KEY --env DD_APP_KEY --env STATIX_TAG --network host $PERF_IMAGE_NAME mix run -e "LoadTest.TestRunner.run()" -- "transactions" 1 200 - run: name: (Perf) Format generated code and check for warnings command: | cd priv/perf # run format ONLY on formatted code so that it cleans up quoted atoms because # we cannot exclude folders to --warnings-as-errors mix format apps/*_api/lib/*_api/model/*.ex export $(cat ../../localchain_contract_addresses.env | xargs) make format-code-check-warnings - save_cache: key: v2-mix-specs-cache-{{ .Branch }}-{{ checksum "mix.lock" }} paths: - "priv/perf/deps" - run: name: (Perf) Credo and formatting command: | cd priv/perf mix do credo, format --check-formatted --dry-run test_docker_compose_reorg: machine: image: ubuntu-2004:202010-01 environment: REORG: true LD_LIBRARY_PATH: /usr/local/lib CLOUDSDK_PYTHON: /usr/bin/python steps: - checkout - run: name: "Pull Submodules" command: | git submodule init git submodule update --remote - run: name: Setup data dir command: | [ -d data1 ] || mkdir data1 && chmod 777 data1 [ -d data2 ] || mkdir data2 && chmod 777 data2 [ -d data ] || mkdir data && chmod 777 data - docker_login - make_docker_images - install_and_setup_gcloud - run: name: Start daemon services command: | make init_test_reorg cp ./localchain_contract_addresses.env ./priv/cabbage/apps/itest/localchain_contract_addresses.env docker-compose -f docker-compose.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml up -d || (START_RESULT=$?; docker-compose logs; exit $START_RESULT;) - run: name: Log daemon services command: make cabbage-logs-reorg background: true - check_docker_status - install_elixir - run: sh .circleci/status.sh - run: name: Print watcher logs command: make cabbage-reorg-watcher-logs background: true - run: name: Print watcher_info logs command: make cabbage-reorg-watcher_info-logs background: true - run: name: Print childchain logs command: make cabbage-reorg-childchain-logs background: true - run: name: Print geth logs command: make cabbage-reorg-geth-logs background: true - run: name: Print reorg logs command: make cabbage-reorgs-logs background: true - run: name: Run specs command: | cd priv/cabbage make install make generate_api_code mix deps.get mix test --only deposit --trace no_output_timeout: 30m test_barebone_release: machine: image: ubuntu-2004:202010-01 environment: TERM: xterm-256color LD_LIBRARY_PATH: /usr/local/lib CLOUDSDK_PYTHON: /usr/bin/python steps: - checkout - run: name: "Pull Submodules" command: | git submodule init git submodule update --remote - run: echo 'export PATH=~/.cargo/bin:$PATH' >> $BASH_ENV - install_and_setup_gcloud - docker_login - run: name: Start geth, postgres, feefeed and pull in blockchain snapshot command: make start-services background: true - run: echo 'export PATH=~/.cargo/bin:$PATH' >> $BASH_ENV - install_elixir - install_deps - run: make install-hex-rebar - restore_cache: key: v1-dev-release-cache-{{ checksum "mix.lock" }} - run: name: Compile command: | set -e make deps-elixir-omg mix compile no_output_timeout: 2400 - save_cache: key: v1-dev-release-cache-{{ checksum "mix.lock" }} paths: - "deps_docker/" - "deps/" - "_build/dev/" - "_build/dev/" - run: name: Run Watcher command: | set -e make start-watcher OVERRIDING_START=start_iex OVERRIDING_VARIABLES=./bin/variables_test_barebone background: true no_output_timeout: 2400 - run: name: Run Watcher Info command: | set -e make start-watcher_info OVERRIDING_START=start_iex OVERRIDING_VARIABLES=./bin/variables_test_barebone background: true no_output_timeout: 2400 - run: name: Print docker and process states command: | docker ps ps axww | grep watcher ps axww | grep watcher_info ps axww | grep child_chain - run: name: Has Watcher started? command: | attempt_counter=0 max_attempts=25 until $(curl --output /dev/null --silent --head --fail http://localhost:7434/alarm.get); do if [ ${attempt_counter} -eq ${max_attempts} ];then echo "Max attempts reached" exit 1 fi printf '.' attempt_counter=$(($attempt_counter+1)) sleep 5 done - run: name: Has Watcher Info started? command: | attempt_counter=0 max_attempts=25 until $(curl --output /dev/null --silent --head --fail http://localhost:7534/alarm.get); do if [ ${attempt_counter} -eq ${max_attempts} ];then echo "Max attempts reached" exit 1 fi printf '.' attempt_counter=$(($attempt_counter+1)) sleep 5 done publish_watcher: machine: image: ubuntu-2004:202010-01 environment: WATCHER_IMAGE_NAME: "omisego/watcher" steps: - checkout - run: make docker-watcher WATCHER_IMAGE_NAME=$WATCHER_IMAGE_NAME - run: name: "cp release" command: | mkdir current_release/ cp _build_docker/prod/watcher-$(git describe --tags).tar.gz current_release/ md5sum current_release/watcher-$(git describe --tags).tar.gz | awk '{print $1}' >> current_release/md5 - store_artifacts: path: current_release/ - run: IMAGE_NAME=$WATCHER_IMAGE_NAME sh .circleci/ci_publish.sh publish_perf: machine: image: ubuntu-2004:202010-01 environment: PERF_IMAGE_NAME: "omisego/perf" steps: - checkout - run: make docker-perf IMAGE_NAME=$PERF_IMAGE_NAME - run: IMAGE_NAME=$PERF_IMAGE_NAME sh .circleci/ci_publish.sh publish_watcher_info: machine: image: ubuntu-2004:202010-01 environment: WATCHER_INFO_IMAGE_NAME: "omisego/watcher_info" steps: - checkout - run: make docker-watcher_info WATCHER_INFO_IMAGE_NAME=$WATCHER_INFO_IMAGE_NAME - run: name: "cp release" command: | mkdir current_release/ cp _build_docker/prod/watcher_info-$(git describe --tags).tar.gz current_release/ md5sum current_release/watcher_info-$(git describe --tags).tar.gz | awk '{print $1}' >> current_release/md5 - store_artifacts: path: current_release/ - run: IMAGE_NAME=$WATCHER_INFO_IMAGE_NAME sh .circleci/ci_publish.sh increase_chart_version_watcher_master: docker: - image: cimg/base:2020.01 environment: CHART_NAME: watcher HELM_CHART_REPO: helm-development UPDATE_DEV: true steps: - checkout - run: APP_VERSION="$(echo "$CIRCLE_SHA1" | head -c 7)" sh .circleci/ci_increase_chart_version.sh increase_chart_version_watcher_info_master: docker: - image: cimg/base:2020.01 environment: CHART_NAME: watcher-info HELM_CHART_REPO: helm-development UPDATE_DEV: true steps: - checkout - run: APP_VERSION="$(echo "$CIRCLE_SHA1" | head -c 7)" sh .circleci/ci_increase_chart_version.sh increase_chart_version_watcher_release: docker: - image: cimg/base:2020.01 environment: CHART_NAME: watcher HELM_CHART_REPO: helm-development UPDATE_DEV: false steps: - checkout - run: APP_VERSION="${CIRCLE_TAG#*v}" sh .circleci/ci_increase_chart_version.sh increase_chart_version_watcher_info_release: docker: - image: cimg/base:2020.01 environment: CHART_NAME: watcher-info HELM_CHART_REPO: helm-development UPDATE_DEV: false steps: - checkout - run: APP_VERSION="${CIRCLE_TAG#*v}" sh .circleci/ci_increase_chart_version.sh release: docker: - image: node:15.2.1 steps: - checkout - run: npx -y semantic-release@17.2.3 coveralls_report: docker: - image: omisegoimages/elixir-omg-circleci:v1.8-20190129-02 environment: MIX_ENV: test steps: - run: name: Tell coveralls.io build is done command: curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" notify_services: executor: builder_pg steps: - run: name: Send development deployment markers command: | curl -X POST -H 'Content-type: application/json' -d '{"title": "Starting Service", "text": "Starting with git SHA '"$CIRCLE_SHA1"'", "alert_type": "info" }' 'https://app.datadoghq.com/api/v1/events?api_key='"$DD_API_KEY"'' curl -X POST -H 'Content-type: application/json' -H 'Authorization: Bearer '"$SENTRY_TOKEN"'' -d '{"projects": ["elixir-omg"], "ref": "'"$CIRCLE_SHA1"'", "version": "Watcher-ChildChain-'"$CIRCLE_SHA1"'"}' 'https://sentry.io/api/0/organizations/omisego/releases/' GH_URL="https://github.com/omisego/elixir-omg/tree/${CIRCLE_BRANCH}" CIRCLE_URL="https://circleci.com/gh/omisego/elixir-omg/${CIRCLE_BUILD_NUM}" WORKFLOW_URL="https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID}" MESSAGE="omisego/elixir-omg branch ${CIRCLE_BRANCH} has deployed a new version" RICH_MESSAGE="*omisego/elixir-omg* branch *${CIRCLE_BRANCH}* has been deployed" curl -X POST -H 'Content-Type: application/json' --data "{ \ \"attachments\": [ \ { \ \"fallback\": \"${MESSAGE}\", \ \"text\": \"Deployment: ${RICH_MESSAGE}\", \ \"mrkdwn\": true, \ \"color\": \"#2ced49\", \ \"fields\": [ \ { \ \"title\": \"Git SHA\", \ \"value\": \"<$GH_URL|$CIRCLE_SHA1>\", \ \"short\": true \ }, { \ \"title\": \"Branch\", \ \"value\": \"<$GH_URL|$CIRCLE_BRANCH>\", \ \"short\": true \ }, { \ \"title\": \"Build\", \ \"value\": \"<$CIRCLE_URL|$CIRCLE_BUILD_NUM>\", \ \"short\": true \ } \ ] \ } \ ] \ }" ${SLACK_WEBHOOK} workflows: version: 2 nightly: triggers: - schedule: cron: "30 8 * * 1-5" filters: branches: only: - master jobs: - build - integration_tests: requires: [build] - barebuild_macos #- test_barebone_release build-test-deploy: jobs: - build: filters: &all_branches_and_tags branches: only: /.+/ tags: only: /.+/ # - test_barebone_release: # filters: *all_branches_and_tags - notify_services: requires: - increase_chart_version_watcher_master - increase_chart_version_watcher_info_master filters: branches: only: - master - coveralls_report: requires: - watcher_coveralls_and_integration_tests - watcher_info_coveralls_and_integration_tests - common_coveralls_and_integration_tests - test - watcher_coveralls_and_integration_tests: requires: [build] filters: *all_branches_and_tags - watcher_info_coveralls_and_integration_tests: requires: [build] filters: *all_branches_and_tags - common_coveralls_and_integration_tests: requires: [build] filters: *all_branches_and_tags - test_docker_compose_release: filters: *all_branches_and_tags # - test_docker_compose_performance: # filters: *all_branches_and_tags - test_docker_compose_reorg: filters: branches: only: - master - master-v2 - audit_deps: requires: [build] filters: *all_branches_and_tags - lint: requires: [build] filters: *all_branches_and_tags - sobelow: requires: [build] filters: *all_branches_and_tags - dialyzer: requires: [build] filters: *all_branches_and_tags - test: requires: [build] filters: *all_branches_and_tags - property_tests: requires: [build] filters: &master_and_version_branches_and_all_tags branches: only: - master # vMAJOR.MINOR (e.g. v0.1, v0.2, v1.0, v2.1, etc.) - /^v[0-9]+\.[0-9]+/ tags: only: - /.+/ - watcher_mix_based_childchain: filters: *all_branches_and_tags - publish_watcher: requires: [ # test_barebone_release, test_docker_compose_release, watcher_coveralls_and_integration_tests, watcher_info_coveralls_and_integration_tests, common_coveralls_and_integration_tests, test, # property_tests, dialyzer, lint, audit_deps ] filters: &master_and_version_branches_and_all_tags branches: only: - master # vMAJOR.MINOR (e.g. v0.1, v0.2, v1.0, v2.1, etc.) - /^v[0-9]+\.[0-9]+/ tags: only: - /.+/ - publish_watcher_info: requires: [ # test_barebone_release, test_docker_compose_release, watcher_coveralls_and_integration_tests, watcher_info_coveralls_and_integration_tests, common_coveralls_and_integration_tests, test, property_tests, dialyzer, lint, audit_deps ] filters: *master_and_version_branches_and_all_tags # - publish_perf: # requires: [test_docker_compose_performance] # filters: # branches: # only: # - master # # vMAJOR.MINOR (e.g. v0.1, v0.2, v1.0, v2.1, etc.) # - /^v[0-9]+\.[0-9]+/ # tags: # only: # - /.+/ # Increase chart version for master, this will end up trigger deployment on dev - increase_chart_version_watcher_master: requires: [publish_watcher, publish_watcher_info] filters: branches: only: - master - increase_chart_version_watcher_info_master: requires: [publish_watcher, publish_watcher_info] filters: branches: only: - master # Increase chart version for new release - increase_chart_version_watcher_release: requires: [publish_watcher, publish_watcher_info] filters: &only_release_tag branches: ignore: /.*/ tags: only: # eg. v1.0.3-pre.0, v1.0.3, ... - /^v[0-9]+\.[0-9]+\.[0-9]+.*/ - increase_chart_version_watcher_info_release: requires: [publish_watcher, publish_watcher_info] filters: *only_release_tag - release: requires: [ # test_barebone_release, test_docker_compose_release, watcher_coveralls_and_integration_tests, watcher_info_coveralls_and_integration_tests, common_coveralls_and_integration_tests, test, property_tests, dialyzer, lint, audit_deps ] context: - shared-semantic-release filters: branches: only: /^master$/ tags: ignore: /.*/ ================================================ FILE: .circleci/status.sh ================================================ #!/bin/sh retries=0 status=1 # Retries roughly every 5 seconds up to 2 minutes while [ $retries -lt 24 ]; do alarms=$(make get-alarms) status=$? echo ${alarms} if [ "$status" -eq "0" ]; then exit 0 fi retries=$(( ${retries} + 1 )) sleep 5 done exit ${status} ================================================ FILE: .circleci/test_runner.py ================================================ #!/usr/bin/python3 import logging import os import sys import time import requests def create_job(test_runner: str) -> str: ''' Create a job in the test runner. Returns the job ID ''' payload = { "job": { "command": "npm", "args": ["run", "ci-test-fast"], "cwd": "/home/omg/omg-js" } } try: request = requests.post(test_runner + '/job', json=payload) except ConnectionError: logging.critical('Could not connect to the test runner') sys.exit(1) # Return a non-zero exit code so CircleCI fails logging.info('Job created: {}'.format( request.content.decode('utf-8')) ) return request.content.decode('utf-8') def check_job_completed(test_runner: str, job_id: str): ''' Get the status of the job from the test runner ''' start_time = int(time.time()) while True: if start_time >= (start_time + 360): logging.critical('Test runner did not complete within six minutes') sys.exit(1) # Return a non-zero exit code so CircleCI fails try: request = requests.get( '{}/job/{}/status'.format(test_runner, job_id), headers={'Cache-Control': 'no-cache'} ) except ConnectionError: logging.critical('Could not connect to the test runner') sys.exit(1) # Return a non-zero exit code so CircleCI fails if 'Exited' in request.content.decode('utf-8'): logging.info('Job completed successfully') break def check_job_result(test_runner: str, job_id: str): ''' Check the result of the job. This is the result of the tests that are executed against the push. If they all pass 'true' is returned. ''' try: request = requests.get( test_runner + '/job/{}/success'.format(job_id), headers={'Cache-Control': 'no-cache'} ) except ConnectionError: logging.critical('Could not connect to the test runner') sys.exit(1) if 'true' in request.content.decode('utf-8'): logging.info('Tests completed successfully') def get_envs() -> dict: ''' Get the environment variables for the workflow ''' envs = {} test_runner = os.getenv('TEST_RUNNER_SERVICE') if test_runner is None: logging.critical('Test runner service ENV missing') sys.exit(1) # Return a non-zero exit code so CircleCI fails envs['TEST_RUNNER_SERVICE'] = test_runner return envs def start_workflow(): ''' Get the party started ''' logging.info('Workflow started') envs = get_envs() job_id = str(create_job(envs['TEST_RUNNER_SERVICE'])) check_job_completed(envs['TEST_RUNNER_SERVICE'], job_id) check_job_result(envs['TEST_RUNNER_SERVICE'], job_id) def set_logger(): ''' Sets the logging module parameters ''' root = logging.getLogger('') for handler in root.handlers: root.removeHandler(handler) format = '%(asctime)s %(levelname)-8s:%(message)s' logging.basicConfig(format=format, level='INFO') if __name__ == '__main__': set_logger() start_workflow() ================================================ FILE: .formatter.exs ================================================ # Used by "mix format" [ inputs: [ "config/*.exs", "rel/config.exs", "mix.exs", "apps/*/mix.exs", "apps/*/{lib,test,config}/**/*.{ex,exs}", "priv/*/config/config.exs", "priv/*/mix.exs", ] ++ (Path.wildcard("priv/*/apps/*/mix.exs") -- Enum.flat_map( ["priv/*/apps/watcher_info_api/mix.exs", "priv/*/apps/watcher_security_critical_api/mix.exs"], &Path.wildcard/1 )) ++ (Path.wildcard("priv/*/apps/*/{lib,test,config}/**/*.{ex,exs}") -- (Path.wildcard("priv/*/apps/watcher_info_api/{lib,test,config}/**/*.{ex,exs}") ++ Path.wildcard("priv/*/apps/watcher_security_critical_api/{lib,test,config}/**/*.{ex,exs}"))), line_length: 120 ] ================================================ FILE: .githooks/pre-commit ================================================ #!/bin/sh echo "Is your code formatted?" exec mix format --check-formatted ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ :clipboard: Add associated issues, tickets, docs URL here. ## Overview Describe what your Pull Request is about in a few sentences. ## Changes Describe your changes and implementation choices. More details make PRs easier to review. - Change 1 - Change 2 - ... ## Testing Describe how to test your new feature/bug fix and if possible, a step by step guide on how to demo this. ================================================ FILE: .github/workflows/auto-merge-pr.yml ================================================ name: Auto Merge PR without conflicts on: pull_request: branches: [ master-v2 ] jobs: auto-merge-pr: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Attach Label to PR run: | set -o xtrace curl -X PATCH "https://api.github.com/repos/omgnetwork/elixir-omg/issues/${{ github.event.pull_request.number }}" \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.HOUSE_KEEPER_BOT_TOKEN }}" \ --data "{\"labels\": [\"sync master-v2\"]}" - name: Merge PR if: github.head_ref == 'master' run: | set -o xtrace function check_merge() { curl "https://api.github.com/repos/omgnetwork/elixir-omg/pulls/${{ github.event.pull_request.number }}" \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.HOUSE_KEEPER_BOT_TOKEN }}" | jq -r '.mergeable' } until [ "$(check_merge)" = true -o "$(check_merge)" = false ]; do echo -n 'waiting...' sleep 10 done if [ "$(check_merge)" = true ]; then curl -X PUT "https://api.github.com/repos/omgnetwork/elixir-omg/pulls/${{ github.event.pull_request.number }}/merge" \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.HOUSE_KEEPER_BOT_TOKEN }}" \ --data "{\"commit_title\": \"auto-merged ${{ github.event.pull_request.number }}\"}" else echo "PR unmerged" fi ================================================ FILE: .github/workflows/auto-pr-for-branch-syncing.yml ================================================ name: Auto PR for syncing master to master-v2 on: push: branches: [master] jobs: auto-pr-for-branch-syncing: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Create Pull Request run: | set -o xtrace readonly FROM_BRANCH="master" readonly TO_BRANCH="master-v2" readonly TITLE="sync: auto syncing from ${FROM_BRANCH} to ${TO_BRANCH}" readonly BODY="Time to sync \`${TO_BRANCH}\` with updates from \`${FROM_BRANCH}\`!" curl -X POST "https://api.github.com/repos/omgnetwork/elixir-omg/pulls" \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.HOUSE_KEEPER_BOT_TOKEN }}" \ --data "{\"title\": \"${TITLE}\", \"head\": \"${FROM_BRANCH}\", \"base\": \"${TO_BRANCH}\", \"body\": \"${BODY}\"}" ================================================ FILE: .github/workflows/enforce-changelog-labels.yml ================================================ name: Enforce changelog labels on: pull_request: types: [opened, labeled, unlabeled, synchronize, reopened] branches: [master] jobs: enforce-changelog-label: runs-on: ubuntu-latest env: # When updating the labels here, also update the `configure-sections` of the `.github_changelog_generator` file ONE_OF_LABELS: "api|enhancement|breaking|bug|chore|documentation" steps: - name: Check the PR for a changelog label id: check-changelog-label run: | set -o xtrace # Using the issues API instead of pulls because it can return only the labels curl "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${{ github.event.pull_request.number }}/labels" \ | grep -o '"name": "[^"]*' \ | cut -d'"' -f4 \ | grep -E $ONE_OF_LABELS \ || (echo "::error::The PR is missing a valid changelog label. Label the PR with one of: ${ONE_OF_LABELS//|/, }." && exit 1) ================================================ FILE: .github_changelog_generator ================================================ # Issue/PR filter release-branch=master since-tag=v0.4.8 exclude-tags-regex=.*-pre.* unreleased=true issues=false pull-requests=true pr-wo-labels=true # Categories pr-label=### Untagged pull requests # When updating the sections here, also update the `ONE_OF_LABELS` env vars in the `.github/workflows/enforce-changelog-labels.yml` file configure-sections={"api":{"prefix":"### API changes","labels":["api"]}, "breaking":{"prefix":"### Breaking changes","labels":["breaking"]}, "enhancement":{"prefix":"### Enhancements","labels":["enhancement"]}, "bug":{"prefix":"### Bug fixes","labels":["bug"]}, "chore":{"prefix":"### Chores","labels":["chore"]}, "documentation":{"prefix":"### Documentation updates","labels":["documentation"]}} ================================================ FILE: .gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # The directory Mix will write compiled artifacts to when in docker. /_build_docker/ # If you run "mix test --cover", coverage assets end up here. /cover/ # app-specific coverage results go to their respective apps /apps/*/cover/ # The directory Mix downloads your dependencies sources to when in docker. /deps_docker/ # The directory Mix downloads your dependencies sources to. /deps/ # Where 3rd-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Dev sqllite db *ecto_simple.sqlite3* # Developers config file your_config_file.exs # results of perftesting and profiling with default arguments fprof.trace perf_result_* *.statistics # Common virtualenv directory env/ # Elixirls data directory .elixir_ls # VS Code config file .vscode/ # Docker src/ docker-compose.override.yml # Sobelow .sobelow #generated applications and source priv/apps/childchain_api priv/apps/watcher_info_api priv/apps/watcher_security_critical_api # where Geth gets it's snapshot data data/ data1/ data2/ # local test setup localchain_contract_addresses.env # the famous mac .DS_Store .DS_Store # IntelliJ files .idea/ *.iml #vs code .tool_versions ================================================ FILE: .gitmodules ================================================ [submodule "priv/cabbage"] path = priv/cabbage url = https://github.com/omgnetwork/specs.git branch = master ================================================ FILE: .releaserc.yaml ================================================ plugins: - - '@semantic-release/commit-analyzer' - preset: 'angular' releaseRules: - type: 'refactor' release: 'patch' - type: 'style' release: 'patch' - type: 'feat' release: 'patch' - type: 'chore' release: 'patch' - breaking: true release: 'minor' - '@semantic-release/release-notes-generator' - '@semantic-release/github' tagFormat: 'v${version}' dryRun: true # observing that job would not be running as it was considered as from PR branch # looks that unfortunately that is how it is designed: # https://github.com/semantic-release/semantic-release/issues/1166#issuecomment-500094323 # this workaround is from: # https://github.com/semantic-release/semantic-release/issues/1074#issuecomment-696922883 ci: false ================================================ FILE: .tool-versions ================================================ elixir 1.11.2 erlang 23.1.4 rust 1.46.0 ================================================ FILE: AUTHORS ================================================ OMG Network Pte Ltd ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [v1.0.5](https://github.com/omgnetwork/elixir-omg/tree/v1.0.5) (2020-10-01) [Full Changelog](https://github.com/omgnetwork/elixir-omg/compare/v1.0.4-pre.2...v1.0.5) ### Enhancements - deposits performance tests bot [\#1745](https://github.com/omgnetwork/elixir-omg/pull/1745) ([ayrat555](https://github.com/ayrat555)) - feat: handle in-flight exits deletions [\#1701](https://github.com/omgnetwork/elixir-omg/pull/1701) ([pgebal](https://github.com/pgebal)) - feat: comply with new Infura API [\#1754](https://github.com/omgnetwork/elixir-omg/pull/1754) ([pgebal](https://github.com/pgebal)) ### Bug fixes - fix: handle metrics for in flight exit deleted processor [\#1742](https://github.com/omgnetwork/elixir-omg/pull/1742) ([pgebal](https://github.com/pgebal)) - revert: reverts ife deletion commits [\#1725](https://github.com/omgnetwork/elixir-omg/pull/1725) ([pgebal](https://github.com/pgebal)) - fix: set :last\_ife\_exit\_deleted\_eth\_height on deplyment if it's not set yet [\#1720](https://github.com/omgnetwork/elixir-omg/pull/1720) ([pgebal](https://github.com/pgebal)) - fix: fix in-flight exit deleted bug and add tests [\#1714](https://github.com/omgnetwork/elixir-omg/pull/1714) ([pgebal](https://github.com/pgebal)) - fix: block submission stall monitor should ignore block\_submitting that are already mined [\#1703](https://github.com/omgnetwork/elixir-omg/pull/1703) ([unnawut](https://github.com/unnawut)) - fix: recheck PR label on synchronize and reopen [\#1748](https://github.com/omgnetwork/elixir-omg/pull/1748) ([unnawut](https://github.com/unnawut)) ### Chores - Chore: parallelize tests by tags [\#1744](https://github.com/omgnetwork/elixir-omg/pull/1744) ([ayrat555](https://github.com/ayrat555)) - Chore: use exexec from upstream [\#1743](https://github.com/omgnetwork/elixir-omg/pull/1743) ([ayrat555](https://github.com/ayrat555)) - move dev env deployment job to helm repo [\#1738](https://github.com/omgnetwork/elixir-omg/pull/1738) ([boolafish](https://github.com/boolafish)) - Inomurko/remove child chain [\#1737](https://github.com/omgnetwork/elixir-omg/pull/1737) ([InoMurko](https://github.com/InoMurko)) - Kevsul/standard exit perf test [\#1732](https://github.com/omgnetwork/elixir-omg/pull/1732) ([kevsul](https://github.com/kevsul)) - update change log v1.0.4 [\#1731](https://github.com/omgnetwork/elixir-omg/pull/1731) ([jarindr](https://github.com/jarindr)) - chore: add test for 64\_000 txs block hash [\#1729](https://github.com/omgnetwork/elixir-omg/pull/1729) ([ayrat555](https://github.com/ayrat555)) - Allow to run docker-compose without feefeed docker [\#1726](https://github.com/omgnetwork/elixir-omg/pull/1726) ([boolafish](https://github.com/boolafish)) - rm mix based chch part 1 [\#1716](https://github.com/omgnetwork/elixir-omg/pull/1716) ([InoMurko](https://github.com/InoMurko)) - feat: reintroduce automated changelog [\#1708](https://github.com/omgnetwork/elixir-omg/pull/1708) ([unnawut](https://github.com/unnawut)) - add feefeed docker to elixir-omg setup [\#1700](https://github.com/omgnetwork/elixir-omg/pull/1700) ([boolafish](https://github.com/boolafish)) - move omg\_performance json rpc tests to perf project [\#1691](https://github.com/omgnetwork/elixir-omg/pull/1691) ([ayrat555](https://github.com/ayrat555)) - chore: bump version to 1.0.4 [\#1751](https://github.com/omgnetwork/elixir-omg/pull/1751) ([boolafish](https://github.com/boolafish)) - Chore: try to fix flaky reorg tests [\#1739](https://github.com/omgnetwork/elixir-omg/pull/1739) ([ayrat555](https://github.com/ayrat555)) - feat: transaction.create optimisation [\#1683](https://github.com/omgnetwork/elixir-omg/pull/1683) ([okalouti](https://github.com/okalouti)) ## [v1.0.4](https://github.com/omgnetwork/elixir-omg/tree/v1.0.4) (2020-09-03) [Full Changelog](https://github.com/omgnetwork/elixir-omg/compare/v1.0.4-pre.1...v1.0.4) ### API changes - /block.validate endpoint [\#1668](https://github.com/omgnetwork/elixir-omg/pull/1668) ([okalouti](https://github.com/okalouti)) ### Enhancements - Block Validation: New Checks [\#1693](https://github.com/omgnetwork/elixir-omg/pull/1693) ([okalouti](https://github.com/okalouti)) - feat: configurable DB pool size, queue target and queue interval [\#1689](https://github.com/omgnetwork/elixir-omg/pull/1689) ([unnawut](https://github.com/unnawut)) - feat: block queue metrics and stalled submission alarm [\#1649](https://github.com/omgnetwork/elixir-omg/pull/1649) ([unnawut](https://github.com/unnawut)) ### Bug fixes - corrrectly serialize PIDs in alarms.get [\#1678](https://github.com/omgnetwork/elixir-omg/pull/1678) ([ayrat555](https://github.com/ayrat555)) - account.get\_exitable\_utxos is unaware of in-flight exited inputs [\#1676](https://github.com/omgnetwork/elixir-omg/pull/1676) ([pnowosie](https://github.com/pnowosie)) - Add missing clause on witness validation check [\#1656](https://github.com/omgnetwork/elixir-omg/pull/1656) ([mederic-p](https://github.com/mederic-p)) - fix: unexpected http method [\#1651](https://github.com/omgnetwork/elixir-omg/pull/1651) ([ripzery](https://github.com/ripzery)) ### Chores - bump version 1.0.4 [\#1722](https://github.com/omgnetwork/elixir-omg/pull/1722) ([jarindr](https://github.com/jarindr)) - auto trigger chart version bump [\#1695](https://github.com/omgnetwork/elixir-omg/pull/1695) ([boolafish](https://github.com/boolafish)) - bump phoenix [\#1680](https://github.com/omgnetwork/elixir-omg/pull/1680) ([InoMurko](https://github.com/InoMurko)) - chore: increase timeouts for childchain healthchecks [\#1671](https://github.com/omgnetwork/elixir-omg/pull/1671) ([ayrat555](https://github.com/ayrat555)) - fix integration tests [\#1654](https://github.com/omgnetwork/elixir-omg/pull/1654) ([ayrat555](https://github.com/ayrat555)) - feat: pin elixir and erlang versions for asdf [\#1648](https://github.com/omgnetwork/elixir-omg/pull/1648) ([unnawut](https://github.com/unnawut)) - chore: change log and version file change for v1.0.3 \(\#1638\) [\#1639](https://github.com/omgnetwork/elixir-omg/pull/1639) ([boolafish](https://github.com/boolafish)) - use cabbage tests from a separate repo [\#1636](https://github.com/omgnetwork/elixir-omg/pull/1636) ([ayrat555](https://github.com/ayrat555)) - set OMG.State GenServer timeout to 10s [\#1517](https://github.com/omgnetwork/elixir-omg/pull/1517) ([achiurizo](https://github.com/achiurizo)) ### Documentation updates - v.1.0.4 change log [\#1719](https://github.com/omgnetwork/elixir-omg/pull/1719) ([jarindr](https://github.com/jarindr)) - docs: extend description of running cabbage tests [\#1658](https://github.com/omgnetwork/elixir-omg/pull/1658) ([pnowosie](https://github.com/pnowosie)) ## [v1.0.3](https://github.com/omgnetwork/elixir-omg/tree/v1.0.3) (2020-07-09) [Full Changelog](https://github.com/omgnetwork/elixir-omg/compare/v1.0.3-pre.2...v1.0.3) ### API changes - Add Transaction filter by end\_datetime [\#1595](https://github.com/omgnetwork/elixir-omg/pull/1595) ([jarindr](https://github.com/jarindr)) ### Enhancements - Add block processing queue to watcher info [\#1560](https://github.com/omgnetwork/elixir-omg/pull/1560) ([mederic-p](https://github.com/mederic-p)) ### Bug fixes - remove trace decorator from OMG.WatcherInfo.DB.EthEvent.get/1 [\#1640](https://github.com/omgnetwork/elixir-omg/pull/1640) ([ayrat555](https://github.com/ayrat555)) - get call\_data and rename it [\#1635](https://github.com/omgnetwork/elixir-omg/pull/1635) ([InoMurko](https://github.com/InoMurko)) - fix: handle "transaction underpriced" and other unknown server error responses [\#1617](https://github.com/omgnetwork/elixir-omg/pull/1617) ([unnawut](https://github.com/unnawut)) ### Chores - chore: change log and version file change for v1.0.3 [\#1638](https://github.com/omgnetwork/elixir-omg/pull/1638) ([boolafish](https://github.com/boolafish)) - sync v1.0.2 back to master [\#1626](https://github.com/omgnetwork/elixir-omg/pull/1626) ([boolafish](https://github.com/boolafish)) - enable margin [\#1622](https://github.com/omgnetwork/elixir-omg/pull/1622) ([InoMurko](https://github.com/InoMurko)) - Auto PR with Auto merge for syncing master-v2 [\#1604](https://github.com/omgnetwork/elixir-omg/pull/1604) ([souradeep-das](https://github.com/souradeep-das)) - integrate spandex ecto [\#1602](https://github.com/omgnetwork/elixir-omg/pull/1602) ([ayrat555](https://github.com/ayrat555)) - Revert "explain analyze updates \(\#1569\)" [\#1601](https://github.com/omgnetwork/elixir-omg/pull/1601) ([boolafish](https://github.com/boolafish)) - feat: sync v1.0.1 changes back to master [\#1599](https://github.com/omgnetwork/elixir-omg/pull/1599) ([unnawut](https://github.com/unnawut)) - release artifacts [\#1597](https://github.com/omgnetwork/elixir-omg/pull/1597) ([InoMurko](https://github.com/InoMurko)) - Add reorged docker compose [\#1579](https://github.com/omgnetwork/elixir-omg/pull/1579) ([ayrat555](https://github.com/ayrat555)) - Kevin/load test erc20 token [\#1577](https://github.com/omgnetwork/elixir-omg/pull/1577) ([kevsul](https://github.com/kevsul)) ## [v1.0.2](https://github.com/omgnetwork/elixir-omg/tree/v1.0.2) (2020-06-30) [Full Changelog](https://github.com/omgnetwork/elixir-omg/compare/v1.0.2-pre.0...v1.0.2) ### Enhancements - global block get interval [\#1576](https://github.com/omgnetwork/elixir-omg/pull/1576) ([InoMurko](https://github.com/InoMurko)) - install telemetry handler for authority balance [\#1567](https://github.com/omgnetwork/elixir-omg/pull/1567) ([InoMurko](https://github.com/InoMurko)) - restart strategy [\#1565](https://github.com/omgnetwork/elixir-omg/pull/1565) ([InoMurko](https://github.com/InoMurko)) ### Bug fixes - async stream + timeout [\#1593](https://github.com/omgnetwork/elixir-omg/pull/1593) ([InoMurko](https://github.com/InoMurko)) - fix: error attempting to log txhash in binary [\#1532](https://github.com/omgnetwork/elixir-omg/pull/1532) ([unnawut](https://github.com/unnawut)) - use fixed version of ex\_abi [\#1519](https://github.com/omgnetwork/elixir-omg/pull/1519) ([ayrat555](https://github.com/ayrat555)) ### Chores - chore: bump version in VERSION file [\#1613](https://github.com/omgnetwork/elixir-omg/pull/1613) ([boolafish](https://github.com/boolafish)) - docs: v1.0.2 change logs [\#1611](https://github.com/omgnetwork/elixir-omg/pull/1611) ([boolafish](https://github.com/boolafish)) - chore: merge master back to v1.0.2 [\#1606](https://github.com/omgnetwork/elixir-omg/pull/1606) ([boolafish](https://github.com/boolafish)) - chore: minor fixes [\#1584](https://github.com/omgnetwork/elixir-omg/pull/1584) ([boolafish](https://github.com/boolafish)) - explain analyze updates [\#1569](https://github.com/omgnetwork/elixir-omg/pull/1569) ([InoMurko](https://github.com/InoMurko)) - Sync v1.0.0 [\#1563](https://github.com/omgnetwork/elixir-omg/pull/1563) ([T-Dnzt](https://github.com/T-Dnzt)) ### Documentation updates - Update request body swagger [\#1609](https://github.com/omgnetwork/elixir-omg/pull/1609) ([jarindr](https://github.com/jarindr)) - Update README.md [\#1564](https://github.com/omgnetwork/elixir-omg/pull/1564) ([InoMurko](https://github.com/InoMurko)) ## [v1.0.1](https://github.com/omgnetwork/elixir-omg/tree/v1.0.1) (2020-06-18) [Full Changelog](https://github.com/omgnetwork/elixir-omg/compare/v1.0.0-pre.2...v1.0.1) ### Chores - feat: increase ExitProcessor timeouts [\#1592](https://github.com/omgnetwork/elixir-omg/pull/1592) ([InoMurko](https://github.com/InoMurko)) ## [v1.0.0](https://github.com/omgnetwork/elixir-omg/tree/v1.0.0) (2020-06-12) [Full Changelog](https://github.com/omgnetwork/elixir-omg/compare/v1.0.0-pre.1...v1.0.0) ### API changes - Add deposit.all endpoint and fetch eth\_height retroactively [\#1509](https://github.com/omgnetwork/elixir-omg/pull/1509) ([okalouti](https://github.com/okalouti)) - Add timestamp and scheduled finalisation time to InvalidExit and UnchallengedExit events [\#1495](https://github.com/omgnetwork/elixir-omg/pull/1495) ([okalouti](https://github.com/okalouti)) - Introduce spending\_txhash in invalid exit events [\#1492](https://github.com/omgnetwork/elixir-omg/pull/1492) ([mederic-p](https://github.com/mederic-p)) - \[2\] Add root chain transaction hash to InvalidExit and UnchallengedExit events [\#1485](https://github.com/omgnetwork/elixir-omg/pull/1485) ([okalouti](https://github.com/okalouti)) - Add root chain transaction hash to InvalidExit and UnchallengedExit events [\#1479](https://github.com/omgnetwork/elixir-omg/pull/1479) ([okalouti](https://github.com/okalouti)) - Input validation enhancements for endpoints [\#1469](https://github.com/omgnetwork/elixir-omg/pull/1469) ([okalouti](https://github.com/okalouti)) - account.get\_utxo pagination [\#1436](https://github.com/omgnetwork/elixir-omg/pull/1436) ([jarindr](https://github.com/jarindr)) - Filtering Input Parameters to Childchain/Watcher API depending on HTTP Method [\#1424](https://github.com/omgnetwork/elixir-omg/pull/1424) ([okalouti](https://github.com/okalouti)) - Prevent split/merge creation in /transaction.create [\#1416](https://github.com/omgnetwork/elixir-omg/pull/1416) ([T-Dnzt](https://github.com/T-Dnzt)) ### Enhancements - Inomurko/reorg block getter [\#1554](https://github.com/omgnetwork/elixir-omg/pull/1554) ([InoMurko](https://github.com/InoMurko)) - add: logging for ethereum tasks [\#1550](https://github.com/omgnetwork/elixir-omg/pull/1550) ([okalouti](https://github.com/okalouti)) - feat: env configurable block\_submit\_max\_gas\_price [\#1548](https://github.com/omgnetwork/elixir-omg/pull/1548) ([unnawut](https://github.com/unnawut)) - cache blocks into ets [\#1547](https://github.com/omgnetwork/elixir-omg/pull/1547) ([InoMurko](https://github.com/InoMurko)) - feat: add event type when consumer is spending utxos [\#1538](https://github.com/omgnetwork/elixir-omg/pull/1538) ([pnowosie](https://github.com/pnowosie)) - cache status get [\#1535](https://github.com/omgnetwork/elixir-omg/pull/1535) ([InoMurko](https://github.com/InoMurko)) - refactor: consistent log message for new events [\#1534](https://github.com/omgnetwork/elixir-omg/pull/1534) ([unnawut](https://github.com/unnawut)) - transaction rewrite, increase pg connection timeout [\#1525](https://github.com/omgnetwork/elixir-omg/pull/1525) ([InoMurko](https://github.com/InoMurko)) - Making Child-chain work with fee feed [\#1500](https://github.com/omgnetwork/elixir-omg/pull/1500) ([pnowosie](https://github.com/pnowosie)) - Papa/sec 27 watcher info ife support [\#1496](https://github.com/omgnetwork/elixir-omg/pull/1496) ([pnowosie](https://github.com/pnowosie)) - feat: make invalid piggyback cause unchallenged exit event when it's close to being finalized [\#1493](https://github.com/omgnetwork/elixir-omg/pull/1493) ([pgebal](https://github.com/pgebal)) - who monitors the monitor [\#1488](https://github.com/omgnetwork/elixir-omg/pull/1488) ([InoMurko](https://github.com/InoMurko)) - feat: system memory monitor that considers buffered and cached memory [\#1474](https://github.com/omgnetwork/elixir-omg/pull/1474) ([unnawut](https://github.com/unnawut)) - Break down incoming events to publish separately [\#1472](https://github.com/omgnetwork/elixir-omg/pull/1472) ([souradeep-das](https://github.com/souradeep-das)) - feat: add child chain metrics for transaction submissions, successes and failures [\#1470](https://github.com/omgnetwork/elixir-omg/pull/1470) ([unnawut](https://github.com/unnawut)) - feat: configurable fee specs path from env var [\#1385](https://github.com/omgnetwork/elixir-omg/pull/1385) ([mederic-p](https://github.com/mederic-p)) ### Bug fixes - prevent race condition for status cache [\#1558](https://github.com/omgnetwork/elixir-omg/pull/1558) ([InoMurko](https://github.com/InoMurko)) - fix: add Ink's log\_encoding\_error config [\#1512](https://github.com/omgnetwork/elixir-omg/pull/1512) ([unnawut](https://github.com/unnawut)) - fix: exclude active exiting utxos from calls to /account.get\_exitable\_utxos [\#1505](https://github.com/omgnetwork/elixir-omg/pull/1505) ([pgebal](https://github.com/pgebal)) - feat: update ink to v1.1 to fix Mix module not found [\#1504](https://github.com/omgnetwork/elixir-omg/pull/1504) ([unnawut](https://github.com/unnawut)) - fix: MemoryMonitor breaking on OS that does not provide buffered and cached memory data [\#1486](https://github.com/omgnetwork/elixir-omg/pull/1486) ([unnawut](https://github.com/unnawut)) ### Chores - Changelog for v1.0.0 [\#1556](https://github.com/omgnetwork/elixir-omg/pull/1556) ([T-Dnzt](https://github.com/T-Dnzt)) - updating httpoison [\#1542](https://github.com/omgnetwork/elixir-omg/pull/1542) ([InoMurko](https://github.com/InoMurko)) - use backport ex\_plasma [\#1537](https://github.com/omgnetwork/elixir-omg/pull/1537) ([achiurizo](https://github.com/achiurizo)) - chore: sync v0.4.8 into master [\#1531](https://github.com/omgnetwork/elixir-omg/pull/1531) ([unnawut](https://github.com/unnawut)) - refactor: remove fixture-based start exit test [\#1514](https://github.com/omgnetwork/elixir-omg/pull/1514) ([unnawut](https://github.com/unnawut)) - test: watcher's /status.get cabbage test [\#1508](https://github.com/omgnetwork/elixir-omg/pull/1508) ([unnawut](https://github.com/unnawut)) - refactor: move exit info related functions to smaller responsibility module [\#1503](https://github.com/omgnetwork/elixir-omg/pull/1503) ([boolafish](https://github.com/boolafish)) - fix: lint\_version compatibility with bash [\#1502](https://github.com/omgnetwork/elixir-omg/pull/1502) ([unnawut](https://github.com/unnawut)) - feat: merge latest v0.4 to master [\#1499](https://github.com/omgnetwork/elixir-omg/pull/1499) ([unnawut](https://github.com/unnawut)) - Kevin/load test cleanup [\#1490](https://github.com/omgnetwork/elixir-omg/pull/1490) ([kevsul](https://github.com/kevsul)) - Revert "Add root chain transaction hash to InvalidExit and UnchallengedExit events" [\#1483](https://github.com/omgnetwork/elixir-omg/pull/1483) ([okalouti](https://github.com/okalouti)) - refactor: add prerequisites for makefile targets involving docker-compose [\#1476](https://github.com/omgnetwork/elixir-omg/pull/1476) ([pgebal](https://github.com/pgebal)) - Move db storage out of docker containers [\#1473](https://github.com/omgnetwork/elixir-omg/pull/1473) ([kevsul](https://github.com/kevsul)) - Inomurko/macos nightly build fix [\#1464](https://github.com/omgnetwork/elixir-omg/pull/1464) ([InoMurko](https://github.com/InoMurko)) - fix: circleci to return the original start-services result after logging the failure [\#1463](https://github.com/omgnetwork/elixir-omg/pull/1463) ([unnawut](https://github.com/unnawut)) - Update alpine base image in Dockerfiles to v3.11 [\#1450](https://github.com/omgnetwork/elixir-omg/pull/1450) ([arthurk](https://github.com/arthurk)) ### Documentation updates - Watcher configs [\#1536](https://github.com/omgnetwork/elixir-omg/pull/1536) ([dmitrydao](https://github.com/dmitrydao)) - Update README.md [\#1468](https://github.com/omgnetwork/elixir-omg/pull/1468) ([dmitrydao](https://github.com/dmitrydao)) - Update installation instructions [\#1465](https://github.com/omgnetwork/elixir-omg/pull/1465) ([pnowosie](https://github.com/pnowosie)) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* ================================================ FILE: CODEOWNERS ================================================ mix.lock @InoMurko ================================================ FILE: Dockerfile.watcher ================================================ FROM alpine:3.11 LABEL maintainer="OMG Network Team " LABEL description="Official image for OMG Network (Watcher) Plasma Network" ENV LANG=C.UTF-8 ## S6 ## ENV S6_VERSION="1.21.4.0" RUN set -xe \ && apk add --update --no-cache --virtual .fetch-deps \ curl \ ca-certificates \ && S6_DOWNLOAD_URL="https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-amd64.tar.gz" \ && S6_DOWNLOAD_SHA256="e903f138dea67e75afc0f61e79eba529212b311dc83accc1e18a449d58a2b10c" \ && curl -fsL -o s6-overlay.tar.gz "${S6_DOWNLOAD_URL}" \ && echo "${S6_DOWNLOAD_SHA256} s6-overlay.tar.gz" |sha256sum -c - \ && tar -xzC / -f s6-overlay.tar.gz \ && rm s6-overlay.tar.gz \ && apk del .fetch-deps ## Application ## #rocksdb need libstdc++ RUN apk add --update --no-cache --virtual .watcher-runtime \ bash \ libressl \ libressl-dev \ libstdc++ \ lksctp-tools #libsecp256k1 RUN apk add --no-cache gmp-dev COPY rootfs / # USER directive is not being used here since privileges are dropped via # s6-setuigid in /entrypoint. s6-overlay is required to be run as root. ARG user=watcher ARG group=watcher ARG uid=10000 ARG gid=10000 RUN set -xe \ && addgroup -g ${gid} ${group} \ && adduser -D -h /app -u ${uid} -G ${group} ${user} \ && chown "${uid}:${gid}" "/app" \ && chmod +x /watcher_entrypoint #rocksdb from builder image to deployer image RUN mkdir -p /usr/local/rocksdb/lib RUN mkdir /usr/local/rocksdb/include COPY --from=omisegoimages/elixir-omg-builder:stable-20201207 /usr/local/rocksdb/ /usr/local/rocksdb/ ARG release_version ADD _build_docker/prod/watcher-${release_version}.tar.gz /app RUN chown -R "${uid}:${gid}" /app WORKDIR /app # Watcher app is using PORT environment variable to determine which port to run # the application server. ENV PORT 7434 EXPOSE $PORT # These are ports required for clustering. The range is defined in vm.args # in inet_dist_listen_min and inet_dist_listen_max. #EXPOSE 4369 6900 6901 6902 6903 6904 6905 6906 6907 6908 6909 # curl for healthchecks RUN apk add --no-cache curl ENTRYPOINT ["/init", "/watcher_entrypoint"] CMD ["foreground"] ================================================ FILE: Dockerfile.watcher_info ================================================ FROM alpine:3.11 LABEL maintainer="OMG Network Team " LABEL description="Official image for OMG Network (WatcherInfo) Plasma Network" ENV LANG=C.UTF-8 ## S6 ## ENV S6_VERSION="1.21.4.0" RUN set -xe \ && apk add --update --no-cache --virtual .fetch-deps \ curl \ ca-certificates \ && S6_DOWNLOAD_URL="https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-amd64.tar.gz" \ && S6_DOWNLOAD_SHA256="e903f138dea67e75afc0f61e79eba529212b311dc83accc1e18a449d58a2b10c" \ && curl -fsL -o s6-overlay.tar.gz "${S6_DOWNLOAD_URL}" \ && echo "${S6_DOWNLOAD_SHA256} s6-overlay.tar.gz" |sha256sum -c - \ && tar -xzC / -f s6-overlay.tar.gz \ && rm s6-overlay.tar.gz \ && apk del .fetch-deps ## Application ## #rocksdb need libstdc++ RUN apk add --update --no-cache --virtual .watcher-runtime \ bash \ libressl \ libressl-dev \ libstdc++ \ lksctp-tools #libsecp256k1 RUN apk add --no-cache gmp-dev COPY rootfs / # USER directive is not being used here since privileges are dropped via # s6-setuigid in /entrypoint. s6-overlay is required to be run as root. ARG user=watcher ARG group=watcher ARG uid=10000 ARG gid=10000 RUN set -xe \ && addgroup -g ${gid} ${group} \ && adduser -D -h /app -u ${uid} -G ${group} ${user} \ && chown "${uid}:${gid}" "/app" \ && chmod +x /watcher_info_entrypoint #rocksdb from builder image to deployer image RUN mkdir -p /usr/local/rocksdb/lib RUN mkdir /usr/local/rocksdb/include COPY --from=omisegoimages/elixir-omg-builder:stable-20201207 /usr/local/rocksdb/ /usr/local/rocksdb/ ARG release_version ADD _build_docker/prod/watcher_info-${release_version}.tar.gz /app RUN chown -R "${uid}:${gid}" /app WORKDIR /app # Watcher app is using PORT environment variable to determine which port to run # the application server. ENV PORT 7534 EXPOSE $PORT # These are ports required for clustering. The range is defined in vm.args # in inet_dist_listen_min and inet_dist_listen_max. #EXPOSE 4369 6900 6901 6902 6903 6904 6905 6906 6907 6908 6909 # curl for healthchecks RUN apk add --no-cache curl ENTRYPOINT ["/init", "/watcher_info_entrypoint"] CMD ["foreground"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2017-2019 OMG Network Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ MAKEFLAGS += --silent OVERRIDING_START ?= start_iex OVERRIDING_VARIABLES ?= bin/variables SNAPSHOT ?= SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_20 BAREBUILD_ENV ?= dev help: @echo "Dont Fear the Makefile" @echo "" @echo "PRE-LUMPHINI" @echo "------------------" @echo @echo "If you want to connect to an existing network (Pre-Lumphini) with a Watcher \c" @echo "and validate transactions. Run:" @echo " - \`make start-pre-lumphini-watcher\` \c" @echo "" @echo @echo "DOCKER CLUSTER USAGE" @echo "------------------" @echo "" @echo " - \`make docker-start-cluster\`: start everything for you, but if there are no local images \c" @echo "for Watcher and Child chain tagged with latest they will get pulled from our repository." @echo "" @echo " - \`make docker-start-cluster-with-infura\`: start everything but connect to Infura \c" @echo "instead of your own local geth network. Note: you will need to configure the environment \c" @echo "variables defined in docker-compose-infura.yml" @echo "" @echo "DOCKER DEVELOPMENT" @echo "------------------" @echo "" @echo " - \`make docker-build-start-cluster\`: watcher and watcher_info images \c" @echo "from your current code base, then start a cluster with these freshly built images." @echo "" @echo " - \`make docker-build\`" watcher and watcher_info images from your current code base @echo "" @echo " - \`make docker-update-watcher\`, \`make docker-update-watcher_info\`" @echo "replaces containers with your code changes\c" @echo "for rapid development." @echo "" @echo " - \`make docker-nuke\`: wipe docker clean, including containers, images, networks \c" @echo "and build cache." @echo "" @echo " - \`make docker-remote-watcher\`: remote console (IEx-style) into the watcher application." @echo "" @echo " - \`make docker-remote-watcher_info\`: remote console (IEx-style) into the \c" @echo "watcher_info application." @echo "" @echo "BARE METAL DEVELOPMENT" @echo "----------------------" @echo @echo "This presumes you want to run geth and postgres as containers \c" @echo "but Watcher and Child Chain bare metal. You will need four terminal windows." @echo "" @echo "1. In the first one, start geth, postgres:" @echo " make start-services" @echo "" @echo "3. In the third terminal window, run:" @echo " make start-watcher" @echo "" @echo "4. In the fourth terminal window, run:" @echo " make start-watcher_info" @echo "" @echo "5. Wait until they all boot. And run in the fifth terminal window:" @echo " make get-alarms" @echo "" @echo " make remote-watcher" @echo "" @echo "or" @echo " make remote-watcher_info" @echo "" @echo "MISCELLANEOUS" @echo "-------------" @echo " - \`make diagnostics\`: generate comprehensive diagnostics info for troubleshooting" @echo " - \`make list\`: list all available make targets" @echo "" .PHONY: list list: @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' all: clean build-watcher-prod build-watcher_info-prod WATCHER_IMAGE_NAME ?= "omisego/watcher:latest" WATCHER_INFO_IMAGE_NAME ?= "omisego/watcher_info:latest" IMAGE_BUILDER ?= "omisegoimages/elixir-omg-builder:stable-20201207" IMAGE_BUILD_DIR ?= $(PWD) ENV_DEV ?= env MIX_ENV=dev ENV_TEST ?= env MIX_ENV=test ENV_PROD ?= env MIX_ENV=prod WATCHER_PORT ?= 7434 WATCHER_INFO_PORT ?= 7534 HEX_URL ?= https://repo.hex.pm/installs/1.8.0/hex-0.20.5.ez HEX_SHA ?= cb7fdddbc4e5051b403cfb5e874ceb5cb0ecbe981a2a1517b97f9f76c67d234692e901ff48ee10dc712f728ae6ed0a51b11b8bd65b5db5582896123de20e7d49 REBAR_URL ?= https://repo.hex.pm/installs/1.0.0/rebar-2.6.2 REBAR_SHA ?= ff1c5ddfce1fcfd73fd65b8bfc0ff1c13aefc2e98921d528cbc1f35e86c9caa1c9c4e848b9ce6404d9a81c50cfcf0e45dd0dddb23cd42708664c41fce6618900 REBAR3_URL ?= https://repo.hex.pm/installs/1.0.0/rebar3-3.5.1 REBAR3_SHA ?= 86e998642991d384e9a6d4f216552609496da0e6ec4eb235df5b8b637d078c1a118bc7cdab501d1d54d24e0b6642adf32cc0c43019d948304301ceef227bedfd # # Setting-up # deps: deps-elixir-omg deps-elixir-omg: HEX_HTTP_TIMEOUT=120 mix deps.get # Mimicks `mix local.hex --force && mix local.rebar --force` but with version pinning. See: # - https://github.com/elixir-lang/elixir/blob/master/lib/mix/lib/mix/tasks/local.hex.ex # - https://github.com/elixir-lang/elixir/blob/master/lib/mix/lib/mix/tasks/local.rebar.ex install-hex-rebar: mix archive.install ${HEX_URL} --force --sha512 ${HEX_SHA} mix local.rebar rebar ${REBAR_URL} --force --sha512 ${REBAR_SHA} mix local.rebar rebar3 ${REBAR3_URL} --force --sha512 ${REBAR3_SHA} .PHONY: deps deps-elixir-omg # # Cleaning # clean: clean-elixir-omg clean-elixir-omg: rm -rf _build/* rm -rf deps/* rm -rf _build_docker/* rm -rf deps_docker/* clean-contracts: rm -rf data/* .PHONY: clean clean-elixir-omg clean-contracts # # Linting # format: mix format check-format: mix format --check-formatted 2>&1 check-credo: $(ENV_TEST) mix credo 2>&1 check-dialyzer: $(ENV_TEST) mix dialyzer 2>&1 .PHONY: format check-format check-credo # # Building # build-watcher-prod: deps-elixir-omg $(ENV_PROD) mix do compile, release watcher --overwrite build-watcher-dev: deps-elixir-omg $(ENV_DEV) mix do compile, release watcher --overwrite build-watcher_info-prod: deps-elixir-omg $(ENV_PROD) mix do compile, release watcher_info --overwrite build-watcher_info-dev: deps-elixir-omg $(ENV_DEV) mix do compile, release watcher_info --overwrite build-test: deps-elixir-omg $(ENV_TEST) mix compile .PHONY: build-prod build-dev build-test # # Contracts initialization # # Get the SNAPSHOT url from the snapshots file based on the SNAPSHOT env value # untar the snapshot and fetch values from the files in build dir that came from plasma-deployer # put these values into an localchain_contract_addresses.env via the script in bin # localchain_contract_addresses.env is used by docker, exunit tests and end2end tests init-contracts: clean-contracts mkdir data/ || true && \ chmod 777 data && \ URL=$$(grep "^$(SNAPSHOT)" snapshots.env | cut -d'=' -f2-) && \ curl -o data/snapshot.tar.gz $$URL && \ cd data && \ tar --strip-components 1 -zxvf snapshot.tar.gz data/geth && \ tar --exclude=data/* -xvzf snapshot.tar.gz && \ AUTHORITY_ADDRESS=$$(cat plasma-contracts/build/authority_address) && \ ETH_VAULT=$$(cat plasma-contracts/build/eth_vault) && \ ERC20_VAULT=$$(cat plasma-contracts/build/erc20_vault) && \ PAYMENT_EXIT_GAME=$$(cat plasma-contracts/build/payment_exit_game) && \ PLASMA_FRAMEWORK_TX_HASH=$$(cat plasma-contracts/build/plasma_framework_tx_hash) && \ PLASMA_FRAMEWORK=$$(cat plasma-contracts/build/plasma_framework) && \ PAYMENT_EIP712_LIBMOCK=$$(cat plasma-contracts/build/paymentEip712LibMock) && \ MERKLE_WRAPPER=$$(cat plasma-contracts/build/merkleWrapper) && \ ERC20_MINTABLE=$$(cat plasma-contracts/build/erc20Mintable) && \ sh ../bin/generate-localchain-env AUTHORITY_ADDRESS=$$AUTHORITY_ADDRESS ETH_VAULT=$$ETH_VAULT \ ERC20_VAULT=$$ERC20_VAULT PAYMENT_EXIT_GAME=$$PAYMENT_EXIT_GAME \ PLASMA_FRAMEWORK_TX_HASH=$$PLASMA_FRAMEWORK_TX_HASH PLASMA_FRAMEWORK=$$PLASMA_FRAMEWORK \ PAYMENT_EIP712_LIBMOCK=$$PAYMENT_EIP712_LIBMOCK MERKLE_WRAPPER=$$MERKLE_WRAPPER ERC20_MINTABLE=$$ERC20_MINTABLE init-contracts-reorg: clean-contracts mkdir data1/ || true && \ mkdir data2/ || true && \ mkdir data/ || true && \ URL=$$(grep "SNAPSHOT" snapshot_reorg.env | cut -d'=' -f2-) && \ curl -o data1/snapshot.tar.gz $$URL && \ cd data1 && \ tar --strip-components 1 -zxvf snapshot.tar.gz data/geth && \ tar --exclude=data/* -xvzf snapshot.tar.gz && \ mv snapshot.tar.gz ../data2/snapshot.tar.gz && \ cd ../data2 && \ tar --strip-components 1 -zxvf snapshot.tar.gz data/geth && \ tar --exclude=data/* -xvzf snapshot.tar.gz && \ mv snapshot.tar.gz ../data/snapshot.tar.gz && \ cd ../data && \ tar --strip-components 1 -zxvf snapshot.tar.gz data/geth && \ tar --exclude=data/* -xvzf snapshot.tar.gz && \ AUTHORITY_ADDRESS=$$(cat plasma-contracts/build/authority_address) && \ ETH_VAULT=$$(cat plasma-contracts/build/eth_vault) && \ ERC20_VAULT=$$(cat plasma-contracts/build/erc20_vault) && \ PAYMENT_EXIT_GAME=$$(cat plasma-contracts/build/payment_exit_game) && \ PLASMA_FRAMEWORK_TX_HASH=$$(cat plasma-contracts/build/plasma_framework_tx_hash) && \ PLASMA_FRAMEWORK=$$(cat plasma-contracts/build/plasma_framework) && \ PAYMENT_EIP712_LIBMOCK=$$(cat plasma-contracts/build/paymentEip712LibMock) && \ MERKLE_WRAPPER=$$(cat plasma-contracts/build/merkleWrapper) && \ ERC20_MINTABLE=$$(cat plasma-contracts/build/erc20Mintable) && \ sh ../bin/generate-localchain-env AUTHORITY_ADDRESS=$$AUTHORITY_ADDRESS ETH_VAULT=$$ETH_VAULT \ ERC20_VAULT=$$ERC20_VAULT PAYMENT_EXIT_GAME=$$PAYMENT_EXIT_GAME \ PLASMA_FRAMEWORK_TX_HASH=$$PLASMA_FRAMEWORK_TX_HASH PLASMA_FRAMEWORK=$$PLASMA_FRAMEWORK \ PAYMENT_EIP712_LIBMOCK=$$PAYMENT_EIP712_LIBMOCK MERKLE_WRAPPER=$$MERKLE_WRAPPER ERC20_MINTABLE=$$ERC20_MINTABLE .PHONY: init-contracts # # Testing # init_test: init-contracts init_test_reorg: init-contracts-reorg test: mix test --include test --exclude common --exclude watcher --exclude watcher_info test-watcher: mix test --include watcher --exclude watcher_info --exclude common --exclude test test-watcher_info: mix test --include watcher_info --exclude watcher --exclude common --exclude test test-common: mix test --include common --exclude watcher --exclude watcher_info --exclude test # # Documentation # changelog: github_changelog_generator --user omgnetwork --project elixir-omg .PHONY: changelog ### start-integration-watcher: docker-compose -f docker-compose-watcher.yml up ### # # Docker # docker-watcher-prod: docker run --rm -it \ -v $(PWD):/app \ -u root \ --entrypoint /bin/sh \ $(IMAGE_BUILDER) \ -c "cd /app && make build-watcher-prod" docker-watcher_info-prod: docker run --rm -it \ -v $(PWD):/app \ -u root \ --entrypoint /bin/sh \ $(IMAGE_BUILDER) \ -c "cd /app && make build-watcher_info-prod" docker-watcher-build: docker build -f Dockerfile.watcher \ --build-arg release_version=$$(git describe --tags) \ --cache-from $(WATCHER_IMAGE_NAME) \ -t $(WATCHER_IMAGE_NAME) \ . docker-watcher_info-build: docker build -f Dockerfile.watcher_info \ --build-arg release_version=$$(git describe --tags) \ --cache-from $(WATCHER_INFO_IMAGE_NAME) \ -t $(WATCHER_INFO_IMAGE_NAME) \ . docker-watcher: docker-watcher-prod docker-watcher-build docker-watcher_info: docker-watcher_info-prod docker-watcher_info-build docker-perf: docker build -f ./priv/perf/Dockerfile -t $(IMAGE_NAME) . docker-build: docker-watcher docker-watcher_info docker-push: docker docker push $(WATCHER_IMAGE_NAME) docker push $(WATCHER_INFO_IMAGE_NAME) ### Cabbage logs cabbage-logs: docker-compose -f docker-compose.yml -f docker-compose.feefeed.yml -f docker-compose.specs.yml logs --follow cabbage-logs-reorg: docker-compose -f docker-compose.yml -f docker-compose.feefeed.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml logs --follow ### Cabbage reorg docker logs cabbage-reorg-watcher-logs: docker-compose -f docker-compose.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml logs --follow watcher cabbage-reorg-watcher_info-logs: docker-compose -f docker-compose.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml logs --follow watcher_info cabbage-reorg-childchain-logs: docker-compose -f docker-compose.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml logs --follow childchain cabbage-reorg-geth-logs: docker-compose -f docker-compose.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml logs --follow | grep "geth" cabbage-reorgs-logs: docker-compose -f docker-compose.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml logs --follow | grep "reorg" ### Cabbage service commands cabbage-start-services: make init_test && \ cp ./localchain_contract_addresses.env ./priv/cabbage/apps/itest/localchain_contract_addresses.env && \ docker-compose -f docker-compose.yml -f docker-compose.specs.yml up -d || \ (START_RESULT=$?; docker-compose logs; exit $START_RESULT;) cabbage-start-services-reorg: make init_test_reorg && \ cp ./localchain_contract_addresses.env ./priv/cabbage/apps/itest/localchain_contract_addresses.env && \ docker-compose -f docker-compose.yml -f docker-compose.reorg.yml -f docker-compose.specs.yml up -d || \ (START_RESULT=$?; docker-compose logs; exit $START_RESULT;) ###OTHER docker-start-cluster: SNAPSHOT=SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120 make init_test && \ docker-compose build --no-cache && docker-compose up docker-build-start-cluster: $(MAKE) docker-build SNAPSHOT=SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120 make init_test && \ docker-compose build --no-cache && docker-compose up docker-stop-cluster: localchain_contract_addresses.env docker-compose down docker-update-watcher: localchain_contract_addresses.env docker stop elixir-omg_watcher_1 $(MAKE) docker-watcher docker-compose up watcher docker-update-watcher_info: localchain_contract_addresses.env docker stop elixir-omg_watcher_info_1 $(MAKE) docker-watcher_info docker-compose up watcher_info docker-start-cluster-with-infura: localchain_contract_addresses.env if [ -f ./docker-compose.override.yml ]; then \ docker-compose -f docker-compose.yml -f docker-compose-infura.yml -f docker-compose.override.yml up; \ else \ echo "Starting infura requires overriding docker-compose-infura.yml values in a docker-compose.override.yml"; \ fi docker-start-cluster-with-datadog: localchain_contract_addresses.env docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.datadog.yml up watcher watcher_info childchain docker-stop-cluster-with-datadog: localchain_contract_addresses.env docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.datadog.yml down docker-nuke: localchain_contract_addresses.env docker-compose down --remove-orphans --volumes docker system prune --all $(MAKE) clean $(MAKE) init-contracts docker-remote-watcher: docker exec -it watcher /app/bin/watcher remote docker-remote-watcher_info: docker exec -ti watcher_info /app/bin/watcher_info remote .PHONY: docker-nuke docker-remote-watcher docker-remote-watcher_info ### ### barebone stuff ### start-services: SNAPSHOT=SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120 make init_test && \ docker-compose -f ./docker-compose.yml -f ./docker-compose.feefeed.yml up feefeed geth nginx postgres start-watcher: . ${OVERRIDING_VARIABLES} && \ echo "Building Watcher" && \ make build-watcher-${BAREBUILD_ENV} && \ echo "Potential cleanup" && \ rm -f ./_build/${BAREBUILD_ENV}/rel/watcher/var/sys.config || true && \ echo "Init Watcher DBs" && \ _build/${BAREBUILD_ENV}/rel/watcher/bin/watcher eval "OMG.DB.ReleaseTasks.InitKeyValueDB.run()" && \ _build/${BAREBUILD_ENV}/rel/watcher/bin/watcher eval "OMG.DB.ReleaseTasks.InitKeysWithValues.run()" && \ echo "Run Watcher" && \ . ${OVERRIDING_VARIABLES} && \ PORT=${WATCHER_PORT} _build/${BAREBUILD_ENV}/rel/watcher/bin/watcher $(OVERRIDING_START) start-watcher_info: . ${OVERRIDING_VARIABLES} && \ echo "Building Watcher Info" && \ make build-watcher_info-${BAREBUILD_ENV} && \ echo "Potential cleanup" && \ rm -f ./_build/${BAREBUILD_ENV}/rel/watcher_info/var/sys.config || true && \ echo "Init Watcher Info DBs" && \ _build/${BAREBUILD_ENV}/rel/watcher_info/bin/watcher_info eval "OMG.DB.ReleaseTasks.InitKeyValueDB.run()" && \ _build/${BAREBUILD_ENV}/rel/watcher_info/bin/watcher_info eval "OMG.DB.ReleaseTasks.InitKeysWithValues.run()" && \ _build/${BAREBUILD_ENV}/rel/watcher_info/bin/watcher_info eval "OMG.WatcherInfo.ReleaseTasks.InitPostgresqlDB.migrate()" && \ echo "Run Watcher Info" && \ . ${OVERRIDING_VARIABLES} && \ PORT=${WATCHER_INFO_PORT} _build/${BAREBUILD_ENV}/rel/watcher_info/bin/watcher_info $(OVERRIDING_START) update-watcher: _build/dev/rel/watcher/bin/watcher stop ; \ $(ENV_DEV) mix do compile, release watcher --overwrite && \ . ${OVERRIDING_VARIABLES} && \ exec PORT=${WATCHER_PORT} _build/dev/rel/watcher/bin/watcher $(OVERRIDING_START) & update-watcher_info: _build/dev/rel/watcher_info/bin/watcher_info stop ; \ $(ENV_DEV) mix do compile, release watcher_info --overwrite && \ . ${OVERRIDING_VARIABLES} && \ exec PORT=${WATCHER_INFO_PORT} _build/dev/rel/watcher_info/bin/watcher_info $(OVERRIDING_START) & stop-watcher: . ${OVERRIDING_VARIABLES} && \ _build/dev/rel/watcher/bin/watcher stop stop-watcher_info: . ${OVERRIDING_VARIABLES} && \ _build/dev/rel/watcher_info/bin/watcher_info stop remote-watcher: . ${OVERRIDING_VARIABLES} && \ _build/dev/rel/watcher/bin/watcher remote remote-watcher_info: . ${OVERRIDING_VARIABLES} && \ _build/dev/rel/watcher_info/bin/watcher_info remote get-alarms: echo "Child Chain alarms" ; \ curl -s -X GET http://localhost:9656/alarm.get ; \ echo "\nWatcher alarms" ; \ curl -s -X GET http://localhost:${WATCHER_PORT}/alarm.get ; \ echo "\nWatcherInfo alarms" ; \ curl -s -X GET http://localhost:${WATCHER_INFO_PORT}/alarm.get cluster-stop: localchain_contract_addresses.env ${MAKE} stop-watcher ; ${MAKE} stop-watcher_info ; docker-compose down ### git setup init: git config core.hooksPath .githooks #old git #init: # find .git/hooks -type l -exec rm {} \; # find .githooks -type f -exec ln -sf ../../{} .git/hooks/ \; ### ### SWAGGER openapi ### security_critical_api_specs: swagger-cli bundle -r -t yaml -o apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/swagger.yaml info_api_specs: swagger-cli bundle -r -t yaml -o apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml apps/omg_watcher_rpc/priv/swagger/info_api_specs/swagger.yaml api_specs: security_critical_api_specs info_api_specs operator_api_specs ### ### Diagnostics report ### diagnostics: localchain_contract_addresses.env echo "---------- START OF DIAGNOSTICS REPORT ----------" echo "\n---------- CHILDCHAIN LOGS ----------" docker-compose logs childchain echo "\n---------- WATCHER LOGS ----------" docker-compose logs watcher echo "\n---------- WATCHER_INFO LOGS ----------" docker-compose logs watcher_info echo "\n---------- GIT ----------" echo "Git commit: $$(git rev-parse HEAD)" git status echo "\n---------- DOCKER-COMPOSE CONTAINERS ----------" docker-compose ps echo "\n---------- DOCKER CONTAINERS ----------" docker ps echo "\n---------- DOCKER IMAGES ----------" docker image ls echo "\n ---------- END OF DIAGNOSTICS REPORT ----------" .PHONY: diagnostics localchain_contract_addresses.env: $(MAKE) init-contracts ================================================ FILE: README.md ================================================ The `elixir-omg` repository contains OMG Network's Elixir implementation of Plasma and forms the basis for the OMG Network. [![Build Status](https://circleci.com/gh/omgnetwork/elixir-omg.svg?style=svg)](https://circleci.com/gh/omgnetwork/elixir-omg) [![Coverage Status](https://coveralls.io/repos/github/omisego/elixir-omg/badge.svg?branch=master)](https://coveralls.io/github/omisego/elixir-omg?branch=master) **IMPORTANT NOTICE: Heavily WIP, expect anything** **Table of Contents** * [Getting Started](#getting-started) * [Service start up using Docker Compose](#service-start-up-using-docker-compose) * [Troubleshooting Docker](#troubleshooting-docker) * [Install on a Linux host](#install-on-a-linux-host) * [Installing Plasma contract snapshots](#installing-plasma-contract-snapshots) * [Testing & development](#testing--development) * [Working with API Spec's](#working-with-api-specs) # Getting Started A public testnet for the OMG Network is coming soon. However, if you are brave and want to test being a Plasma chain operator, read on! ## Service start up using Docker Compose This is the recommended method of starting the blockchain services, with the auxiliary services automatically provisioned through Docker. Before attempting the start up please ensure that you are not running any services that are listening on the following TCP ports: 9656, 7434, 7534, 5000, 8545, 5432, 5433. All commands should be run from the root of the repo. To bring the entire system up you will first need to bring in the compatible Geth snapshot of plasma contracts: ```sh make init_test ``` It creates a file `./localchain_contract_addresses.env`. It is required to have this file in current directory for running any `docker-compose` command. ```sh docker-compose up ``` To bring only specific services up (eg: the childchain service, geth, etc...): ```sh docker-compose up childchain geth ... ``` _(Note: This will also bring up any services childchain depends on.)_ To run a Watcher only, first make sure you sent an ENV variable called with `INFURA_API_KEY` with your api key and then run: ```sh docker-compose -f docker-compose-watcher.yml up ``` ### Troubleshooting Docker You can view the running containers via `docker ps` If service start up is unsuccessful, containers can be left hanging which impacts the start of services on the future attempts of `docker-compose up`. You can stop all running containers via `docker kill $(docker ps -q)`. If the blockchain services are not already present on the host, docker-compose will attempt to pull the latest build coming from master. If you want Docker to use the latest commit from `elixir-omg` you can trigger a fresh build by building all three services with `make docker-childchain`, `make docker-watcher` and `make docker-watcher_info`. # Install on a Linux host Follow the guide to **[install](docs/install.md)** the Child Chain server, Watcher and Watcher Info. # Installing Plasma contract snapshots To pull in the compatible snapshot for Geth: ```bash make init_test ``` # Testing & development Docker building of source code and dependencies used to directly use common `mix` folders like `_build` and `deps`. To support workflows that switch between bare metal and Docker containers we've introduced `_build_docker` and `deps_docker` folders: ```sh sudo rm -rf _build_docker sudo rm -rf deps_docker mkdir _build_docker && chmod 777 _build_docker mkdir deps_docker && chmod 777 deps_docker ``` Pull in the compatible Plasma contracts snapshot: ```bash make init_test ``` You can setup the docker environment to run testing and development tasks: ```sh docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.datadog.yml run --rm --entrypoint bash elixir-omg ``` Once the shell has loaded, you can continue and run additional tasks. Get the necessary dependencies for building: ```bash cd app && mix deps.get ``` Quick test (no integration tests): ```bash mix test ``` Longer-running integration tests (requires compiling contracts): ```bash mix test --only integration ``` For other kinds of checks, refer to the CI/CD pipeline (https://app.circleci.com/pipelines/github/omgnetwork/elixir-omg) or build steps (https://github.com/omgnetwork/elixir-omg/blob/master/.circleci/config.yml). To run a development `iex` REPL with all code loaded: ```bash MIX_ENV=test iex -S mix run --no-start ``` ## Running integration cabbage tests Integration tests are written using the [`cabbage`](https://github.com/cabbage-ex/cabbage) library and they are located in a separated repo - [specs](https://github.com/omgnetwork/specs). This repo is added to `elixir-omg` as a git submodule. So to fetch them run: ```bash git submodule init git submodule update --remote ``` Create a directory for geth: ```bash mkdir data && chmod 777 data ``` Make services: ```bash make docker-watcher make docker-watcher_info ``` Start geth and postgres: ```bash cd priv/cabbage make start_daemon_services-2 ``` If the above command fails with the message similar to: ``` Creating network "omisego_chain_net" with driver "bridge" ERROR: Pool overlaps with other one on this address space ``` try the following remedy and retry: ```bash make stop_daemon_services rm -rf ../../data/* docker network prune ``` Build the integration tests project and run tests: ```bash cd priv/cabbage make install make generate_api_code mix deps.get mix test ``` ## Running reorg cabbage tests Reorg tests test different assumptions against chain reorgs. They also use the same submodule as regular integration cabbage tests. Fetch submodule: ```bash git submodule init git submodule update --remote ``` Create a directory for geth nodes: ```bash mkdir data1 && chmod 777 data1 && mkdir data2 && chmod 777 data2 && mkdir data && chmod 777 data ``` Make services: ```bash make docker-watcher make docker-watcher_info ``` Start geth nodes and postgres: ```bash cd priv/cabbage make start_daemon_services_reorg-2 ``` Build the integration tests project and run reorg tests: ```bash cd priv/cabbage make install make generate_api_code mix deps.get REORG=true mix test --only reorg ``` # Working with API Spec's This repo contains `gh-pages` branch intended to host [Swagger-based](https://docs.omg.network/elixir-omg/) API specification. Branch `gh-pages` is totally diseparated from other development branches and contains just Slate generated page's files. See [gh-pages README](https://github.com/omgnetwork/elixir-omg/tree/gh-pages) for more details. # More details about the design and architecture Details about the repository, code, architecture and design decisions are available **[here](docs/details.md)**. ================================================ FILE: apps/omg_bus/lib/omg_bus/application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Bus.Application do @moduledoc false use Application def start(_type, _args) do OMG.Bus.Supervisor.start_link() end end ================================================ FILE: apps/omg_bus/lib/omg_bus/event.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Bus.Event do @moduledoc """ Representation of a single event to be published on OMG event bus """ @enforce_keys [:topic, :event, :payload] @type topic_t() :: {atom(), binary()} | binary() @type t() :: %__MODULE__{topic: binary(), event: atom, payload: any()} defstruct [:topic, :event, :payload] @spec new(__MODULE__.topic_t(), atom(), any()) :: __MODULE__.t() def new({origin, topic}, event, payload) when is_atom(origin) and is_atom(event) do %__MODULE__{topic: "#{origin}:#{topic}", event: event, payload: payload} end end ================================================ FILE: apps/omg_bus/lib/omg_bus/pubsub.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Bus.PubSub do @moduledoc """ Thin wrapper around the pubsub mechanism allowing us to not repeat ourselves when starting/broadcasting/subscribing All of the messages published will have `:internal_bus_event` prepended to the tuple to distinguish them ### Topics and messages #### `enqueue_block` Is being broadcast on a local node whenever `OMG.Watcher.State` completes forming of a new child chain block Message: {:internal_event_bus, :enqueue_block, OMG.Watcher.Block.t()} """ alias Phoenix.PubSub def child_spec(args \\ []) do args |> Keyword.put_new(:name, __MODULE__) |> Keyword.put_new(:adapter, PubSub.PG2) |> PubSub.child_spec() end defmacro __using__(_) do quote do alias OMG.Bus.Event alias Phoenix.PubSub @doc """ Fixes the name of the PubSub server and the variant of `Phoenix.PubSub` used """ @doc """ Subscribes the current process to the internal bus topic """ def subscribe(topic, opts \\ []) def subscribe({origin, topic}, opts) when is_atom(origin) do PubSub.subscribe(OMG.Bus.PubSub, "#{origin}:#{topic}", opts) end def subscribe(topic, opts) do PubSub.subscribe(OMG.Bus.PubSub, topic, opts) end @doc """ Broadcast a message with a prefix indicating that it is originating from the internal event bus Handle the message in the receiving process by e.g. ``` def handle_info({:internal_bus_event, :some_event, my_payload}, state) ``` """ def broadcast(%Event{topic: topic, event: event, payload: payload}) when is_atom(event) do PubSub.broadcast(OMG.Bus.PubSub, topic, {:internal_event_bus, event, payload}) end @doc """ Same as `broadcast/1`, but performed on the local node """ def direct_local_broadcast(%Event{topic: topic, event: event, payload: payload}) when is_atom(event) do PubSub.local_broadcast(OMG.Bus.PubSub, topic, {:internal_event_bus, event, payload}) end end end end ================================================ FILE: apps/omg_bus/lib/omg_bus/supervisor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Bus.Supervisor do @moduledoc """ OMG Bus top level supervisor. """ use Supervisor require Logger def start_link() do Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) end def init(:ok) do children = [{OMG.Bus.PubSub, []}] opts = [strategy: :one_for_one] _ = Logger.info("Starting #{inspect(__MODULE__)}") Supervisor.init(children, opts) end end ================================================ FILE: apps/omg_bus/lib/omg_bus.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Bus do @moduledoc """ Modules purpose is to serve as a event bus, the implementation is in the `OMG.Bus.PubSub` macro. """ use OMG.Bus.PubSub end ================================================ FILE: apps/omg_bus/mix.exs ================================================ defmodule OMG.Bus.MixProject do use Mix.Project def project() do [ app: :omg_bus, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end def application() do [ mod: {OMG.Bus.Application, []}, extra_applications: [:logger], included_applications: [] ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end # Specifies which paths to compile per environment. defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp deps(), do: [{:phoenix_pubsub, "~> 2.0"}] end ================================================ FILE: apps/omg_bus/test/omg_bus/event_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Bus.EventTest do @moduledoc false use ExUnit.Case alias OMG.Bus.Event test "creates a root chain event" do topic = "Deposit" event = :deposit payload = ["payload"] assert %Event{topic: "root_chain:" <> topic, event: event, payload: payload} == Event.new({:root_chain, topic}, event, payload) end test "creates a child chain event" do topic = "blocks" event = :deposit payload = ["payload"] assert %Event{topic: "child_chain:" <> topic, event: event, payload: payload} == Event.new({:child_chain, topic}, event, payload) end end ================================================ FILE: apps/omg_bus/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.start() ================================================ FILE: apps/omg_conformance/mix.exs ================================================ defmodule OMG.Conformance.MixProject do use Mix.Project def project() do [ app: :omg_conformance, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end def application() do [ extra_applications: [:logger] ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp deps() do [ {:propcheck, "~> 1.1", only: [:test]}, {:omg_watcher, in_umbrella: true} ] end end ================================================ FILE: apps/omg_conformance/test/omg_conformance/conformance/merkle_proof_property_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Conformance.MerkleProofPropertyTest do @moduledoc """ Checks if some properties about the merkle proving (proof generation and validation) are consistent across implementations (currently `elixir-omg` and `plasma-contracts`, Elixir and Solidity) """ alias OMG.Watcher.Merkle alias Support.Conformance.MerkleProofContext alias Support.SnapshotContracts import Support.Conformance.MerkleProofs, only: [solidity_proof_valid: 5] use PropCheck use ExUnit.Case, async: false @moduletag :property @moduletag timeout: 450_000 setup_all do {:ok, exit_fn} = Support.DevNode.start() contracts = SnapshotContracts.parse_contracts() merkle_wrapper_address_hex = contracts["CONTRACT_ADDRESS_MERKLE_WRAPPER"] on_exit(exit_fn) [contract: OMG.Eth.Encoding.from_hex(merkle_wrapper_address_hex)] end property "any root hash and proof created by the Elixir implementation validates in the contract, for all leaves", [500, :verbose, max_size: 256, constraint_tries: 100_000], %{contract: contract} do forall leaves <- list(binary()) do root_hash = Merkle.hash(leaves) leaves |> Enum.with_index() |> Enum.all?(fn {leaf, txindex} -> proof = Merkle.create_tx_proof(leaves, txindex) solidity_proof_valid(leaf, txindex, root_hash, proof, contract) end) end end property "no proof can prove a mutated leaf", [5000, :verbose, max_size: 256, constraint_tries: 100_000], %{contract: contract} do forall proof <- MerkleProofContext.correct() do forall mutated <- MerkleProofContext.mutated_leaf(proof) do not solidity_proof_valid(mutated.leaf, mutated.txindex, mutated.root_hash, mutated.proof, contract) end end end property "no proof can prove at different index", [5000, :verbose, max_size: 256, constraint_tries: 100_000], %{contract: contract} do forall proof <- MerkleProofContext.correct() do forall mutated <- MerkleProofContext.mutated_txindex(proof) do not solidity_proof_valid(mutated.leaf, mutated.txindex, mutated.root_hash, mutated.proof, contract) end end end property "no mutated proof bytes can prove anything that the original proved", [5000, :verbose, max_size: 256, constraint_tries: 100_000], %{contract: contract} do forall proof <- MerkleProofContext.correct() do forall mutated <- MerkleProofContext.mutated_proof(proof) do not solidity_proof_valid(mutated.leaf, mutated.txindex, mutated.root_hash, mutated.proof, contract) end end end property "no proof can prove a different leaf/txindex if proof bytes mutated", [5000, :verbose, max_size: 256, constraint_tries: 100_000], %{contract: contract} do forall proof <- MerkleProofContext.correct() do forall mutated <- MerkleProofContext.mutated_to_prove_sth_else(proof) do not solidity_proof_valid(mutated.leaf, mutated.txindex, mutated.root_hash, mutated.proof, contract) end end end end ================================================ FILE: apps/omg_conformance/test/omg_conformance/conformance/merkle_proof_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Conformance.MerkleProofTest do @moduledoc """ Checks if some particular cases of merkle proofs (proof generation and validation) behave consistently across implementations (currently `elixir-omg` and `plasma-contracts`, Elixir and Solidity) """ alias OMG.Eth.Encoding alias OMG.Watcher.Crypto alias OMG.Watcher.Merkle alias Support.SnapshotContracts import Support.Conformance.MerkleProofs, only: [solidity_proof_valid: 5] use ExUnit.Case, async: false @moduletag :integration @moduletag :common @proof_length 16 @max_block_size trunc(:math.pow(2, @proof_length)) setup_all do {:ok, exit_fn} = Support.DevNode.start() contracts = SnapshotContracts.parse_contracts() merkle_wrapper_address_hex = contracts["CONTRACT_ADDRESS_MERKLE_WRAPPER"] on_exit(exit_fn) [contract: Encoding.from_hex(merkle_wrapper_address_hex)] end test "a simple, 3-leaf merkle proof validates fine", %{contract: contract} do leaves = [<<1>>, <<0>>, <<>>] root_hash = Merkle.hash(leaves) leaves |> Enum.with_index() |> Enum.each(fn {leaf, txindex} -> proof = Merkle.create_tx_proof(leaves, txindex) assert solidity_proof_valid(leaf, txindex, root_hash, proof, contract) end) end @tag timeout: 240_000 test "a full-tree merkle proof validates fine", %{contract: contract} do # why? # 1. we'd like to test all proofs on a full tree # 2. that's 65K proofs # 3. so we're pre-building the merkle tree by using raw `MerkleTree` calls instead of `OMG.Watcher.Merkle` # This is slightly inconsistent, but otherwise the test takes forever full_leaves = Enum.map(1..@max_block_size, &:binary.encode_unsigned/1) full_root_hash = Merkle.hash(full_leaves) full_tree = MerkleTree.build(full_leaves, hash_function: &Crypto.hash/1, height: 16, default_data_block: <<0::256>> ) full_leaves |> Enum.with_index() |> Enum.each(fn {leaf, txindex} -> proof = full_tree |> MerkleTree.Proof.prove(txindex) |> Enum.reverse() |> Enum.join() assert solidity_proof_valid(leaf, txindex, full_root_hash, proof, contract) end) end end ================================================ FILE: apps/omg_conformance/test/omg_conformance/conformance/signature_property_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Conformance.SignaturePropertyTest do @moduledoc """ Checks if some properties about the signatures (structural, EIP-712 hashes to be precise) hold for the Elixir and Solidity implementations. NOTE: if this fails with something like ``` Assertion with == failed code: assert solidity_hash!(tx, contract) == elixir_hash(tx) left: <<8, 195, 121, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> ``` where `Support.Conformance.SignaturesHashes.signature_hash!/2` return an "almost zero" binary, it means the contract unexpectedly refused to signhash a generated transaction. """ alias Support.Conformance.PropertyGenerators import Support.Conformance.SignaturesHashes, only: [verify: 2, verify_distinct: 3, verify_both_error: 2, verify_distinct_or_erroring: 3] use PropCheck use Support.Conformance.SignaturesHashesCase, async: false @moduletag :property @moduletag timeout: 450_000 property "any tx hashes/signhashes the same in all implementations", [1000, :verbose, max_size: 100, constraint_tries: 100_000], %{contract: contract} do forall tx <- PropertyGenerators.payment_tx() do # TODO: expand with verifying the non-signature-related hash, Transaction.raw_txhash # This occurs multiple times, wherever transaction/implementation identity/conformance is tested verify(tx, contract) end end property "any 2 different txs hash/signhash differently, regardless of implementation", [1000, :verbose, max_size: 100, constraint_tries: 100_000], %{contract: contract} do forall [{tx1, tx2} <- PropertyGenerators.distinct_payment_txs()] do verify_distinct(tx1, tx2, contract) end end property "any crude-mutated tx binary either fails to decode to a transaction object or is recognized as different", [1000, :verbose, max_size: 100, constraint_tries: 100_000], %{contract: contract} do forall {tx1_binary, tx2_binary} <- PropertyGenerators.tx_binary_with_mutation() do verify_distinct_or_erroring(tx1_binary, tx2_binary, contract) end end # this is by far the most interesting-case-yielding test, hence number of cases is set to x10 the others property "any rlp-mutated tx binary either fails to decode to a transaction object or is recognized as different", [10_000, :verbose, max_size: 100, constraint_tries: 100_000], %{contract: contract} do forall {tx1_binary, tx2_binary} <- PropertyGenerators.tx_binary_with_rlp_mutation() do verify_distinct_or_erroring(tx1_binary, tx2_binary, contract) end end property "arbitrary binaries never decode", [1000, :verbose, max_size: 1000], %{contract: contract} do forall some_binary <- binary() do verify_both_error(some_binary, contract) end end end ================================================ FILE: apps/omg_conformance/test/omg_conformance/conformance/signature_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Conformance.SignatureTest do @moduledoc """ Tests that EIP-712-compliant signatures generated `somehow` (via Elixir code as it happens) are treated the same by both Elixir signature code and contract signature code. """ alias OMG.Watcher.State.Transaction import Support.Conformance.SignaturesHashes, only: [verify: 2, verify_distinct: 3] use Support.Conformance.SignaturesHashesCase, async: false @moduletag :integration @moduletag :common @good_metadata <<1::size(32)-unit(8)>> describe "elixir vs solidity conformance test" do test "no inputs test", %{contract: contract} do tx = Transaction.Payment.new([], [{@alice, @eth, 100}]) verify(tx, contract) end test "signature test - small tx", %{contract: contract} do tx = Transaction.Payment.new([{1, 0, 0}], [{@alice, @eth, 100}]) verify(tx, contract) end test "signature test - full tx", %{contract: contract} do tx = Transaction.Payment.new( [{1, 0, 0}, {1000, 555, 3}, {2000, 333, 1}, {15_015, 0, 0}], [{@alice, @eth, 100}, {@alice, @token, 50}, {@bob, @token, 75}, {@bob, @eth, 25}] ) verify(tx, contract) end test "signature test transaction with metadata", %{contract: contract} do tx = Transaction.Payment.new( [{1, 0, 0}, {1000, 555, 3}, {2000, 333, 1}, {15_015, 0, 0}], [{@alice, @eth, 100}, {@alice, @eth, 50}, {@bob, @eth, 75}, {@bob, @eth, 25}], @good_metadata ) verify(tx, contract) end end describe "distinct transactions yield distinct sign hashes" do test "different inputs - txs hash differently but same in both implementations", %{contract: contract} do tx1 = Transaction.Payment.new([{1, 0, 0}], [{@alice, @eth, 100}]) tx2 = Transaction.Payment.new([{2, 0, 0}], [{@alice, @eth, 100}]) verify_distinct(tx1, tx2, contract) end test "different outputs - txs hash differently but same in both implementations", %{contract: contract} do tx1 = Transaction.Payment.new([{1, 0, 0}], [{@alice, @eth, 110}]) tx2 = Transaction.Payment.new([{1, 0, 0}], [{@alice, @eth, 100}]) verify_distinct(tx1, tx2, contract) end test "different metadata - txs hash differently but same in both implementations", %{contract: contract} do tx1 = Transaction.Payment.new([{1, 0, 0}], [{@alice, @eth, 100}]) tx2 = Transaction.Payment.new([{1, 0, 0}], [{@alice, @eth, 100}], <<1::256>>) verify_distinct(tx1, tx2, contract) end end end ================================================ FILE: apps/omg_conformance/test/support/conformance/merkle_proof_context.ex ================================================ # Copyright 2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.Conformance.MerkleProofContext do @moduledoc """ A package of data associated with a single proof to assert about. Contains the proof, what it proves, and the under- -lying merkle tree leaves as well """ defstruct [:leaves, :root_hash, :leaf, :txindex, :proof] alias OMG.Watcher.Merkle use PropCheck @doc """ A correct context - a proof proves something it should """ def correct() do let leaves <- such_that(leaves <- list(pragmatic_binary()), when: length(leaves) > 0) do leaves_length = length(leaves) root_hash = Merkle.hash(leaves) let txindex <- integer(0, leaves_length - 1) do proof = Merkle.create_tx_proof(leaves, txindex) leaf = Enum.at(leaves, txindex) %__MODULE__{leaves: leaves, root_hash: root_hash, leaf: leaf, txindex: txindex, proof: proof} end end end @doc """ A mutated context where only the leaf is different from the original, correct proof """ def mutated_leaf(%__MODULE__{} = base) do # TODO: add borrowing leaf from proof # Some of the generators under `union/1` are only valid on certain conditions. Setting weight to 0 prevents them # if the condition is not met zero_out_leaf_weight = if base.leaf == <<0::256>>, do: 0, else: 1 trimmed_leaf_weight = if base.leaf == "", do: 0, else: 1 get_other_leaf_weight = if base.leaves |> Enum.uniq() |> length() < 2, do: 0, else: 1 weighted_union([ {zero_out_leaf_weight, zero_out_leaf(base)}, {1, random_leaf(base)}, {trimmed_leaf_weight, trimmed_leaf(base)}, {1, expanded_leaf(base)}, {get_other_leaf_weight, get_other_leaf(base)} ]) end @doc """ A mutated context where only the txindex proven is different from the original, correct proof """ def mutated_txindex(%__MODULE__{} = base) do # The trick here is that it can be any index (even beyond the scope of leaves list!), but can't point to an # identical leaf, in case we have 2 in the leaves list. # So this is slightly different from `distinct_leaf_index` in `get_other_leaf/1` distinct_leaf_index = such_that(i <- non_neg_integer(), when: Enum.at(base.leaves, i) != base.leaf) let other_txindex <- distinct_leaf_index do %{base | txindex: other_txindex} end end @doc """ A mutated context where only the proof bytes are different from the original, correct proof """ def mutated_proof(%__MODULE__{} = base) do union([ bitwise_modify_proof(base), chunkwise_modify_proof(base) ]) end @doc """ A mutated context where we're trying to alter both the proof and what we prove, aiming to "reuse" parts of a proof that worked (the original, `base`) and produce a proof that works when it shouldn't """ def mutated_to_prove_sth_else(%__MODULE__{} = base) do let [ other_proof <- mutated_proof(base), proving_something_else <- union([mutated_leaf(base), mutated_txindex(base)]) ] do # first get a context that's proving something else (other leaf or other index) and after that modify proof %{proving_something_else | proof: other_proof.proof} end end # # leaf mutations defp zero_out_leaf(%__MODULE__{} = base) do %{base | leaf: <<0::256>>} end defp random_leaf(%__MODULE__{} = base) do let b <- such_that(b <- pragmatic_binary(), when: b != base.leaf) do %{base | leaf: b} end end defp trimmed_leaf(%__MODULE__{} = base) do length_leaf = byte_size(base.leaf) let to_keep <- integer(0, length_leaf - 1) do %{base | leaf: binary_part(base.leaf, 0, to_keep)} end end defp expanded_leaf(%__MODULE__{} = base) do let [b <- non_empty_binary(), append? <- boolean()] do if append?, do: %{base | leaf: base.leaf <> b}, else: %{base | leaf: b <> base.leaf} end end defp get_other_leaf(%__MODULE__{} = base) do length_leaves = length(base.leaves) distinct_leaf_index = such_that(i <- integer(0, length_leaves - 1), when: Enum.at(base.leaves, i) != base.leaf) let other_index <- distinct_leaf_index do %{base | leaf: Enum.at(base.leaves, other_index)} end end # # proof mutations defp bitwise_modify_proof(%__MODULE__{} = base) do # TODO: more cases pending union([ bitwise_append(base) ]) end defp chunkwise_modify_proof(%__MODULE__{} = base) do insert_leaf_chunk_weight = if base.leaf == "", do: 0, else: 1 weighted_union([ {1, insert_zero_chunk(base)}, {insert_leaf_chunk_weight, insert_leaf_chunk(base)}, {1, drop_chunk(base)}, {1, swap_neighbors(base)} ]) end defp insert_zero_chunk(%__MODULE__{} = base) do # @proof length doesn't work for some reason let position <- integer(0, 16) do %{base | proof: base.proof |> chunk() |> List.insert_at(position, <<0::256>>) |> unchunk()} end end defp insert_leaf_chunk(%__MODULE__{} = base) do # @proof length doesn't work for some reason let position <- integer(0, 16) do %{base | proof: base.proof |> chunk() |> List.insert_at(position, base.leaf) |> unchunk()} end end defp drop_chunk(%__MODULE__{} = base) do # @proof length doesn't work for some reason let position <- integer(0, 16 - 1) do %{base | proof: base.proof |> chunk() |> List.delete_at(position) |> unchunk()} end end defp swap_neighbors(%__MODULE__{} = base) do # @proof length doesn't work for some reason let position <- integer(0, 16 - 1 - 1) do chunked_proof = chunk(base.proof) [neighbor1, neighbor2] = Enum.slice(chunked_proof, position, 2) swapped_proof = chunked_proof |> List.replace_at(position, neighbor2) |> List.replace_at(position + 1, neighbor1) |> unchunk %{base | proof: swapped_proof} end end defp bitwise_append(%__MODULE__{} = base) do let to_append <- union([non_empty_binary(), <<0>>, <<0::256>>, <<0::512>>]) do %{base | proof: base.proof <> to_append} end end # `pragmatic_binary/0` generator is here to speed up the generator a bit and also to allow for more repetition in the # explored domain # TODO: rethink this again, and compare with discussions here: # https://github.com/omgnetwork/elixir-omg/pull/1251 # Can the binaries be generated more efficiently and explore the cases interesting to us better? @n_prescribed_binaries 20 @prescribed_binaries for i <- 0..@n_prescribed_binaries, do: :binary.encode_unsigned(i) defp prescribed_binary(), do: union(@prescribed_binaries) defp pragmatic_binary(), do: union([binary(), prescribed_binary()]) defp non_empty_binary(), do: such_that(b <- pragmatic_binary(), when: b != "") # # auxiliary helper functions defp chunk(proof), do: for(<>, do: <>) defp unchunk(chunked_proof), do: Enum.join(chunked_proof) end ================================================ FILE: apps/omg_conformance/test/support/conformance/merkle_proofs.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.Conformance.MerkleProofs do @moduledoc """ Utility functions used when testing Elixir vs Solidity implementation conformance """ import ExUnit.Assertions, only: [assert: 1] alias OMG.Eth.Encoding @doc """ Checks if the provided proof data returns true (valid proof) in the contract """ def solidity_proof_valid(leaf, index, root_hash, proof, contract) do signature = "checkMembership(bytes,uint256,bytes32,bytes)" args = [leaf, index, root_hash, proof] return_types = [:bool] try do {:ok, result} = call_contract(contract, signature, args, return_types) result # Some incorrect proofs throw, and end up returning something that the ABI decoder borks on, hence rescue rescue e in CaseClauseError -> # this term holds the failure reason, but attempted to be decoded as a bool. It is a huge int %{term: failed_decoding_reason} = e # now we bring it back to binary form binary_reason = :binary.encode_unsigned(failed_decoding_reason) # it should contain 4 bytes of the function selector and then zeros assert_contract_reverted(binary_reason) false end end # see similar function in `Support.Conformance.SignaturesHashes` defp assert_contract_reverted(chopped_reason_binary_result) do # only geth is supported for the merkle proof conformance tests for now :geth = Application.fetch_env!(:omg_eth, :eth_node) # revert from `call_contract` it returns something resembling a reason # binary (beginning with 4-byte function selector). We need to assume that this is in fact a revert assert <<0::size(28)-unit(8)>> = binary_part(chopped_reason_binary_result, 4, 28) end defp call_contract(contract, signature, args, return_types) do data = ABI.encode(signature, args) {:ok, return} = Ethereumex.HttpClient.eth_call(%{ from: Encoding.to_hex(contract), to: Encoding.to_hex(contract), data: Encoding.to_hex(data) }) decode_answer(return, return_types) end defp decode_answer(enc_return, return_types) do single_return = enc_return |> Encoding.from_hex() |> ABI.TypeDecoder.decode(return_types) |> hd() {:ok, single_return} end end ================================================ FILE: apps/omg_conformance/test/support/conformance/property.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.Conformance.PropertyGenerators do @moduledoc """ Utility functions (mainly `:propcheck` generators) useful for building property tests for conformance tests """ alias OMG.Watcher.State.Transaction use PropCheck require Transaction.Payment @doc """ Generates a payment transaction, as valid as possible """ def payment_tx() do let [inputs <- valid_inputs_list(), outputs <- valid_outputs_list(), metadata <- hash()] do Transaction.Payment.new(inputs, outputs, metadata) end end @doc """ Generates a pair of _distinct_ payment transactions, as valid as possible. Mimicks the `payment_tx/0` generator, but uses mutations to generate the other transaction """ def distinct_payment_txs() do proposition_result = let [inputs <- valid_inputs_list(), outputs <- valid_outputs_list(), metadata <- hash()] do tx1 = Transaction.Payment.new(inputs, outputs, metadata) tx2 = let [ inputs2 <- union([inputs, mutated_inputs(inputs), Enum.reverse(inputs)]), outputs2 <- union([outputs, mutated_outputs(outputs), Enum.reverse(outputs)]), metadata2 <- union([metadata, mutated_hash(metadata), hash()]) ] do Transaction.Payment.new(inputs2, outputs2, metadata2) end {tx1, tx2} end such_that(pair <- proposition_result, when: is_pair_of_distinct_terms?(pair)) end @doc """ Generates a valid payment transaction using `payment_tx/0` then mutates it using a structure-blind binary mutation """ def tx_binary_with_mutation() do proposition_result = let [tx1 <- payment_tx()] do tx1_binary = Transaction.raw_txbytes(tx1) {tx1_binary, mutate_binary(tx1_binary)} end such_that(pair <- proposition_result, when: is_pair_of_distinct_terms?(pair)) end @doc """ Generates a valid payment transaction using `payment_tx/0` then mutates it using a RLP-aware mutation """ def tx_binary_with_rlp_mutation() do proposition_result = let [tx1 <- payment_tx()] do tx1_binary = Transaction.raw_txbytes(tx1) {tx1_binary, rlp_mutate_binary(tx1_binary)} end such_that(pair <- proposition_result, when: is_pair_of_distinct_terms?(pair)) end defp is_pair_of_distinct_terms?({base_term, new_term}), do: base_term != new_term defp non_zero_address(), do: union([exactly(<<1::160>>), binary(20)]) defp address(), do: union([exactly(<<0::160>>), exactly(<<1::160>>), binary(20)]) defp hash(), do: union([exactly(<<0::256>>), exactly(<<1::256>>), binary(32)]) defp injectable_binary() do union([ binary(), <<0::8>>, <<1::8>>, <<0::16>>, <<1::16>>, <<0::32>>, <<1::32>>, <<0::128>>, <<1::128>>, <<0::256>>, <<1::256>> ]) end # taken from ex_plasma, where they have been taken from: # Contract settings # These are being hard-coded from the same values on the contracts. # See: https://github.com/omgnetwork/plasma-contracts/blob/master/plasma_framework/contracts/src/utils/PosLib.sol#L16-L23 # TODO: when this moves to `ex_plasma`, fix this properly @block_offset 1_000_000_000 @transaction_offset 10_000 @max_txindex trunc(:math.pow(2, 16) - 1) @max_blknum trunc((:math.pow(2, 54) - 1 - @max_txindex) / (@block_offset / @transaction_offset)) defp valid_blknum(), do: integer(0, @max_blknum) defp valid_txndex(), do: integer(0, @max_txindex) defp valid_oindex(), do: integer(0, @transaction_offset - 1) # TODO: revisit this to generate logic-wise invalid txs like zero inputs/outputs (H6) defp valid_input_tuple() do proposition_result = let [blknum <- valid_blknum(), txindex <- valid_txndex(), oindex <- valid_oindex()] do {blknum, txindex, oindex} end such_that({blknum, txindex, oindex} <- proposition_result, when: blknum + txindex + oindex > 0) end # TODO: revisit the case of negative amounts, funny things happen defp valid_output_tuple() do let [owner <- non_zero_address(), currency <- address(), amount <- pos_integer()] do {owner, currency, amount} end end defp valid_inputs_list() do such_that(l <- list(valid_input_tuple()), when: length(l) <= Transaction.Payment.max_inputs()) end defp valid_outputs_list() do such_that(l <- list(valid_output_tuple()), when: length(l) > 0 && length(l) <= Transaction.Payment.max_outputs()) end defp mutated_hash(base_hash) do # TODO: provide more cases OMG.Watcher.Crypto.hash(base_hash) end defp mutated_inputs(inputs) do # TODO: provide more cases if Enum.empty?(inputs), do: valid_inputs_list(), else: tl(inputs) end defp mutated_outputs(outputs) do # TODO: provide more cases if length(outputs) == 1, do: valid_outputs_list(), else: tl(outputs) end defp prepend_binary(base_binary) do let(random_binary <- injectable_binary(), do: random_binary <> base_binary) end defp apend_binary(base_binary) do let(random_binary <- injectable_binary(), do: base_binary <> random_binary) end defp substring_binary(base_binary) do base_length = byte_size(base_binary) let [from <- integer(0, base_length - 1)] do max_substring_length = max(1, base_length - from) let [substring_length <- integer(1, max_substring_length)] do binary_part(base_binary, from, substring_length) end end end defp insert_into_binary(base_binary) do base_length = byte_size(base_binary) let [from <- integer(0, base_length - 1), random_binary <- injectable_binary()] do binary_part(base_binary, 0, from) <> random_binary <> binary_part(base_binary, from, base_length - from) end end defp mutate_binary(base_binary) do union([ prepend_binary(base_binary), apend_binary(base_binary), substring_binary(base_binary), insert_into_binary(base_binary) ]) end defp inject_extra_item(base_rlp_items) when is_list(base_rlp_items) do rlp_items_length = length(base_rlp_items) let [new_item <- rlp_item_generator(), index <- integer(0, rlp_items_length)] do List.insert_at(base_rlp_items, index, new_item) end end # base wasn't a list so we make one now! defp inject_extra_item(base_rlp_items) do union([[rlp_item_generator(), base_rlp_items], [base_rlp_items, rlp_item_generator()]]) end defp try_reversing(rlp_item) when is_list(rlp_item) and length(rlp_item) > 1, do: Enum.reverse(rlp_item) defp try_reversing(rlp_item), do: rlp_item defp swap_in_rlp([]), do: rlp_item_generator() defp swap_in_rlp(rlp_items) when is_list(rlp_items) do rlp_items_length = length(rlp_items) # first we pick were we _could_ change the list let [index <- integer(0, rlp_items_length - 1)] do to_swap = Enum.at(rlp_items, index) # now we either go deeper to change it or change right here let [mutated_rlp <- mutate_sub_rlp(to_swap)] do List.replace_at(rlp_items, index, mutated_rlp) end end end defp swap_in_rlp(rlp_item) when is_integer(rlp_item) or is_binary(rlp_item), do: rlp_item_generator() defp rlp_item_generator(), do: union([[], injectable_binary(), non_neg_integer(), list(union([injectable_binary(), non_neg_integer()]))]) defp mutate_sub_rlp(base_rlp_items) do union([ rlp_item_generator(), swap_in_rlp(base_rlp_items), try_reversing(base_rlp_items), inject_extra_item(base_rlp_items) ]) end defp rlp_mutate_binary(base_binary) do # TODO: these mutations used could use improving/extending base_rlp_items = base_binary |> Transaction.decode!() |> Transaction.Protocol.get_data_for_rlp() let([mutated_rlp <- mutate_sub_rlp(base_rlp_items)]) do ExRLP.encode(mutated_rlp) end end end ================================================ FILE: apps/omg_conformance/test/support/conformance/signatures_hashes.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.Conformance.SignaturesHashes do @moduledoc """ Utility functions that used when testing Elixir vs Solidity implementation conformance """ import ExUnit.Assertions, only: [assert: 1] alias OMG.Eth.Encoding alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash @doc """ Check if both implementations treat distinct transactions as distinct but produce sign hashes consistently """ def verify_distinct(tx1, tx2, contract) do # NOTE: those two verifies might be redundant, rethink sometimes. For now keeping to increase chance of picking up # discrepancies verify(tx1, contract) verify(tx2, contract) assert solidity_hash!(tx1, contract) != solidity_hash!(tx2, contract) assert elixir_hash(tx1) != elixir_hash(tx2) end @doc """ Check if both implementations product the same signature hash """ def verify(tx, contract) do assert solidity_hash!(tx, contract) == elixir_hash(tx) end @doc """ Check if both implementations error for a binary that's known to not be a validly decoding transaction """ def verify_both_error(some_binary, contract) do # elixir implementation errors assert {:error, _} = Transaction.decode(some_binary) # solidity implementation errors some_binary |> solidity_hash(contract) |> assert_contract_reverted() true end @doc """ Check if both implementations either: - treat distinct transactions as distinct but produce sign hashes consistently - both error _under the condition that `tx2_binary` decodes fine in the "native" implementation in Elixir_ """ def verify_distinct_or_erroring(tx1_binary, tx2_binary, contract) do # TODO - think of a better approach to handling the different treatment of valid/admissible tx/output types # there shouldn't be that many cases, 2 (`{:ok, _}` and `{:error, _}`) should ideally do case Transaction.decode(tx2_binary) do # if the mutated transaction decodes fine, we check whether signature hashes match across impls and are distinct {:ok, _} -> verify_distinct(Transaction.decode!(tx1_binary), Transaction.decode!(tx2_binary), contract) # NOTE: unrecognized tx/output type is never picked up in the contract, since there, decoding assumes already a # particular type (i.e. Payment) and only checks if delivered type (`1`, `2`, ...) is correct in later stage # when fetching and verifying the `ISpendingCondition` {:error, :unrecognized_transaction_type} -> true {:error, :unrecognized_output_type} -> true # NOTE: another temporary special case handling, until a better idea comes. `tx_type` 3 is `Transaction.Fee` # transaction which pops out as `malformed` in `elixir-omg` and is accepted by contracts {:error, :malformed_transaction} -> case ExRLP.decode(tx2_binary) do # first RLP item of the transaction specifies the tx type as `Transaction.Fee` - can't test further [<<3>> | _] -> true # in all other cases the contract should revert _ -> verify_both_error(tx2_binary, contract) end # in other cases of errors, we check whether both implementations reject the mutated transaction {:error, _} -> verify_both_error(tx2_binary, contract) end end # NOTE: `solidity_hash!/2` returns `<<8, 195, 121, 160, 0, 0, 0, more zeroes...>>` on revert, see note on # `solidity_hash/2` defp solidity_hash!(tx, contract) do {:ok, solidity_hash} = solidity_hash(tx, contract) solidity_hash end defp solidity_hash(%{} = tx, contract), do: tx |> Transaction.raw_txbytes() |> solidity_hash(contract) # NOTE: `solidity_hash/2` returns something like `{:ok, <<8, 195, 121, 160, 0, 0, 0, more zeroes...>>}`, on contract # revert, when using `:geth` Ethereum node. If an assertion fails with such a result, it indicates the contract # rejected some transaction to signhash unexpectedly. defp solidity_hash(encoded_tx, contract) when is_binary(encoded_tx) do call_contract(contract, "hashTx(address,bytes)", [contract, encoded_tx], [{:bytes, 32}]) end defp elixir_hash(%{} = tx), do: TypedDataHash.hash_struct(tx) defp elixir_hash(encoded_tx), do: encoded_tx |> Transaction.decode!() |> elixir_hash() defp assert_contract_reverted(result) do {:ok, chopped_reason_binary_result} = result assert <<0::size(28)-unit(8)>> = binary_part(chopped_reason_binary_result, 4, 28) end defp call_contract(contract, signature, args, return_types) do data = ABI.encode(signature, args) {:ok, return} = Ethereumex.HttpClient.eth_call(%{ from: Encoding.to_hex(contract), to: Encoding.to_hex(contract), data: Encoding.to_hex(data) }) decode_answer(return, return_types) end defp decode_answer(enc_return, return_types) do single_return = enc_return |> Encoding.from_hex() |> ABI.TypeDecoder.decode(return_types) |> hd() {:ok, single_return} end end ================================================ FILE: apps/omg_conformance/test/support/conformance/signatures_hashes_case.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.Conformance.SignaturesHashesCase do @moduledoc """ `ExUnit` test case for the setup required by a test of Elixir and Solidity implementation conformance """ alias Support.SnapshotContracts use ExUnit.CaseTemplate using do quote do @alice <<215, 32, 17, 47, 111, 72, 20, 47, 149, 226, 138, 242, 35, 254, 141, 212, 16, 22, 155, 182>> @bob <<141, 246, 138, 77, 76, 3, 78, 54, 173, 40, 234, 195, 29, 170, 154, 64, 99, 14, 118, 139>> @eth <<0::160>> @token <<235, 169, 32, 193, 242, 237, 159, 137, 184, 46, 124, 13, 178, 171, 61, 87, 179, 179, 135, 146>> @zero_address <<0::160>> end end setup_all do {:ok, exit_fn} = Support.DevNode.start() contracts = SnapshotContracts.parse_contracts() signtest_addr_hex = contracts["CONTRACT_ADDRESS_PAYMENT_EIP_712_LIB_MOCK"] old_config = Application.get_all_env(:omg_eth) :ok = Application.put_env(:omg_eth, :contract_addr, %{plasma_framework: signtest_addr_hex}) on_exit(fn -> # reverting to the original values from `omg_eth/config/test.exs` :ok = Application.put_all_env(omg_eth: old_config) exit_fn.() end) [contract: OMG.Eth.Encoding.from_hex(signtest_addr_hex)] end end ================================================ FILE: apps/omg_conformance/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.configure(exclude: [integration: true, property: true]) {:ok, _} = Application.ensure_all_started(:propcheck) ExUnit.start() ================================================ FILE: apps/omg_db/lib/db.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB do @moduledoc """ DB API module provides an interface to all needed functions that need to be implemented by the underlying database layer. """ use Spandex.Decorators alias OMG.DB.RocksDB @type utxo_pos_db_t :: {pos_integer, non_neg_integer, non_neg_integer} @callback start_link(term) :: GenServer.on_start() @callback child_spec() :: Supervisor.child_spec() @callback child_spec(term) :: Supervisor.child_spec() @callback init(String.t()) :: :ok @callback init() :: :ok @callback initiation_multiupdate() :: :ok | {:error, any} @callback multi_update(term()) :: :ok | {:error, any} @callback blocks(block_to_fetch :: list()) :: {:ok, list(term)} @callback utxos() :: {:ok, list({utxo_pos_db_t, term})} @callback utxo(utxo_pos_db_t) :: {:ok, term} | :not_found @callback competitors_info() :: {:ok, list(term)} @callback spent_blknum(utxo_pos_db_t()) :: {:ok, pos_integer} | :not_found @callback block_hashes(integer()) :: {:ok, list()} @callback child_top_block_number() :: {:ok, non_neg_integer()} | :not_found @callback get_single_value(atom()) :: {:ok, term} | :not_found @callback batch_get(atom(), list(term)) :: {:ok, list(term)} | :not_found @callback get_all_by_type(atom()) :: {:ok, list(term)} | :not_found # callbacks useful for injecting a specific server implementation @callback initiation_multiupdate(GenServer.server()) :: :ok | {:error, any} @callback multi_update(term(), GenServer.server()) :: :ok | {:error, any} @callback blocks(block_to_fetch :: list(), GenServer.server()) :: {:ok, list()} | {:error, any} @callback utxos(GenServer.server()) :: {:ok, list({utxo_pos_db_t, term})} | {:error, any} @callback utxo(utxo_pos_db_t, GenServer.server()) :: {:ok, term} | :not_found @callback competitors_info(GenServer.server()) :: {:ok, list(term)} | {:error, any} @callback spent_blknum(utxo_pos_db_t(), GenServer.server()) :: {:ok, pos_integer} | :not_found @callback block_hashes(integer(), GenServer.server()) :: {:ok, list()} @callback child_top_block_number(GenServer.server()) :: {:ok, non_neg_integer()} | :not_found @callback get_single_value(atom(), GenServer.server()) :: {:ok, term} | :not_found @callback batch_get(atom(), list(term), keyword()) :: {:ok, list(term)} | :not_found @callback get_all_by_type(atom(), keyword()) :: {:ok, list(term)} | :not_found @optional_callbacks child_spec: 1, initiation_multiupdate: 1, multi_update: 2, blocks: 2, utxos: 1, utxo: 2, spent_blknum: 2, block_hashes: 2, child_top_block_number: 1, get_single_value: 2 def start_link(args), do: RocksDB.start_link(args) def child_spec(), do: RocksDB.child_spec() def child_spec(args), do: RocksDB.child_spec(args) def init(path) do RocksDB.init(path) end def init() do RocksDB.init() end @doc """ Puts all zeroes and other init values to a generically initialized `OMG.DB` """ def initiation_multiupdate(), do: RocksDB.initiation_multiupdate() def initiation_multiupdate(server), do: RocksDB.initiation_multiupdate(server) @decorate span(service: :ethereum_event_listener, type: :backend, name: "multi_update/1") def multi_update(db_updates), do: RocksDB.multi_update(db_updates) def multi_update(db_updates, server), do: RocksDB.multi_update(db_updates, server) def blocks(blocks_to_fetch), do: RocksDB.blocks(blocks_to_fetch) def blocks(blocks_to_fetch, server), do: RocksDB.blocks(blocks_to_fetch, server) def utxos(), do: RocksDB.utxos() def utxos(server), do: RocksDB.utxos(server) def utxo(utxo_pos), do: RocksDB.utxo(utxo_pos) def utxo(utxo_pos, server), do: RocksDB.utxo(utxo_pos, server) def competitors_info(), do: RocksDB.competitors_info() def competitors_info(server), do: RocksDB.competitors_info(server) def spent_blknum(utxo_pos), do: RocksDB.spent_blknum(utxo_pos) def spent_blknum(utxo_pos, server), do: RocksDB.spent_blknum(utxo_pos, server) def block_hashes(block_numbers_to_fetch), do: RocksDB.block_hashes(block_numbers_to_fetch) def block_hashes(block_numbers_to_fetch, server), do: RocksDB.block_hashes(block_numbers_to_fetch, server) def child_top_block_number(), do: RocksDB.child_top_block_number() def get_single_value(parameter_name), do: RocksDB.get_single_value(parameter_name) def get_single_value(parameter_name, server), do: RocksDB.get_single_value(parameter_name, server) @doc """ This is generic DB function that can batch get the specific data of a specific type with the given specific keys of the type. """ def batch_get(type, specific_keys), do: RocksDB.batch_get(type, specific_keys) def batch_get(type, specific_keys, opts), do: RocksDB.batch_get(type, specific_keys, opts) @doc """ This is generic DB function that can get all data of a specific type. """ def get_all_by_type(type), do: RocksDB.get_all_by_type(type) def get_all_by_type(type, opts), do: RocksDB.get_all_by_type(type, opts) @doc """ A list of all atoms that we use as single-values stored in the database (i.e. markers/flags of all kinds) """ def single_value_parameter_names() do [ # child chain - used at block forming :child_top_block_number, # watcher :last_block_getter_eth_height, :last_ife_exit_deleted_eth_height, # watcher and child chain :last_depositor_eth_height, :last_exiter_eth_height, :last_piggyback_exit_eth_height, :last_in_flight_exit_eth_height, :last_exit_processor_eth_height, :last_exit_finalizer_eth_height, :last_exit_challenger_eth_height, :last_in_flight_exit_processor_eth_height, :last_ife_exit_deleted_eth_height, :last_piggyback_processor_eth_height, :last_competitor_processor_eth_height, :last_challenges_responds_processor_eth_height, :last_piggyback_challenges_processor_eth_height, :last_ife_exit_finalizer_eth_height, :omg_eth_contracts ] end end ================================================ FILE: apps/omg_db/lib/omg_db/application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.Application do @moduledoc false use Application def start(_type, _args) do children = [OMG.DB.child_spec()] opts = [strategy: :one_for_one, name: OMG.DB.Supervisor] Supervisor.start_link(children, opts) end def start_phase(:attach_telemetry, :normal, _phase_args) do handlers = [["measure-db", OMG.DB.Measure.supported_events(), &OMG.DB.Measure.handle_event/4, nil]] Enum.each(handlers, fn handler -> case apply(:telemetry, :attach_many, handler) do :ok -> :ok {:error, :already_exists} -> :ok end end) end end ================================================ FILE: apps/omg_db/lib/omg_db/measure.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.Measure do @moduledoc """ A telemetry handler for DB related metrics. """ alias OMG.Status.Metric.Datadog import OMG.Status.Metric.Event, only: [name: 1] alias OMG.DB.RocksDB.Server @write :write @read :read @multiread :multiread @keys [@write, @read, @multiread] @services [Server] @supported_events List.foldl(@services, [], fn service, acc -> acc ++ [ [:process, service], [:update_write, service], [:update_read, service], [:update_multiread, service] ] end) def supported_events(), do: @supported_events def handle_event([:process, service_name], _, state, _config) when service_name in @services do value = self() |> Process.info(:message_queue_len) |> elem(1) _ = Datadog.gauge(name(:db_message_queue_len), value, tags: ["service_name:#{service_name}"]) Enum.each(@keys, fn table_key -> case :ets.take(state.name, table_key) do [{key, value}] -> _ = Datadog.gauge(name(key), value) _ -> # handling the case where the entry doesn't exist yet :skip end end) end def handle_event([:update_write, service_name], _, state, _config) when service_name in @services do :ets.update_counter(state.name, @write, {2, 1}, {@write, 0}) end def handle_event([:update_read, service_name], _, state, _config) when service_name in @services do :ets.update_counter(state.name, @read, {2, 1}, {@read, 0}) end def handle_event([:update_multiread, service_name], _, state, _config) when service_name in @services do :ets.update_counter(state.name, @multiread, {2, 1}, {@multiread, 0}) end end ================================================ FILE: apps/omg_db/lib/omg_db/models/payment_exit_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.Models.PaymentExitInfo do @moduledoc """ DB model wrapper that is responsible for Payment (V1) Exit Info. """ alias OMG.DB @server_name OMG.DB.RocksDB.Server @ten_seconds 10_000 @one_minute 60_000 def exit_info(utxo_pos, server \\ @server_name) do {:ok, data} = DB.batch_get(:exit_info, [utxo_pos], server: server) {:ok, hd(data)} end def exit_infos(utxo_pos_list, server \\ @server_name) when is_list(utxo_pos_list) do DB.batch_get(:exit_info, utxo_pos_list, server: server, timeout: @ten_seconds) end def all_exit_infos(server \\ @server_name) do DB.get_all_by_type(:exit_info, server: server, timeout: @one_minute) end def all_in_flight_exits_infos(server \\ @server_name) do DB.get_all_by_type(:in_flight_exit_info, server: server, timeout: @one_minute) end end ================================================ FILE: apps/omg_db/lib/omg_db/release_tasks/init_key_value_db.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.ReleaseTasks.InitKeyValueDB do @moduledoc """ Creates an empty instance of OMG DB storage and fills it with the required initial data. """ @start_apps [:logger, :crypto, :ssl] require Logger def run() do _ = on_load() path = Application.get_env(:omg_db, :path) process(path) end defp process(path) do _ = Logger.warn("Creating database at #{inspect(path)}") result = init_kv_db(path) Enum.each(Enum.reverse(@start_apps), &Application.stop/1) result end defp init_kv_db(path) do case OMG.DB.init(path) do {:error, term} -> _ = Logger.error("Could not initialize the DB in #{path}. Reason #{inspect(term)}") {:error, term} :ok -> _ = Logger.warn("The database at #{inspect(path)} has been created") end end defp on_load() do _ = Enum.each(@start_apps, &Application.ensure_all_started/1) _ = Application.load(:omg_db) end end ================================================ FILE: apps/omg_db/lib/omg_db/release_tasks/init_keys_with_values.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.ReleaseTasks.InitKeysWithValues do @moduledoc """ Sets values for keys stored in RocksDB, if they are not set. """ require Logger @keys_to_values [last_ife_exit_deleted_eth_height: 0] def run() do {:ok, _} = Application.ensure_all_started(:logger) path = Application.get_env(:omg_db, :path) Application.put_env(:omg_db, :path, path) case Application.ensure_all_started(:omg_db) do {:ok, _} -> Enum.each(@keys_to_values, &set_single_value/1) {:error, _} -> _ = Logger.info("DB not initialized yet, no action required") :ok end end defp set_single_value({key, init_val}) do case OMG.DB.RocksDB.get_single_value(key) do :not_found -> :ok = OMG.DB.RocksDB.multi_update([{:put, key, init_val}]) _ = Logger.info("#{key} not set. Setting it to #{inspect(init_val)}") :ok {:ok, _} -> :ok end end end ================================================ FILE: apps/omg_db/lib/omg_db/release_tasks/set_key_value_db.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.ReleaseTasks.SetKeyValueDB do @moduledoc false @behaviour Config.Provider require Logger @app :omg_db def init(args) do args end def load(config, args) do _ = on_load() release = Keyword.get(args, :release) case get_env("DB_PATH") do root_path when is_binary(root_path) -> set_db(config, root_path, release) _ -> root_path = Path.join([System.user_home!(), ".omg/data"]) set_db(config, root_path, release) end end defp set_db(config, root_path, release) do path = Path.join([root_path, "#{release}"]) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DB_PATH Value: #{inspect(path)}.") # if we want to access the updated path in the same VM instance, we need to update it imidiatelly Application.put_env(@app, :path, path) Config.Reader.merge(config, omg_db: [path: path]) end defp get_env(key), do: System.get_env(key) defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(@app) end end ================================================ FILE: apps/omg_db/lib/omg_db/rocks_db.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.RocksDB do @moduledoc """ Our-types-aware port/adapter to a database backend. Contains functions to access data stored in the database """ alias OMG.DB @behaviour OMG.DB require Logger @server_name OMG.DB.RocksDB.Server @default_genserver_timeout 5000 @one_minute 60_000 @ten_minutes 10 * @one_minute @type utxo_pos_db_t :: {pos_integer, non_neg_integer, non_neg_integer} def start_link(args) do @server_name.start_link(args) end def child_spec() do db_path = Application.fetch_env!(:omg_db, :path) args = [db_path: db_path, name: OMG.DB.RocksDB.Server] %{ id: OMG.DB.RocksDB.Server, start: {OMG.DB.RocksDB.Server, :start_link, [args]}, type: :worker } end def child_spec([db_path: _db_path, name: server_name] = args) do %{ id: server_name, start: {OMG.DB.RocksDB.Server, :start_link, [args]}, type: :worker } end def multi_update(db_updates, server_name \\ @server_name) do GenServer.call(server_name, {:multi_update, db_updates}) end @spec blocks(block_to_fetch :: list(), atom) :: {:ok, list()} | {:error, any} def blocks(blocks_to_fetch, server_name \\ @server_name) def blocks([], _server_name), do: {:ok, []} def blocks(blocks_to_fetch, server_name) do GenServer.call(server_name, {:blocks, blocks_to_fetch}) end def utxos(server_name \\ @server_name) do _ = Logger.info("Reading UTXO set, this might take a while. Allowing #{inspect(@ten_minutes)} ms") GenServer.call(server_name, :utxos, @ten_minutes) end def utxo(utxo_pos, server_name \\ @server_name) do GenServer.call(server_name, {:utxo, utxo_pos}) end def competitors_info(server_name \\ @server_name) do _ = Logger.info("Reading competitors' info, this might take a while. Allowing #{inspect(@one_minute)} ms") GenServer.call(server_name, :competitors_info, @one_minute) end def spent_blknum(utxo_pos, server_name \\ @server_name) do GenServer.call(server_name, {:spent_blknum, utxo_pos}) end def block_hashes(block_numbers_to_fetch, server_name \\ @server_name) do GenServer.call(server_name, {:block_hashes, block_numbers_to_fetch}) end def child_top_block_number(server_name \\ @server_name) do GenServer.call(server_name, :child_top_block_number) end # Note: *_eth_height values below denote actual Ethereum height service has processed. # It might differ from "latest" Ethereum block. def get_single_value(parameter_name, server_name \\ @server_name) do GenServer.call(server_name, {:get_single_value, parameter_name}) end @doc """ Batch get data of a type with the given specific keys. optional args includes: 1. timeout (in ms). Defaults to 5000 which is the same default value of Genserver. 2. server (type in Genserver.server()). Defaults to OMG.DB.RocksDB.Server. """ def batch_get(type, specific_keys, opts \\ []) do timeout = opts[:timeout] || @default_genserver_timeout server = opts[:server] || @server_name _ = Logger.info( "Batch get data for type #{inspect(type)} with the following keys #{inspect(specific_keys)}." <> " Allowing #{inspect(timeout)} ms" ) GenServer.call(server, {:get, type, specific_keys}, timeout) end @doc """ Get ALL data of a type. optional args includes: 1. timeout (in ms). Defaults to 5000 which is the same default value of Genserver. 2. server (type in Genserver.server()). Defaults to OMG.DB.RocksDB.Server. """ def get_all_by_type(type, opts \\ []) do timeout = opts[:timeout] || @default_genserver_timeout server = opts[:server] || @server_name _ = Logger.info( "Reading all data for type #{inspect(type)}, this might take a while. Allowing #{inspect(timeout)} ms" ) GenServer.call(server, {:get_all_by_type, type}, timeout) end def initiation_multiupdate(server_name \\ @server_name) do # setting a number of markers to zeroes DB.single_value_parameter_names() |> Enum.map(&{:put, &1, 0}) |> multi_update(server_name) end @doc """ Does all of the initialization of `OMG.DB` based on the configured path """ def init(), do: do_init(@server_name, Application.fetch_env!(:omg_db, :path)) def init(path) when is_binary(path) do :ok = Application.put_env(:omg_db, :path, path, persistent: true) do_init(@server_name, path) end def init(server_name), do: do_init(server_name, Application.fetch_env!(:omg_db, :path)) def init(server_name, path), do: do_init(server_name, path) # File.mkdir_p is called at the application start # sobelow_skip ["Traversal"] defp do_init(server_name, path) do :ok = File.mkdir_p(path) with :ok <- server_name.init_storage(path), {:ok, started_apps} <- Application.ensure_all_started(:omg_db), :ok <- initiation_multiupdate(server_name) do started_apps |> Enum.reverse() |> Enum.each(fn app -> :ok = Application.stop(app) end) :ok else error -> _ = Logger.error("Unable to init: #{inspect(error)}") error end end end ================================================ FILE: apps/omg_db/lib/omg_db/rocksdb/core.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.RocksDB.Core do @moduledoc """ Responsible for converting type-aware, logic-specific queries and updates into rocksdb specific queries and updates """ # adapter - testable, if we really really want to use Spandex.Decorators @single_value_parameter_names OMG.DB.single_value_parameter_names() # if we keep the prefix byte size consistent across all keys, we're able to use # prefix extractor to reduce the number of IO scans # more https://github.com/facebook/rocksdb/wiki/Prefix-Seek-API-Changes @keys_prefixes %{ # watcher (Exit Processor) and child chain (Fresh Blocks) block: "block", # watcher (Exit Processor) and child chain (Block Queue) block_hash: "hashb", # watcher and child chain utxo: "utxoi", # watcher and child chain exit_info: "exiti", # watcher only in_flight_exit_info: "infle", # watcher only competitor_info: "compi", # watcher only spend: "spend", # watcher and child chain omg_eth_contracts: "omg_eth_contracts" } @key_types Map.keys(@keys_prefixes) def parse_multi_updates(db_updates), do: Enum.flat_map(db_updates, &parse_multi_update/1) @doc """ Interprets the response from rocksdb and returns a success-decorated result """ @spec decode_value({:ok, binary()} | :not_found) :: {:ok, term()} | :not_found def decode_value(db_response) do case decode_response(db_response) do :not_found -> :not_found other -> {:ok, other} end end @doc """ Interprets an enumerable of responses from rocksdb and decorates the enumerable with a `{:ok, _enumerable}` if no errors occurred """ @spec decode_values(Enumerable.t()) :: {:ok, list} def decode_values(encoded_enumerable) do raw_decoded = Enum.map(encoded_enumerable, fn encoded -> decode_response(encoded) end) {:ok, raw_decoded} end def filter_keys(key_stream, type) when type in @key_types, do: do_filter_keys(key_stream, Map.get(@keys_prefixes, type)) @doc """ Produces a type-specific LevelDB key for a combination of type and type-agnostic/LevelDB-ignorant key """ def key(:block, hash) when is_binary(hash), do: @keys_prefixes.block <> hash def key(parameter, _) when parameter in @single_value_parameter_names, do: Atom.to_string(parameter) def key(type, specific_key) when type in @key_types, do: Map.get(@keys_prefixes, type) <> :erlang.term_to_binary(specific_key) # `key_for_item` gets the type-specific key to persist a whole item at, as used by `:put` updates defp key_for_item(:block, %{hash: hash} = _block), do: key(:block, hash) defp key_for_item(:utxo, {position, _utxo}), do: key(:utxo, position) defp key_for_item(:spend, {position, _blknum}), do: key(:spend, position) defp key_for_item(:exit_info, {position, _exit_info}), do: key(:exit_info, position) defp key_for_item(:in_flight_exit_info, {position, _info}), do: key(:in_flight_exit_info, position) defp key_for_item(:competitor_info, {position, _info}), do: key(:competitor_info, position) defp key_for_item(parameter, value) when parameter in @single_value_parameter_names, do: key(parameter, value) defp parse_multi_update({:put, :block, %{number: number, hash: hash} = item}) do [ {:put, key_for_item(:block, item), encode_value(:block, item)}, {:put, key(:block_hash, number), encode_value(:block_hash, hash)} ] end defp parse_multi_update({:put, type, item}), do: [{:put, key_for_item(type, item), encode_value(type, item)}] defp parse_multi_update({:delete, type, item}), do: [{:delete, key(type, item)}] defp encode_value(:spend, {_position, blknum}), do: :erlang.term_to_binary(blknum) defp encode_value(_type, value), do: :erlang.term_to_binary(value) # sobelow_skip ["Misc.BinToTerm"] defp decode_response(db_response) do case db_response do :not_found -> :not_found {:ok, encoded} -> :erlang.binary_to_term(encoded, [:safe]) encoded -> # iterator search returns raw values :erlang.binary_to_term(encoded, [:safe]) end end defp do_filter_keys(reference, prefix) do # https://github.com/facebook/rocksdb/wiki/Prefix-Seek-API-Changes#use-readoptionsprefix_seek {:ok, iterator} = :rocksdb.iterator(reference, prefix_same_as_start: true) move_iterator = :rocksdb.iterator_move(iterator, {:seek, prefix}) Enum.reverse(search(reference, iterator, move_iterator, [])) end defp search(_reference, _iterator, {:error, :invalid_iterator}, acc), do: acc defp search(reference, iterator, {:ok, _key, value}, acc), do: do_search(reference, iterator, [value | acc]) defp do_search(reference, iterator, acc) do case :rocksdb.iterator_move(iterator, :next) do {:error, :invalid_iterator} -> # we've reached the end :rocksdb.iterator_close(iterator) acc {:ok, _key, value} -> do_search(reference, iterator, [value | acc]) end end end ================================================ FILE: apps/omg_db/lib/omg_db/rocksdb/server.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.RocksDB.Server do @moduledoc """ Handles connection to rocksdb """ # All complex operations on data written/read should go into OMG.DB.RocksDB.Core use GenServer alias OMG.DB.RocksDB.Core require Logger defstruct [:db_ref, :name] @type t() :: %__MODULE__{ db_ref: :rocksdb.db_handle(), name: GenServer.name() } @doc """ Initializes an empty RocksDB instance explicitly, so we can have control over it. NOTE: `init` here is to init the GenServer and that assumes that `init_storage` has already been called """ @spec init_storage(binary) :: :ok | {:error, atom} def init_storage(db_path) do do_init_storage(String.to_charlist(db_path)) end defp do_init_storage(db_path) do with {:ok, db_ref} <- :rocksdb.open(db_path, create_if_missing: true), true <- :rocksdb.is_empty(db_ref) || {:error, :rocksdb_not_empty}, do: :rocksdb.close(db_ref) end def start_link([db_path: _db_path, name: name] = args) do GenServer.start_link(__MODULE__, args, name: name) end # https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide#prefix-databases def init(db_path: db_path, name: name) do # needed so that terminate callback is called on normal close db_path = String.to_charlist(db_path) Process.flag(:trap_exit, true) ^name = create_stats_table(name) setup = [{:create_if_missing, false}, {:prefix_extractor, {:fixed_prefix_transform, 5}}] case :rocksdb.open(db_path, setup) do {:ok, db_ref} -> {:ok, _} = :timer.send_interval(Application.fetch_env!(:omg_db, :metrics_collection_interval), self(), :send_metrics) _ = Logger.info("Started #{inspect(__MODULE__)}") {:ok, %__MODULE__{name: name, db_ref: db_ref}} error -> _ = Logger.error("It seems that database is not initialized. Check README.md") error end end def handle_info(:send_metrics, state) do :ok = :telemetry.execute([:process, __MODULE__], %{}, state) {:noreply, state} end def handle_call({:multi_update, db_updates}, _from, state) do do_multi_update(db_updates, state) end def handle_call({:blocks, blocks_to_fetch}, _from, state) do do_blocks(blocks_to_fetch, state) end def handle_call(:utxos, _from, state) do do_utxos(state) end def handle_call({:utxo, utxo_pos}, _from, state) do do_utxo(utxo_pos, state) end def handle_call({:block_hashes, block_numbers_to_fetch}, _from, state) do do_block_hashes(block_numbers_to_fetch, state) end def handle_call(:competitors_info, _from, state) do do_competitors_info(state) end def handle_call({:get_single_value, parameter}, _from, state) when is_atom(parameter) do do_get_single_value(parameter, state) end def handle_call({:spent_blknum, utxo_pos}, _from, state) do do_spent_blknum(utxo_pos, state) end def handle_call({:get, type, specific_keys}, _from, state) do result = Enum.map( specific_keys, fn key -> get_decoded_data_with_type_and_specific_key(type, key, state) end ) {:reply, {:ok, result}, state} end def handle_call({:get_all_by_type, type}, _from, state) do result = get_all_by_type(type, state) {:reply, result, state} end # WARNING, terminate below will be called only if :trap_exit is set to true def terminate(_reason, %__MODULE__{db_ref: db_ref}) do :ok = :rocksdb.close(db_ref) end defp do_multi_update(db_updates, state) do result = db_updates |> Core.parse_multi_updates() |> write(state) {:reply, result, state} end defp do_blocks(blocks_to_fetch, state) do # we could avoid the number of IO random searches by using an iterator # that would traverse all keys from :block prefix. Whether that's faster we don't have the data yet. result = blocks_to_fetch |> Enum.map(fn block -> Core.key(:block, block) end) |> Enum.map(fn key -> get(key, state) end) |> Core.decode_values() {:reply, result, state} end defp do_utxos(state) do result = get_all_by_type(:utxo, state) {:reply, result, state} end defp do_utxo(utxo_pos, state) do result = Core.key(:utxo, utxo_pos) |> get(state) |> Core.decode_value() {:reply, result, state} end defp do_block_hashes(block_numbers_to_fetch, state) do result = block_numbers_to_fetch |> Enum.map(fn block_number -> Core.key(:block_hash, block_number) end) |> Enum.map(fn key -> get(key, state) end) |> Core.decode_values() {:reply, result, state} end defp do_competitors_info(state) do result = get_all_by_type(:competitor_info, state) {:reply, result, state} end defp do_get_single_value(parameter, state) do result = parameter |> Core.key(nil) |> get(state) |> Core.decode_value() {:reply, result, state} end defp do_spent_blknum(utxo_pos, state) do result = :spend |> Core.key(utxo_pos) |> get(state) |> Core.decode_value() {:reply, result, state} end # iterator options # same as read options # this might be a use case for seek() https://github.com/facebook/rocksdb/wiki/Prefix-Seek-API-Changes defp do_get_all_by_type(type, db_ref) do Core.decode_values(Core.filter_keys(db_ref, type)) end defp create_stats_table(name) do case :ets.whereis(name) do :undefined -> true = name == :ets.new(name, table_settings()) name _ -> name end end defp table_settings() do [:named_table, :set, :public, write_concurrency: true] end # Argument order flipping tools :( # write options # write_options() = [{sync, boolean()} | {disable_wal, boolean()} | {ignore_missing_column_families, boolean()} | # {no_slowdown, boolean()} | {low_pri, boolean()}] # @spec write(Exleveldb.write_actions(), t) :: :ok | {:error, any} defp write(operations, %__MODULE__{db_ref: db_ref} = state) do :ok = :telemetry.execute([:update_write, __MODULE__], %{}, state) :rocksdb.write(db_ref, operations, []) end defp get_decoded_data_with_type_and_specific_key(type, specific_key, state) do {:ok, result} = type |> Core.key(specific_key) |> get(state) |> Core.decode_value() result end # get read options # read_options() = [{verify_checksums, boolean()} | {fill_cache, boolean()} | {iterate_upper_bound, binary()} | # {iterate_lower_bound, binary()} | {tailing, boolean()} | {total_order_seek, boolean()} | # {prefix_same_as_start, boolean()} | {snapshot, snapshot_handle()}] @spec get(atom() | binary(), t) :: {:ok, binary()} | :not_found defp get(key, %__MODULE__{db_ref: db_ref} = state) do :ok = :telemetry.execute([:update_read, __MODULE__], %{}, state) :rocksdb.get(db_ref, key, []) end defp get_all_by_type(type, %__MODULE__{db_ref: db_ref} = state) do :ok = :telemetry.execute([:update_multiread, __MODULE__], %{}, state) do_get_all_by_type(type, db_ref) end end ================================================ FILE: apps/omg_db/mix.exs ================================================ defmodule OMG.DB.MixProject do use Mix.Project def project() do [ app: :omg_db, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end def application() do [ extra_applications: [:logger, :telemetry], start_phases: [{:attach_telemetry, []}], mod: {OMG.DB.Application, []} ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end # Specifies which paths to compile per environment. defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp deps() do [ {:rocksdb, "~> 1.6", system_env: [{"ERLANG_ROCKSDB_OPTS", "-DWITH_SYSTEM_ROCKSDB=ON -DPORTABLE=1 PORTABLE=1"}]}, {:omg_status, in_umbrella: true}, # NOTE: we only need in :dev and :test here, but we need in :prod too in performance # then there's some unexpected behavior of mix that won't allow to mix these, see # [here](https://elixirforum.com/t/mix-dependency-is-not-locked-error-when-building-with-edeliver/7069/3) # OMG-373 (Elixir 1.8) should fix this # TEST ONLY {:briefly, "~> 0.3.0", only: [:dev, :test]}, {:telemetry, "~> 0.4.1"}, {:omg_utils, in_umbrella: true} ] end end ================================================ FILE: apps/omg_db/test/fixtures.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.Fixtures do @moduledoc """ Contains fixtures for tests that require db """ use ExUnitFixtures.FixtureModule deffixture db_initialized do db_path = Briefly.create!(directory: true) Application.put_env(:omg_db, :path, db_path, persistent: true) :ok = OMG.DB.init(db_path) {:ok, started_apps} = Application.ensure_all_started(:omg_db) on_exit(fn -> Application.put_env(:omg_db, :path, nil) started_apps |> Enum.reverse() |> Enum.map(fn app -> :ok = Application.stop(app) end) end) :ok end end ================================================ FILE: apps/omg_db/test/omg_db/application_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.ApplicationTest do @moduledoc """ Only tests if the application can start and stop and the db can init at some location """ use ExUnitFixtures use ExUnit.Case, async: false @moduletag :wrappers @moduletag :common @tag fixtures: [:db_initialized] test "starts and stops app, inits", %{db_initialized: db_result} do assert :ok = db_result end end ================================================ FILE: apps/omg_db/test/omg_db/db_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DBTest do @moduledoc """ A smoke test of the LevelDB support. The intention here is to **only** test minimally, that the pipes work. For more detailed persistence test look for `...PersistenceTest` tests throughout the apps. Note the excluded moduletag, this test requires an explicit `--include wrappers` """ use ExUnitFixtures use OMG.DB.RocksDBCase, async: false alias OMG.DB @moduletag :wrappers @moduletag :common @writes 10 test "handles object storage", %{db_dir: dir, db_pid: pid} do :ok = DB.multi_update( [{:put, :block, %{hash: "xyz"}}, {:put, :block, %{hash: "vxyz"}}, {:put, :block, %{hash: "wvxyz"}}], pid ) assert {:ok, [%{hash: "wvxyz"}, %{hash: "xyz"}]} == DB.blocks(["wvxyz", "xyz"], pid) :ok = DB.multi_update([{:delete, :block, "xyz"}], pid) checks = fn pid -> assert {:ok, [%{hash: "wvxyz"}, :not_found, %{hash: "vxyz"}]} == DB.blocks(["wvxyz", "xyz", "vxyz"], pid) end checks.(pid) # check actual persistence pid = restart(dir, pid) checks.(pid) end test "handles single value storage", %{db_dir: dir, db_pid: pid} do :ok = DB.multi_update([{:put, :last_exit_finalizer_eth_height, 12}], pid) checks = fn pid -> assert {:ok, 12} == DB.get_single_value(:last_exit_finalizer_eth_height, pid) end checks.(pid) # check actual persistence pid = restart(dir, pid) checks.(pid) end test "block hashes return the correct range", %{db_dir: _dir, db_pid: pid} do :ok = DB.multi_update( [ {:put, :block, %{hash: "xyz", number: 1}}, {:put, :block, %{hash: "vxyz", number: 2}}, {:put, :block, %{hash: "wvxyz", number: 3}} ], pid ) {:ok, ["xyz", "vxyz", "wvxyz"]} = OMG.DB.block_hashes([1, 2, 3], pid) end test "utxo can be fetched by utxo position", %{db_pid: pid} do index = 123 item = {{index, index, index}, %{test: :crypto.strong_rand_bytes(index)}} db_writes = [{:put, :utxo, item}] :ok = write(db_writes, pid) assert {:ok, ^item} = DB.utxo({index, index, index}, pid) end test "utxo is not found by utxo position", %{db_pid: pid} do assert :not_found = DB.utxo({1, 0, 0}, pid) end test "if multi reading utxos returns writen results", %{db_dir: _dir, db_pid: pid} do db_writes = create_write(:utxo, pid) {:ok, utxos} = DB.utxos(pid) [] = utxos -- db_writes end test "if multi reading competitor infos returns writen results", %{db_dir: _dir, db_pid: pid} do db_writes = create_write(:competitor_info, pid) {:ok, competitors_info} = DB.competitors_info(pid) [] = competitors_info -- db_writes end defp create_write(:utxo = type, pid) do db_writes = Enum.map(1..@writes, fn index -> {:put, type, {{index, index, index}, %{test: :crypto.strong_rand_bytes(index)}}} end) :ok = write(db_writes, pid) get_raw_values(db_writes) end defp create_write(:competitor_info = type, pid) do db_writes = Enum.map(1..@writes, fn index -> {:put, type, {:crypto.strong_rand_bytes(index), index}} end) :ok = write(db_writes, pid) get_raw_values(db_writes) end defp write(db_writes, pid), do: OMG.DB.multi_update(db_writes, pid) defp get_raw_values(db_writes), do: Enum.map(db_writes, &elem(&1, 2)) defp restart(dir, pid) do :ok = GenServer.stop(pid) name = :"TestDB_#{make_ref() |> inspect()}" {:ok, pid} = start_supervised(OMG.DB.child_spec(db_path: dir, name: name), restart: :temporary) pid end end ================================================ FILE: apps/omg_db/test/omg_db/models/payment_exit_info_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.PaymentExitInfoTest do @moduledoc """ A smoke test of the RocksDB implementation for PaymentExitInfo. Note the excluded moduletag, this test requires an explicit `--include wrappers` """ use ExUnitFixtures use OMG.DB.RocksDBCase, async: false alias OMG.DB.Models.PaymentExitInfo @moduletag :wrappers @moduletag :common @writes 10 describe "exit_info" do test "should return single exit info when given the utxo position", %{db_dir: _dir, db_pid: pid} do {utxo_pos, _} = db_write = :exit_info |> create_write(pid) |> Enum.at(0) {:ok, result} = PaymentExitInfo.exit_info(utxo_pos, pid) assert result == db_write end end describe "exit_infos" do test "should return empty list if given empty list of positions", %{db_dir: _dir, db_pid: pid} do _db_writes = create_write(:exit_info, pid) {:ok, exits} = PaymentExitInfo.exit_infos([], pid) assert exits == [] end test "should return all exit infos with the given utxo positions", %{db_dir: _dir, db_pid: pid} do test_range = 0..Integer.floor_div(@writes, 2) db_writes = create_write(:exit_info, pid) sliced_db_writes = Enum.slice(db_writes, test_range) utxo_pos_list = Enum.map(sliced_db_writes, fn {utxo_pos, _} = _write -> utxo_pos end) {:ok, exits} = PaymentExitInfo.exit_infos(utxo_pos_list, pid) assert exits == sliced_db_writes end end describe "all_exit_infos" do test "should return all exit infos", %{db_dir: _dir, db_pid: pid} do db_writes = create_write(:exit_info, pid) {:ok, exits} = PaymentExitInfo.all_exit_infos(pid) assert exits == db_writes end end describe "all_in_flight_exits_infos" do test "should return all in-flight exits info", %{db_dir: _dir, db_pid: pid} do db_writes = create_write(:in_flight_exit_info, pid) {:ok, in_flight_exits_infos} = PaymentExitInfo.all_in_flight_exits_infos(pid) assert in_flight_exits_infos == db_writes end end defp create_write(:exit_info = type, pid) do db_writes = Enum.map(1..@writes, fn index -> {:put, type, {{index, index, index}, :crypto.strong_rand_bytes(index)}} end) :ok = write(db_writes, pid) get_raw_values(db_writes) end defp create_write(:in_flight_exit_info = type, pid) do db_writes = Enum.map(1..@writes, fn index -> {:put, type, {:crypto.strong_rand_bytes(index), index}} end) :ok = write(db_writes, pid) get_raw_values(db_writes) end defp write(db_writes, pid), do: OMG.DB.multi_update(db_writes, pid) defp get_raw_values(db_writes), do: Enum.map(db_writes, &elem(&1, 2)) end ================================================ FILE: apps/omg_db/test/omg_db/release_tasks/init_key_value_db_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.ReleaseTasks.InitKeyValueDBTest do use ExUnit.Case, async: false alias OMG.DB.ReleaseTasks.InitKeyValueDB alias OMG.DB.ReleaseTasks.SetKeyValueDB @apps [:logger, :crypto, :ssl] setup_all do _ = Enum.each(@apps, &Application.ensure_all_started/1) on_exit(fn -> @apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) :ok end test "init works and DB starts" do {:ok, dir} = Briefly.create(directory: true) :ok = System.put_env("DB_PATH", dir) _ = SetKeyValueDB.load([], release: :child_chain) :ok = InitKeyValueDB.run() started_apps = Enum.map(Application.started_applications(), fn {app, _, _} -> app end) [true, true, true] = Enum.map(@apps, fn app -> not Enum.member?(started_apps, app) end) {:ok, _} = Application.ensure_all_started(:omg_db) :ok = Application.stop(:omg_db) :ok = System.delete_env("DB_PATH") _ = File.rm_rf!(dir) end test "can't init non empty dir" do {:ok, dir} = Briefly.create(directory: true) :ok = System.put_env("DB_PATH", dir) _ = SetKeyValueDB.load([], release: :watcher) :ok = InitKeyValueDB.run() {:error, _} = InitKeyValueDB.run() :ok = System.delete_env("DB_PATH") _ = File.rm_rf!(dir) end test "if init isn't called, DB doesn't start" do _ = Application.stop(:omg_db) {:ok, dir} = Briefly.create(directory: true) :ok = System.put_env("DB_PATH", dir) _ = SetKeyValueDB.load([], release: :child_chain) try do {:ok, _} = Application.ensure_all_started(:omg_db) catch _, {:badmatch, {:error, {:omg_db, {{:shutdown, {:failed_to_start_child, _, {:bad_return_value, {:error, {:db_open, _}}}}}, {OMG.DB.Application, :start, [:normal, []]}}}}} -> :ok end :ok = System.delete_env("DB_PATH") end end ================================================ FILE: apps/omg_db/test/omg_db/release_tasks/init_keys_with_values_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.ReleaseTasks.InitKeysWithValuesTest do use ExUnit.Case, async: false alias OMG.DB.ReleaseTasks.InitKeysWithValues alias OMG.DB.RocksDB alias OMG.DB.RocksDB.Server setup do {:ok, dir} = Briefly.create(directory: true) :ok = Server.init_storage(dir) :ok = Application.put_env(:omg_db, :path, dir, persistent: true) {:ok, started_apps} = Application.ensure_all_started(:omg_db) on_exit(fn -> :ok = Application.put_env(:omg_db, :path, nil) Enum.map(started_apps, fn app -> _ = Application.stop(app) end) end) {:ok, %{}} end test ":last_ife_exit_deleted_eth_height is set if it wasn't set previously" do :ok = RocksDB.multi_update([{:delete, :last_ife_exit_deleted_eth_height, 0}]) assert InitKeysWithValues.run() == :ok assert RocksDB.get_single_value(:last_ife_exit_deleted_eth_height) == {:ok, 0} end test "value under :last_ife_exit_deleted_eth_height is not changed if it was already set" do initial_value = 5 :ok = RocksDB.multi_update([{:put, :last_ife_exit_deleted_eth_height, initial_value}]) assert InitKeysWithValues.run() == :ok assert RocksDB.get_single_value(:last_ife_exit_deleted_eth_height) == {:ok, initial_value} end test "does not fail when omg db is not started" do :ok = Application.stop(:omg_db) :ok = Application.put_env(:omg_db, :path, nil) assert InitKeysWithValues.run() == :ok end end ================================================ FILE: apps/omg_db/test/omg_db/release_tasks/set_key_value_db_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.ReleaseTasks.SetKeyValueDBTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.DB.ReleaseTasks.SetKeyValueDB @app :omg_db setup do _ = Application.ensure_all_started(:logger) on_exit(fn -> :ok = System.delete_env("DB_PATH") end) :ok end test "if environment variables get applied in the configuration" do test_path = "/tmp/YOLO/" release = :watcher_info :ok = System.put_env("DB_PATH", test_path) capture_log(fn -> config = SetKeyValueDB.load([], release: release) path = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:path) assert path == test_path <> "#{release}" end) end test "if default configuration is used when there's no environment variables" do :ok = System.delete_env("DB_PATH") capture_log(fn -> config = SetKeyValueDB.load([], release: :watcher_info) path = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:path) assert path == Path.join([System.get_env("HOME"), ".omg/data"]) <> "/watcher_info" end) end end ================================================ FILE: apps/omg_db/test/omg_db/rocks_db_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.RocksDBTest do @moduledoc """ A smoke test of the RocksDB support. The intention here is to **only** test minimally, that the pipes work. For more detailed persistence test look for `...PersistenceTest` tests throughout the apps. Note the excluded moduletag, this test requires an explicit `--include wrappers` """ use ExUnitFixtures use OMG.DB.RocksDBCase, async: false alias OMG.DB @moduletag :wrappers @moduletag :common @writes 10 test "rocks db handles object storage", %{db_dir: dir, db_pid: pid} do :ok = DB.multi_update( [{:put, :block, %{hash: "xyz"}}, {:put, :block, %{hash: "vxyz"}}, {:put, :block, %{hash: "wvxyz"}}], pid ) assert {:ok, [%{hash: "wvxyz"}, %{hash: "xyz"}]} == DB.blocks(["wvxyz", "xyz"], pid) :ok = DB.multi_update([{:delete, :block, "xyz"}], pid) checks = fn pid -> assert {:ok, [%{hash: "wvxyz"}, :not_found, %{hash: "vxyz"}]} == DB.blocks(["wvxyz", "xyz", "vxyz"], pid) end checks.(pid) # check actual persistence pid = restart(dir, pid) checks.(pid) end test "rocks db handles single value storage", %{db_dir: dir, db_pid: pid} do :ok = DB.multi_update([{:put, :last_exit_finalizer_eth_height, 12}], pid) checks = fn pid -> assert {:ok, 12} == DB.get_single_value(:last_exit_finalizer_eth_height, pid) end checks.(pid) # check actual persistence pid = restart(dir, pid) checks.(pid) end test "block hashes return the correct range", %{db_dir: _dir, db_pid: pid} do :ok = DB.multi_update( [ {:put, :block, %{hash: "xyz", number: 1}}, {:put, :block, %{hash: "vxyz", number: 2}}, {:put, :block, %{hash: "wvxyz", number: 3}} ], pid ) {:ok, ["xyz", "vxyz", "wvxyz"]} = OMG.DB.block_hashes([1, 2, 3], pid) end describe "batch_get" do test "can get single data with the type and single specific key", %{db_dir: _dir, db_pid: pid} do type = :exit_info specific_key = {1, 1, 1} data = {specific_key, :crypto.strong_rand_bytes(123)} :ok = DB.multi_update([{:put, type, data}], pid) assert {:ok, [data]} == DB.batch_get(type, [specific_key], server: pid) end test "can get multiple data with the type and multiple specific keys", %{db_dir: _dir, db_pid: pid} do type = :exit_info specific_keys = [{1, 1, 1}, {2, 2, 2}] data_list = Enum.map(specific_keys, fn key -> {key, :crypto.strong_rand_bytes(123)} end) :ok = data_list |> Enum.map(fn data -> {:put, type, data} end) |> DB.multi_update(pid) assert {:ok, data_list} == DB.batch_get(type, specific_keys, server: pid) end end test "it can get all data with the type", %{db_dir: _dir, db_pid: pid} do db_writes = create_write(:utxo, pid) assert {:ok, db_writes} == DB.get_all_by_type(:utxo, server: pid) end test "if multi reading utxos returns writen results", %{db_dir: _dir, db_pid: pid} do db_writes = create_write(:utxo, pid) {:ok, utxos} = DB.utxos(pid) [] = utxos -- db_writes end test "if multi reading competitor infos returns writen results", %{db_dir: _dir, db_pid: pid} do db_writes = create_write(:competitor_info, pid) {:ok, competitors_info} = DB.competitors_info(pid) [] = competitors_info -- db_writes end defp create_write(:utxo = type, pid) do db_writes = Enum.map(1..@writes, fn index -> {:put, type, {{index, index, index}, %{test: :crypto.strong_rand_bytes(index)}}} end) :ok = write(db_writes, pid) get_raw_values(db_writes) end defp create_write(:competitor_info = type, pid) do db_writes = Enum.map(1..@writes, fn index -> {:put, type, {:crypto.strong_rand_bytes(index), index}} end) :ok = write(db_writes, pid) get_raw_values(db_writes) end defp write(db_writes, pid), do: OMG.DB.multi_update(db_writes, pid) defp get_raw_values(db_writes), do: Enum.map(db_writes, &elem(&1, 2)) defp restart(dir, pid) do :ok = GenServer.stop(pid) name = :"TestDB_#{make_ref() |> inspect()}" {:ok, pid} = start_supervised(OMG.DB.child_spec(db_path: dir, name: name), restart: :temporary) pid end end ================================================ FILE: apps/omg_db/test/support/rocks_db_case.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.DB.RocksDBCase do @moduledoc """ Defines the useful common setup for all `...PersistenceTests`: - creates temp dir with `briefly` - initializes the low-level LevelDB storage and starts the test DB server """ use ExUnit.CaseTemplate alias OMG.DB.RocksDB.Server setup %{test: test_name} do {:ok, dir} = Briefly.create(directory: true) :ok = Server.init_storage(dir) name = :"TestDB_#{test_name}" {:ok, pid} = start_supervised(OMG.DB.child_spec(db_path: dir, name: name), restart: :temporary) {:ok, %{db_dir: dir, db_pid: pid, db_pid_name: name}} end end ================================================ FILE: apps/omg_db/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.configure(exclude: [integration: true, property: true, wrappers: true]) ExUnitFixtures.start() ExUnitFixtures.load_fixture_files() ExUnit.start() {:ok, _} = Application.ensure_all_started(:briefly) ================================================ FILE: apps/omg_eth/lib/eth.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth do @moduledoc """ Library for common code of the adapter/port to contracts deployed on Ethereum. NOTE: The library code is not intended to be used outside of `OMG.Eth`: use `OMG.Eth.RootChain` and `OMG.Eth.Token` as main entrypoints to the contract-interaction functionality. NOTE: This wrapper is intended to be as thin as possible, only offering a consistent API to the Ethereum JSONRPC client and contracts. Handles other non-contract queries to the Ethereum client. Notes on encoding: All APIs of `OMG.Eth` and the submodules with contract APIs always use raw, decoded binaries for binaries - never use hex encoded binaries. Such binaries may be passed as is onto `ABI` related functions, however they must be encoded/decoded when entering/leaving the `Ethereumex` realm """ alias OMG.Eth.Configuration alias OMG.Eth.RootChain.SubmitBlock require Logger import OMG.Eth.Encoding, only: [from_hex: 1, to_hex: 1, int_from_hex: 1] @type address :: <<_::160>> @type hash :: <<_::256>> @type send_transaction_opts() :: [send_transaction_option()] @type send_transaction_option() :: {:passphrase, binary()} def get_block_timestamp_by_number(height) do case Ethereumex.HttpClient.eth_get_block_by_number(to_hex(height), false) do {:ok, %{"timestamp" => timestamp_hex}} -> {:ok, int_from_hex(timestamp_hex)} other -> other end end @spec submit_block(binary(), pos_integer(), pos_integer()) :: {:error, binary() | atom() | map()} | {:ok, <<_::256>>} def submit_block(hash, nonce, gas_price) do contract = from_hex(Configuration.contracts().plasma_framework) from = from_hex(Configuration.authority_address()) backend = Configuration.eth_node() SubmitBlock.submit(backend, hash, nonce, gas_price, from, contract) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Application do @moduledoc false alias OMG.DB alias OMG.Eth.Configuration alias OMG.Eth.Metric.Ethereumex use Application require Logger def start(_type, _args) do _ = Logger.info( "Started #{inspect(__MODULE__)}, config used: contracts #{inspect(Configuration.contracts())} txhash_contract #{ inspect(Configuration.txhash_contract()) } authority_address #{inspect(Configuration.authority_address())}" ) valid_contracts() OMG.Eth.Supervisor.start_link() end def start_phase(:attach_telemetry, :normal, _phase_args) do handler = [ "measure-ethereumex-rpc", Ethereumex.supported_events(), &Ethereumex.handle_event/4, nil ] case apply(:telemetry, :attach, handler) do :ok -> :ok {:error, :already_exists} -> :ok end end defp valid_contracts() do contracts_hash = Configuration.contracts() |> Map.put(:txhash_contract, Configuration.txhash_contract()) # authority_addr to keep backwards compatibility |> Map.put(:authority_addr, Configuration.authority_address()) |> :erlang.phash2() case DB.get_single_value(:omg_eth_contracts) do result when result == :not_found or result == {:ok, 0} -> multi_update = [{:put, :omg_eth_contracts, contracts_hash}] :ok == DB.multi_update(multi_update) {:ok, ^contracts_hash} -> true _ -> _ = Logger.error("Contract addresses have changed since last boot!") exit(:contracts_missmatch) end end end ================================================ FILE: apps/omg_eth/lib/omg_eth/blockchain/bit_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.BitHelper do @moduledoc """ Helpers for common operations on the blockchain. Extracted from: https://github.com/exthereum/blockchain """ use Bitwise alias ExPlasma.Crypto @type keccak_hash :: <<_::256>> @doc """ Returns the keccak sha256 of a given input. ## Examples iex> OMG.Eth.Blockchain.BitHelper.kec("hello world") <<71, 23, 50, 133, 168, 215, 52, 30, 94, 151, 47, 198, 119, 40, 99, 132, 248, 2, 248, 239, 66, 165, 236, 95, 3, 187, 250, 37, 76, 176, 31, 173>> iex> OMG.Eth.Blockchain.BitHelper.kec(<<0x01, 0x02, 0x03>>) <<241, 136, 94, 218, 84, 183, 160, 83, 49, 140, 212, 30, 32, 147, 34, 13, 171, 21, 214, 83, 129, 177, 21, 122, 54, 51, 168, 59, 253, 92, 146, 57>> """ @spec kec(binary()) :: keccak_hash def kec(data) do Crypto.keccak_hash(data) end @doc """ Similar to `:binary.encode_unsigned/1`, except we encode `0` as `<<>>`, the empty string. This is because the specification says that we cannot have any leading zeros, and so having <<0>> by itself is leading with a zero and prohibited. ## Examples iex> OMG.Eth.Blockchain.BitHelper.encode_unsigned(0) <<>> iex> OMG.Eth.Blockchain.BitHelper.encode_unsigned(5) <<5>> iex> OMG.Eth.Blockchain.BitHelper.encode_unsigned(5_000_000) <<76, 75, 64>> """ @spec encode_unsigned(non_neg_integer()) :: binary() def encode_unsigned(0), do: <<>> def encode_unsigned(n), do: :binary.encode_unsigned(n) end ================================================ FILE: apps/omg_eth/lib/omg_eth/blockchain/private_key.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.PrivateKey do @moduledoc """ Extracts private key from environment """ require Integer def get() do private_key = System.get_env("PRIVATE_KEY") maybe_hex(private_key) end @spec maybe_hex(String.t() | nil) :: binary() | nil defp maybe_hex(hex_data, type \\ :raw) defp maybe_hex(nil, _), do: nil defp maybe_hex(hex_data, :raw), do: load_raw_hex(hex_data) @spec load_raw_hex(String.t()) :: binary() defp load_raw_hex("0x" <> hex_data), do: load_raw_hex(hex_data) defp load_raw_hex(hex_data) when Integer.is_odd(byte_size(hex_data)), do: load_raw_hex("0" <> hex_data) defp load_raw_hex(hex_data) do Base.decode16!(hex_data, case: :mixed) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/blockchain/transaction/hash.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.Transaction.Hash do @moduledoc """ Defines helper functions for signing and getting the signature of a transaction, as defined in Appendix F of the Yellow Paper. For any of the following functions, if chain_id is specified, it's assumed that we're post-fork and we should follow the specification EIP-155 from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md Extracted from: https://github.com/exthereum/blockchain """ alias OMG.Eth.Blockchain.BitHelper alias OMG.Eth.Blockchain.Transaction @base_recovery_id 27 @base_recovery_id_eip_155 35 @type private_key :: <<_::256>> @type hash_v :: integer() @type hash_r :: integer() @type hash_s :: integer() @doc """ Returns a hash of a given transaction according to the formula defined in Eq.(214) and Eq.(215) of the Yellow Paper. Note: As per EIP-155 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md), we will append the chain-id and nil elements to the serialized transaction. ## Examples iex> OMG.Eth.Blockchain.Transaction.Hash.transaction_hash(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}) <<127, 113, 209, 76, 19, 196, 2, 206, 19, 198, 240, 99, 184, 62, 8, 95, 9, 122, 135, 142, 51, 22, 61, 97, 70, 206, 206, 39, 121, 54, 83, 27>> iex> OMG.Eth.Blockchain.Transaction.Hash.transaction_hash(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1>>, value: 5, data: <<1>>}) <<225, 195, 128, 181, 3, 211, 32, 231, 34, 10, 166, 198, 153, 71, 210, 118, 51, 117, 22, 242, 87, 212, 229, 37, 71, 226, 150, 160, 50, 203, 127, 180>> iex> OMG.Eth.Blockchain.Transaction.Hash.transaction_hash(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1>>, value: 5, data: <<1>>}, 1) <<132, 79, 28, 4, 212, 58, 235, 38, 66, 211, 167, 102, 36, 58, 229, 88, 238, 251, 153, 23, 121, 163, 212, 64, 83, 111, 200, 206, 54, 43, 112, 53>> """ @spec transaction_hash(Transaction.t(), integer() | nil) :: BitHelper.keccak_hash() def transaction_hash(trx, chain_id \\ nil) do Transaction.serialize(trx, false) # See EIP-155 |> Kernel.++(if chain_id, do: [:binary.encode_unsigned(chain_id), <<>>, <<>>], else: []) |> ExRLP.encode() |> BitHelper.kec() end @doc """ Returns a ECDSA signature (v,r,s) for a given hashed value. This implementes Eq.(207) of the Yellow Paper. ## Examples iex> OMG.Eth.Blockchain.Transaction.Hash.sign_hash(<<2::256>>, <<1::256>>) {28, 38938543279057362855969661240129897219713373336787331739561340553100525404231, 23772455091703794797226342343520955590158385983376086035257995824653222457926} iex> OMG.Eth.Blockchain.Transaction.Hash.sign_hash(<<5::256>>, <<1::256>>) {27, 74927840775756275467012999236208995857356645681540064312847180029125478834483, 56037731387691402801139111075060162264934372456622294904359821823785637523849} iex> data = Base.decode16!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080", case: :lower) iex> hash = OMG.Eth.Blockchain.BitHelper.kec(data) iex> private_key = Base.decode16!("4646464646464646464646464646464646464646464646464646464646464646", case: :lower) iex> OMG.Eth.Blockchain.Transaction.Hash.sign_hash(hash, private_key, 1) { 37, 18515461264373351373200002665853028612451056578545711640558177340181847433846, 46948507304638947509940763649030358759909902576025900602547168820602576006531 } """ @spec sign_hash(BitHelper.keccak_hash(), private_key, integer() | nil) :: {hash_v, hash_r, hash_s} def sign_hash(hash, private_key, chain_id \\ nil) do {:ok, {<>, recovery_id}} = ExSecp256k1.sign_compact(hash, private_key) # Fork Ψ EIP-155 recovery_id = if chain_id do chain_id * 2 + @base_recovery_id_eip_155 + recovery_id else @base_recovery_id + recovery_id end {recovery_id, r, s} end end ================================================ FILE: apps/omg_eth/lib/omg_eth/blockchain/transaction/signature.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.Transaction.Signature do @moduledoc """ Defines helper functions for signing and getting the signature of a transaction, as defined in Appendix F of the Yellow Paper. For any of the following functions, if chain_id is specified, it's assumed that we're post-fork and we should follow the specification EIP-155 from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md Extracted from: https://github.com/exthereum/blockchain """ require Integer alias OMG.Eth.Blockchain.Transaction alias OMG.Eth.Blockchain.Transaction.Hash @type private_key :: <<_::256>> @doc """ Takes a given transaction and returns a version signed with the given private key. This is defined in Eq.(216) and Eq.(217) of the Yellow Paper. ## Examples iex> OMG.Eth.Blockchain.Transaction.Signature.sign_transaction(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, <<1::256>>) %OMG.Eth.Blockchain.Transaction{data: <<>>, gas_limit: 7, gas_price: 6, init: <<1>>, nonce: 5, r: 97037709922803580267279977200525583527127616719646548867384185721164615918250, s: 31446571475787755537574189222065166628755695553801403547291726929250860527755, to: "", v: 27, value: 5} iex> OMG.Eth.Blockchain.Transaction.Signature.sign_transaction(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, <<1::256>>, 1) %OMG.Eth.Blockchain.Transaction{data: <<>>, gas_limit: 7, gas_price: 6, init: <<1>>, nonce: 5, r: 25739987953128435966549144317523422635562973654702886626580606913510283002553, s: 41423569377768420285000144846773344478964141018753766296386430811329935846420, to: "", v: 38, value: 5} """ @spec sign_transaction(Transaction.t(), private_key, integer() | nil) :: Transaction.t() def sign_transaction(trx, private_key, chain_id \\ nil) do {v, r, s} = trx |> Hash.transaction_hash(chain_id) |> Hash.sign_hash(private_key, chain_id) %{trx | v: v, r: r, s: s} end end ================================================ FILE: apps/omg_eth/lib/omg_eth/blockchain/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.Transaction do alias OMG.Eth.Blockchain.BitHelper @moduledoc """ This module encodes the transaction object, defined in Section 4.3 of the Yellow Paper (http://gavwood.com/Paper.pdf). We are focused on implementing 𝛶, as defined in Eq.(1). Extracted from: https://github.com/exthereum/blockchain """ defstruct nonce: 0, # Tn # Tp gas_price: 0, # Tg gas_limit: 0, # Tt to: <<>>, # Tv value: 0, # Tw v: nil, # Tr r: nil, # Ts s: nil, # Ti init: <<>>, # Td data: <<>> @type t :: %__MODULE__{ nonce: integer(), gas_price: integer(), gas_limit: integer(), to: <<_::160>> | <<_::0>>, value: integer(), v: integer(), r: integer(), s: integer(), init: binary(), data: binary() } @doc """ Encodes a transaction such that it can be RLP-encoded. This is defined at L_T Eq.(14) in the Yellow Paper. ## Examples iex> OMG.Eth.Blockchain.Transaction.serialize(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1::160>>, value: 8, v: 27, r: 9, s: 10, data: "hi"}) [<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>] iex> OMG.Eth.Blockchain.Transaction.serialize(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 8, v: 27, r: 9, s: 10, init: <<1, 2, 3>>}) [<<5>>, <<6>>, <<7>>, <<>>, <<8>>, <<1, 2, 3>>, <<27>>, <<9>>, <<10>>] iex> OMG.Eth.Blockchain.Transaction.serialize(%OMG.Eth.Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 8, v: 27, r: 9, s: 10, init: <<1, 2, 3>>}, false) [<<5>>, <<6>>, <<7>>, <<>>, <<8>>, <<1, 2, 3>>] iex> OMG.Eth.Blockchain.Transaction.serialize(%OMG.Eth.Blockchain.Transaction{ data: "", gas_limit: 21000, gas_price: 20000000000, init: "", nonce: 9, r: 0, s: 0, to: "55555555555555555555", v: 1, value: 1000000000000000000 }) ["\t", <<4, 168, 23, 200, 0>>, "R\b", "55555555555555555555", <<13, 224, 182, 179, 167, 100, 0, 0>>, "", <<1>>, "", ""] """ @spec serialize(t) :: ExRLP.t() def serialize(trx, include_vrs \\ true) do base = [ BitHelper.encode_unsigned(trx.nonce), BitHelper.encode_unsigned(trx.gas_price), BitHelper.encode_unsigned(trx.gas_limit), trx.to, BitHelper.encode_unsigned(trx.value), if(trx.to == <<>>, do: trx.init, else: trx.data) ] if include_vrs do base ++ [ BitHelper.encode_unsigned(trx.v), BitHelper.encode_unsigned(trx.r), BitHelper.encode_unsigned(trx.s) ] else base end end end ================================================ FILE: apps/omg_eth/lib/omg_eth/client.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Client do @moduledoc """ Interface to Ethereum Client (not plasma contracts) """ alias OMG.Eth.Encoding @spec get_ethereum_height() :: {:ok, non_neg_integer()} | Ethereumex.Client.Behaviour.error() @spec get_ethereum_height(module()) :: {:ok, non_neg_integer()} | Ethereumex.Client.Behaviour.error() def get_ethereum_height(client \\ Ethereumex.HttpClient) do case client.eth_block_number() do {:ok, height_hex} -> {:ok, Encoding.int_from_hex(height_hex)} other -> other end end @spec node_ready() :: :ok | {:error, :geth_still_syncing} @spec node_ready(module()) :: :ok | {:error, :geth_still_syncing} def node_ready(client \\ Ethereumex.HttpClient) do case client.eth_syncing() do {:ok, false} -> :ok {:ok, _} -> {:error, :geth_still_syncing} end end end ================================================ FILE: apps/omg_eth/lib/omg_eth/configuration.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Configuration do @moduledoc """ Provides access to applications configuration """ @app :omg_eth def contract_semver() do Application.get_env(@app, :contract_semver) end def network() do Application.get_env(@app, :network) end @spec min_exit_period_seconds() :: no_return | pos_integer() def min_exit_period_seconds() do Application.fetch_env!(@app, :min_exit_period_seconds) end @spec ethereum_block_time_seconds() :: no_return | pos_integer() def ethereum_block_time_seconds() do Application.fetch_env!(@app, :ethereum_block_time_seconds) end @spec contracts() :: no_return | map() def contracts() do Application.fetch_env!(@app, :contract_addr) end @spec txhash_contract() :: no_return | binary() def txhash_contract() do Application.fetch_env!(@app, :txhash_contract) end @spec authority_address() :: no_return | binary() def authority_address() do Application.fetch_env!(@app, :authority_address) end @spec environment() :: :test | nil def environment() do Application.get_env(@app, :environment) end @spec child_block_interval() :: pos_integer | no_return def child_block_interval() do Application.fetch_env!(@app, :child_block_interval) end @spec eth_node() :: atom | no_return def eth_node() do Application.fetch_env!(@app, :eth_node) end @spec ethereum_events_check_interval_ms() :: pos_integer | no_return def ethereum_events_check_interval_ms() do Application.fetch_env!(@app, :ethereum_events_check_interval_ms) end @spec ethereum_stalled_sync_threshold_ms() :: pos_integer | no_return def ethereum_stalled_sync_threshold_ms() do Application.fetch_env!(@app, :ethereum_stalled_sync_threshold_ms) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/encoding/contract_constructor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Encoding.ContractConstructor do @moduledoc """ Prepares data for a contract's constructor. """ @doc """ Extracts a list of 2-element tuples with {type, value}, into a list of types and a list of values that can be passed into `ABI.TypeEncoder.encode_raw/1`. ## Examples iex> OMG.Eth.Encoding.ContractConstructor.extract_params([ ...> {:address, "0x1234"}, ...> {{:uint, 256}, 1000}, ...> {:bool, true} ...> ]) { [:address, {:uint, 256}, :bool], ["0x1234", 1000, true] } """ @spec extract_params(types_values :: [tuple()]) :: {types :: [term()], values :: [term()]} def extract_params(types_values) do {types, values} = Enum.reduce(types_values, {[], []}, fn item, {types, values} -> case item do {:tuple, elements} -> {tuple_types, tuple_values} = extract_params(elements) {[{:tuple, tuple_types} | types], [List.to_tuple(tuple_values) | values]} {type, arg} -> {[type | types], [arg | values]} end end) {Enum.reverse(types), Enum.reverse(values)} end end ================================================ FILE: apps/omg_eth/lib/omg_eth/encoding.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Encoding do @moduledoc """ Internal encoding helpers to talk to ethereum. For use in `OMG.Eth` and `OMG.Eth.DevHelper` """ alias OMG.Eth.Encoding.ContractConstructor @doc """ Ethereum JSONRPC and Ethereumex' specific encoding and decoding of binaries and ints We are enforcing the users of Eth and Eth. APIs to always use integers and raw decoded binaries, when interacting. Configuration entries are expected to be written in "0xhex-style" """ @spec to_hex(binary | non_neg_integer) :: binary def to_hex(non_hex) def to_hex(raw) when is_binary(raw), do: "0x" <> Base.encode16(raw, case: :lower) def to_hex(int) when is_integer(int), do: "0x" <> Integer.to_string(int, 16) @doc """ Decodes to a raw binary, see `to_hex` """ # because https://github.com/rrrene/credo/issues/583, we need to: # credo:disable-for-next-line Credo.Check.Consistency.SpaceAroundOperators @spec from_hex(<<_::16, _::_*8>>, atom()) :: binary def from_hex("0x" <> encoded, format \\ :lower), do: Base.decode16!(encoded, case: format) @doc """ Decodes to an integer, see `to_hex` """ # because https://github.com/rrrene/credo/issues/583, we need to: # credo:disable-for-next-line Credo.Check.Consistency.SpaceAroundOperators @spec int_from_hex(<<_::16, _::_*8>>) :: non_neg_integer def int_from_hex("0x" <> encoded) do {return, ""} = Integer.parse(encoded, 16) return end @doc """ Encodes a list of smart contract constructor parameters into a base16 encoded-ABI that solidity expects. ## Examples iex> OMG.Eth.Encoding.encode_constructor_params([ ...> {{:uint, 8}, 255}, ...> ]) "00000000000000000000000000000000000000000000000000000000000000ff" iex> OMG.Eth.Encoding.encode_constructor_params([ ...> {{:uint, 8}, 255}, ...> {:string, "hello"}, ...> ]) "00000000000000000000000000000000000000000000000000000000000000ff0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000568656c6c6f000000000000000000000000000000000000000000000000000000" """ @spec encode_constructor_params(types_values :: [tuple()]) :: abi_base16_encoded :: binary() def encode_constructor_params(types_values) do {types, values} = ContractConstructor.extract_params(types_values) values |> ABI.TypeEncoder.encode_raw(types) # NOTE: we're not using `to_hex` because the `0x` will be appended to the bytecode already |> Base.encode16(case: :lower) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/ethereum_height.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.EthereumHeight do @moduledoc """ A GenServer that subscribes to `ethereum_new_height` events coming from the internal event bus, decodes and saves only the height to be consumed by other services. """ use GenServer require Logger alias OMG.Eth.Client @spec get() :: {:ok, non_neg_integer()} | {:error, :error_ethereum_height} def get() do GenServer.call(__MODULE__, :get) end def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init(opts) do event_bus = Keyword.fetch!(opts, :event_bus) :ok = event_bus.subscribe({:root_chain, "ethereum_new_height"}, link: true) {:ok, get_ethereum_height()} end def handle_call(:get, _from, ethereum_height) when is_atom(ethereum_height) do {:reply, {:error, ethereum_height}, ethereum_height} end def handle_call(:get, _from, ethereum_height) do {:reply, {:ok, ethereum_height}, ethereum_height} end def handle_info({:internal_event_bus, :ethereum_new_height, new_height}, _state) do _ = Logger.debug("Got an internal :ethereum_new_height event with height: #{new_height}.") {:noreply, new_height} end @spec get_ethereum_height() :: non_neg_integer() | :error_ethereum_height defp get_ethereum_height() do {:ok, rootchain_height} = eth().get_ethereum_height() rootchain_height rescue _check_error -> :error_ethereum_height end defp eth(), do: Application.get_env(:omg_eth, :eth_integration_module, Client) end ================================================ FILE: apps/omg_eth/lib/omg_eth/ethereum_height_monitor/alarm_handler.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.EthereumHeightMonitor.AlarmHandler do @moduledoc """ Listens for :ethereum_connection_error and :ethereum_stalled_sync alarms and reflect the alarm's state back to the monitor. """ require Logger # The alarm reporter and monitor happen to be the same module here because we are just # reflecting the alarm's state back to the reporter. @reporter OMG.Eth.EthereumHeightMonitor @monitor OMG.Eth.EthereumHeightMonitor def init(_args) do {:ok, %{}} end def handle_call(_request, state), do: {:ok, :ok, state} def handle_event({:set_alarm, {:ethereum_connection_error, %{reporter: @reporter}}}, state) do _ = Logger.warn(":ethereum_connection_error alarm raised.") :ok = GenServer.cast(@monitor, {:set_alarm, :ethereum_connection_error}) {:ok, state} end def handle_event({:clear_alarm, {:ethereum_connection_error, %{reporter: @reporter}}}, state) do _ = Logger.warn(":ethereum_connection_error alarm cleared.") :ok = GenServer.cast(@monitor, {:clear_alarm, :ethereum_connection_error}) {:ok, state} end def handle_event({:set_alarm, {:ethereum_stalled_sync, %{reporter: @reporter}}}, state) do _ = Logger.warn(":ethereum_stalled_sync alarm raised.") :ok = GenServer.cast(@monitor, {:set_alarm, :ethereum_stalled_sync}) {:ok, state} end def handle_event({:clear_alarm, {:ethereum_stalled_sync, %{reporter: @reporter}}}, state) do _ = Logger.warn(":ethereum_stalled_sync alarm cleared.") :ok = GenServer.cast(@monitor, {:clear_alarm, :ethereum_stalled_sync}) {:ok, state} end def handle_event(event, state) do _ = Logger.info("#{__MODULE__} got event: #{inspect(event)}. Ignoring.") {:ok, state} end end ================================================ FILE: apps/omg_eth/lib/omg_eth/ethereum_height_monitor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.EthereumHeightMonitor do @moduledoc """ Periodically calls the Ethereum client node to check for Ethereumm's block height. Publishes internal events or raises alarms accordingly. When a new block height is received, it publishes an internal event under the topic `"ethereum_new_height"` with the payload `{:ethereum_new_height, height}`. The event is only published when the received block height is higher than the previously published height. When the call to the Ethereum client fails or returns an invalid responnse, it raises an `:ethereum_connection_error` alarm. The alarm is cleared once a valid block height is seen. When the call to the Ethereum client returns the same block height for longer than `:ethereum_stalled_sync_threshold_ms`, it raises an `:ethereum_stalled_sync` alarm. The alarm is cleared once the block height starts increasing again. """ use GenServer require Logger @type t() :: %__MODULE__{ check_interval_ms: pos_integer(), stall_threshold_ms: pos_integer(), tref: reference() | nil, eth_module: module(), alarm_module: module(), event_bus_module: module(), ethereum_height: integer(), synced_at: DateTime.t(), connection_alarm_raised: boolean(), stall_alarm_raised: boolean() } defstruct check_interval_ms: 10_000, stall_threshold_ms: 20_000, tref: nil, eth_module: nil, alarm_module: nil, event_bus_module: nil, ethereum_height: 0, synced_at: nil, connection_alarm_raised: false, stall_alarm_raised: false # # GenServer APIs # def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end # # GenServer behaviors # def init(opts) do _ = Logger.info("Starting Ethereum height monitor.") _ = install_alarm_handler() state = %__MODULE__{ check_interval_ms: Keyword.fetch!(opts, :check_interval_ms), stall_threshold_ms: Keyword.fetch!(opts, :stall_threshold_ms), synced_at: DateTime.utc_now(), eth_module: Keyword.fetch!(opts, :eth_module), alarm_module: Keyword.fetch!(opts, :alarm_module), event_bus_module: Keyword.fetch!(opts, :event_bus_module) } {:ok, state, {:continue, :first_check}} end # We want the first check immediately upon start, but we cannot do it while the monitor # is not fully initialized, so we need to trigger it in a :continue instruction. def handle_continue(:first_check, state) do _ = send(self(), :check_new_height) {:noreply, state} end def handle_info({:ssl_closed, _}, state) do # eat this bug https://github.com/benoitc/hackney/issues/464 {:noreply, state} end def handle_info(:check_new_height, state) do height = fetch_height(state.eth_module) stalled? = stalled?(height, state.ethereum_height, state.synced_at, state.stall_threshold_ms) :ok = broadcast_on_new_height(state.event_bus_module, height) _ = connection_alarm(state.alarm_module, state.connection_alarm_raised, height) _ = stall_alarm(state.alarm_module, state.stall_alarm_raised, stalled?) state = update_height(state, height) {:ok, tref} = :timer.send_after(state.check_interval_ms, :check_new_height) {:noreply, %{state | tref: tref}} end # # Handle incoming alarms # # These functions are called by the AlarmHandler so that this monitor process can update # its internal state according to the raised alarms. # def handle_cast({:set_alarm, :ethereum_connection_error}, state) do {:noreply, %{state | connection_alarm_raised: true}} end def handle_cast({:clear_alarm, :ethereum_connection_error}, state) do {:noreply, %{state | connection_alarm_raised: false}} end def handle_cast({:set_alarm, :ethereum_stalled_sync}, state) do {:noreply, %{state | stall_alarm_raised: true}} end def handle_cast({:clear_alarm, :ethereum_stalled_sync}, state) do {:noreply, %{state | stall_alarm_raised: false}} end # # Private functions # @spec update_height(t(), non_neg_integer() | :error) :: t() defp update_height(state, :error), do: state defp update_height(state, height) do case height > state.ethereum_height do true -> %{state | ethereum_height: height, synced_at: DateTime.utc_now()} false -> state end end @spec stalled?(non_neg_integer() | :error, non_neg_integer(), DateTime.t(), non_neg_integer()) :: boolean() defp stalled?(height, previous_height, synced_at, stall_threshold_ms) do case height do height when is_integer(height) and height > previous_height -> false _ -> DateTime.diff(DateTime.utc_now(), synced_at, :millisecond) > stall_threshold_ms end end @spec fetch_height(module()) :: non_neg_integer() | :error defp fetch_height(eth_module) do case eth_module.get_ethereum_height() do {:ok, height} -> height error -> _ = Logger.warn("Error retrieving Ethereum height: #{inspect(error)}") :error end end @spec broadcast_on_new_height(module(), non_neg_integer() | :error) :: :ok | {:error, term()} defp broadcast_on_new_height(_event_bus_module, :error), do: :ok # we need to publish every height we fetched so that we can re-examine blocks in case of re-orgs # clients subscribed to this topic need to be aware of that and if a block number repeats, # it needs to re-write logs, for example defp broadcast_on_new_height(event_bus_module, height) do event = OMG.Bus.Event.new({:root_chain, "ethereum_new_height"}, :ethereum_new_height, height) apply(event_bus_module, :broadcast, [event]) end # # Alarms management # defp install_alarm_handler() do case Enum.member?(:gen_event.which_handlers(:alarm_handler), __MODULE__.AlarmHandler) do true -> :ok _ -> :alarm_handler.add_alarm_handler(__MODULE__.AlarmHandler) end end # Raise or clear the :ethereum_client_connnection alarm @spec connection_alarm(module(), boolean(), non_neg_integer() | :error) :: :ok | :duplicate defp connection_alarm(alarm_module, connection_alarm_raised, raise_alarm) defp connection_alarm(alarm_module, false, :error) do alarm_module.set(alarm_module.ethereum_connection_error(__MODULE__)) end defp connection_alarm(alarm_module, true, height) when is_integer(height) do alarm_module.clear(alarm_module.ethereum_connection_error(__MODULE__)) end defp connection_alarm(_alarm_module, _, _), do: :ok # Raise or clear the :ethereum_stalled_sync alarm @spec stall_alarm(module(), boolean(), boolean()) :: :ok | :duplicate defp stall_alarm(alarm_module, stall_alarm_raised, raise_alarm) defp stall_alarm(alarm_module, false, true) do alarm_module.set(alarm_module.ethereum_stalled_sync(__MODULE__)) end defp stall_alarm(alarm_module, true, false) do alarm_module.clear(alarm_module.ethereum_stalled_sync(__MODULE__)) end defp stall_alarm(_alarm_module, _, _), do: :ok end ================================================ FILE: apps/omg_eth/lib/omg_eth/metric/ethereumex.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Metric.Ethereumex do @moduledoc """ Telemetry handler for Ethereumex events """ alias OMG.Status.Metric.Datadog def supported_events(), do: [:ethereumex] def handle_event([:ethereumex], %{counter: counter}, %{method_name: method_name} = _metadata, _config) do Datadog.increment(method_name, counter) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/release_tasks/set_contract.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetContract do @moduledoc false @behaviour Config.Provider require Logger alias OMG.Eth.Encoding alias OMG.Eth.RootChain.Abi alias OMG.Eth.RootChain.Rpc @networks ["RINKEBY", "ROPSTEN", "GOERLI", "KOVAN", "MAINNET", "LOCALCHAIN"] @error "Set ETHEREUM_NETWORK to #{Enum.join(@networks, ",")} with TXHASH_CONTRACT, AUTHORITY_ADDRESS and CONTRACT_ADDRESS environment variables or CONTRACT_EXCHANGER_URL." @ether_vault_id 1 @erc20_vault_id 2 @doc """ The contract values can currently come either from ENV variables for deployments in - development - stagind - production or, they're manually deployed for local development: """ def init(args) do args end def load(config, args) do _ = on_load() rpc_api = Keyword.get(args, :rpc_api, Rpc) exchanger = get_env("CONTRACT_EXCHANGER_URL") via_env = get_env("ETHEREUM_NETWORK") network = get_network(via_env) {txhash_contract, authority_address, plasma_framework} = case exchanger do exchanger when is_binary(exchanger) -> body = try do {:ok, %{body: body}} = HTTPoison.get(exchanger) body rescue reason -> exit("CONTRACT_EXCHANGER_URL #{exchanger} is not reachable because of #{inspect(reason)}") end %{ authority_address: authority_address, plasma_framework: plasma_framework, plasma_framework_tx_hash: txhash_contract } = Jason.decode!(body, keys: :atoms!) {txhash_contract, authority_address, plasma_framework} _ -> txhash_contract = get_env("TXHASH_CONTRACT") authority_address = get_env("AUTHORITY_ADDRESS") plasma_framework = get_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK") {txhash_contract, authority_address, plasma_framework} end # get all the data from external sources {payment_exit_game, eth_vault, erc20_vault, min_exit_period_seconds, contract_semver, child_block_interval} = get_external_data(plasma_framework, rpc_api) contract_addresses = %{ plasma_framework: plasma_framework, eth_vault: eth_vault, erc20_vault: erc20_vault, payment_exit_game: payment_exit_game } merge_configuration( config, txhash_contract, authority_address, contract_addresses, min_exit_period_seconds, contract_semver, network, child_block_interval ) end defp get_external_data(plasma_framework, rpc_api) do min_exit_period_seconds = get_min_exit_period(plasma_framework, rpc_api) payment_exit_game = plasma_framework |> exit_game_contract_address(ExPlasma.payment_v1(), rpc_api) |> Encoding.to_hex() eth_vault = plasma_framework |> get_vault(@ether_vault_id, rpc_api) |> Encoding.to_hex() erc20_vault = plasma_framework |> get_vault(@erc20_vault_id, rpc_api) |> Encoding.to_hex() contract_semver = get_contract_semver(plasma_framework, rpc_api) child_block_interval = get_child_block_interval(plasma_framework, rpc_api) {payment_exit_game, eth_vault, erc20_vault, min_exit_period_seconds, contract_semver, child_block_interval} end defp merge_configuration( config, txhash_contract, authority_address, contract_addresses, min_exit_period_seconds, contract_semver, network, child_block_interval ) when is_binary(txhash_contract) and is_binary(authority_address) and is_map(contract_addresses) and is_integer(min_exit_period_seconds) and is_binary(contract_semver) and is_binary(network) do contract_addresses = Enum.into(contract_addresses, %{}, fn {name, addr} -> {name, String.downcase(addr)} end) Config.Reader.merge(config, omg_eth: [ txhash_contract: String.downcase(txhash_contract), authority_address: String.downcase(authority_address), contract_addr: contract_addresses, min_exit_period_seconds: min_exit_period_seconds, contract_semver: contract_semver, network: network, child_block_interval: child_block_interval ] ) end defp merge_configuration(_, _, _, _, _, _, _, _), do: exit(@error) defp get_min_exit_period(plasma_framework_contract, rpc_api) do signature = "minExitPeriod()" {:ok, data} = call(plasma_framework_contract, signature, [], rpc_api) %{"min_exit_period" => min_exit_period} = Abi.decode_function(data, signature) min_exit_period end defp get_contract_semver(plasma_framework_contract, rpc_api) do signature = "getVersion()" {:ok, data} = call(plasma_framework_contract, signature, [], rpc_api) %{"version" => version} = Abi.decode_function(data, signature) version end defp get_child_block_interval(plasma_framework_contract, rpc_api) do signature = "childBlockInterval()" {:ok, data} = call(plasma_framework_contract, signature, [], rpc_api) %{"child_block_interval" => child_block_interval} = Abi.decode_function(data, signature) child_block_interval end defp exit_game_contract_address(plasma_framework_contract, tx_type, rpc_api) do signature = "exitGames(uint256)" {:ok, data} = call(plasma_framework_contract, signature, [tx_type], rpc_api) %{"exit_game_address" => exit_game_address} = Abi.decode_function(data, signature) exit_game_address end defp get_vault(plasma_framework_contract, id, rpc_api) do signature = "vaults(uint256)" {:ok, data} = call(plasma_framework_contract, signature, [id], rpc_api) %{"vault_address" => vault_address} = Abi.decode_function(data, signature) vault_address end defp call(plasma_framework_contract, signature, args, rpc_api) do retries_left = 3 call(plasma_framework_contract, signature, args, retries_left, rpc_api) end defp call(plasma_framework_contract, signature, args, 0, rpc_api) do rpc_api.call_contract(plasma_framework_contract, signature, args) end defp call(plasma_framework_contract, signature, args, retries_left, rpc_api) do case rpc_api.call_contract(plasma_framework_contract, signature, args) do {:ok, _data} = result -> result {:error, :closed} -> Process.sleep(1000) call(plasma_framework_contract, signature, args, retries_left - 1, rpc_api) end end defp get_env(key), do: System.get_env(key) defp get_network(nil), do: exit(@error) defp get_network(data) do case Enum.member?(@networks, String.upcase(data)) do true -> String.upcase(data) _ -> exit(@error) end end defp on_load() do {:ok, _} = Application.ensure_all_started(:logger) {:ok, _} = Application.ensure_all_started(:ethereumex) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/release_tasks/set_ethereum_block_time.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumBlockTime do @moduledoc """ Configures the average ethereum block time for the network used. """ @behaviour Config.Provider require Logger @app :omg_eth @env_key "ETHEREUM_BLOCK_TIME_SECONDS" def init(args) do args end def load(config, _args) do _ = on_load() ethereum_block_time = get_ethereum_block_time() Config.Reader.merge(config, omg_eth: [ethereum_block_time_seconds: ethereum_block_time]) end defp get_ethereum_block_time() do ethereum_block_time_seconds = Application.get_env(@app, :ethereum_block_time_seconds) ethereum_block_time_seconds = validate_integer(get_env(@env_key), ethereum_block_time_seconds) _ = Logger.info( "CONFIGURATION: App: #{@app} Key: ethereum_block_time_seconds Value: #{inspect(ethereum_block_time_seconds)}." ) ethereum_block_time_seconds end defp get_env(key), do: System.get_env(key) defp validate_integer(value, _default) when is_binary(value), do: String.to_integer(value) defp validate_integer(_, default), do: default defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(@app) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/release_tasks/set_ethereum_client.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumClient do @moduledoc false @behaviour Config.Provider require Logger @app :omg_eth @doc """ Gets the environment setting for the ethereum client location. """ def init(args) do args end def load(config, _args) do _ = on_load() rpc_url = get_ethereum_rpc_url() rpc_client_type = get_rpc_client_type() # we need to get this imidiatelly in effect because we use ethereumex in SetContract Application.put_env(:ethereumex, :url, rpc_url, persistent: true) Config.Reader.merge(config, ethereumex: [url: rpc_url], omg_eth: [eth_node: rpc_client_type] ) end defp get_ethereum_rpc_url() do url = validate_string(get_env("ETHEREUM_RPC_URL"), Application.get_env(:ethereumex, :url)) _ = Logger.info("CONFIGURATION: App: #{@app} Key: ETHEREUM_RPC_URL Value: #{inspect(url)}.") url end defp get_rpc_client_type() do rpc_client_type = validate_rpc_client_type(get_env("ETH_NODE"), Application.get_env(@app, :eth_node)) _ = Logger.info("CONFIGURATION: App: #{@app} Key: ETH_NODE Value: #{inspect(rpc_client_type)}.") rpc_client_type end defp validate_rpc_client_type(value, _default) when is_binary(value), do: to_rpc_client_type(String.upcase(value)) defp validate_rpc_client_type(_value, default), do: default defp to_rpc_client_type("GETH"), do: :geth defp to_rpc_client_type("PARITY"), do: :parity defp to_rpc_client_type("INFURA"), do: :infura defp to_rpc_client_type(_), do: exit("You need to choose between geth, parity or infura.") defp validate_string(value, _default) when is_binary(value), do: value defp validate_string(_, default), do: default defp get_env(key), do: System.get_env(key) defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.ensure_all_started(:omg_status) _ = Application.load(@app) _ = Application.load(:ethereumex) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/release_tasks/set_ethereum_events_check_interval.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumEventsCheckInterval do @moduledoc """ Configures the interval to check for new events from Ethereum, including checking for new heights. This is essentially the same as `OMG.Watcher.ReleaseTasks.SetEthereumEventsCheckInterval` but for a different subapp. """ @behaviour Config.Provider require Logger @app :omg_eth @env_key "ETHEREUM_EVENTS_CHECK_INTERVAL_MS" def init(args) do args end def load(config, _args) do _ = on_load() interval_ms = get_interval_ms() Config.Reader.merge(config, omg_eth: [ethereum_events_check_interval_ms: interval_ms], omg_watcher: [ethereum_events_check_interval_ms: interval_ms] ) end defp get_interval_ms() do ethereum_events_check_interval_ms = Application.get_env(@app, :ethereum_events_check_interval_ms) interval_ms = validate_integer(get_env(@env_key), ethereum_events_check_interval_ms) _ = Logger.info("CONFIGURATION: App: #{@app} Key: ethereum_events_check_interval_ms Value: #{inspect(interval_ms)}.") interval_ms end defp get_env(key), do: System.get_env(key) defp validate_integer(value, _default) when is_binary(value), do: String.to_integer(value) defp validate_integer(_, default), do: default defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(@app) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/release_tasks/set_ethereum_stalled_sync_threshold.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumStalledSyncThreshold do @moduledoc false @behaviour Config.Provider require Logger @app :omg_eth @env_name "ETHEREUM_STALLED_SYNC_THRESHOLD_MS" def init(args) do args end def load(config, _args) do _ = on_load() threshold_ms = stalled_sync_threshold_ms() Config.Reader.merge(config, omg_eth: [ethereum_stalled_sync_threshold_ms: threshold_ms]) end defp stalled_sync_threshold_ms() do ethereum_stalled_sync_threshold_ms = Application.get_env(@app, :ethereum_stalled_sync_threshold_ms) threshold_ms = validate_integer(get_env(@env_name), ethereum_stalled_sync_threshold_ms) _ = Logger.info( "CONFIGURATION: App: #{@app} Key: ethereum_stalled_sync_threshold_ms Value: #{inspect(threshold_ms)}." ) threshold_ms end defp get_env(key), do: System.get_env(key) defp validate_integer(value, _default) when is_binary(value), do: String.to_integer(value) defp validate_integer(_, default), do: default defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(@app) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain/abi.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.Abi do @moduledoc """ Functions that provide ethereum log decoding """ alias ExPlasma.Crypto alias OMG.Eth.Encoding alias OMG.Eth.RootChain.AbiEventSelector alias OMG.Eth.RootChain.AbiFunctionSelector alias OMG.Eth.RootChain.Fields def decode_function(enriched_data, signature) do "0x" <> data = enriched_data <> = Crypto.keccak_hash(signature) method_id |> Encoding.to_hex() |> Kernel.<>(data) |> Encoding.from_hex() |> decode_function() end def decode_function(enriched_data) do function_specs = Enum.reduce(AbiFunctionSelector.module_info(:exports), [], fn {:module_info, 0}, acc -> acc {function, 0}, acc -> [apply(AbiFunctionSelector, function, []) | acc] _, acc -> acc end) {function_spec, data} = ABI.find_and_decode(function_specs, enriched_data) decode_function_call_result(function_spec, data) end def decode_log(log) do event_specs = Enum.reduce(AbiEventSelector.module_info(:exports), [], fn {:module_info, 0}, acc -> acc {function, 0}, acc -> [apply(AbiEventSelector, function, []) | acc] _, acc -> acc end) topics = Enum.map(log["topics"], fn nil -> nil topic -> Encoding.from_hex(topic) end) data = Encoding.from_hex(log["data"]) {event_spec, data} = ABI.Event.find_and_decode( event_specs, Enum.at(topics, 0), Enum.at(topics, 1), Enum.at(topics, 2), Enum.at(topics, 3), data ) data |> Enum.into(%{}, fn {key, _type, _indexed, value} -> {key, value} end) |> Fields.rename(event_spec) |> common_parse_event(log) end def common_parse_event( result, %{"blockNumber" => eth_height, "transactionHash" => root_chain_txhash, "logIndex" => log_index} = event ) do # NOTE: we're using `put_new` here, because `merge` would allow us to overwrite data fields in case of conflict result |> Map.put_new(:eth_height, Encoding.int_from_hex(eth_height)) |> Map.put_new(:root_chain_txhash, Encoding.from_hex(root_chain_txhash)) |> Map.put_new(:log_index, Encoding.int_from_hex(log_index)) # just copy `event_signature` over, if it's present (could use tidying up) |> Map.put_new(:event_signature, event[:event_signature]) end defp decode_function_call_result(function_spec, [values]) when is_tuple(values) do function_spec.input_names |> Enum.zip(Tuple.to_list(values)) |> Enum.into(%{}) |> Fields.rename(function_spec) end # workaround for https://github.com/omgnetwork/elixir-omg/issues/1632 defp decode_function_call_result(%{function: "startExit"} = function_spec, values) do function_spec.input_names |> Enum.zip(values) |> Enum.into(%{}) |> Fields.rename(function_spec) end defp decode_function_call_result(function_spec, values) do function_spec.input_names |> Enum.zip(values) |> Enum.into(%{}) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain/abi_event_selector.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.AbiEventSelector do @moduledoc """ We define Solidity Event selectors that help us decode returned values from function calls. Function names are to be used as inputs to Event Fetcher. Function names describe the type of the event Event Fetcher will retrieve. """ @spec exit_started() :: ABI.FunctionSelector.t() def exit_started() do %ABI.FunctionSelector{ function: "ExitStarted", input_names: ["owner", "exitId"], inputs_indexed: [true, false], method_id: <<221, 111, 117, 92>>, returns: [], type: :event, types: [:address, {:uint, 160}] } end @spec in_flight_exit_started() :: ABI.FunctionSelector.t() def in_flight_exit_started() do %ABI.FunctionSelector{ function: "InFlightExitStarted", input_names: ["initiator", "txHash"], inputs_indexed: [true, true], method_id: <<213, 241, 254, 157>>, returns: [], type: :event, types: [:address, {:bytes, 32}] } end @spec in_flight_exit_deleted() :: ABI.FunctionSelector.t() def in_flight_exit_deleted() do %ABI.FunctionSelector{ function: "InFlightExitDeleted", input_names: ["exitId"], inputs_indexed: [true], method_id: <<25, 145, 196, 195>>, returns: [], type: :event, types: [uint: 160] } end @spec in_flight_exit_challenged() :: ABI.FunctionSelector.t() def in_flight_exit_challenged() do %ABI.FunctionSelector{ function: "InFlightExitChallenged", input_names: ["challenger", "txHash", "challengeTxPosition"], inputs_indexed: [true, true, false], method_id: <<104, 116, 1, 150>>, returns: [], type: :event, types: [:address, {:bytes, 32}, {:uint, 256}] } end @spec deposit_created() :: ABI.FunctionSelector.t() def deposit_created() do %ABI.FunctionSelector{ function: "DepositCreated", input_names: ["depositor", "blknum", "token", "amount"], inputs_indexed: [true, true, true, false], method_id: <<24, 86, 145, 34>>, returns: [], type: :event, types: [:address, {:uint, 256}, :address, {:uint, 256}] } end @spec in_flight_exit_input_piggybacked() :: ABI.FunctionSelector.t() def in_flight_exit_input_piggybacked() do %ABI.FunctionSelector{ function: "InFlightExitInputPiggybacked", input_names: ["exitTarget", "txHash", "inputIndex"], inputs_indexed: [true, true, false], method_id: <<169, 60, 14, 155>>, returns: [], type: :event, types: [:address, {:bytes, 32}, {:uint, 16}] } end @spec in_flight_exit_output_piggybacked() :: ABI.FunctionSelector.t() def in_flight_exit_output_piggybacked() do %ABI.FunctionSelector{ function: "InFlightExitOutputPiggybacked", input_names: ["exitTarget", "txHash", "outputIndex"], inputs_indexed: [true, true, false], method_id: <<110, 205, 142, 121>>, returns: [], type: :event, types: [:address, {:bytes, 32}, {:uint, 16}] } end @spec block_submitted() :: ABI.FunctionSelector.t() def block_submitted() do %ABI.FunctionSelector{ function: "BlockSubmitted", input_names: ["blockNumber"], inputs_indexed: [false], method_id: <<90, 151, 143, 71>>, returns: [], type: :event, types: [uint: 256] } end @spec exit_finalized() :: ABI.FunctionSelector.t() def exit_finalized() do %ABI.FunctionSelector{ function: "ExitFinalized", input_names: ["exitId"], inputs_indexed: [true], method_id: <<10, 219, 41, 176>>, returns: [], type: :event, types: [uint: 160] } end @spec in_flight_exit_challenge_responded() :: ABI.FunctionSelector.t() def in_flight_exit_challenge_responded() do # <<99, 124, 196, 167>> == "c|ħ" %ABI.FunctionSelector{ function: "InFlightExitChallengeResponded", input_names: ["challenger", "txHash", "challengeTxPosition"], inputs_indexed: [true, true, false], # method_id: "c|ħ", method_id: <<99, 124, 196, 167>>, returns: [], type: :event, types: [:address, {:bytes, 32}, {:uint, 256}] } end @spec exit_challenged() :: ABI.FunctionSelector.t() def exit_challenged() do %ABI.FunctionSelector{ function: "ExitChallenged", input_names: ["utxoPos"], inputs_indexed: [true], method_id: <<93, 251, 165, 38>>, returns: [], type: :event, types: [uint: 256] } end @spec in_flight_exit_input_blocked() :: ABI.FunctionSelector.t() def in_flight_exit_input_blocked() do %ABI.FunctionSelector{ function: "InFlightExitInputBlocked", input_names: ["challenger", "txHash", "inputIndex"], inputs_indexed: [true, true, false], method_id: <<71, 148, 4, 88>>, returns: [], type: :event, types: [:address, {:bytes, 32}, {:uint, 16}] } end @spec in_flight_exit_output_blocked() :: ABI.FunctionSelector.t() def in_flight_exit_output_blocked() do %ABI.FunctionSelector{ function: "InFlightExitOutputBlocked", input_names: ["challenger", "txHash", "outputIndex"], inputs_indexed: [true, true, false], method_id: <<203, 232, 218, 210>>, returns: [], type: :event, types: [:address, {:bytes, 32}, {:uint, 16}] } end @spec in_flight_exit_input_withdrawn() :: ABI.FunctionSelector.t() def in_flight_exit_input_withdrawn() do %ABI.FunctionSelector{ function: "InFlightExitInputWithdrawn", input_names: ["exitId", "inputIndex"], inputs_indexed: [true, false], method_id: <<68, 70, 236, 17>>, returns: [], type: :event, types: [uint: 160, uint: 16] } end @spec in_flight_exit_output_withdrawn() :: ABI.FunctionSelector.t() def in_flight_exit_output_withdrawn() do %ABI.FunctionSelector{ function: "InFlightExitOutputWithdrawn", input_names: ["exitId", "outputIndex"], inputs_indexed: [true, false], method_id: <<162, 65, 198, 222>>, returns: [], type: :event, types: [uint: 160, uint: 16] } end end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain/abi_function_selector.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.AbiFunctionSelector do @moduledoc """ We define Solidity Function selectors that help us decode returned values from function calls """ # workaround for https://github.com/omgnetwork/elixir-omg/issues/1632 def start_exit() do %ABI.FunctionSelector{ function: "startExit", input_names: [ "utxoPosToExit", "rlpOutputTxToContract", "outputTxToContractInclusionProof", "rlpInputCreationTx", "inputCreationTxInclusionProof", "utxoPosInput" ], inputs_indexed: nil, method_id: <<191, 31, 49, 109>>, returns: [], type: :function, types: [{:uint, 256}, :bytes, :bytes, :bytes, :bytes, {:uint, 256}] } end def start_standard_exit() do %ABI.FunctionSelector{ function: "startStandardExit", input_names: ["utxoPos", "rlpOutputTx", "outputTxInclusionProof"], inputs_indexed: nil, method_id: <<112, 224, 20, 98>>, returns: [], type: :function, types: [tuple: [{:uint, 256}, :bytes, :bytes]] } end def challenge_in_flight_exit_not_canonical() do %ABI.FunctionSelector{ function: "challengeInFlightExitNotCanonical", input_names: [ "inputTx", "inputUtxoPos", "inFlightTx", "inFlightTxInputIndex", "competingTx", "competingTxInputIndex", "competingTxPos", "competingTxInclusionProof", "competingTxWitness" ], inputs_indexed: [true, true, true, true, true, true, true, true, true], method_id: <<232, 54, 34, 152>>, returns: [], type: :function, types: [ tuple: [ :bytes, {:uint, 256}, :bytes, {:uint, 16}, :bytes, {:uint, 16}, {:uint, 256}, :bytes, :bytes ] ] } end def start_in_flight_exit() do %ABI.FunctionSelector{ function: "startInFlightExit", input_names: ["inFlightTx", "inputTxs", "inputUtxosPos", "inputTxsInclusionProofs", "inFlightTxWitnesses"], inputs_indexed: nil, method_id: <<90, 82, 133, 20>>, returns: [], type: :function, types: [ tuple: [ :bytes, {:array, :bytes}, {:array, {:uint, 256}}, {:array, :bytes}, {:array, :bytes} ] ] } end # min_exit_period/0, get_version/0, exit_games/0, vaults/0 are # victims of unfortinate bug: https://github.com/poanetwork/ex_abi/issues/25 # All these selectors were intially pulled in with # `ABI.parse_specification(contract_abi_json_decoded,include_events?: true)` # and later modified so that `types` hold what `returns` should have because of # issue 25. # the commented properties of the struct is what it was generated, # the new types were added to mitigate the bug. def min_exit_period() do %ABI.FunctionSelector{ function: "minExitPeriod", input_names: ["min_exit_period"], inputs_indexed: nil, method_id: <<212, 162, 180, 239>>, # returns: [uint: 256], type: :function, # types: [] types: [uint: 256] } end def get_version() do %ABI.FunctionSelector{ function: "getVersion", input_names: ["version"], inputs_indexed: nil, method_id: <<13, 142, 110, 44>>, # returns: [:string], type: :function, # types: [] types: [:string] } end def exit_games() do %ABI.FunctionSelector{ function: "exitGames", input_names: ["exit_game_address"], inputs_indexed: nil, method_id: <<175, 7, 151, 100>>, # returns: [:address], type: :function, # types: [uint: 256] types: [:address] } end def vaults() do %ABI.FunctionSelector{ function: "vaults", input_names: ["vault_address"], inputs_indexed: nil, method_id: <<140, 100, 234, 74>>, # returns: [:address], type: :function, # types: [uint: 256] types: [:address] } end def child_block_interval() do %ABI.FunctionSelector{ function: "childBlockInterval", input_names: ["child_block_interval"], inputs_indexed: nil, method_id: <<56, 169, 224, 188>>, # returns: [uint: 256], type: :function, # types: [] types: [uint: 256] } end def next_child_block() do %ABI.FunctionSelector{ function: "nextChildBlock", input_names: ["block_number"], inputs_indexed: nil, method_id: <<76, 168, 113, 79>>, # returns: [uint: 256], type: :function, # types: [] types: [uint: 256] } end def blocks() do %ABI.FunctionSelector{ function: "blocks", input_names: ["block_hash", "block_timestamp"], inputs_indexed: nil, method_id: <<242, 91, 63, 153>>, # returns: [bytes: 32, uint: 256], type: :function, # types: [uint: 256] types: [bytes: 32, uint: 256] } end def standard_exits() do %ABI.FunctionSelector{ function: "standardExits", input_names: ["standard_exit_structs"], inputs_indexed: nil, method_id: <<12, 165, 182, 118>>, # returns: [ # array: {:tuple, [:bool, {:uint, 256}, {:bytes, 32}, :address, {:uint, 256}, {:uint, 256}]} # ], type: :function, # types: [array: {:uint, 160}] types: [ array: {:tuple, [:bool, {:uint, 256}, {:bytes, 32}, :address, {:uint, 256}, {:uint, 256}]} ] } end def in_flight_exits() do %ABI.FunctionSelector{ function: "inFlightExits", input_names: ["in_flight_exit_structs"], inputs_indexed: nil, method_id: <<206, 201, 225, 167>>, # returns: [ # array: {:tuple, # [ # :bool, # {:uint, 64}, # {:uint, 256}, # {:uint, 256}, # {:array, :tuple, 4}, # {:array, :tuple, 4}, # :address, # {:uint, 256}, # {:uint, 256} # ]} # ], type: :function, # types: [array: {:uint, 160}] types: [ {:array, {:tuple, [:bool, {:uint, 64}, {:uint, 256}, {:uint, 256}, :address, {:uint, 256}, {:uint, 256}]}} ] } end end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain/event.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.Event do @moduledoc """ Parse signatures from Event definitions so that we're able to create eth_getLogs topics """ alias OMG.Eth.RootChain.AbiEventSelector @spec get_events(list(atom())) :: list(binary()) def get_events(wanted_events) do events = events() wanted_events |> Enum.reduce([], fn wanted_event_name, acc -> get_event(events, wanted_event_name, acc) end) |> Enum.reverse() end # pull all exported functions out the AbiEventSelector module # and create an event signature # function_name(arguments) @spec events() :: list({atom(), binary()}) defp events() do Enum.reduce(AbiEventSelector.module_info(:exports), [], fn {:module_info, 0}, acc -> acc {function, 0}, acc -> [{function, describe_event(apply(AbiEventSelector, function, []))} | acc] _, acc -> acc end) end defp describe_event(selector) do "#{selector.function}(" <> build_types_string(selector.types) <> ")" end defp build_types_string(types), do: build_types_string(types, "") defp build_types_string([], string), do: string defp build_types_string([{type, size} | [] = types], string) do build_types_string(types, string <> "#{type}" <> "#{size}") end defp build_types_string([{type, size} | types], string) do build_types_string(types, string <> "#{type}" <> "#{size}" <> ",") end defp build_types_string([type | [] = types], string) do build_types_string(types, string <> "#{type}") end defp build_types_string([type | types], string) do build_types_string(types, string <> "#{type}" <> ",") end def get_event(events, wanted_event_name, acc) do case Enum.find(events, fn {function_name, _signature} -> function_name == wanted_event_name end) do nil -> acc {_, signature} -> [signature | acc] end end end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain/fields.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.Fields do @moduledoc """ Adapt to naming from contracts to elixir-omg. I need to do this even though I'm bleeding out of my eyes. """ def rename(data, %ABI.FunctionSelector{function: "DepositCreated"}) do # key is naming coming from plasma contracts # value is what we use contracts_naming = [{"token", :currency}, {"depositor", :owner}, {"blknum", :blknum}, {"amount", :amount}] reduce_naming(data, contracts_naming) end # we always call it output_index, which is kinda weird? def rename(data, %ABI.FunctionSelector{function: "InFlightExitInputPiggybacked"}) do # key is naming coming from plasma contracts # value is what we use # in_flight_exit_input_piggybacked -> has "inputIndex" that needs to be converted to :output_index # in_flight_exit_output_piggybacked -> has "outputIndex" that needs to be converted to :output_index # not a typo, both are output_index. # InFlightExitInput contracts_naming = [ {"inputIndex", :output_index}, {"exitTarget", :owner}, {"txHash", :tx_hash} ] key = :piggyback_type value = :input Map.update(reduce_naming(data, contracts_naming), :omg_data, %{key => value}, &Map.put(&1, key, value)) end # we always call it output_index, which is kinda weird? def rename(data, %ABI.FunctionSelector{function: "InFlightExitOutputPiggybacked"}) do # key is naming coming from plasma contracts # value is what we use # in_flight_exit_input_piggybacked -> has "inputIndex" that needs to be converted to :output_index # in_flight_exit_output_piggybacked -> has "outputIndex" that needs to be converted to :output_index # not a typo, both are output_index. contracts_naming = [ {"outputIndex", :output_index}, {"exitTarget", :owner}, {"txHash", :tx_hash} ] key = :piggyback_type value = :output Map.update(reduce_naming(data, contracts_naming), :omg_data, %{key => value}, &Map.put(&1, key, value)) end def rename(data, %ABI.FunctionSelector{function: "BlockSubmitted"}) do contracts_naming = [{"blockNumber", :blknum}] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "ExitFinalized"}) do contracts_naming = [{"exitId", :exit_id}] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitDeleted"}) do contracts_naming = [{"exitId", :exit_id}] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitChallenged"}) do contracts_naming = [ {"challenger", :challenger}, {"challengeTxPosition", :competitor_position}, {"txHash", :tx_hash} ] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "ExitChallenged"}) do contracts_naming = [ {"utxoPos", :utxo_pos} ] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitChallengeResponded"}) do contracts_naming = [ {"challengeTxPosition", :challenge_position}, {"challenger", :challenger}, {"txHash", :tx_hash} ] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitOutputBlocked"}) do # InFlightExitOutputBlocked has outputIndex that's renamed into output_index # InFlightExitInputBlocked has inputIndex that's renamed into output_index as well contracts_naming = [ {"challenger", :challenger}, {"outputIndex", :output_index}, {"txHash", :tx_hash} ] key = :piggyback_type value = :output Map.update(reduce_naming(data, contracts_naming), :omg_data, %{key => value}, &Map.put(&1, key, value)) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitInputBlocked"}) do # InFlightExitOutputBlocked has outputIndex that's renamed into output_index # InFlightExitInputBlocked has inputIndex that's renamed into output_index as well contracts_naming = [ {"challenger", :challenger}, {"inputIndex", :output_index}, {"txHash", :tx_hash} ] key = :piggyback_type value = :input Map.update(reduce_naming(data, contracts_naming), :omg_data, %{key => value}, &Map.put(&1, key, value)) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitStarted"}) do contracts_naming = [ {"initiator", :initiator}, {"txHash", :tx_hash} ] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "ExitStarted"}) do contracts_naming = [ {"owner", :owner}, {"exitId", :exit_id} ] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitInputWithdrawn"}) do # InFlightExitInputWithdrawn contracts_naming = [{"exitId", :in_flight_exit_id}, {"inputIndex", :output_index}] key = :piggyback_type value = :input Map.update(reduce_naming(data, contracts_naming), :omg_data, %{key => value}, &Map.put(&1, key, value)) end def rename(data, %ABI.FunctionSelector{function: "InFlightExitOutputWithdrawn"}) do # InFlightExitOutputWithdrawn contracts_naming = [{"exitId", :in_flight_exit_id}, {"outputIndex", :output_index}] key = :piggyback_type value = :output Map.update(reduce_naming(data, contracts_naming), :omg_data, %{key => value}, &Map.put(&1, key, value)) end def rename(data, %ABI.FunctionSelector{function: "startInFlightExit"}) do contracts_naming = [ {"inFlightTx", :in_flight_tx}, {"inputTxs", :input_txs}, {"inputUtxosPos", :input_utxos_pos}, {"inputTxsInclusionProofs", :input_inclusion_proofs}, {"inFlightTxWitnesses", :in_flight_tx_sigs} ] reduce_naming(data, contracts_naming) end def rename(data, %ABI.FunctionSelector{function: "startStandardExit"}) do contracts_naming = [ {"outputTxInclusionProof", :output_tx_inclusion_proof}, {"rlpOutputTx", :output_tx}, {"utxoPos", :utxo_pos} ] # not used and discarded Map.delete(reduce_naming(data, contracts_naming), :output_tx_inclusion_proof) end # workaround for https://github.com/omgnetwork/elixir-omg/issues/1632 def rename(data, %ABI.FunctionSelector{function: "startExit"}) do contracts_naming = [ {"utxoPosToExit", :utxo_pos}, {"rlpOutputTxToContract", :output_tx}, {"outputTxToContractInclusionProof", :output_tx_inclusion_proof}, {"rlpInputCreationTx", :rlp_input_creation_tx}, {"inputCreationTxInclusionProof", :input_creation_tx_inclusion_proof}, {"utxoPosInput", :utxo_pos_input} ] # not used and discarded Map.drop(reduce_naming(data, contracts_naming), [ :output_tx_inclusion_proof, :rlp_input_creation_tx, :input_creation_tx_inclusion_proof, :utxo_pos_input ]) end def rename(data, %ABI.FunctionSelector{function: "challengeInFlightExitNotCanonical"}) do contracts_naming = [ {"competingTx", :competing_tx}, {"competingTxInclusionProof", :competing_tx_inclusion_proof}, {"competingTxInputIndex", :competing_tx_input_index}, {"competingTxPos", :competing_tx_pos}, {"competingTxWitness", :competing_tx_sig}, {"inFlightTx", :in_flight_tx}, {"inFlightTxInputIndex", :in_flight_input_index}, {"inputTx", :input_tx_bytes}, {"inputUtxoPos", :input_utxo_pos} ] # not used and discarded Map.delete(reduce_naming(data, contracts_naming), :competing_tx_inclusion_proof) end defp reduce_naming(data, contracts_naming) do Enum.reduce(contracts_naming, %{}, fn {old_name, new_name}, acc -> value = Map.get(data, old_name) acc |> Map.put_new(new_name, value) |> Map.delete(old_name) end) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain/rpc.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.Rpc do @moduledoc """ Does RPC calls for enriching event functions or bare events polling to plasma contracts. """ require Logger alias ExPlasma.Crypto alias OMG.Eth.Encoding def call_contract(client \\ Ethereumex.HttpClient, contract, signature, args) do data = signature |> ABI.encode(args) |> Encoding.to_hex() client.eth_call(%{from: contract, to: contract, data: data}) end def get_ethereum_events(block_from, block_to, [_ | _] = signatures, [_ | _] = contracts) do topics = Enum.map(signatures, fn signature -> event_topic_for_signature(signature) end) topics_and_signatures = Enum.reduce(Enum.zip(topics, signatures), %{}, fn {topic, signature}, acc -> Map.put(acc, topic, signature) end) contracts = Enum.map(contracts, &Encoding.to_hex(&1)) block_from = Encoding.to_hex(block_from) block_to = Encoding.to_hex(block_to) params = %{ fromBlock: block_from, toBlock: block_to, address: contracts, topics: [topics] } {:ok, logs} = Ethereumex.HttpClient.eth_get_logs(params) filtered_and_enriched_logs = handle_result(logs, topics, topics_and_signatures) {:ok, filtered_and_enriched_logs} end def get_ethereum_events(block_from, block_to, [_ | _] = signatures, contract) do get_ethereum_events(block_from, block_to, signatures, [contract]) end def get_ethereum_events(block_from, block_to, signature, [_ | _] = contracts) do get_ethereum_events(block_from, block_to, [signature], contracts) end def get_ethereum_events(block_from, block_to, signature, contract) do get_ethereum_events(block_from, block_to, [signature], [contract]) end def get_call_data(root_chain_txhash) do {:ok, %{"input" => input}} = root_chain_txhash |> Encoding.to_hex() |> Ethereumex.HttpClient.eth_get_transaction_by_hash() {:ok, input} end defp event_topic_for_signature(signature) do signature |> Crypto.keccak_hash() |> Encoding.to_hex() end defp handle_result(logs, topics, topics_and_signatures) do acc = [] handle_result(logs, topics, topics_and_signatures, acc) end defp handle_result([], _topics, _topics_and_signatures, acc), do: acc defp handle_result([%{"removed" => true} | _logs], _topics, _topics_and_signatures, acc) do acc end defp handle_result([log | logs], topics, topics_and_signatures, acc) do topic = Enum.find(topics, fn topic -> Enum.at(log["topics"], 0) == topic end) enriched_log = put_signature(log, Map.get(topics_and_signatures, topic)) handle_result(logs, topics, topics_and_signatures, [enriched_log | acc]) end defp put_signature(log, signature), do: Map.put(log, :event_signature, signature) end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain/submit_block.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.SubmitBlock do @moduledoc """ Interface to contract block submission. """ alias OMG.Eth.Blockchain.PrivateKey alias OMG.Eth.Encoding alias OMG.Eth.Transaction @type address :: <<_::160>> @type hash :: <<_::256>> @spec submit( atom(), binary(), pos_integer(), pos_integer(), OMG.Eth.address(), OMG.Eth.address() ) :: {:error, binary() | atom() | map()} | {:ok, <<_::256>>} def submit(backend, hash, nonce, gas_price, from, contract) do # NOTE: we're not using any defaults for opts here! contract_transact( backend, from, contract, "submitBlock(bytes32)", [hash], nonce: nonce, gasPrice: gas_price, value: 0, gas: 100_000 ) end @spec contract_transact(atom(), address, address, binary, [any], keyword) :: {:ok, hash()} | {:error, any} defp contract_transact(:infura = backend, _from, to, signature, args, opts) do abi_encoded_data = ABI.encode(signature, args) [nonce: nonce, gasPrice: gas_price, value: value, gas: gas_limit] = opts private_key = PrivateKey.get() transaction_data = %OMG.Eth.Blockchain.Transaction{ data: abi_encoded_data, gas_limit: gas_limit, gas_price: gas_price, init: <<>>, nonce: nonce, to: to, value: value } |> OMG.Eth.Blockchain.Transaction.Signature.sign_transaction(private_key) |> OMG.Eth.Blockchain.Transaction.serialize() |> ExRLP.encode() |> Base.encode16(case: :lower) Transaction.send(backend, "0x" <> transaction_data) end defp contract_transact(backend, from, to, signature, args, opts) do data = encode_tx_data(signature, args) txmap = %{from: Encoding.to_hex(from), to: Encoding.to_hex(to), data: data} |> Map.merge(Map.new(opts)) |> encode_all_integer_opts() Transaction.send(backend, txmap) end defp encode_tx_data(signature, args) do signature |> ABI.encode(args) |> Encoding.to_hex() end defp encode_all_integer_opts(opts) do opts |> Enum.filter(fn {_k, v} -> is_integer(v) end) |> Enum.into(opts, fn {k, v} -> {k, Encoding.to_hex(v)} end) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/root_chain.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain do @moduledoc """ Adapter/port to RootChain contract Handles sending transactions and fetching events. Should remain simple and not contain any business logic, except being aware of the RootChain contract(s) APIs. """ require Logger import OMG.Eth.Encoding, only: [from_hex: 1, int_from_hex: 1] alias OMG.Eth alias OMG.Eth.Configuration alias OMG.Eth.RootChain.Abi alias OMG.Eth.RootChain.Rpc @type optional_address_t() :: %{atom => Eth.address()} | %{atom => nil} def get_mined_child_block() do child_block_interval = Configuration.child_block_interval() mined_num = next_child_block() mined_num - child_block_interval end def next_child_block() do contract_address = Configuration.contracts().plasma_framework %{"block_number" => mined_num} = get_external_data(contract_address, "nextChildBlock()", []) mined_num end def blocks(mined_num) do contract_address = Configuration.contracts().plasma_framework %{"block_hash" => block_hash, "block_timestamp" => block_timestamp} = get_external_data(contract_address, "blocks(uint256)", [mined_num]) {block_hash, block_timestamp} end @doc """ Returns lists of block submissions from Ethereum logs """ def get_block_submitted_events(from_height, to_height) do contract = from_hex(Configuration.contracts().plasma_framework) signature = "BlockSubmitted(uint256)" {:ok, logs} = Rpc.get_ethereum_events(from_height, to_height, signature, contract) {:ok, Enum.map(logs, &Abi.decode_log(&1))} end ## ## these two cannot be parsed with ABI decoder! ## @doc """ Returns standard exits data from the contract for a list of `exit_id`s. Calls contract method. """ def get_standard_exit_structs(exit_ids) do contract = Configuration.contracts().payment_exit_game %{"standard_exit_structs" => standard_exit_structs} = get_external_data(contract, "standardExits(uint160[])", [exit_ids]) {:ok, standard_exit_structs} end @doc """ Returns in flight exits of the specified ids. Calls a contract method. """ def get_in_flight_exit_structs(in_flight_exit_ids) do contract = Configuration.contracts().payment_exit_game %{"in_flight_exit_structs" => in_flight_exit_structs} = get_external_data(contract, "inFlightExits(uint160[])", [in_flight_exit_ids]) {:ok, in_flight_exit_structs} end ######################## # MISC # ######################## @spec get_root_deployment_height() :: {:ok, integer()} | Ethereumex.HttpClient.error() def get_root_deployment_height() do plasma_framework = Configuration.contracts().plasma_framework txhash = Configuration.txhash_contract() case Ethereumex.HttpClient.eth_get_transaction_receipt(txhash) do {:ok, %{"contractAddress" => ^plasma_framework, "blockNumber" => height}} -> {:ok, int_from_hex(height)} {:ok, _} -> {:error, :wrong_contract_address} other -> other end end defp get_external_data(contract_address, signature, args) do {:ok, data} = Rpc.call_contract(contract_address, signature, args) Abi.decode_function(data, signature) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/supervisor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Supervisor do @moduledoc """ OMG Eth top level supervisor is supervising connection monitor towards Eth clients and a gen server that serves as a unified view of reported block height (`OMG.Eth.EthereumHeight`). """ use Supervisor require Logger alias OMG.Eth.Configuration alias OMG.Status.Alert.Alarm def start_link() do Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) end def init(:ok) do check_interval_ms = Configuration.ethereum_events_check_interval_ms() stall_threshold_ms = Configuration.ethereum_stalled_sync_threshold_ms() children = [ {OMG.Eth.EthereumHeightMonitor, [ check_interval_ms: check_interval_ms, stall_threshold_ms: stall_threshold_ms, eth_module: OMG.Eth.Client, alarm_module: Alarm, event_bus_module: OMG.Bus ]}, {OMG.Eth.EthereumHeight, [event_bus: OMG.Bus]} ] opts = [strategy: :one_for_one] _ = Logger.info("Starting #{inspect(__MODULE__)}") Supervisor.init(children, opts) end end ================================================ FILE: apps/omg_eth/lib/omg_eth/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Transaction do @moduledoc """ An interface to Ethereum client transact function. """ require Logger alias OMG.Eth.Encoding @doc """ Send transaction to be singed by a key managed by Ethereum node, geth or parity. For geth, account must be unlocked externally. If using parity, account passphrase must be provided directly or via config. """ @spec send(:infura, binary()) :: {:ok, OMG.Eth.hash()} | {:error, any()} @spec send(atom(), map()) :: {:ok, OMG.Eth.hash()} | {:error, any()} def send(backend, txmap) do transact(backend, txmap) end defp transact(:geth, txmap) do eth_send_transaction = Ethereumex.HttpClient.eth_send_transaction(txmap) case eth_send_transaction do {:ok, receipt_enc} -> {:ok, Encoding.from_hex(receipt_enc)} other -> other end end defp transact(:infura, transaction_data) do case Ethereumex.HttpClient.eth_send_raw_transaction(transaction_data) do {:ok, receipt_enc} -> {:ok, Encoding.from_hex(receipt_enc)} other -> other end end end ================================================ FILE: apps/omg_eth/mix.exs ================================================ defmodule OMG.Eth.MixProject do use Mix.Project require Logger def project() do [ app: :omg_eth, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end def application() do [ mod: {OMG.Eth.Application, []}, start_phases: [{:attach_telemetry, []}], extra_applications: [:sasl, :logger, :ex_plasma, :ex_rlp] ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end # Specifies which paths to compile per environment. # :dev compiles `test/support` to gain access to various `Support.*` helpers defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib", "test/support"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp deps() do [ {:ex_abi, "~> 0.5.1"}, {:ethereumex, "~> 0.6.0"}, {:ex_secp256k1, "~> 0.1.2"}, # Umbrella {:omg_bus, in_umbrella: true}, {:omg_status, in_umbrella: true}, {:omg_utils, in_umbrella: true}, {:omg_db, in_umbrella: true}, # TEST ONLY {:exexec, "~> 0.2.0", only: [:dev, :test]}, {:briefly, "~> 0.3.0", only: [:dev, :test]} ] end end ================================================ FILE: apps/omg_eth/test/fixtures.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Fixtures do @moduledoc """ Contains fixtures for tests that require geth and contract """ use ExUnitFixtures.FixtureModule alias OMG.Eth.Configuration alias OMG.Eth.Encoding alias Support.DevHelper alias Support.DevNode alias Support.RootChainHelper alias Support.SnapshotContracts @test_eth_vault_id 1 @test_erc20_vault_id 2 deffixture eth_node do case System.get_env("DOCKER_GETH") do nil -> if Application.get_env(:omg_eth, :run_test_eth_dev_node, true) do {:ok, exit_fn} = DevNode.start() on_exit(exit_fn) end :ok _ -> :ok end end deffixture contract(eth_node) do :ok = eth_node {:ok, true} = Ethereumex.HttpClient.request("personal_unlockAccount", ["0x6de4b3b9c28e9c3e84c2b2d3a875c947a84de68d", "", 0], []) add_exit_queue = RootChainHelper.add_exit_queue(@test_eth_vault_id, "0x0000000000000000000000000000000000000000") {:ok, %{"status" => _}} = Support.DevHelper.transact_sync!(add_exit_queue) :ok end deffixture token(contract) do :ok = contract contracts = SnapshotContracts.parse_contracts() token_addr = contracts["CONTRACT_ERC20_MINTABLE"] # ensuring that the root chain contract handles token_addr {:ok, _} = has_exit_queue(@test_erc20_vault_id, token_addr) {:ok, _} = DevHelper.transact_sync!(RootChainHelper.add_exit_queue(@test_erc20_vault_id, token_addr)) {:ok, true} = has_exit_queue(@test_erc20_vault_id, token_addr) token_addr end defp has_exit_queue(vault_id, token) do plasma_framework = Configuration.contracts().plasma_framework token = Encoding.from_hex(token, :mixed) call_contract(plasma_framework, "hasExitQueue(uint256,address)", [vault_id, token], [:bool]) end defp call_contract(contract, signature, args, return_types) do data = ABI.encode(signature, args) {:ok, return} = Ethereumex.HttpClient.eth_call(%{from: contract, to: contract, data: Encoding.to_hex(data)}) decode_answer(return, return_types) end defp decode_answer(enc_return, return_types) do single_return = enc_return |> Encoding.from_hex() |> ABI.TypeDecoder.decode(return_types) |> hd() {:ok, single_return} end end ================================================ FILE: apps/omg_eth/test/omg_eth/application_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ApplicationTest do use ExUnit.Case, async: false import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.DB alias OMG.Eth.Configuration setup do db_path = Briefly.create!(directory: true) Application.put_env(:omg_db, :path, db_path, persistent: true) :ok = DB.init() {:ok, apps} = Application.ensure_all_started(:omg_eth) on_exit(fn -> contracts_hash = DB.get_single_value(:omg_eth_contracts) :ok = DB.multi_update([{:delete, :omg_eth_contracts, contracts_hash}]) apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) {:ok, %{apps: apps}} end describe "valid_contracts/0" do test "if contracts hash is persisted when application starts" do contracts_hash = Configuration.contracts() |> Map.put(:txhash_contract, Configuration.txhash_contract()) # authority_addr to keep backwards compatibility |> Map.put(:authority_addr, Configuration.authority_address()) |> :erlang.phash2() assert DB.get_single_value(:omg_eth_contracts) == {:ok, contracts_hash} end test "that if contracts change boot is not permitted", %{apps: apps} do contracts_hash = Configuration.contracts() |> Map.put(:txhash_contract, Configuration.txhash_contract()) # authority_addr to keep backwards compatibility |> Map.put(:authority_addr, Configuration.authority_address()) |> :erlang.phash2() assert DB.get_single_value(:omg_eth_contracts) == {:ok, contracts_hash} apps |> Enum.reverse() |> Enum.each(&Application.stop/1) contracts = Configuration.contracts() Application.put_env(:omg_eth, :contract_addr, %{"test" => "test"}) assert capture_log(fn -> assert Application.ensure_all_started(:omg_eth) == {:error, {:omg_eth, {:bad_return, {{OMG.Eth.Application, :start, [:normal, []]}, {:EXIT, :contracts_missmatch}}}}} end) =~ "[error]" Application.put_env(:omg_eth, :contract_addr, contracts) {:ok, _} = Application.ensure_all_started(:omg_eth) end end end ================================================ FILE: apps/omg_eth/test/omg_eth/blockchain/bit_helper_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.BitHelperTest do use ExUnit.Case, async: true doctest OMG.Eth.Blockchain.BitHelper end ================================================ FILE: apps/omg_eth/test/omg_eth/blockchain/transaction/hash_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.Transaction.HashTest do use ExUnit.Case, async: true doctest OMG.Eth.Blockchain.Transaction.Hash end ================================================ FILE: apps/omg_eth/test/omg_eth/blockchain/transaction/signature_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.Transaction.SignatureTest do use ExUnit.Case, async: true doctest OMG.Eth.Blockchain.Transaction.Signature end ================================================ FILE: apps/omg_eth/test/omg_eth/blockchain/transaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Blockchain.TransactionTest do use ExUnit.Case, async: true doctest OMG.Eth.Blockchain.Transaction end ================================================ FILE: apps/omg_eth/test/omg_eth/client_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ClientTest do use ExUnit.Case, async: true alias OMG.Eth.Client test "get_ethereum_height/0 returns the block number", %{test: test_name} do defmodule test_name do def eth_block_number() do {:ok, "0xfc"} end end {:ok, number} = Client.get_ethereum_height(test_name) assert is_integer(number) end test "node_ready/0 returns not ready", %{test: test_name} do defmodule test_name do def eth_syncing() do {:ok, true} end end assert Client.node_ready(test_name) == {:error, :geth_still_syncing} end test "node_ready/0 returns ready", %{test: test_name} do defmodule test_name do def eth_syncing() do {:ok, false} end end assert Client.node_ready(test_name) == :ok end end ================================================ FILE: apps/omg_eth/test/omg_eth/encoding/contract_constructor_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Encoding.ContractConstructorTest do use ExUnit.Case, async: true alias OMG.Eth.Encoding.ContractConstructor @moduletag :common doctest ContractConstructor describe "extract_params/1" do test "returns a tuple with empty lists when given an empty list" do encoded = ContractConstructor.extract_params([]) assert encoded == {[], []} end test "returns the correct list of types and values when given a list of elementary types" do params = [ {:address, "0x1234"}, {{:uint, 256}, 1000}, {{:uint, 256}, 2000}, {:bool, true} ] encoded = ContractConstructor.extract_params(params) assert encoded == { [:address, {:uint, 256}, {:uint, 256}, :bool], ["0x1234", 1000, 2000, true] } end test "returns the correct list of types and values when given a list with one tuple" do params = [ {:tuple, [ {:address, "0x1234"}, {{:uint, 256}, 1000}, {:bool, true} ]} ] encoded = ContractConstructor.extract_params(params) assert encoded == { [{:tuple, [:address, {:uint, 256}, :bool]}], [{"0x1234", 1000, true}] } end test "returns the correct list of types and values when given a list of tuples" do params = [ {:tuple, [ {:address, "0x1234"}, {{:uint, 256}, 1000}, {:bool, true} ]}, {:tuple, [ {{:uint, 128}, 2000}, {:bool, false} ]} ] encoded = ContractConstructor.extract_params(params) assert encoded == { [{:tuple, [:address, {:uint, 256}, :bool]}, {:tuple, [{:uint, 128}, :bool]}], [{"0x1234", 1000, true}, {2000, false}] } end end end ================================================ FILE: apps/omg_eth/test/omg_eth/encoding_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.EncodingTest do use ExUnit.Case, async: true alias OMG.Eth.Encoding @moduletag :common doctest Encoding describe "encode_constructor_params/1" do test "encoding an empty list of params returns an empty string" do assert Encoding.encode_constructor_params([]) == "" end test "returns a valid base16 string when given a list of elementary types" do params = [ {:address, "0x1234"}, {{:uint, 256}, 1000}, {{:uint, 256}, 2000}, {:bool, true} ] encoded = Encoding.encode_constructor_params(params) # This function mainly does encoding via `Elixir.Base` and `ABI.TypeEncoder`, # so we'll assert just the expected format, but not the content. assert {:ok, _} = Base.decode16(encoded, case: :lower) end test "returns the correct list of types and values when given a list with one tuple" do params = [ {:tuple, [ {:address, "0x1234"}, {{:uint, 256}, 1000}, {:bool, true} ]} ] encoded = Encoding.encode_constructor_params(params) # This function mainly does encoding via `Elixir.Base` and `ABI.TypeEncoder`, # so we'll assert just the expected format, but not the content. assert {:ok, _} = Base.decode16(encoded, case: :lower) end test "returns the correct list of types and values when given a list of tuples" do params = [ {:tuple, [ {:address, "0x1234"}, {{:uint, 256}, 1000}, {:bool, true} ]}, {:tuple, [ {{:uint, 128}, 2000}, {:bool, false} ]} ] encoded = Encoding.encode_constructor_params(params) # This function mainly does encoding via `Elixir.Base` and `ABI.TypeEncoder`, # so we'll assert just the expected format, but not the content. assert {:ok, _} = Base.decode16(encoded, case: :lower) end end end ================================================ FILE: apps/omg_eth/test/omg_eth/eth_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.EthTest do @moduledoc """ Thin smoke test of the Ethereum port/adapter. The purpose of this test to only prod the marshalling and calling functionalities of the `Eth` wrapper. This shouldn't test the contract and should rely as little as possible on the contract logic. `OMG.Eth` is intended to be as thin and deprived of own logic as possible, to not require extensive testing. """ use ExUnit.Case, async: false alias OMG.Eth alias OMG.Eth.Configuration alias Support.DevHelper @moduletag :common setup_all do {:ok, exit_fn} = Support.DevNode.start() authority_address = Configuration.authority_address() {:ok, true} = Ethereumex.HttpClient.request("personal_unlockAccount", [authority_address, "", 0], []) on_exit(exit_fn) :ok end test "get_block_timestamp_by_number/1 the block timestamp by block number" do {:ok, timestamp} = Eth.get_block_timestamp_by_number(2) assert is_integer(timestamp) end test "submit_block/1 submits a block to the contract" do response = Eth.submit_block(<<234::256>>, 1, 20_000_000_000) assert {:ok, _} = DevHelper.transact_sync!(response) end end ================================================ FILE: apps/omg_eth/test/omg_eth/ethereum_height_monitor_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.EthereumHeightMonitorTest do # async:false since `eth_integration_module` is being overridden use ExUnit.Case, async: false alias __MODULE__.EthereumClientMock alias OMG.Eth.EthereumHeightMonitor alias OMG.Status.Alert.Alarm @moduletag :capture_log setup_all do _ = Agent.start_link(fn -> 55_555 end, name: :port_holder) {:ok, status_apps} = Application.ensure_all_started(:omg_status) {:ok, bus_apps} = Application.ensure_all_started(:omg_bus) apps = status_apps ++ bus_apps {:ok, _} = EthereumClientMock.start_link() on_exit(fn -> _ = apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) end setup do check_interval_ms = 10 stall_threshold_ms = 100 {:ok, monitor} = EthereumHeightMonitor.start_link( check_interval_ms: check_interval_ms, stall_threshold_ms: stall_threshold_ms, eth_module: EthereumClientMock, alarm_module: Alarm, event_bus_module: OMG.Bus ) _ = Alarm.clear_all() _ = on_exit(fn -> _ = EthereumClientMock.reset_state() _ = Process.sleep(10) true = Process.exit(monitor, :kill) end) {:ok, %{ monitor: monitor, check_interval_ms: check_interval_ms, stall_threshold_ms: stall_threshold_ms }} end # # Internal event publishing # test "that an ethereum_new_height event is published when the height increases", context do _ = EthereumClientMock.set_stalled(false) {:ok, listener} = __MODULE__.EventBusListener.start(self()) on_exit(fn -> GenServer.stop(listener) end) assert_receive(:got_ethereum_new_height, Kernel.trunc(context.check_interval_ms * 10)) end # # Connection error # test "that the connection alarm gets raised when connection becomes unhealthy" do # Initialize as healthy and alarm not present _ = EthereumClientMock.set_faulty_response(false) :ok = pull_client_alarm([], 100) # Toggle faulty response _ = EthereumClientMock.set_faulty_response(true) # Assert the alarm and event are present assert pull_client_alarm( [ethereum_connection_error: %{node: :nonode@nohost, reporter: OMG.Eth.EthereumHeightMonitor}], 100 ) == :ok end test "that the connection alarm gets cleared when connection becomes healthy" do # Initialize as unhealthy _ = EthereumClientMock.set_faulty_response(true) :ok = pull_client_alarm( [ethereum_connection_error: %{node: :nonode@nohost, reporter: OMG.Eth.EthereumHeightMonitor}], 100 ) # Toggle healthy response _ = EthereumClientMock.set_faulty_response(false) # Assert the alarm and event are no longer present assert pull_client_alarm([], 100) == :ok end # # Stalling sync # test "that the stall alarm gets raised when block height stalls" do # Initialize as healthy and alarm not present _ = EthereumClientMock.set_stalled(false) :ok = pull_client_alarm([], 200) # Toggle stalled height _ = EthereumClientMock.set_stalled(true) # Assert alarm now present assert pull_client_alarm( [ethereum_stalled_sync: %{node: :nonode@nohost, reporter: OMG.Eth.EthereumHeightMonitor}], 200 ) == :ok end test "that the stall alarm gets cleared when block height unstalls" do # Initialize as unhealthy _ = EthereumClientMock.set_stalled(true) :ok = pull_client_alarm([ethereum_stalled_sync: %{node: :nonode@nohost, reporter: OMG.Eth.EthereumHeightMonitor}], 300) # Toggle unstalled height _ = EthereumClientMock.set_stalled(false) # Assert alarm no longer present assert pull_client_alarm([], 300) == :ok end defp pull_client_alarm(_, 0), do: {:cant_match, Alarm.all()} defp pull_client_alarm(match, n) do case Alarm.all() do ^match -> :ok _ -> Process.sleep(50) pull_client_alarm(match, n - 1) end end # # Test submodules # defmodule EthereumClientMock do @moduledoc """ Mocking the ETH module integration point. """ use GenServer @initial_state %{height: 0, faulty: false, stalled: false} def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) def get_ethereum_height(), do: GenServer.call(__MODULE__, :get_ethereum_height) def set_faulty_response(faulty), do: GenServer.call(__MODULE__, {:set_faulty_response, faulty}) def set_long_response(milliseconds), do: GenServer.call(__MODULE__, {:set_long_response, milliseconds}) def set_stalled(stalled), do: GenServer.call(__MODULE__, {:set_stalled, stalled}) def reset_state(), do: GenServer.call(__MODULE__, :reset_state) def stop(), do: GenServer.stop(__MODULE__, :normal) def init(_), do: {:ok, @initial_state} def handle_call(:reset_state, _, _state), do: {:reply, :ok, @initial_state} def handle_call({:set_faulty_response, true}, _, state), do: {:reply, :ok, %{state | faulty: true}} def handle_call({:set_faulty_response, false}, _, state), do: {:reply, :ok, %{state | faulty: false}} def handle_call({:set_long_response, milliseconds}, _, state) do {:reply, :ok, Map.merge(%{long_response: milliseconds}, state)} end def handle_call({:set_stalled, true}, _, state), do: {:reply, :ok, %{state | stalled: true}} def handle_call({:set_stalled, false}, _, state), do: {:reply, :ok, %{state | stalled: false}} # Heights management def handle_call(:get_ethereum_height, _, %{faulty: true} = state) do {:reply, :error, state} end def handle_call(:get_ethereum_height, _, %{long_response: milliseconds} = state) when not is_nil(milliseconds) do _ = Process.sleep(milliseconds) {:reply, {:ok, state.height}, %{state | height: next_height(state.height, state.stalled)}} end def handle_call(:get_ethereum_height, _, state) do {:reply, {:ok, state.height}, %{state | height: next_height(state.height, state.stalled)}} end defp next_height(height, false), do: height + 1 defp next_height(height, true), do: height end defmodule EventBusListener do use GenServer def start(parent), do: GenServer.start(__MODULE__, parent) def init(parent) do :ok = OMG.Bus.subscribe({:root_chain, "ethereum_new_height"}, link: true) {:ok, parent} end def handle_info({:internal_event_bus, :ethereum_new_height, _height}, parent) do _ = send(parent, :got_ethereum_new_height) {:noreply, parent} end end end ================================================ FILE: apps/omg_eth/test/omg_eth/release_tasks/set_contract_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetContractTest do use ExUnit.Case, async: true alias OMG.Eth.ReleaseTasks.SetContract setup_all do plasma_framework = Support.SnapshotContracts.parse_contracts()["CONTRACT_ADDRESS_PLASMA_FRAMEWORK"] contract_addresses_value = %{ plasma_framework: plasma_framework } %{ contract_addresses_value: contract_addresses_value, plasma_framework: plasma_framework } end setup %{} do on_exit(fn -> :ok = System.delete_env("ETHEREUM_NETWORK") :ok = System.delete_env("CONTRACT_EXCHANGER_URL") :ok = System.delete_env("ETHEREUM_NETWORK") :ok = System.delete_env("TXHASH_CONTRACT") :ok = System.delete_env("AUTHORITY_ADDRESS") :ok = System.delete_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK") end) :ok end test "fetching from contract exchanger", %{ contract_addresses_value: contract_addresses_value } do port = 9009 pid = spawn(fn -> start(port) end) :ok = System.put_env("CONTRACT_EXCHANGER_URL", "http://localhost:#{port}") :ok = System.put_env("ETHEREUM_NETWORK", "RINKEBY") config = SetContract.load([], rpc_api: __MODULE__.Rpc) authority_address = config |> Keyword.fetch!(:omg_eth) |> Keyword.fetch!(:authority_address) assert authority_address == "authority_address_value" plasma_framework = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:contract_addr) |> Map.get(:plasma_framework) assert plasma_framework == contract_addresses_value.plasma_framework txhash_contract_value = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:txhash_contract) assert txhash_contract_value == "txhash_contract_value" :ok = Process.send(pid, :stop, []) end test "fetching from contract exchanger sets default exit period seconds" do port = 9010 _pid = spawn(fn -> start(port) end) :ok = System.put_env("CONTRACT_EXCHANGER_URL", "http://localhost:#{port}") :ok = System.put_env("ETHEREUM_NETWORK", "RINKEBY") config = SetContract.load([], rpc_api: __MODULE__.Rpc) min_exit_period_seconds = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:min_exit_period_seconds) assert min_exit_period_seconds == 20 end test "unsuported network throws exception for contract exchanger" do port = 9011 pid = spawn(fn -> start(port) end) :ok = System.put_env("CONTRACT_EXCHANGER_URL", "http://localhost:#{port}") :ok = System.put_env("ETHEREUM_NETWORK", "RINKEBY-GORLI") assert catch_exit(SetContract.load([], rpc_api: __MODULE__.Rpc)) :ok = Process.send(pid, :stop, []) end test "contract details from env", %{ plasma_framework: plasma_framework, contract_addresses_value: contract_addresses_value } do :ok = System.put_env("ETHEREUM_NETWORK", "rinkeby") :ok = System.put_env("TXHASH_CONTRACT", "txhash_contract_value") :ok = System.put_env("AUTHORITY_ADDRESS", "authority_address_value") :ok = System.put_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK", plasma_framework) config = SetContract.load([], rpc_api: __MODULE__.Rpc) authority_address = config |> Keyword.fetch!(:omg_eth) |> Keyword.fetch!(:authority_address) assert authority_address == "authority_address_value" plasma_framework = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:contract_addr) |> Map.get(:plasma_framework) assert plasma_framework == contract_addresses_value.plasma_framework txhash_contract_value = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:txhash_contract) assert txhash_contract_value == "txhash_contract_value" end test "contract details from env, mixed case", %{ plasma_framework: plasma_framework, contract_addresses_value: contract_addresses_value } do :ok = System.put_env("ETHEREUM_NETWORK", "rinkeby") :ok = System.put_env("TXHASH_CONTRACT", "Txhash_contract_value") :ok = System.put_env("AUTHORITY_ADDRESS", "Authority_address_value") :ok = System.put_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK", plasma_framework) config = SetContract.load([], rpc_api: __MODULE__.Rpc) authority_address = config |> Keyword.fetch!(:omg_eth) |> Keyword.fetch!(:authority_address) assert authority_address == "authority_address_value" plasma_framework = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:contract_addr) |> Map.get(:plasma_framework) assert plasma_framework == contract_addresses_value.plasma_framework txhash_contract_value = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:txhash_contract) assert txhash_contract_value == "txhash_contract_value" end test "contract details from env for localchain", %{ plasma_framework: plasma_framework, contract_addresses_value: contract_addresses_value } do :ok = System.put_env("ETHEREUM_NETWORK", "localchain") :ok = System.put_env("TXHASH_CONTRACT", "txhash_contract_value") :ok = System.put_env("AUTHORITY_ADDRESS", "authority_address_value") :ok = System.put_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK", plasma_framework) config = SetContract.load([], rpc_api: __MODULE__.Rpc) authority_address = config |> Keyword.fetch!(:omg_eth) |> Keyword.fetch!(:authority_address) assert authority_address == "authority_address_value" plasma_framework = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:contract_addr) |> Map.get(:plasma_framework) assert plasma_framework == contract_addresses_value.plasma_framework txhash_contract_value = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:txhash_contract) assert txhash_contract_value == "txhash_contract_value" end test "contract details from env sets default exit period seconds", %{ plasma_framework: plasma_framework } do :ok = System.put_env("ETHEREUM_NETWORK", "rinkeby") :ok = System.put_env("TXHASH_CONTRACT", "txhash_contract_value") :ok = System.put_env("AUTHORITY_ADDRESS", "authority_address_value") :ok = System.put_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK", plasma_framework) config = SetContract.load([], rpc_api: __MODULE__.Rpc) min_exit_period_seconds = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:min_exit_period_seconds) assert min_exit_period_seconds == 20 end test "contract details and exit period seconds from env", %{ plasma_framework: plasma_framework, contract_addresses_value: contract_addresses_value } do :ok = System.put_env("ETHEREUM_NETWORK", "rinkeby") :ok = System.put_env("TXHASH_CONTRACT", "txhash_contract_value") :ok = System.put_env("AUTHORITY_ADDRESS", "authority_address_value") :ok = System.put_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK", plasma_framework) config = SetContract.load([], rpc_api: __MODULE__.Rpc) min_exit_period_seconds = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:min_exit_period_seconds) assert min_exit_period_seconds == 20 authority_address = config |> Keyword.fetch!(:omg_eth) |> Keyword.fetch!(:authority_address) assert authority_address == "authority_address_value" plasma_framework = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:contract_addr) |> Map.get(:plasma_framework) assert plasma_framework == contract_addresses_value.plasma_framework txhash_contract_value = config |> Keyword.get(:omg_eth) |> Keyword.fetch!(:txhash_contract) assert txhash_contract_value == "txhash_contract_value" end test "that exit is thrown when env configuration is faulty for network name", %{ plasma_framework: plasma_framework } do :ok = System.put_env("ETHEREUM_NETWORK", "rinkeby is what we are, rinkeby is what we know") :ok = System.put_env("TXHASH_CONTRACT", "txhash_contract_value") :ok = System.put_env("AUTHORITY_ADDRESS", "authority_address_value") :ok = System.put_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK", plasma_framework) assert catch_exit(SetContract.load([], rpc_api: __MODULE__.Rpc)) end test "that exit is thrown when there's no mandatory configuration" do :ok = System.delete_env("ETHEREUM_NETWORK") :ok = System.delete_env("TXHASH_CONTRACT") :ok = System.delete_env("AUTHORITY_ADDRESS") :ok = System.delete_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK") :ok = System.delete_env("CONTRACT_EXCHANGER_URL") assert catch_exit(SetContract.load([], rpc_api: __MODULE__.Rpc)) end # a very simple web server that serves conctract exchanger requests defp start(port) do {:ok, sock} = :gen_tcp.listen(port, [{:active, false}]) spawn(fn -> loop(sock) end) receive do :stop -> :gen_tcp.close(sock) end end defp loop(sock) do case :gen_tcp.accept(sock) do {:ok, conn} -> handler = spawn(fn -> handle(conn) end) :gen_tcp.controlling_process(conn, handler) loop(sock) _ -> :ok end end defp handle(conn) do plasma_framework = Support.SnapshotContracts.parse_contracts()["CONTRACT_ADDRESS_PLASMA_FRAMEWORK"] exchanger_body = %{ plasma_framework_tx_hash: "txhash_contract_value", plasma_framework: nil, authority_address: "authority_address_value" } body = exchanger_body |> Map.put(:plasma_framework, plasma_framework) |> Jason.encode!() :ok = :gen_tcp.send(conn, ["HTTP/1.0 ", Integer.to_charlist(200), "\r\n", [], "\r\n", body]) :gen_tcp.close(conn) end defmodule Rpc do def call_contract(_, "vaults(uint256)", _) do {:ok, "0x0000000000000000000000004e3aeff70f022a6d4cc5947423887e7152826cf7"} end def call_contract(_, "exitGames(uint256)", _) do {:ok, "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"} end def call_contract(_, "childBlockInterval()", _) do {:ok, "0x00000000000000000000000000000000000000000000000000000000000003e8"} end def call_contract(_, "getVersion()", _) do {:ok, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d312e302e342b6136396337363300000000000000000000000000000000000000"} end def call_contract(_, "minExitPeriod()", _) do {:ok, "0x0000000000000000000000000000000000000000000000000000000000000014"} end end end ================================================ FILE: apps/omg_eth/test/omg_eth/release_tasks/set_ethereum_block_time_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumBlockTimeTest do use ExUnit.Case, async: true alias OMG.Eth.ReleaseTasks.SetEthereumBlockTime @app :omg_eth @env_key "ETHEREUM_BLOCK_TIME_SECONDS" @config_key :ethereum_block_time_seconds test "that block time is set when the env var is present" do :ok = System.put_env(@env_key, "1234") config = SetEthereumBlockTime.load([], []) ethereum_block_time_seconds = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_block_time_seconds == 1234 :ok = System.delete_env(@env_key) end test "that the default config is used when the env var is not set" do old_config = Application.get_env(@app, @config_key) :ok = System.delete_env(@env_key) config = SetEthereumBlockTime.load([], []) ethereum_block_time_seconds = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_block_time_seconds == old_config end end ================================================ FILE: apps/omg_eth/test/omg_eth/release_tasks/set_ethereum_client_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumClientTest do use ExUnit.Case, async: true alias OMG.Eth.ReleaseTasks.SetEthereumClient @app :omg_eth test "if defaults are used when env vars are not set" do default_url = Application.get_env(:ethereumex, :url) default_eth_node = Application.get_env(@app, :eth_node) config = SetEthereumClient.load([], []) eth_node = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:eth_node) url = config |> Keyword.fetch!(:ethereumex) |> Keyword.fetch!(:url) assert url == default_url assert eth_node == default_eth_node end test "if values are used when env vars set" do :ok = System.put_env("ETHEREUM_RPC_URL", "url") :ok = System.put_env("ETH_NODE", "geth") config = SetEthereumClient.load([], []) eth_node = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:eth_node) url = config |> Keyword.fetch!(:ethereumex) |> Keyword.fetch!(:url) assert url == "url" assert eth_node == :geth :ok = System.put_env("ETH_NODE", "parity") config = SetEthereumClient.load([], []) eth_node = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:eth_node) url = config |> Keyword.fetch!(:ethereumex) |> Keyword.fetch!(:url) assert url == "url" assert eth_node == :parity :ok = System.put_env("ETH_NODE", "infura") config = SetEthereumClient.load([], []) eth_node = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:eth_node) url = config |> Keyword.fetch!(:ethereumex) |> Keyword.fetch!(:url) assert url == "url" assert eth_node == :infura # cleanup :ok = System.delete_env("ETHEREUM_RPC_URL") :ok = System.delete_env("ETH_NODE") end test "if faulty eth node exits" do :ok = System.put_env("ETH_NODE", "random random random") assert catch_exit(SetEthereumClient.load([], [])) :ok = System.delete_env("ETH_NODE") end end ================================================ FILE: apps/omg_eth/test/omg_eth/release_tasks/set_ethereum_events_check_interval_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumEventsCheckIntervalTest do use ExUnit.Case, async: true alias OMG.Eth.ReleaseTasks.SetEthereumEventsCheckInterval @app :omg_eth @env_key "ETHEREUM_EVENTS_CHECK_INTERVAL_MS" @config_key :ethereum_events_check_interval_ms test "that interval is set when the env var is present" do :ok = System.put_env(@env_key, "1234") config = SetEthereumEventsCheckInterval.load([], []) ethereum_events_check_interval_ms = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_events_check_interval_ms == 1234 :ok = System.delete_env(@env_key) end test "that the default config is used when the env var is not set" do old_config = Application.get_env(@app, @config_key) :ok = System.delete_env(@env_key) config = SetEthereumEventsCheckInterval.load([], []) ethereum_events_check_interval_ms = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_events_check_interval_ms == old_config end end ================================================ FILE: apps/omg_eth/test/omg_eth/release_tasks/set_ethereum_stalled_sync_threshold_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.ReleaseTasks.SetEthereumStalledSyncThresholdTest do use ExUnit.Case, async: true alias OMG.Eth.ReleaseTasks.SetEthereumStalledSyncThreshold @app :omg_eth @env_key "ETHEREUM_STALLED_SYNC_THRESHOLD_MS" @config_key :ethereum_stalled_sync_threshold_ms test "that interval is set when the env var is present" do :ok = System.put_env(@env_key, "9999") config = SetEthereumStalledSyncThreshold.load([], []) ethereum_stalled_sync_threshold_ms = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_stalled_sync_threshold_ms == 9999 :ok = System.delete_env(@env_key) end test "that the default config is used when the env var is not set" do old_config = Application.get_env(@app, @config_key) :ok = System.delete_env(@env_key) config = SetEthereumStalledSyncThreshold.load([], []) ethereum_stalled_sync_threshold_ms = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_stalled_sync_threshold_ms == old_config end end ================================================ FILE: apps/omg_eth/test/omg_eth/root_chain/abi_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChain.AbiTest do @moduledoc false use ExUnit.Case, async: true alias OMG.Eth.RootChain.Abi test "if deposit created event can be decoded from log" do deposit_created_log = %{ :event_signature => "DepositCreated(address,uint256,address,uint256)", "address" => "0x4e3aeff70f022a6d4cc5947423887e7152826cf7", "blockHash" => "0xe5b0487de36b161f2d3e8c228ad4e1e84ab1ae25ca4d5ef53f9f03298ab3545f", "blockNumber" => "0x186", "data" => "0x000000000000000000000000000000000000000000000000000000000000000a", "logIndex" => "0x0", "removed" => false, "topics" => [ "0x18569122d84f30025bb8dffb33563f1bdbfb9637f21552b11b8305686e9cb307", "0x0000000000000000000000003b9f4c1dd26e0be593373b1d36cee2008cbeb837", "0x0000000000000000000000000000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000000000000000000000000000000" ], "transactionHash" => "0x4d72a63ff42f1db50af2c36e8b314101d2fea3e0003575f30298e9153fe3d8ee", "transactionIndex" => "0x0" } expected_event_parsed = %{ amount: 10, blknum: 1, currency: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, eth_height: 390, event_signature: "DepositCreated(address,uint256,address,uint256)", log_index: 0, owner: <<59, 159, 76, 29, 210, 110, 11, 229, 147, 55, 59, 29, 54, 206, 226, 0, 140, 190, 184, 55>>, root_chain_txhash: <<77, 114, 166, 63, 244, 47, 29, 181, 10, 242, 195, 110, 139, 49, 65, 1, 210, 254, 163, 224, 0, 53, 117, 243, 2, 152, 233, 21, 63, 227, 216, 238>> } assert Abi.decode_log(deposit_created_log) == expected_event_parsed end test "if input piggybacked event log can be decoded" do input_piggybacked_log = %{ :event_signature => "InFlightExitInputPiggybacked(address,bytes32,uint16)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x6d95b14290cc2ac112f1560f2cd7aa0d747b91ec9cb1d47e11c205270d83c88c", "blockNumber" => "0x19a", "data" => "0x0000000000000000000000000000000000000000000000000000000000000001", "logIndex" => "0x0", "removed" => false, "topics" => [ "0xa93c0e9b202feaf554acf6ef1185b898c9f214da16e51740b06b5f7487b018e5", "0x0000000000000000000000001513abcd3590a25e0bed840652d957391dde9955", "0xff90b77303e56bd230a9adf4a6553a95f5ffb563486205d6fba25d3e46594940" ], "transactionHash" => "0x0cc9e5556bbd6eeaf4302f44adca215786ff08cfa44a34be1760eca60f97364f", "transactionIndex" => "0x0" } expected_event_parsed = %{ eth_height: 410, event_signature: "InFlightExitInputPiggybacked(address,bytes32,uint16)", log_index: 0, output_index: 1, owner: <<21, 19, 171, 205, 53, 144, 162, 94, 11, 237, 132, 6, 82, 217, 87, 57, 29, 222, 153, 85>>, root_chain_txhash: <<12, 201, 229, 85, 107, 189, 110, 234, 244, 48, 47, 68, 173, 202, 33, 87, 134, 255, 8, 207, 164, 74, 52, 190, 23, 96, 236, 166, 15, 151, 54, 79>>, tx_hash: <<255, 144, 183, 115, 3, 229, 107, 210, 48, 169, 173, 244, 166, 85, 58, 149, 245, 255, 181, 99, 72, 98, 5, 214, 251, 162, 93, 62, 70, 89, 73, 64>>, omg_data: %{piggyback_type: :input} } assert Abi.decode_log(input_piggybacked_log) == expected_event_parsed end test "if output piggybacked event log can be decoded" do output_piggybacked_log = %{ :event_signature => "InFlightExitOutputPiggybacked(address,bytes32,uint16)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x3e34475a29dafb28cd6deb65bc1782ccf6d73d6673d462a6d404ac0993d1e7eb", "blockNumber" => "0x198", "data" => "0x0000000000000000000000000000000000000000000000000000000000000001", "logIndex" => "0x1", "removed" => false, "topics" => [ "0x6ecd8e79a5f67f6c12b54371ada2ffb41bc128c61d9ac1e969f0aa2aca46cd78", "0x0000000000000000000000001513abcd3590a25e0bed840652d957391dde9955", "0xff90b77303e56bd230a9adf4a6553a95f5ffb563486205d6fba25d3e46594940" ], "transactionHash" => "0x7cf43a6080e99677dee0b26c23e469b1df9cfb56a5c3f2a0123df6edae7b5b5e", "transactionIndex" => "0x0" } expected_event_parsed = %{ eth_height: 408, event_signature: "InFlightExitOutputPiggybacked(address,bytes32,uint16)", log_index: 1, output_index: 1, owner: <<21, 19, 171, 205, 53, 144, 162, 94, 11, 237, 132, 6, 82, 217, 87, 57, 29, 222, 153, 85>>, root_chain_txhash: <<124, 244, 58, 96, 128, 233, 150, 119, 222, 224, 178, 108, 35, 228, 105, 177, 223, 156, 251, 86, 165, 195, 242, 160, 18, 61, 246, 237, 174, 123, 91, 94>>, tx_hash: <<255, 144, 183, 115, 3, 229, 107, 210, 48, 169, 173, 244, 166, 85, 58, 149, 245, 255, 181, 99, 72, 98, 5, 214, 251, 162, 93, 62, 70, 89, 73, 64>>, omg_data: %{piggyback_type: :output} } assert Abi.decode_log(output_piggybacked_log) == expected_event_parsed end test "if block emitted event log can be decoded" do block_submitted_log = %{ :event_signature => "BlockSubmitted(uint256)", "address" => "0xc673e4ffcb8464faff908a6804fe0e635af0ea2f", "blockHash" => "0x31285f2f55e9334ae24a2bab8d5211b6f85177820f5ddf42cba652e0a88488c1", "blockNumber" => "0x18e", "data" => "0x00000000000000000000000000000000000000000000000000000000000003e8", "logIndex" => "0x0", "removed" => false, "topics" => ["0x5a978f4723b249ccf79cd7a658a8601ce1ff8b89fc770251a6be35216351ce32"], "transactionHash" => "0x297559979b5efa854ad29e216c76a64c3f43621bbf3dc16e4b31fb0cb6dcebf4", "transactionIndex" => "0x0" } expected_event_parsed = %{ blknum: 1000, eth_height: 398, event_signature: "BlockSubmitted(uint256)", log_index: 0, root_chain_txhash: <<41, 117, 89, 151, 155, 94, 250, 133, 74, 210, 158, 33, 108, 118, 166, 76, 63, 67, 98, 27, 191, 61, 193, 110, 75, 49, 251, 12, 182, 220, 235, 244>> } assert Abi.decode_log(block_submitted_log) == expected_event_parsed end test "if exit finalized event log can be decoded" do exit_finalized_log = %{ :event_signature => "ExitFinalized(uint160)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0xcafbc4b710c5fab8f3d719f65053637407231ecde31a859f1709e3478a2eda54", "blockNumber" => "0x14a", "data" => "0x", "logIndex" => "0x2", "removed" => false, "topics" => [ "0x0adb29b0831e081044cefe31155c1f2b2b85ad3613a480a5f901ee287addef55", "0x000000000000000000000000003fd275046f2823936fd97c1e3c8b225464d7f1" ], "transactionHash" => "0xbe310ade41278c5607620311b79363aa520ac46c7ba754bf3027d501c5a95f40", "transactionIndex" => "0x0" } assert Abi.decode_log(exit_finalized_log) == %{ eth_height: 330, event_signature: "ExitFinalized(uint160)", exit_id: 1_423_280_346_484_099_708_949_144_162_169_101_241_792_387_057, log_index: 2, root_chain_txhash: <<190, 49, 10, 222, 65, 39, 140, 86, 7, 98, 3, 17, 183, 147, 99, 170, 82, 10, 196, 108, 123, 167, 84, 191, 48, 39, 213, 1, 197, 169, 95, 64>> } end test "if in flight exit challanged can be decoded" do in_flight_exit_challanged_log = %{ :event_signature => "InFlightExitChallenged(address,bytes32,uint256)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0xcfffb9645dc8d73acc4c825b67ba62924c62402cc125564b655f469e0adeef32", "blockNumber" => "0x196", "data" => "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "logIndex" => "0x0", "removed" => false, "topics" => [ "0x687401968e501bda2d2d6f880dd1a0a56ff50b1787185ee0b6f4c3fb9fc417ab", "0x0000000000000000000000007ae8190d9968cbb3b52e56a56b2cd4cd5e15a44f", "0x7532528ec22439a9a1ed5f4fce6cd66d71625add6202cefb970c10d04f2d5091" ], "transactionHash" => "0xd9e3b3aaff8156dab8b004882d3bce834ba842c95deff7ec97da8f942f870ab4", "transactionIndex" => "0x0" } assert Abi.decode_log(in_flight_exit_challanged_log) == %{ challenger: <<122, 232, 25, 13, 153, 104, 203, 179, 181, 46, 86, 165, 107, 44, 212, 205, 94, 21, 164, 79>>, competitor_position: 115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935, eth_height: 406, event_signature: "InFlightExitChallenged(address,bytes32,uint256)", log_index: 0, root_chain_txhash: <<217, 227, 179, 170, 255, 129, 86, 218, 184, 176, 4, 136, 45, 59, 206, 131, 75, 168, 66, 201, 93, 239, 247, 236, 151, 218, 143, 148, 47, 135, 10, 180>>, tx_hash: <<117, 50, 82, 142, 194, 36, 57, 169, 161, 237, 95, 79, 206, 108, 214, 109, 113, 98, 90, 221, 98, 2, 206, 251, 151, 12, 16, 208, 79, 45, 80, 145>> } end test "if exit challenged can be decoded " do exit_challenged_log = %{ :event_signature => "ExitChallenged(uint256)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x95948e75cb18f299ba10e528401a9a2debf19e26425190582f7e01d888cbb7d0", "blockNumber" => "0x11f", "data" => "0x", "logIndex" => "0x0", "removed" => false, "topics" => [ "0x5dfba526c59b25f899f935c5b0d5b8739e97e4d89c38c158eca3192ea34b87d8", "0x000000000000000000000000000000000000000000000000000000e8d4a51000" ], "transactionHash" => "0x4252551c98e590863df08fd6389c616aab511038306ab8f78224a82d15070325", "transactionIndex" => "0x0" } assert Abi.decode_log(exit_challenged_log) == %{ eth_height: 287, event_signature: "ExitChallenged(uint256)", log_index: 0, root_chain_txhash: <<66, 82, 85, 28, 152, 229, 144, 134, 61, 240, 143, 214, 56, 156, 97, 106, 171, 81, 16, 56, 48, 106, 184, 247, 130, 36, 168, 45, 21, 7, 3, 37>>, utxo_pos: 1_000_000_000_000 } end test "if in flight exit challenge responded can be decoded" do in_flight_exit_challenge_responded_log = %{ :event_signature => "InFlightExitChallengeResponded(address,bytes32,uint256)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x4f3960b70634b34d69fa4a05c5d3561809cb66f2890539a01187f040a44988d1", "blockNumber" => "0x125", "data" => "0x000000000000000000000000000000000000000000000000000000e8d4a51000", "logIndex" => "0x0", "removed" => false, "topics" => [ "0x637cc4a7148767df19331a5c7dfb6d31f0a7e159a3dbb28a716be18c8c74f768", "0x00000000000000000000000018e688329ff9d6197108a66619912cda5d9ea163", "0xe60f426cbc3714ba7235df24027bf296d4d52a1a0cb36d46d6c88a3940f98d6b" ], "transactionHash" => "0x3fb63662a52fdc05d471fed92b65c9c53a9b0d990b7baefce318a6e4fa6cd517", "transactionIndex" => "0x0" } assert Abi.decode_log(in_flight_exit_challenge_responded_log) == %{ challenge_position: 1_000_000_000_000, challenger: <<24, 230, 136, 50, 159, 249, 214, 25, 113, 8, 166, 102, 25, 145, 44, 218, 93, 158, 161, 99>>, eth_height: 293, event_signature: "InFlightExitChallengeResponded(address,bytes32,uint256)", log_index: 0, root_chain_txhash: <<63, 182, 54, 98, 165, 47, 220, 5, 212, 113, 254, 217, 43, 101, 201, 197, 58, 155, 13, 153, 11, 123, 174, 252, 227, 24, 166, 228, 250, 108, 213, 23>>, tx_hash: <<230, 15, 66, 108, 188, 55, 20, 186, 114, 53, 223, 36, 2, 123, 242, 150, 212, 213, 42, 26, 12, 179, 109, 70, 214, 200, 138, 57, 64, 249, 141, 107>> } end test "if challenge in flight exit not cannonical can be decoded" do eth_tx_input = <<232, 54, 34, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 248, 83, 1, 192, 238, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 165, 248, 163, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 248, 92, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 118, 248, 116, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 238, 237, 1, 235, 148, 130, 28, 224, 68, 235, 159, 239, 63, 140, 241, 0, 192, 44, 230, 131, 216, 224, 52, 2, 224, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 213, 14, 110, 137, 144, 125, 5, 4, 94, 64, 55, 85, 66, 96, 210, 166, 41, 110, 42, 187, 199, 54, 83, 228, 31, 85, 4, 44, 153, 33, 56, 182, 104, 35, 67, 129, 11, 98, 78, 229, 81, 4, 199, 65, 155, 47, 3, 187, 179, 69, 65, 239, 135, 219, 72, 233, 93, 232, 14, 157, 74, 187, 190, 63, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> assert Abi.decode_function(eth_tx_input) == %{ competing_tx: <<248, 116, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 238, 237, 1, 235, 148, 130, 28, 224, 68, 235, 159, 239, 63, 140, 241, 0, 192, 44, 230, 131, 216, 224, 52, 2, 224, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, competing_tx_input_index: 0, competing_tx_pos: 0, competing_tx_sig: <<213, 14, 110, 137, 144, 125, 5, 4, 94, 64, 55, 85, 66, 96, 210, 166, 41, 110, 42, 187, 199, 54, 83, 228, 31, 85, 4, 44, 153, 33, 56, 182, 104, 35, 67, 129, 11, 98, 78, 229, 81, 4, 199, 65, 155, 47, 3, 187, 179, 69, 65, 239, 135, 219, 72, 233, 93, 232, 14, 157, 74, 187, 190, 63, 28>>, in_flight_input_index: 0, in_flight_tx: <<248, 163, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 248, 92, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, input_tx_bytes: <<248, 83, 1, 192, 238, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, input_utxo_pos: 1_000_000_000 } end test "if in flight exit input/output blocked can be decoded " do in_flight_exit_output_blocked_log = %{ :event_signature => "InFlightExitOutputBlocked(address,bytes32,uint16)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x1be26da1ab54eaf962157ae6c2079179a3024eeaa993d4f326e659e99cf8215e", "blockNumber" => "0x1b6", "data" => "0x0000000000000000000000000000000000000000000000000000000000000001", "logIndex" => "0x0", "removed" => false, "topics" => [ "0xcbe8dad2e7fcbfe0dcba2f9b2e44f122c66cd26dc0808a0f7e9ec41e4fe285bf", "0x000000000000000000000000d5089cfa403a6031a1f383bd467e980ed0bd5cba", "0x2a3f2ef50884e123a32a2c40d86758e8fe5b82a9a2b82e2c0849be6f13c95702" ], "transactionHash" => "0x984796ba697b532be624029990fc6d4f72e4e1434cf68dcf3b05b34b7987c468", "transactionIndex" => "0x0" } assert Abi.decode_log(in_flight_exit_output_blocked_log) == %{ challenger: <<213, 8, 156, 250, 64, 58, 96, 49, 161, 243, 131, 189, 70, 126, 152, 14, 208, 189, 92, 186>>, eth_height: 438, event_signature: "InFlightExitOutputBlocked(address,bytes32,uint16)", log_index: 0, output_index: 1, root_chain_txhash: <<152, 71, 150, 186, 105, 123, 83, 43, 230, 36, 2, 153, 144, 252, 109, 79, 114, 228, 225, 67, 76, 246, 141, 207, 59, 5, 179, 75, 121, 135, 196, 104>>, tx_hash: <<42, 63, 46, 245, 8, 132, 225, 35, 163, 42, 44, 64, 216, 103, 88, 232, 254, 91, 130, 169, 162, 184, 46, 44, 8, 73, 190, 111, 19, 201, 87, 2>>, omg_data: %{piggyback_type: :output} } end test "if in flight exit started can be decoded" do in_flight_exit_started_log = %{ :event_signature => "InFlightExitStarted(address,bytes32)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0xc8d61620144825f38394feb2c9c1d721a161ed67c123c3cb1af787fb366866c1", "blockNumber" => "0x2d6", "data" => "0x", "logIndex" => "0x0", "removed" => false, "topics" => [ "0xd5f1fe9d48880b57daa227004b16d320c0eb885d6c49d472d54c16a05fa3179e", "0x0000000000000000000000002c6a9f42318025cd6627baf21c468201622020df", "0x4f46053b5df585094cc652ddd8c365962a3889c2053592f18331b95a7dff620e" ], "transactionHash" => "0xf0e44af0d26443b9e5133c64f5a71f06a4d4d0d40c5e7412b5ea0dfcb2f1a133", "transactionIndex" => "0x0" } assert Abi.decode_log(in_flight_exit_started_log) == %{ eth_height: 726, event_signature: "InFlightExitStarted(address,bytes32)", initiator: <<44, 106, 159, 66, 49, 128, 37, 205, 102, 39, 186, 242, 28, 70, 130, 1, 98, 32, 32, 223>>, log_index: 0, root_chain_txhash: <<240, 228, 74, 240, 210, 100, 67, 185, 229, 19, 60, 100, 245, 167, 31, 6, 164, 212, 208, 212, 12, 94, 116, 18, 181, 234, 13, 252, 178, 241, 161, 51>>, tx_hash: <<79, 70, 5, 59, 93, 245, 133, 9, 76, 198, 82, 221, 216, 195, 101, 150, 42, 56, 137, 194, 5, 53, 146, 241, 131, 49, 185, 90, 125, 255, 98, 14>> } end test "if start in flight exit can be decoded " do in_flight_exit_start_log = <<90, 82, 133, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 248, 124, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 210, 32, 127, 180, 0, 246, 245, 1, 243, 148, 118, 78, 248, 3, 28, 17, 248, 220, 42, 92, 18, 141, 145, 248, 79, 186, 190, 47, 160, 172, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 69, 99, 145, 130, 68, 244, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 248, 91, 1, 192, 246, 245, 1, 243, 148, 118, 78, 248, 3, 28, 17, 248, 220, 42, 92, 18, 141, 145, 248, 79, 186, 190, 47, 160, 172, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 138, 199, 35, 4, 137, 232, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 210, 32, 127, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 243, 154, 134, 159, 98, 231, 92, 245, 240, 191, 145, 70, 136, 166, 178, 137, 202, 242, 4, 148, 53, 216, 230, 140, 92, 94, 109, 5, 228, 73, 19, 243, 78, 213, 192, 45, 109, 72, 200, 147, 36, 134, 201, 157, 58, 217, 153, 229, 216, 148, 157, 195, 190, 59, 48, 88, 204, 41, 121, 105, 12, 62, 58, 98, 28, 121, 43, 20, 191, 102, 248, 42, 243, 111, 0, 245, 251, 167, 1, 79, 160, 193, 226, 255, 60, 124, 39, 59, 254, 82, 60, 26, 207, 103, 220, 63, 95, 160, 128, 166, 134, 165, 160, 208, 92, 61, 72, 34, 253, 84, 214, 50, 220, 156, 192, 75, 22, 22, 4, 110, 186, 44, 228, 153, 235, 154, 247, 159, 94, 185, 73, 105, 10, 4, 4, 171, 244, 206, 186, 252, 124, 255, 250, 56, 33, 145, 183, 221, 158, 125, 247, 120, 88, 30, 111, 183, 142, 250, 179, 95, 211, 100, 201, 213, 218, 218, 212, 86, 155, 109, 212, 127, 127, 234, 186, 250, 53, 113, 248, 66, 67, 68, 37, 84, 131, 53, 172, 110, 105, 13, 208, 113, 104, 216, 188, 91, 119, 151, 156, 26, 103, 2, 51, 79, 82, 159, 87, 131, 247, 158, 148, 47, 210, 205, 3, 246, 229, 90, 194, 207, 73, 110, 132, 159, 222, 156, 68, 111, 171, 70, 168, 210, 125, 177, 227, 16, 15, 39, 90, 119, 125, 56, 91, 68, 227, 203, 192, 69, 202, 186, 201, 218, 54, 202, 224, 64, 173, 81, 96, 130, 50, 76, 150, 18, 124, 242, 159, 69, 53, 235, 91, 126, 186, 207, 226, 161, 214, 211, 170, 184, 236, 4, 131, 211, 32, 121, 168, 89, 255, 112, 249, 33, 89, 112, 168, 190, 235, 177, 193, 100, 196, 116, 232, 36, 56, 23, 76, 142, 235, 111, 188, 140, 180, 89, 75, 136, 201, 68, 143, 29, 64, 176, 155, 234, 236, 172, 91, 69, 219, 110, 65, 67, 74, 18, 43, 105, 92, 90, 133, 134, 45, 142, 174, 64, 179, 38, 143, 111, 55, 228, 20, 51, 123, 227, 142, 186, 122, 181, 187, 243, 3, 208, 31, 75, 122, 224, 127, 215, 62, 220, 47, 59, 224, 94, 67, 148, 138, 52, 65, 138, 50, 114, 80, 156, 67, 194, 129, 26, 130, 30, 92, 152, 43, 165, 24, 116, 172, 125, 201, 221, 121, 168, 12, 194, 240, 95, 111, 102, 76, 157, 187, 46, 69, 68, 53, 19, 125, 160, 108, 228, 77, 228, 85, 50, 165, 106, 58, 112, 7, 162, 208, 198, 180, 53, 247, 38, 249, 81, 4, 191, 166, 231, 7, 4, 111, 193, 84, 186, 233, 24, 152, 208, 58, 26, 10, 198, 249, 180, 94, 71, 22, 70, 226, 85, 90, 199, 158, 63, 232, 126, 177, 120, 30, 38, 242, 5, 0, 36, 12, 55, 146, 116, 254, 145, 9, 110, 96, 209, 84, 90, 128, 69, 87, 31, 218, 185, 181, 48, 208, 214, 231, 232, 116, 110, 120, 191, 159, 32, 244, 232, 111, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 52, 191, 197, 222, 130, 0, 246, 100, 25, 133, 115, 123, 250, 19, 77, 122, 226, 50, 133, 34, 71, 195, 27, 188, 147, 104, 200, 235, 121, 231, 64, 251, 107, 58, 88, 55, 118, 117, 53, 9, 224, 81, 93, 0, 167, 62, 195, 202, 233, 207, 237, 254, 185, 95, 207, 246, 144, 69, 242, 160, 58, 161, 96, 70, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> assert Abi.decode_function(in_flight_exit_start_log) == %{ in_flight_tx: <<248, 124, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 210, 32, 127, 180, 0, 246, 245, 1, 243, 148, 118, 78, 248, 3, 28, 17, 248, 220, 42, 92, 18, 141, 145, 248, 79, 186, 190, 47, 160, 172, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 69, 99, 145, 130, 68, 244, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, in_flight_tx_sigs: [ <<52, 191, 197, 222, 130, 0, 246, 100, 25, 133, 115, 123, 250, 19, 77, 122, 226, 50, 133, 34, 71, 195, 27, 188, 147, 104, 200, 235, 121, 231, 64, 251, 107, 58, 88, 55, 118, 117, 53, 9, 224, 81, 93, 0, 167, 62, 195, 202, 233, 207, 237, 254, 185, 95, 207, 246, 144, 69, 242, 160, 58, 161, 96, 70, 28>> ], input_inclusion_proofs: [ <<243, 154, 134, 159, 98, 231, 92, 245, 240, 191, 145, 70, 136, 166, 178, 137, 202, 242, 4, 148, 53, 216, 230, 140, 92, 94, 109, 5, 228, 73, 19, 243, 78, 213, 192, 45, 109, 72, 200, 147, 36, 134, 201, 157, 58, 217, 153, 229, 216, 148, 157, 195, 190, 59, 48, 88, 204, 41, 121, 105, 12, 62, 58, 98, 28, 121, 43, 20, 191, 102, 248, 42, 243, 111, 0, 245, 251, 167, 1, 79, 160, 193, 226, 255, 60, 124, 39, 59, 254, 82, 60, 26, 207, 103, 220, 63, 95, 160, 128, 166, 134, 165, 160, 208, 92, 61, 72, 34, 253, 84, 214, 50, 220, 156, 192, 75, 22, 22, 4, 110, 186, 44, 228, 153, 235, 154, 247, 159, 94, 185, 73, 105, 10, 4, 4, 171, 244, 206, 186, 252, 124, 255, 250, 56, 33, 145, 183, 221, 158, 125, 247, 120, 88, 30, 111, 183, 142, 250, 179, 95, 211, 100, 201, 213, 218, 218, 212, 86, 155, 109, 212, 127, 127, 234, 186, 250, 53, 113, 248, 66, 67, 68, 37, 84, 131, 53, 172, 110, 105, 13, 208, 113, 104, 216, 188, 91, 119, 151, 156, 26, 103, 2, 51, 79, 82, 159, 87, 131, 247, 158, 148, 47, 210, 205, 3, 246, 229, 90, 194, 207, 73, 110, 132, 159, 222, 156, 68, 111, 171, 70, 168, 210, 125, 177, 227, 16, 15, 39, 90, 119, 125, 56, 91, 68, 227, 203, 192, 69, 202, 186, 201, 218, 54, 202, 224, 64, 173, 81, 96, 130, 50, 76, 150, 18, 124, 242, 159, 69, 53, 235, 91, 126, 186, 207, 226, 161, 214, 211, 170, 184, 236, 4, 131, 211, 32, 121, 168, 89, 255, 112, 249, 33, 89, 112, 168, 190, 235, 177, 193, 100, 196, 116, 232, 36, 56, 23, 76, 142, 235, 111, 188, 140, 180, 89, 75, 136, 201, 68, 143, 29, 64, 176, 155, 234, 236, 172, 91, 69, 219, 110, 65, 67, 74, 18, 43, 105, 92, 90, 133, 134, 45, 142, 174, 64, 179, 38, 143, 111, 55, 228, 20, 51, 123, 227, 142, 186, 122, 181, 187, 243, 3, 208, 31, 75, 122, 224, 127, 215, 62, 220, 47, 59, 224, 94, 67, 148, 138, 52, 65, 138, 50, 114, 80, 156, 67, 194, 129, 26, 130, 30, 92, 152, 43, 165, 24, 116, 172, 125, 201, 221, 121, 168, 12, 194, 240, 95, 111, 102, 76, 157, 187, 46, 69, 68, 53, 19, 125, 160, 108, 228, 77, 228, 85, 50, 165, 106, 58, 112, 7, 162, 208, 198, 180, 53, 247, 38, 249, 81, 4, 191, 166, 231, 7, 4, 111, 193, 84, 186, 233, 24, 152, 208, 58, 26, 10, 198, 249, 180, 94, 71, 22, 70, 226, 85, 90, 199, 158, 63, 232, 126, 177, 120, 30, 38, 242, 5, 0, 36, 12, 55, 146, 116, 254, 145, 9, 110, 96, 209, 84, 90, 128, 69, 87, 31, 218, 185, 181, 48, 208, 214, 231, 232, 116, 110, 120, 191, 159, 32, 244, 232, 111, 6>> ], input_txs: [ <<248, 91, 1, 192, 246, 245, 1, 243, 148, 118, 78, 248, 3, 28, 17, 248, 220, 42, 92, 18, 141, 145, 248, 79, 186, 190, 47, 160, 172, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 138, 199, 35, 4, 137, 232, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> ], input_utxos_pos: [2_002_000_000_000] } end test "if in flight exit deleted can be decoded" do in_flight_exit_deleted_log = %{ :event_signature => "InFlightExitDeleted(uint160)", "address" => "0x89afce326e7da55647d22e24336c6a2816c99f6b", "blockHash" => "0xa27ed6299f3d74954e2c32629a5d807743627f8e57f83c8cbeaa4351da73f597", "blockNumber" => "0x3e8", "data" => "0x", "logIndex" => "0x0", "removed" => false, "topics" => [ "0x1991c4c350498b0cc937c6a08bc5bdecf2e4fdd9d918052a880f102e43dbe45c", "0x00000000000000000000000000d1d291fd21f1899f4c9d621f65dd1e0aa2355d" ], "transactionHash" => "0xbe310ade41278c5607620311b79363aa520ac46c7ba754bf3027d501c5a95f40", "transactionIndex" => "0x0" } assert Abi.decode_log(in_flight_exit_deleted_log) == %{ eth_height: 1000, event_signature: "InFlightExitDeleted(uint160)", exit_id: 4_679_199_003_952_701_118_642_806_135_853_996_264_334_177_629, log_index: 0, root_chain_txhash: <<190, 49, 10, 222, 65, 39, 140, 86, 7, 98, 3, 17, 183, 147, 99, 170, 82, 10, 196, 108, 123, 167, 84, 191, 48, 39, 213, 1, 197, 169, 95, 64>> } end test "if in flight exit output withdrawn can be decoded" do in_flight_exit_output_withdrawn_log = %{ :event_signature => "InFlightExitOutputWithdrawn(uint160,uint16)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x2218cd9358fd6ed3b720b512b645a88a9a3ed9f472e6192fae202f60e40ac7a2", "blockNumber" => "0x14f", "data" => "0x0000000000000000000000000000000000000000000000000000000000000001", "logIndex" => "0x1", "removed" => false, "topics" => [ "0xa241c6deaf193e53a1b002d779e4f247bf5d57ba0be5a753e628dfcee645a4f7", "0x00000000000000000000000000acccc8410b2139de37be92bb345c4fa10644a4" ], "transactionHash" => "0x50f80a28c7b45e5700d6e756a49d4c6ceebd5c4a5285b28abeb97058c941b966", "transactionIndex" => "0x0" } assert Abi.decode_log(in_flight_exit_output_withdrawn_log) == %{ eth_height: 335, event_signature: "InFlightExitOutputWithdrawn(uint160,uint16)", in_flight_exit_id: 3_853_567_223_408_339_354_111_409_210_931_346_801_537_991_844, log_index: 1, output_index: 1, root_chain_txhash: <<80, 248, 10, 40, 199, 180, 94, 87, 0, 214, 231, 86, 164, 157, 76, 108, 238, 189, 92, 74, 82, 133, 178, 138, 190, 185, 112, 88, 201, 65, 185, 102>>, omg_data: %{piggyback_type: :output} } end test "if exit started can be decoded" do exit_started_log = %{ :event_signature => "ExitStarted(address,uint160)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x1bee6f75c74ceeb4817dc160e2fb56dd1337a9fc2980a2b013252cf1e620f246", "blockNumber" => "0x2f7", "data" => "0x000000000000000000000000002b191e750d8d4d3dcad14a9c8e5a5cf0c81761", "logIndex" => "0x1", "removed" => false, "topics" => [ "0xdd6f755cba05d0a420007aef6afc05e4889ab424505e2e440ecd1c434ba7082e", "0x00000000000000000000000008858124b3b880c68b360fd319cc61da27545e9a" ], "transactionHash" => "0x4a8248b88a17b2be4c6086a1984622de1a60dda3c9dd9ece1ef97ed18efa028c", "transactionIndex" => "0x0" } assert Abi.decode_log(exit_started_log) == %{ eth_height: 759, event_signature: "ExitStarted(address,uint160)", exit_id: 961_120_214_746_159_734_848_620_722_848_998_552_444_082_017, log_index: 1, owner: <<8, 133, 129, 36, 179, 184, 128, 198, 139, 54, 15, 211, 25, 204, 97, 218, 39, 84, 94, 154>>, root_chain_txhash: <<74, 130, 72, 184, 138, 23, 178, 190, 76, 96, 134, 161, 152, 70, 34, 222, 26, 96, 221, 163, 201, 221, 158, 206, 30, 249, 126, 209, 142, 250, 2, 140>> } end test "if start standard exit can be decoded" do start_standard_exit_log = <<112, 224, 20, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 209, 228, 228, 234, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 248, 91, 1, 192, 246, 245, 1, 243, 148, 8, 133, 129, 36, 179, 184, 128, 198, 139, 54, 15, 211, 25, 204, 97, 218, 39, 84, 94, 154, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 13, 224, 182, 179, 167, 100, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 243, 154, 134, 159, 98, 231, 92, 245, 240, 191, 145, 70, 136, 166, 178, 137, 202, 242, 4, 148, 53, 216, 230, 140, 92, 94, 109, 5, 228, 73, 19, 243, 78, 213, 192, 45, 109, 72, 200, 147, 36, 134, 201, 157, 58, 217, 153, 229, 216, 148, 157, 195, 190, 59, 48, 88, 204, 41, 121, 105, 12, 62, 58, 98, 28, 121, 43, 20, 191, 102, 248, 42, 243, 111, 0, 245, 251, 167, 1, 79, 160, 193, 226, 255, 60, 124, 39, 59, 254, 82, 60, 26, 207, 103, 220, 63, 95, 160, 128, 166, 134, 165, 160, 208, 92, 61, 72, 34, 253, 84, 214, 50, 220, 156, 192, 75, 22, 22, 4, 110, 186, 44, 228, 153, 235, 154, 247, 159, 94, 185, 73, 105, 10, 4, 4, 171, 244, 206, 186, 252, 124, 255, 250, 56, 33, 145, 183, 221, 158, 125, 247, 120, 88, 30, 111, 183, 142, 250, 179, 95, 211, 100, 201, 213, 218, 218, 212, 86, 155, 109, 212, 127, 127, 234, 186, 250, 53, 113, 248, 66, 67, 68, 37, 84, 131, 53, 172, 110, 105, 13, 208, 113, 104, 216, 188, 91, 119, 151, 156, 26, 103, 2, 51, 79, 82, 159, 87, 131, 247, 158, 148, 47, 210, 205, 3, 246, 229, 90, 194, 207, 73, 110, 132, 159, 222, 156, 68, 111, 171, 70, 168, 210, 125, 177, 227, 16, 15, 39, 90, 119, 125, 56, 91, 68, 227, 203, 192, 69, 202, 186, 201, 218, 54, 202, 224, 64, 173, 81, 96, 130, 50, 76, 150, 18, 124, 242, 159, 69, 53, 235, 91, 126, 186, 207, 226, 161, 214, 211, 170, 184, 236, 4, 131, 211, 32, 121, 168, 89, 255, 112, 249, 33, 89, 112, 168, 190, 235, 177, 193, 100, 196, 116, 232, 36, 56, 23, 76, 142, 235, 111, 188, 140, 180, 89, 75, 136, 201, 68, 143, 29, 64, 176, 155, 234, 236, 172, 91, 69, 219, 110, 65, 67, 74, 18, 43, 105, 92, 90, 133, 134, 45, 142, 174, 64, 179, 38, 143, 111, 55, 228, 20, 51, 123, 227, 142, 186, 122, 181, 187, 243, 3, 208, 31, 75, 122, 224, 127, 215, 62, 220, 47, 59, 224, 94, 67, 148, 138, 52, 65, 138, 50, 114, 80, 156, 67, 194, 129, 26, 130, 30, 92, 152, 43, 165, 24, 116, 172, 125, 201, 221, 121, 168, 12, 194, 240, 95, 111, 102, 76, 157, 187, 46, 69, 68, 53, 19, 125, 160, 108, 228, 77, 228, 85, 50, 165, 106, 58, 112, 7, 162, 208, 198, 180, 53, 247, 38, 249, 81, 4, 191, 166, 231, 7, 4, 111, 193, 84, 186, 233, 24, 152, 208, 58, 26, 10, 198, 249, 180, 94, 71, 22, 70, 226, 85, 90, 199, 158, 63, 232, 126, 177, 120, 30, 38, 242, 5, 0, 36, 12, 55, 146, 116, 254, 145, 9, 110, 96, 209, 84, 90, 128, 69, 87, 31, 218, 185, 181, 48, 208, 214, 231, 232, 116, 110, 120, 191, 159, 32, 244, 232, 111, 6>> assert Abi.decode_function(start_standard_exit_log) == %{ output_tx: <<248, 91, 1, 192, 246, 245, 1, 243, 148, 8, 133, 129, 36, 179, 184, 128, 198, 139, 54, 15, 211, 25, 204, 97, 218, 39, 84, 94, 154, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 13, 224, 182, 179, 167, 100, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, utxo_pos: 2_001_000_000_000 } end test "blocks(uint256) function call gets decoded properly" do data = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" %{ "block_hash" => <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, "block_timestamp" => 0 } = Abi.decode_function(data, "blocks(uint256)") end test "nextChildBlock() function call gets decoded properly" do data = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" %{ "block_number" => next_child_block } = Abi.decode_function(data, "nextChildBlock()") assert is_integer(next_child_block) end test "minExitPeriod() function call gets decoded properly" do data = "0x0000000000000000000000000000000000000000000000000000000000000014" %{"min_exit_period" => 20} = Abi.decode_function(data, "minExitPeriod()") end test "exitGames(uint256) function call gets decoded properly" do data = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" %{ "block_hash" => <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, "block_timestamp" => 0 } = Abi.decode_function(data, "blocks(uint256)") end test "vaults(uint256) function call gets decoded properly" do data = "0x0000000000000000000000004e3aeff70f022a6d4cc5947423887e7152826cf7" %{"vault_address" => vault_address} = Abi.decode_function(data, "vaults(uint256)") assert is_binary(vault_address) end test "getVersion() function call gets decoded properly" do data = "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d312e302e342b6136396337363300000000000000000000000000000000000000" %{"version" => version} = Abi.decode_function(data, "getVersion()") assert is_binary(version) end test "childBlockInterval() function call gets decoded properly" do data = "0x00000000000000000000000000000000000000000000000000000000000003e8" %{"child_block_interval" => 1000} = Abi.decode_function(data, "childBlockInterval()") end # workaround for https://github.com/omgnetwork/elixir-omg/issues/1632 test "decode liquidity standard exit" do "0x" <> encoded = "0xbf1f316d000000000000000000000000000000000000000000000000000647fc93f6800000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000064713bf517001000000000000000000000000000000000000000000000000000000000000007ef87c01e1a000000000000000000000000000000000000000000000000000064713bf517001f6f501f394584128cad14df97a6eb38c83293a23bd3297d3429400000000000000000000000000000000000000008801632a2f7232200080a0000000000000000000000000000000000000466173742045786974205465737400000000000000000000000000000000000000000000000000000000000000000200c79632b66c36683bee7872870431dbe49ddd78158135be7929f0897aabf6fdf74ed5c02d6d48c8932486c99d3ad999e5d8949dc3be3b3058cc2979690c3e3a621c792b14bf66f82af36f00f5fba7014fa0c1e2ff3c7c273bfe523c1acf67dc3f5fa080a686a5a0d05c3d4822fd54d632dc9cc04b1616046eba2ce499eb9af79f5eb949690a0404abf4cebafc7cfffa382191b7dd9e7df778581e6fb78efab35fd364c9d5dadad4569b6dd47f7feabafa3571f842434425548335ac6e690dd07168d8bc5b77979c1a6702334f529f5783f79e942fd2cd03f6e55ac2cf496e849fde9c446fab46a8d27db1e3100f275a777d385b44e3cbc045cabac9da36cae040ad516082324c96127cf29f4535eb5b7ebacfe2a1d6d3aab8ec0483d32079a859ff70f9215970a8beebb1c164c474e82438174c8eeb6fbc8cb4594b88c9448f1d40b09beaecac5b45db6e41434a122b695c5a85862d8eae40b3268f6f37e414337be38eba7ab5bbf303d01f4b7ae07fd73edc2f3be05e43948a34418a3272509c43c2811a821e5c982ba51874ac7dc9dd79a80cc2f05f6f664c9dbb2e454435137da06ce44de45532a56a3a7007a2d0c6b435f726f95104bfa6e707046fc154bae91898d03a1a0ac6f9b45e471646e2555ac79e3fe87eb1781e26f20500240c379274fe91096e60d1545a8045571fdab9b530d0d6e7e8746e78bf9f20f4e86f0600000000000000000000000000000000000000000000000000000000000000b5f8b301e1a00000000000000000000000000000000000000000000000000006445941624000f86cf501f3944a6848f78cefad797d025241cc8557d71a2e294394000000000000000000000000000000000000000088042963456b4006a0f501f3946878616891f0e320f0b52906d4cba11677acd77294000000000000000000000000000000000000000088016345785d8a000080a00000000000000000000000000000000000004661737420457869742054657374000000000000000000000000000000000000000000000000000000000000000000000000000000000002000bfbbf0e382245c5cf76853509353a24f83cefca7eefc6109173e873950b46224ed5c02d6d48c8932486c99d3ad999e5d8949dc3be3b3058cc2979690c3e3a621c792b14bf66f82af36f00f5fba7014fa0c1e2ff3c7c273bfe523c1acf67dc3f5fa080a686a5a0d05c3d4822fd54d632dc9cc04b1616046eba2ce499eb9af79f5eb949690a0404abf4cebafc7cfffa382191b7dd9e7df778581e6fb78efab35fd364c9d5dadad4569b6dd47f7feabafa3571f842434425548335ac6e690dd07168d8bc5b77979c1a6702334f529f5783f79e942fd2cd03f6e55ac2cf496e849fde9c446fab46a8d27db1e3100f275a777d385b44e3cbc045cabac9da36cae040ad516082324c96127cf29f4535eb5b7ebacfe2a1d6d3aab8ec0483d32079a859ff70f9215970a8beebb1c164c474e82438174c8eeb6fbc8cb4594b88c9448f1d40b09beaecac5b45db6e41434a122b695c5a85862d8eae40b3268f6f37e414337be38eba7ab5bbf303d01f4b7ae07fd73edc2f3be05e43948a34418a3272509c43c2811a821e5c982ba51874ac7dc9dd79a80cc2f05f6f664c9dbb2e454435137da06ce44de45532a56a3a7007a2d0c6b435f726f95104bfa6e707046fc154bae91898d03a1a0ac6f9b45e471646e2555ac79e3fe87eb1781e26f20500240c379274fe91096e60d1545a8045571fdab9b530d0d6e7e8746e78bf9f20f4e86f06" decoded = encoded |> Base.decode16!(case: :lower) |> Abi.decode_function() assert decoded == %{ output_tx: "\xF8|\x01\xE1\xA0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06G\x13\xBFQp\x01\xF6\xF5\x01\xF3\x94XA(\xCA\xD1M\xF9zn\xB3\x8C\x83):#\xBD2\x97\xD3B\x94\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x88\x01c*/r2 \0\x80\xA0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0Fast Exit Test", utxo_pos: 1_768_000_000_000_000 } end end ================================================ FILE: apps/omg_eth/test/omg_eth/root_chain/event_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.RootChain.EventTest do use ExUnit.Case, async: true alias OMG.Eth.RootChain.Event test "that filter and building an event definition works as expected" do assert Event.get_events([:deposit_created]) == ["DepositCreated(address,uint256,address,uint256)"] end test "that order of returned events is preserved" do assert Event.get_events([:deposit_created, :in_flight_exit_challenged, :in_flight_exit_started]) == [ "DepositCreated(address,uint256,address,uint256)", "InFlightExitChallenged(address,bytes32,uint256)", "InFlightExitStarted(address,bytes32)" ] end end ================================================ FILE: apps/omg_eth/test/omg_eth/root_chain_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.RootChainTest do use ExUnit.Case, async: false alias ExPlasma.Builder alias ExPlasma.Crypto alias ExPlasma.Transaction.Type.PaymentV1 alias OMG.Eth.Configuration alias OMG.Eth.Encoding alias OMG.Eth.RootChain alias OMG.Eth.RootChain.Abi alias Support.DevHelper alias Support.RootChainHelper @eth "0x0000000000000000000000000000000000000000" @moduletag :common setup_all do {:ok, exit_fn} = Support.DevNode.start() on_exit(exit_fn) :ok end test "get_root_deployment_height/2 returns current block number" do {:ok, number} = RootChain.get_root_deployment_height() assert is_integer(number) end describe "get_standard_exit_structs/2" do test "returns a list of standard exits by the given exit ids" do authority_address = Configuration.authority_address() {:ok, true} = Ethereumex.HttpClient.request("personal_unlockAccount", [authority_address, "", 0], []) # Make 3 deposits so we can do 3 exits. 1 exit will not be queried, so we can check for false positives _ = add_queue(authority_address) {utxo_pos_1, exit_1} = deposit_then_start_exit(authority_address, 1, @eth) {utxo_pos_2, _exit_2} = deposit_then_start_exit(authority_address, 2, @eth) {utxo_pos_3, exit_3} = deposit_then_start_exit(authority_address, 3, @eth) # Now get the exits by their ids and asserts the result exit_id_1 = exit_id_from_receipt(exit_1) exit_id_3 = exit_id_from_receipt(exit_3) {:ok, exits} = RootChain.get_standard_exit_structs([exit_id_1, exit_id_3]) assert length(exits) == 2 assert Enum.any?(exits, fn e -> elem(e, 1) == utxo_pos_1 end) refute Enum.any?(exits, fn e -> elem(e, 1) == utxo_pos_2 end) assert Enum.any?(exits, fn e -> elem(e, 1) == utxo_pos_3 end) end end defp deposit_then_start_exit(owner, amount, currency) do owner = Encoding.from_hex(owner) currency = Encoding.from_hex(currency) rlp = deposit_transaction(amount, owner, currency) {:ok, deposit_tx} = rlp |> RootChainHelper.deposit(amount, owner) |> DevHelper.transact_sync!() deposit_txlog = hd(deposit_tx["logs"]) deposit_blknum = RootChainHelper.deposit_blknum_from_receipt(deposit_tx) deposit_txindex = OMG.Eth.Encoding.int_from_hex(deposit_txlog["transactionIndex"]) utxo_pos = ExPlasma.Output.Position.pos(%{blknum: deposit_blknum, txindex: deposit_txindex, oindex: 0}) proof = ExPlasma.Merkle.proof([rlp], 0) {:ok, start_exit_tx} = utxo_pos |> RootChainHelper.start_exit(rlp, proof, owner) |> DevHelper.transact_sync!() {utxo_pos, start_exit_tx} end defp exit_id_from_receipt(%{"logs" => logs}) do topic = "ExitStarted(address,uint160)" |> Crypto.keccak_hash() |> Encoding.to_hex() [%{exit_id: exit_id}] = logs |> Enum.filter(&(topic in &1["topics"])) |> Enum.map(fn log -> Abi.decode_log(log) end) exit_id end defp add_queue(authority_address) do {:ok, true} = Ethereumex.HttpClient.request("personal_unlockAccount", [authority_address, "", 0], []) add_exit_queue = RootChainHelper.add_exit_queue(1, "0x0000000000000000000000000000000000000000") {:ok, %{"status" => "0x1"}} = Support.DevHelper.transact_sync!(add_exit_queue) end defp deposit_transaction(amount_in_wei, address, currency) do address |> deposit(currency, amount_in_wei) |> ExPlasma.encode!(signed: false) end defp deposit(owner, token, amount) do output = PaymentV1.new_output(owner, token, amount) Builder.new(ExPlasma.payment_v1(), outputs: [output]) end end ================================================ FILE: apps/omg_eth/test/support/defaults.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Defaults do @moduledoc """ Internal defaults of non-production critical calls to `OMG.Eth.RootChain` and `OMG.Eth.Token`. Don't ever use this for `OMG.Eth.RootChain.submit_block/5` or any other production related code. Don't ever use this for `OMG.Eth.submit_block/5` or any other production related code. """ alias OMG.Eth.Encoding # safe, reasonable amount, equal to the testnet block gas limit @lots_of_gas 5_712_388 @gas_price 1_000_000_000 def tx_defaults() do Enum.map([value: 0, gasPrice: @gas_price, gas: @lots_of_gas], fn {k, v} -> {k, Encoding.to_hex(v)} end) end end ================================================ FILE: apps/omg_eth/test/support/dev_geth.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.DevGeth do use GenServer @moduledoc """ Helper module for deployment of contracts to dev geth. """ @doc """ Run geth in temp dir, kill it with SIGKILL when done. """ require Logger alias Support.WaitFor def start() do {:ok, homedir} = Briefly.create(directory: true) snapshot_dir = Path.expand(Path.join([Mix.Project.build_path(), "../../", "data/geth/"])) {"", 0} = System.cmd("cp", ["-rf", snapshot_dir, homedir]) keystore = Path.join([homedir, "/geth/keystore"]) datadir = Path.join([homedir, "/geth"]) :ok = File.write!("/tmp/geth-blank-password", "") geth = ~s(geth --miner.gastarget 7500000 \ --nodiscover \ --maxpeers 0 \ --miner.gasprice "10" \ --syncmode 'full' \ --networkid 1337 \ --gasprice '1' \ --keystore #{keystore} \ --password /tmp/geth-blank-password \ --unlock "0,1" \ --rpc --rpcapi personal,web3,eth,net --rpcaddr 0.0.0.0 --rpcvhosts='*' --rpcport=8545 \ --ws --wsaddr 0.0.0.0 --wsorigins='*' \ --allow-insecure-unlock \ --mine --datadir #{datadir} 2>&1) _pid = launch(geth) {:ok, :ready} = WaitFor.eth_rpc(20_000) on_exit = fn -> Exexec.run("pkill -9 geth") end {:ok, on_exit} end @impl true def init(cmd) do _ = Logger.debug("Starting geth") {:ok, geth_proc, os_proc} = Exexec.run(cmd, stdout: true) {:ok, %{geth_proc: geth_proc, os_proc: os_proc, ready?: false}} end @impl true def handle_info({:stdout, pid, stdout}, %{os_proc: pid} = state) do new_state = if String.contains?(stdout, "IPC endpoint opened") do Map.put(state, :ready?, true) else state end _ = case Application.get_env(:omg_eth, :node_logging_in_debug) do true -> Logger.debug("eth node: " <> stdout) _ -> :ok end {:noreply, new_state} end @impl true def handle_call(:ready?, _from, state) do {:reply, state.ready?, state} end # PRIVATE defp launch(cmd) do {:ok, pid} = start_link(cmd) waiting_task = fn -> wait_for_rpc(pid) end waiting_task |> Task.async() |> Task.await(90_000) pid end defp wait_for_rpc(pid) do if ready?(pid) do :ok else Process.sleep(2_000) wait_for_rpc(pid) end end defp start_link(cmd) do GenServer.start_link(__MODULE__, cmd) end defp ready?(pid) do GenServer.call(pid, :ready?) end end ================================================ FILE: apps/omg_eth/test/support/dev_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.DevHelper do @moduledoc """ Helpers used when setting up development environment and test fixtures, related to contracts and ethereum. Run against `geth --dev` and similar. """ import OMG.Eth.Encoding, only: [to_hex: 1, from_hex: 1, int_from_hex: 1] require Logger alias OMG.Eth alias OMG.Eth.Client alias OMG.Eth.Configuration alias OMG.Eth.RootChain alias OMG.Eth.Transaction alias Support.WaitFor @one_hundred_eth trunc(:math.pow(10, 18) * 100) # about 4 Ethereum blocks on "realistic" networks, use to timeout synchronous operations in demos on testnets # NOTE: such timeout works only in dev setting; on mainnet one must track its transactions carefully @about_4_blocks_time 60_000 @passphrase "ThisIsATestnetPassphrase" @doc """ Will take a map with eth-account information (from &generate_entity/0) and then import priv key->unlock->fund with test ETH on that account Options: - :faucet - the address to send the test ETH from, assumed to be unlocked and have the necessary funds - :initial_funds_wei - the amount of test ETH that will be granted to every generated user """ def import_unlock_fund(account, opts \\ []) do {:ok, account_enc} = create_account_from_secret(account, @passphrase) {:ok, _} = fund_address_from_faucet(account_enc, opts) {:ok, account_enc} end @doc """ Use with contract-transacting functions that return {:ok, txhash}, e.g. `Eth.Token.mint`, for synchronous waiting for mining of a successful result """ @spec transact_sync!({:ok, Eth.hash()}, keyword()) :: {:ok, map} def transact_sync!({:ok, txhash} = _transaction_submission_result, opts \\ []) when byte_size(txhash) == 32 do timeout = Keyword.get(opts, :timeout, @about_4_blocks_time) {:ok, _} = txhash |> WaitFor.eth_receipt(timeout) |> case do {:ok, %{"status" => "0x1"} = receipt} -> {:ok, Map.update!(receipt, "blockNumber", &int_from_hex(&1))} {:ok, %{"status" => "0x0"} = receipt} -> case get_reason(txhash) do "Exit queue exists" -> {:ok, Map.update!(receipt, "blockNumber", &int_from_hex(&1))} reason -> {:error, Map.put(receipt, "reason", reason)} end other -> other end end @doc """ Uses `transact_sync!` for synchronous deploy-transaction sending and extracts important data from the receipt """ @spec deploy_sync!({:ok, Eth.hash()}) :: {:ok, Eth.hash(), Eth.address()} def deploy_sync!({:ok, txhash} = transaction_submission_result) do {:ok, %{"contractAddress" => contract, "status" => "0x1", "gasUsed" => _gas_used}} = transact_sync!(transaction_submission_result) {:ok, txhash, from_hex(contract)} end def wait_for_root_chain_block(awaited_eth_height, timeout \\ 600_000) do f = fn -> {:ok, eth_height} = Client.get_ethereum_height() if eth_height < awaited_eth_height, do: :repeat, else: {:ok, eth_height} end WaitFor.ok(f, timeout) end def wait_for_next_child_block(blknum) do timeout = 10_000 f = fn -> next_num = RootChain.next_child_block() if next_num < blknum, do: :repeat, else: {:ok, next_num} end WaitFor.ok(f, timeout) end def create_account_from_secret(account, passphrase) do method_name = "personal_importRawKey" secret = Base.encode16(account.priv) case Ethereumex.HttpClient.request(method_name, [secret, passphrase], []) do {:ok, response} -> {:ok, response} {:error, %{"code" => -32_000, "message" => "account already exists"}} -> {:ok, "0x" <> Base.encode16(account.addr)} end end defp fund_address_from_faucet(account_enc, opts) do {:ok, [default_faucet | _]} = Ethereumex.HttpClient.eth_accounts() defaults = [faucet: default_faucet, initial_funds_wei: @one_hundred_eth] %{faucet: faucet, initial_funds_wei: initial_funds_wei} = defaults |> Keyword.merge(opts) |> Enum.into(%{}) unlock_if_possible(account_enc) params = %{from: faucet, to: account_enc, value: to_hex(initial_funds_wei)} {:ok, tx_fund} = Transaction.send(Configuration.eth_node(), params) case Keyword.get(opts, :timeout) do nil -> WaitFor.eth_receipt(tx_fund, @about_4_blocks_time) timeout -> WaitFor.eth_receipt(tx_fund, timeout) end end defp unlock_if_possible(account_enc) do Ethereumex.HttpClient.request("personal_unlockAccount", [account_enc, @passphrase, 0], []) end # gets the `revert` reason for a failed transaction by txhash # based on https://gist.github.com/gluk64/fdea559472d957f1138ed93bcbc6f78a defp get_reason(txhash) do # we get the exact transaction details {:ok, tx} = Ethereumex.HttpClient.eth_get_transaction_by_hash(to_hex(txhash)) # we use them (with minor tweak) to be called on the Ethereum client at the exact block of the original call {:ok, call_result} = tx |> Map.put("data", tx["input"]) |> Ethereumex.HttpClient.eth_call(tx["blockNumber"]) # this call result is hex decoded and then additionally decoded with ABI, should yield a readable ascii-string if call_result == "0x", do: "out of gas, reason is 0x", else: call_result |> from_hex() |> abi_decode_reason() end defp abi_decode_reason(result) do bytes_to_throw_away = 2 * 32 + 4 # trimming the 4-byte function selector, 32 byte size of size and 32 byte size result |> binary_part(bytes_to_throw_away, byte_size(result) - bytes_to_throw_away) |> String.trim(<<0>>) end end ================================================ FILE: apps/omg_eth/test/support/dev_node.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.DevNode do @moduledoc """ Common library for running geth and parity in dev mode. """ require Logger def start() do OMG.Eth.DevGeth.start() end end ================================================ FILE: apps/omg_eth/test/support/root_chain_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.RootChainHelper do @moduledoc """ Helper functions for RootChain. """ import OMG.Eth.Encoding, only: [to_hex: 1, from_hex: 1] alias ExPlasma.Crypto alias OMG.Eth.Blockchain.BitHelper alias OMG.Eth.Configuration alias OMG.Eth.RootChain.Abi alias OMG.Eth.TransactionHelper @tx_defaults OMG.Eth.Defaults.tx_defaults() @type optional_addr_t() :: <<_::160>> | nil @gas_add_exit_queue 800_000 @gas_start_exit 400_000 @gas_challenge_exit 300_000 @gas_deposit 180_000 @gas_deposit_from 250_000 @gas_init 1_000_000 @gas_start_in_flight_exit 1_500_000 @gas_respond_to_non_canonical_challenge 1_000_000 @gas_challenge_in_flight_exit_not_canonical 1_000_000 @gas_piggyback 1_000_000 @standard_exit_bond 14_000_000_000_000_000 @ife_bond 37_000_000_000_000_000 @piggyback_bond 28_000_000_000_000_000 @type in_flight_exit_piggybacked_event() :: %{owner: <<_::160>>, tx_hash: <<_::256>>, output_index: non_neg_integer} def start_exit(utxo_pos, tx_bytes, proof, from) do opts = @tx_defaults |> Keyword.put(:gas, @gas_start_exit) |> Keyword.put(:value, @standard_exit_bond) contract = from_hex(Configuration.contracts().payment_exit_game) backend = :geth TransactionHelper.contract_transact( backend, from, contract, "startStandardExit((uint256,bytes,bytes))", [{utxo_pos, tx_bytes, proof}], opts ) end def piggyback_in_flight_exit_on_input(in_flight_tx, input_index, from) do opts = @tx_defaults |> Keyword.put(:gas, @gas_piggyback) |> Keyword.put(:value, @piggyback_bond) contract = from_hex(Configuration.contracts().payment_exit_game) signature = "piggybackInFlightExitOnInput((bytes,uint16))" args = [{in_flight_tx, input_index}] backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end def piggyback_in_flight_exit_on_output(in_flight_tx, output_index, from) do opts = @tx_defaults |> Keyword.put(:gas, @gas_piggyback) |> Keyword.put(:value, @piggyback_bond) contract = from_hex(Configuration.contracts().payment_exit_game) signature = "piggybackInFlightExitOnOutput((bytes,uint16))" args = [{in_flight_tx, output_index}] backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end def deposit(tx_bytes, value, from) do opts = [] defaults = Keyword.put(@tx_defaults, :gas, @gas_deposit) opts = defaults |> Keyword.merge(opts) |> Keyword.put(:value, value) contract = from_hex(Configuration.contracts().eth_vault) backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, "deposit(bytes)", [tx_bytes], opts) end def deposit_from(tx, from) do opts = Keyword.put(@tx_defaults, :gas, @gas_deposit_from) contract = from_hex(Configuration.contracts().erc20_vault) backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, "deposit(bytes)", [tx], opts) end def add_exit_queue(vault_id, token) do opts = Keyword.put(@tx_defaults, :gas, @gas_add_exit_queue) contract = from_hex(Configuration.contracts().plasma_framework) token = from_hex(token) {:ok, [from | _]} = Ethereumex.HttpClient.eth_accounts() backend = Configuration.eth_node() TransactionHelper.contract_transact( backend, from_hex(from), contract, "addExitQueue(uint256, address)", [vault_id, token], opts ) end def challenge_exit(exit_id, exiting_tx, challenge_tx, input_index, challenge_tx_sig, from) do opts = Keyword.put(@tx_defaults, :gas, @gas_challenge_exit) sender_data = BitHelper.kec(from) contract = from_hex(Configuration.contracts().payment_exit_game) signature = "challengeStandardExit((uint160,bytes,bytes,uint16,bytes,bytes32))" args = [{exit_id, exiting_tx, challenge_tx, input_index, challenge_tx_sig, sender_data}] backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end def activate_child_chain(from \\ nil) do opts = Keyword.put(@tx_defaults, :gas, @gas_init) contract = Configuration.contracts().plasma_framework from = from || from_hex(Configuration.authority_address()) backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, "activateChildChain()", [], opts) end def in_flight_exit( in_flight_tx, input_txs, input_utxos_pos, input_txs_inclusion_proofs, in_flight_tx_sigs, from ) do opts = @tx_defaults |> Keyword.put(:value, @ife_bond) |> Keyword.put(:gas, @gas_start_in_flight_exit) contract = from_hex(Configuration.contracts().payment_exit_game) signature = "startInFlightExit((bytes,bytes[],uint256[],bytes[],bytes[]))" args = [{in_flight_tx, input_txs, input_utxos_pos, input_txs_inclusion_proofs, in_flight_tx_sigs}] backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end def process_exits(vault_id, token, top_exit_id, exits_to_process, from) do opts = @tx_defaults token = from_hex(token) contract = from_hex(Configuration.contracts().plasma_framework) signature = "processExits(uint256,address,uint160,uint256)" args = [vault_id, token, top_exit_id, exits_to_process] backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end # credo:disable-for-next-line Credo.Check.Refactor.FunctionArity def challenge_in_flight_exit_not_canonical( input_tx_bytes, input_utxo_pos, in_flight_txbytes, in_flight_input_index, competing_txbytes, competing_input_index, competing_tx_pos, competing_proof, competing_sig, from ) do opts = Keyword.put(@tx_defaults, :gas, @gas_challenge_in_flight_exit_not_canonical) contract = from_hex(Configuration.contracts().payment_exit_game) signature = "challengeInFlightExitNotCanonical((bytes,uint256,bytes,uint16,bytes,uint16,uint256,bytes,bytes))" args = [ {input_tx_bytes, input_utxo_pos, in_flight_txbytes, in_flight_input_index, competing_txbytes, competing_input_index, competing_tx_pos, competing_proof, competing_sig} ] backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end def respond_to_non_canonical_challenge( in_flight_tx, in_flight_tx_pos, in_flight_tx_inclusion_proof, from ) do opts = Keyword.put(@tx_defaults, :gas, @gas_respond_to_non_canonical_challenge) contract = from_hex(Configuration.contracts().payment_exit_game) signature = "respondToNonCanonicalChallenge(bytes,uint256,bytes)" args = [in_flight_tx, in_flight_tx_pos, in_flight_tx_inclusion_proof] backend = Configuration.eth_node() TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end # credo:disable-for-next-line Credo.Check.Refactor.FunctionArity def challenge_in_flight_exit_input_spent( in_flight_txbytes, in_flight_input_index, spending_txbytes, spending_tx_input_index, spending_tx_sig, input_txbytes, input_utxo_pos, from ) do opts = @tx_defaults contract = from_hex(Configuration.contracts().payment_exit_game) signature = "challengeInFlightExitInputSpent((bytes,uint16,bytes,uint16,bytes,bytes,uint256))" args = [ {in_flight_txbytes, in_flight_input_index, spending_txbytes, spending_tx_input_index, spending_tx_sig, input_txbytes, input_utxo_pos} ] backend = Application.fetch_env!(:omg_eth, :eth_node) TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end # credo:disable-for-next-line Credo.Check.Refactor.FunctionArity def challenge_in_flight_exit_output_spent( in_flight_txbytes, in_flight_output_pos, in_flight_tx_inclusion_proof, spending_txbytes, spending_tx_input_index, spending_tx_sig, from ) do opts = @tx_defaults contract = from_hex(Configuration.contracts().payment_exit_game) signature = "challengeInFlightExitOutputSpent((bytes,bytes,uint256,bytes,uint16,bytes))" args = [ {in_flight_txbytes, in_flight_tx_inclusion_proof, in_flight_output_pos, spending_txbytes, spending_tx_input_index, spending_tx_sig} ] backend = Application.fetch_env!(:omg_eth, :eth_node) TransactionHelper.contract_transact(backend, from, contract, signature, args, opts) end def deposit_blknum_from_receipt(%{"logs" => logs}) do topic = "DepositCreated(address,uint256,address,uint256)" |> Crypto.keccak_hash() |> to_hex() [%{blknum: deposit_blknum}] = logs |> Enum.filter(&(topic in &1["topics"])) |> Enum.map(&Abi.decode_log/1) deposit_blknum end end ================================================ FILE: apps/omg_eth/test/support/snapshot_contracts.ex ================================================ # Copyright 2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.SnapshotContracts do @moduledoc """ Provides facilities to use contracts from the `geth` snapshot including the `plasma-contracts` dev deployment """ def parse_contracts() do local_umbrella_path = Path.join([File.cwd!(), "../../", "localchain_contract_addresses.env"]) contract_addreses_path = case File.exists?(local_umbrella_path) do true -> local_umbrella_path _ -> # CI/CD Path.join([File.cwd!(), "localchain_contract_addresses.env"]) end contract_addreses_path |> File.read!() |> String.split("\n", trim: true) |> List.flatten() |> Enum.reduce(%{}, fn line, acc -> [key, value] = String.split(line, "=") Map.put(acc, key, value) end) end end ================================================ FILE: apps/omg_eth/test/support/token.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.Token do @moduledoc """ Adapter/port to tokens that implement ERC20 interface """ alias OMG.Eth.Encoding alias OMG.Eth.TransactionHelper @tx_defaults OMG.Eth.Defaults.tx_defaults() @gas_token_ops 80_000 ########## # writes # ########## def mint(owner, amount, token, opts \\ []) do opts = @tx_defaults |> Keyword.put(:gas, @gas_token_ops) |> Keyword.merge(opts) {:ok, [from | _]} = Ethereumex.HttpClient.eth_accounts() backend = Application.fetch_env!(:omg_eth, :eth_node) TransactionHelper.contract_transact( backend, Encoding.from_hex(from), token, "mint(address,uint256)", [owner, amount], opts ) end def approve(from, spender, amount, token, opts \\ []) do opts = @tx_defaults |> Keyword.put(:gas, @gas_token_ops) |> Keyword.merge(opts) backend = Application.fetch_env!(:omg_eth, :eth_node) TransactionHelper.contract_transact(backend, from, token, "approve(address,uint256)", [spender, amount], opts) end end ================================================ FILE: apps/omg_eth/test/support/transaction_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Eth.TransactionHelper do @moduledoc """ Standard interface for transacting with Ethereum """ alias OMG.Eth.Encoding alias OMG.Eth.Transaction @spec contract_transact(atom(), <<_::160>>, <<_::160>>, binary, [any]) :: {:ok, <<_::256>>} | {:error, any} def contract_transact(backend, from, to, signature, args, opts \\ []) do data = encode_tx_data(signature, args) txmap = %{from: Encoding.to_hex(from), to: Encoding.to_hex(to), data: data} |> Map.merge(Map.new(opts)) |> encode_all_integer_opts() Transaction.send(backend, txmap) end defp encode_tx_data(signature, args) do signature |> ABI.encode(args) |> Encoding.to_hex() end defp encode_all_integer_opts(opts) do opts |> Enum.filter(fn {_k, v} -> is_integer(v) end) |> Enum.into(opts, fn {k, v} -> {k, Encoding.to_hex(v)} end) end end ================================================ FILE: apps/omg_eth/test/support/wait_for.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.WaitFor do @moduledoc """ Generic wait_for_* utils, styled after web3 counterparts """ alias OMG.Eth.Encoding alias __MODULE__ def eth_rpc(timeout \\ 10_000) do f = fn -> case Ethereumex.HttpClient.eth_syncing() do {:ok, false} -> {:ok, :ready} _ -> :repeat end end WaitFor.ok(f, timeout) end @doc """ NOTE: `eth_receipt` takes txhash as raw decoded binary, like the rest of Eth APIs, but binaries in the receipt returned are in `0xhex-style` This is low-level, consider using `|> Support.DevHelper.transact_sync!()` for eth-transactions' syncronicity in tests """ def eth_receipt(txhash, timeout \\ 15_000) do f = fn -> txhash |> Encoding.to_hex() |> Ethereumex.HttpClient.eth_get_transaction_receipt() |> case do {:ok, receipt} when receipt != nil -> {:ok, receipt} _ -> :repeat end end WaitFor.ok(f, timeout) end # Repeats f until f returns {:ok, ...}, :ok OR exception is raised (see :erlang.exit, :erlang.error) OR timeout # after `timeout` milliseconds specified # # Simple throws and :badmatch are treated as signals to repeat def ok(f, timeout \\ 5_000) do fn -> repeat_until_ok(f) end |> Task.async() |> Task.await(timeout) end defp repeat_until_ok(f) do Process.sleep(100) try do case f.() do :ok = return -> return {:ok, _} = return -> return _ -> repeat_until_ok(f) end catch _something -> repeat_until_ok(f) :error, {:badmatch, _} = _error -> repeat_until_ok(f) end end end ================================================ FILE: apps/omg_eth/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.configure(exclude: [integration: true, property: true, wrappers: true, common: true]) ExUnit.start() {:ok, _} = Application.ensure_all_started(:ethereumex) {:ok, _} = Application.ensure_all_started(:briefly) {:ok, _} = Application.ensure_all_started(:erlexec) ================================================ FILE: apps/omg_status/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build # If you run "mix test --cover", coverage assets end up here. /cover # The directory Mix downloads your dependencies sources to. /deps # Where 3rd-party dependencies like ExDoc output generated docs. /doc # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez ================================================ FILE: apps/omg_status/README.md ================================================ # Status Status is a umbrella application. Its purpose is to gather and send metrics. ================================================ FILE: apps/omg_status/lib/omg_status/alert/alarm.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Alert.Alarm do @moduledoc """ Interface for raising and clearing alarms related to OMG Status. """ alias OMG.Status.Alert.AlarmHandler @typedoc """ The raw alarm being used to `set` the Alarm """ @type alarm_detail :: %{ node: Node.t(), reporter: module() } @type alarms :: {:boot_in_progress | :ethereum_connection_error | :ethereum_stalled_sync | :invalid_fee_source | :statsd_client_connection | :main_supervisor_halted | :system_memory_too_high | :block_submit_stalled, alarm_detail} def alarm_types(), do: [ :boot_in_progress, :ethereum_connection_error, :ethereum_stalled_sync, :invalid_fee_source, :statsd_client_connection, :main_supervisor_halted, :system_memory_too_high, :block_submit_stalled ] @spec statsd_client_connection(module()) :: {:statsd_client_connection, alarm_detail} def statsd_client_connection(reporter), do: {:statsd_client_connection, %{node: Node.self(), reporter: reporter}} @spec ethereum_connection_error(module()) :: {:ethereum_connection_error, alarm_detail} def ethereum_connection_error(reporter), do: {:ethereum_connection_error, %{node: Node.self(), reporter: reporter}} @spec ethereum_stalled_sync(module()) :: {:ethereum_stalled_sync, alarm_detail} def ethereum_stalled_sync(reporter), do: {:ethereum_stalled_sync, %{node: Node.self(), reporter: reporter}} @spec boot_in_progress(module()) :: {:boot_in_progress, alarm_detail} def boot_in_progress(reporter), do: {:boot_in_progress, %{node: Node.self(), reporter: reporter}} @spec invalid_fee_source(module()) :: {:invalid_fee_source, alarm_detail} def invalid_fee_source(reporter), do: {:invalid_fee_source, %{node: Node.self(), reporter: reporter}} @spec main_supervisor_halted(module()) :: {:main_supervisor_halted, alarm_detail} def main_supervisor_halted(reporter), do: {:main_supervisor_halted, %{node: Node.self(), reporter: reporter}} @spec system_memory_too_high(module()) :: {:system_memory_too_high, alarm_detail} def system_memory_too_high(reporter), do: {:system_memory_too_high, %{node: Node.self(), reporter: reporter}} @spec block_submit_stalled(module()) :: {:block_submit_stalled, alarm_detail} def block_submit_stalled(reporter), do: {:block_submit_stalled, %{node: Node.self(), reporter: reporter}} @spec set(alarms()) :: :ok | :duplicate def set(alarm), do: do_raise(alarm) @spec clear(alarms()) :: :ok | :not_raised def clear(alarm), do: do_clear(alarm) def clear_all() do Enum.each(all(), &:alarm_handler.clear_alarm(&1)) end def all() do :gen_event.call(:alarm_handler, AlarmHandler, :get_alarms) end defp do_raise(alarm) do if Enum.member?(all(), alarm) do :duplicate else :alarm_handler.set_alarm(alarm) end end defp do_clear(alarm) do if Enum.member?(all(), alarm) do :alarm_handler.clear_alarm(alarm) else :not_raised end end end ================================================ FILE: apps/omg_status/lib/omg_status/alert/alarm_handler.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Alert.AlarmHandler do @moduledoc """ This is the SASL alarm handler process. """ alias OMG.Status.Alert.Alarm @table_name :alarms def install() do case Enum.member?(:gen_event.which_handlers(:alarm_handler), __MODULE__) do true -> :ok false -> previous_alarms = :alarm_handler.get_alarms() :ok = :gen_event.swap_handler(:alarm_handler, {:alarm_handler, :swap}, {__MODULE__, :ok}) # migrates old alarms Enum.each(previous_alarms, &:alarm_handler.set_alarm(&1)) end end def table_name(), do: @table_name # ----------------------------------------------------------------- # :gen_event handlers # ----------------------------------------------------------------- def init(_args) do table_setup() :ok = Enum.each(Alarm.alarm_types(), &write_clear/1) {:ok, %{alarms: []}} end def handle_call(:get_alarms, %{alarms: alarms} = state), do: {:ok, alarms, state} def handle_event({:set_alarm, new_alarm}, %{alarms: alarms} = state) do # was the alarm raised already and is this our type of alarm? case Enum.any?(alarms, &(&1 == new_alarm)) do true -> {:ok, state} false -> # the alarm has not been raised before and we're subscribed _ = write_raise(new_alarm) {:ok, %{alarms: [new_alarm | alarms]}} end end def handle_event({:clear_alarm, alarm_id}, %{alarms: alarms}) do new_alarms = alarms |> Enum.filter(&(elem(&1, 0) != alarm_id)) |> Enum.filter(&(&1 != alarm_id)) _ = write_clear(alarm_id) {:ok, %{alarms: new_alarms}} end def handle_event(_event, state) do {:ok, state} end def terminate(:swap, state), do: {__MODULE__, state} def terminate(_, _), do: :ok defp table_setup() do _ = if :undefined == :ets.info(@table_name), do: @table_name = :ets.new(@table_name, table_settings()) end defp table_settings(), do: [:named_table, :set, :protected, read_concurrency: true] defp write_raise(alarm) when is_tuple(alarm), do: write_raise(elem(alarm, 0)) defp write_raise(key) when is_atom(key), do: :ets.update_counter(@table_name, key, {2, 1, 1, 1}, {key, 0}) defp write_clear(alarm) when is_tuple(alarm), do: write_clear(elem(alarm, 0)) defp write_clear(key) when is_atom(key), do: :ets.update_counter(@table_name, key, {2, -1, 0, 0}, {key, 1}) end ================================================ FILE: apps/omg_status/lib/omg_status/alert/alarm_printer.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.AlarmPrinter do @moduledoc """ A loud reminder of raised events """ use GenServer require Logger @interval 5_000 # 5 minutes @max_interval 300_000 def start_link(args) do GenServer.start_link(__MODULE__, args, name: Keyword.get(args, :name, __MODULE__)) end def init(args) do alarm_module = Keyword.fetch!(args, :alarm_module) _ = :timer.send_after(@interval, :print_alarms) {:ok, %{previous_backoff: @interval, alarm_module: alarm_module}} end def handle_info(:print_alarms, state) do :ok = Enum.each(state.alarm_module.all(), fn alarm -> Logger.warn("An alarm was raised #{inspect(alarm)}") end) previous_backoff = case @max_interval < state.previous_backoff do true -> @interval false -> state.previous_backoff end next_backoff = round(previous_backoff * 2) + Enum.random(-1000..1000) _ = :timer.send_after(next_backoff, :print_alarms) {:noreply, Map.put(state, :previous_backoff, next_backoff)} end end ================================================ FILE: apps/omg_status/lib/omg_status/application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Application do @moduledoc """ Top level application module. """ use Application alias OMG.Status.AlarmPrinter alias OMG.Status.Alert.Alarm alias OMG.Status.Alert.AlarmHandler alias OMG.Status.Configuration alias OMG.Status.DatadogEvent.AlarmConsumer alias OMG.Status.Metric.Datadog alias OMG.Status.Metric.Telemetry alias OMG.Status.Metric.VmstatsSink def start(_type, _args) do import Supervisor.Spec, warn: false system_memory_check_interval_ms = Configuration.system_memory_check_interval_ms() system_memory_high_threshold = Configuration.system_memory_high_threshold() release = Configuration.release() current_version = Configuration.current_version() children = if Configuration.datadog_disabled?() do # spandex datadog api server is able to flush when disabled?: true [{SpandexDatadog.ApiServer, spandex_datadog_options()}] else [ {OMG.Status.Monitor.StatsdMonitor, [alarm_module: Alarm, child_module: Datadog]}, { OMG.Status.Monitor.MemoryMonitor, [ alarm_module: Alarm, memsup_module: :memsup, threshold: system_memory_high_threshold, interval_ms: system_memory_check_interval_ms ] }, { Telemetry, [ release: release, current_version: current_version ] }, VmstatsSink.prepare_child(), {SpandexDatadog.ApiServer, spandex_datadog_options()}, { AlarmConsumer, [ dd_alarm_handler: AlarmHandler, release: release, current_version: current_version, publisher: Datadog ] } ] end child = [{AlarmPrinter, [alarm_module: Alarm]}] Supervisor.start_link(children ++ child, strategy: :one_for_one, name: Status.Supervisor) end def start_phase(:install_alarm_handler, _start_type, _phase_args) do :ok = AlarmHandler.install() end defp spandex_datadog_options() do config = Application.get_all_env(:spandex_datadog) config_host = config[:host] config_port = config[:port] config_batch_size = config[:batch_size] config_sync_threshold = config[:sync_threshold] config_http = config[:http] spandex_datadog_options(config_host, config_port, config_batch_size, config_sync_threshold, config_http) end defp spandex_datadog_options(config_host, config_port, config_batch_size, config_sync_threshold, config_http) do [ host: config_host || "localhost", port: config_port || 8126, batch_size: config_batch_size || 10, sync_threshold: config_sync_threshold || 100, http: config_http || HTTPoison ] end end ================================================ FILE: apps/omg_status/lib/omg_status/configuration.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Configuration do @moduledoc """ Provides access to applications configuration """ alias OMG.Status.Metric.Tracer @app :omg_status @spec system_memory_check_interval_ms() :: integer() | no_return() def system_memory_check_interval_ms() do Application.fetch_env!(@app, :system_memory_check_interval_ms) end @spec system_memory_high_threshold() :: float() | no_return() def system_memory_high_threshold() do Application.fetch_env!(@app, :system_memory_high_threshold) end @spec datadog_disabled?() :: boolean() def datadog_disabled?() do Application.fetch_env!(@app, Tracer)[:disabled?] end @spec release() :: atom() | nil def release() do Application.get_env(@app, :release) end @spec current_version() :: String.t() | nil def current_version() do Application.get_env(@app, :current_version) end end ================================================ FILE: apps/omg_status/lib/omg_status/datadog_event/alarm_consumer.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.DatadogEvent.AlarmConsumer do @moduledoc """ Installs a alarm handler and publishes the alarms as events """ require Logger use GenServer @doc """ Returns child_specs for the given `AlarmConsumer` setup, to be included e.g. in Supervisor's children. Mandatory params are in Keyword form: - :publisher is Module that implements a function `event/3` (title, message, option). It's purpose is to forward alarms to a collector (for example, Datadog) - :alarm_handler (http://erlang.org/doc/man/alarm_handler.html) is a gen_event process that allows us to install our alarm handler ontu. Our installed handler will than receive system alarms (set and cleared) and cast us the alarms. - :dd_alarm_handler is the module that we install as alarm_handler and will get notified of set and cleared alarms and forward them to THIS AlarmConsumer process. - :release is the mode this current process is runing under (for example, currently we support watcher, child chain or watcher info) - :current_version is semver of the current code """ @spec prepare_child(keyword()) :: %{id: atom(), start: tuple()} def prepare_child(opts) do %{id: :alarm_consumer, start: {__MODULE__, :start_link, [opts]}, shutdown: :brutal_kill, type: :worker} end @doc """ args explained above in prepare_child/1 """ def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end @doc """ args explained above in prepare_child/1 """ def init(args) do publisher = Keyword.fetch!(args, :publisher) alarm_handler_process = Keyword.get(args, :alarm_handler, :alarm_handler) dd_alarm_handler = Keyword.fetch!(args, :dd_alarm_handler) release = Keyword.fetch!(args, :release) current_version = Keyword.fetch!(args, :current_version) :ok = install_alarm_handler(alarm_handler_process, dd_alarm_handler) _ = Logger.info("Started #{inspect(__MODULE__)}") {:ok, %{publisher: publisher, release: release, current_version: current_version}} end # Gets events from the alarm consumer and send them off def handle_cast(alarm, state) do {alarm_type, data} = elem(alarm, 1) action = elem(alarm, 0) level = case action do :clear_alarm -> [alert_type: :info] _ -> [alert_type: :warning] end aggregation_key = :alarm timestamp = DateTime.to_unix(DateTime.utc_now(), :millisecond) options = tags(aggregation_key, state.release, state.current_version, timestamp) title = "#{action} - #{inspect(alarm_type)}" message = "#{inspect(data)} - Timestamp: #{timestamp}" :ok = apply(state.publisher, :event, create_event_data(title, message, level ++ options)) {:noreply, state} end defp create_event_data(title, message, options) do [title, message, options] end # https://docs.datadoghq.com/api/?lang=bash#api-reference defp tags(aggregation_key, release, current_version, _timestamp) do [ {:aggregation_key, aggregation_key}, {:tags, ["#{aggregation_key}", "#{release}", "vsn-#{current_version}"]} ] end defp install_alarm_handler(alarm_handler, dd_alarm_handler) do case Enum.member?(:gen_event.which_handlers(alarm_handler), dd_alarm_handler) do true -> :ok _ -> alarm_handler.add_alarm_handler(dd_alarm_handler, [self()]) end end end ================================================ FILE: apps/omg_status/lib/omg_status/datadog_event/alarm_handler.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.DatadogEvent.AlarmHandler do @moduledoc """ Is notified of raised and cleared alarms and casts them to AlarmConsumer process. """ require Logger def init([reporter]) do {:ok, reporter} end def handle_call(_request, reporter), do: {:ok, :ok, reporter} def handle_event({:set_alarm, _alarm_details} = alarm, reporter) do :ok = GenServer.cast(reporter, alarm) {:ok, reporter} end def handle_event({:clear_alarm, _alarm_details} = alarm, reporter) do :ok = GenServer.cast(reporter, alarm) {:ok, reporter} end def handle_event(event, reporter) do _ = Logger.info("#{__MODULE__} got event: #{inspect(event)}. Ignoring.") {:ok, reporter} end end ================================================ FILE: apps/omg_status/lib/omg_status/metric/datadog.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Metric.Datadog do @moduledoc """ Datadog connection wrapper """ # we want to override Statix in :test # because we don't want to send metrics in unittests case Application.get_env(:omg_status, :environment) do :test -> use OMG.Status.Metric.Statix _ -> use Statix, runtime_config: true end use GenServer require Logger def start_link(), do: GenServer.start_link(__MODULE__, [], []) def init(_opts) do _ = Process.flag(:trap_exit, true) _ = Logger.info("Starting #{inspect(__MODULE__)} and connecting to Datadog.") __MODULE__.connect() _ = Logger.info("Connection opened #{inspect(current_conn())}") {:ok, current_conn()} end def handle_info({:EXIT, port, reason}, %Statix.Conn{sock: __MODULE__} = state) do _ = Logger.error("Port in #{inspect(__MODULE__)} #{inspect(port)} exited with reason #{reason}") {:stop, :normal, state} end end ================================================ FILE: apps/omg_status/lib/omg_status/metric/event.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Metric.Event do @moduledoc """ A centralised repository of all emitted event types with description. """ @services [ :challenges_responds_processor, :competitor_processor, :depositor, :exit_challenger, :exit_finalizer, :exit_processor, :exiter, :ife_exit_finalizer, :in_flight_exit, :in_flight_exit_processor, :in_flight_exit_deleted_processor, :piggyback, :piggyback_challenges_processor, :piggyback_processor, :block_queue ] @doc """ :transaction_submission - Child Chain API's received transaction submission. :transaction_submission_success - Child Chain API's successful processing of transaction submission. :transaction_submission_failed - Child Chain API's failed processing of transaction submission. :transaction_submission_failed - Childchain OMG.Watcher.State mempool transactions :pending_transactions - Childchain OMG.Watcher.State mempool transactions :block_transactions - Childchain OMG.Watcher.State transactions in formed block :block_submission_attempt - Childchain Block submission attempted :block_submission_success Childchain Block successfully submitted :block_submission_gas Child Chain Block queue gas usage metric :block_queue_blknum_submitting - Child Chain BlockQueue's blknum of the block being submitted :block_queue_blknum_submitted - Child Chain BlockQueue's blknum of the block submitted :block_queue_num_blocks_stalled - Child Chain BlockQueue's number of blocks currently being submitted and stalled :authority_balance - Child Chain authority address balance :balance - OMG.Watcher.State balance per currency :unique_users - OMG.Watcher.State number of unique_users in the system :block_getter_message_queue_len - OMG.Watcher.BlockGetter message queue length :watcher_exit_processor_message_queue_len - OMG.Watcher.ExitProcessor message queue length :eventer_message_queue_len - OMG.Watcher.Eventer message queue length :db_message_queue_len - OMG.DB server implementation (OMG.DB.LevelDB.Server, or OMG.DB.RocksDB.Server,) message queue length :write - OMG.DB KV layer has three types of actions: write, read, multiread :read - OMG.DB KV layer has three types of actions: write, read, multiread :multiread - OMG.DB KV layer has three types of actions: write, read, multiread @services - We're interested in the events queue length that particular OMG.Watcher.EthereumEventListener service process """ def name(:transaction_submission), do: "transaction_submission" def name(:transaction_submission_success), do: "transaction_submission_success" def name(:transaction_submission_failed), do: "transaction_submission_failed" def name(:pending_transactions), do: "pending_transactions" def name(:block_transactions), do: "block_transactions" def name(:block_submission_attempt), do: "block_submission_attempt" def name(:block_submission_success), do: "block_submission_success" def name(:block_submission_gas), do: "block_submission_gas" def name(:block_queue_blknum_submitting), do: "block_queue_blknum_submitting" def name(:block_queue_blknum_submitted), do: "block_queue_blknum_submitted" def name(:block_queue_num_blocks_stalled), do: "block_queue_num_blocks_stalled" def name(:authority_balance), do: "authority_balance" def name(:balance), do: "balance" def name(:unique_users), do: "unique_users" def name(:block_getter_message_queue_len), do: "block_getter_message_queue_len" def name(:watcher_exit_processor_message_queue_len), do: "watcher_exit_processor_message_queue_len" def name(:eventer_message_queue_len), do: "eventer_message_queue_len" def name(:db_message_queue_len), do: "db_message_queue_len" def name(:write), do: "db_write" def name(:read), do: "db_read" def name(:multiread), do: "db_multiread" @doc """ :events - We're interested in the events queue length that particular OMG.Watcher.EthereumEventListener service process is handling. message_queue_len - We're interested in the message queue length of particular OMG.Watcher.EthereumEventListener service process """ def name(service, :events) when service in @services, do: events_name(service) def name(service, :message_queue_len) when service in @services, do: message_queue_len_name(service) defp events_name(:depositor), do: "depositor_ethereum_events" defp events_name(:in_flight_exit), do: "in_flight_exit_ethereum_events" defp events_name(:piggyback), do: "piggyback_ethereum_events" defp events_name(:exiter), do: "exiter_ethereum_events" defp events_name(:exit_processor), do: "exit_processor_ethereum_events" defp events_name(:exit_finalizer), do: "exit_finalizer_ethereum_events" defp events_name(:exit_challenger), do: "exit_challenger_ethereum_events" defp events_name(:in_flight_exit_processor), do: "in_flight_exit_processor_ethereum_events" defp events_name(:in_flight_exit_deleted_processor), do: "in_flight_exit_deleted_processor_ethereum_events" defp events_name(:piggyback_processor), do: "piggyback_processor_ethereum_events" defp events_name(:competitor_processor), do: "competitor_processor_ethereum_events" defp events_name(:challenges_responds_processor), do: "challenges_responds_processor_ethereum_events" defp events_name(:piggyback_challenges_processor), do: "piggyback_challenges_processor_ethereum_events" defp events_name(:ife_exit_finalizer), do: "ife_exit_finalizer_ethereum_events" defp message_queue_len_name(:block_queue), do: "block_queue_message_queue_len" defp message_queue_len_name(:depositor), do: "depositor_message_queue_len" defp message_queue_len_name(:in_flight_exit), do: "in_flight_exit_message_queue_len" defp message_queue_len_name(:piggyback), do: "piggyback_message_queue_len" defp message_queue_len_name(:exiter), do: "exiter_message_queue_len" defp message_queue_len_name(:exit_processor), do: "exit_processor_message_queue_len" defp message_queue_len_name(:exit_finalizer), do: "exit_finalizer_message_queue_len" defp message_queue_len_name(:exit_challenger), do: "exit_challenger_message_queue_len" defp message_queue_len_name(:in_flight_exit_processor), do: "in_flight_exit_processor_message_queue_len" defp message_queue_len_name(:piggyback_processor), do: "piggyback_processor_message_queue_len" defp message_queue_len_name(:competitor_processor), do: "competitor_processor_message_queue_len" defp message_queue_len_name(:challenges_responds_processor), do: "challenges_responds_processor_message_queue_len" defp message_queue_len_name(:piggyback_challenges_processor), do: "piggyback_challenges_processor_message_queue_len" defp message_queue_len_name(:ife_exit_finalizer), do: "ife_exit_finalizer_message_queue_len" end ================================================ FILE: apps/omg_status/lib/omg_status/metric/statix.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Metric.Statix do @moduledoc """ Useful for overwritting Statix behaviour. """ defmacro __using__(_opts) do quote location: :keep do @behaviour Statix def connect(), do: :ok def increment(_), do: :ok def increment(_, _, options \\ []), do: :ok def decrement(_, val \\ 1, options \\ []), do: :ok def gauge(_, val, options \\ []), do: :ok def histogram(_, val, options \\ []), do: :ok def timing(_, val, options \\ []), do: :ok def measure(key, options \\ [], fun), do: :ok def set(key, val, options \\ []), do: :ok def event(key, val, options), do: :ok def service_check(key, val, options), do: :ok def current_conn(), do: %Statix.Conn{sock: __MODULE__} end end end ================================================ FILE: apps/omg_status/lib/omg_status/metric/telemetry.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Metric.Telemetry do @moduledoc """ Metrics handler to send telemetry events to Datadog """ use Supervisor import Telemetry.Metrics def start_link(arg) do Supervisor.start_link(__MODULE__, arg, name: __MODULE__) end def init(arg) do service = Keyword.fetch!(arg, :release) version = Keyword.fetch!(arg, :current_version) dd_host = Application.get_env(:statix, :host) dd_port = Application.get_env(:statix, :port) children = [ { TelemetryMetricsStatsd, metrics: metrics(), global_tags: [ service: service, version: version ], host: dd_host, port: dd_port, prefix: "elixir", formatter: :datadog } ] Supervisor.init(children, strategy: :one_for_one) end defp metrics() do [ # Phoenix Metrics summary( "phoenix.endpoint.stop.duration", tags: [:service, :version], unit: {:native, :millisecond} ), summary( "phoenix.router_dispatch.stop.duration", tags: [:service, :version, :route], unit: {:native, :millisecond} ), summary( "phoenix.router_dispatch.exception.duration", tags: [:service, :version, :kind], unit: {:native, :millisecond} ), summary( "phoenix.error_rendered.duration", tags: [:service, :version, :kind], unit: {:native, :millisecond} ), # Custom web metrics counter( "web.fallback.error", tags: [:service, :version, :route, :error_code] ) ] end end ================================================ FILE: apps/omg_status/lib/omg_status/metric/tracer.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Metric.Tracer do @moduledoc """ Trace requests and reports information to Datadog via Spandex """ use Spandex.Tracer, otp_app: :omg_status end ================================================ FILE: apps/omg_status/lib/omg_status/metric/vmstats_sink.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Metric.VmstatsSink do @moduledoc """ Interface implementation. """ alias OMG.Status.Metric.Datadog @type vm_stat :: {:vmstats_sup, :start_link, [any(), ...]} @behaviour :vmstats_sink @doc """ Returns child_specs for the given metric setup, to be included e.g. in Supervisor's children. """ @spec prepare_child() :: %{id: :vmstats_sup, start: vm_stat()} def prepare_child() do %{id: :vmstats_sup, start: {:vmstats_sup, :start_link, [__MODULE__, base_key()]}} end defp base_key(), do: Application.get_env(:vmstats, :base_key) # statix currently does not support `count` or `monotonic_count`, only increment and decrement # because of that, we're sending counters as gauges def collect(:counter, key, value), do: _ = Datadog.gauge(key, value) def collect(:gauge, key, value), do: _ = Datadog.gauge(key, value) def collect(:timing, key, value), do: _ = Datadog.timing(key, value) end ================================================ FILE: apps/omg_status/lib/omg_status/monitor/memory_monitor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Monitor.MemoryMonitor do @moduledoc """ Monitors and raises the :system_memory_too_high alarm when the system memory reaches the specified threshold. Intentionally raising a different alarm name from :memsup (which uses :system_memory_high_watermark) so there is no ambiguity to which module is responsible for which alarm. See http://erlang.org/pipermail/erlang-questions/2006-September/023144.html """ use GenServer require Logger @type t :: %__MODULE__{ alarm_module: module(), memsup_module: module(), interval_ms: pos_integer(), threshold: float(), raised: boolean(), timer_ref: reference() | nil } defstruct alarm_module: nil, memsup_module: nil, interval_ms: nil, threshold: 1.0, raised: false, timer_ref: nil def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end # monitor init def init([_ | _] = opts) do _ = Logger.info("Starting #{inspect(__MODULE__)}.") install_alarm_handler() alarm_module = Keyword.fetch!(opts, :alarm_module) memsup_module = Keyword.fetch!(opts, :memsup_module) interval_ms = Keyword.fetch!(opts, :interval_ms) threshold = Keyword.fetch!(opts, :threshold) state = %__MODULE__{ alarm_module: alarm_module, memsup_module: memsup_module, interval_ms: interval_ms, threshold: threshold } {:ok, state, {:continue, :first_check}} end # gen_event init def init(_args) do {:ok, %{}} end # We want the first check immediately upon start, but we cannot do it while the monitor # is not fully initialized, so we need to trigger it in a :continue instruction. def handle_continue(:first_check, state) do _ = send(self(), :check) {:noreply, state} end def handle_info(:check, state) do exceed_threshold? = system_memory_exceed_threshold?(state.memsup_module, state.threshold) _ = raise_clear(state.alarm_module, state.raised, exceed_threshold?) {:ok, timer_ref} = :timer.send_after(state.interval_ms, :check) {:noreply, %{state | timer_ref: timer_ref}} end def handle_cast(:set_alarm, state) do {:noreply, %{state | raised: true}} end def handle_cast(:clear_alarm, state) do {:noreply, %{state | raised: false}} end # # gen_event handlers # def handle_call(_request, state), do: {:ok, :ok, state} def handle_event({:set_alarm, {:system_memory_too_high, %{reporter: __MODULE__}}}, state) do _ = Logger.warn("System memory usage is too high. :system_memory_too_high alarm raised.") :ok = GenServer.cast(__MODULE__, :set_alarm) {:ok, state} end def handle_event({:clear_alarm, {:system_memory_too_high, %{reporter: __MODULE__}}}, state) do _ = Logger.warn("System memory usage went below threshold. :system_memory_too_high alarm cleared.") :ok = GenServer.cast(__MODULE__, :clear_alarm) {:ok, state} end def handle_event(event, state) do _ = Logger.info("#{__MODULE__} got event: #{inspect(event)}. Ignoring.") {:ok, state} end # # Memory-checking logic # defp system_memory_exceed_threshold?(memsup_module, threshold) do memory = get_memory(memsup_module) used = memory.total - (memory.free + memory.buffered + memory.cached) used_ratio = used / memory.total used_ratio > threshold end defp get_memory(memsup_module) do data = memsup_module.get_system_memory_data() %{ total: Keyword.fetch!(data, :total_memory), free: Keyword.fetch!(data, :free_memory), buffered: Keyword.get(data, :buffered_memory, 0), cached: Keyword.get(data, :cached_memory, 0) } end # # Alarm management # # if an alarm is raised, we don't have to raise it again. # if an alarm is cleared, we don't need to clear it again # we want to avoid pushing events again @spec raise_clear(module(), boolean(), boolean()) :: :ok | :duplicate defp raise_clear(alarm_module, false, true) do alarm_module.set(alarm_module.system_memory_too_high(__MODULE__)) end defp raise_clear(alarm_module, true, false) do alarm_module.clear(alarm_module.system_memory_too_high(__MODULE__)) end defp raise_clear(_alarm_module, true, true), do: :ok defp raise_clear(_alarm_module, false, false), do: :ok defp install_alarm_handler() do case Enum.member?(:gen_event.which_handlers(:alarm_handler), __MODULE__) do true -> :ok _ -> :alarm_handler.add_alarm_handler(__MODULE__) end end end ================================================ FILE: apps/omg_status/lib/omg_status/monitor/statsd_monitor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Monitor.StatsdMonitor do @moduledoc """ This module is a custom implemented supervisor that monitors all it's chilldren. """ use GenServer require Logger @type t :: %__MODULE__{ alarm_module: module(), child_module: module(), interval: pos_integer(), pid: pid(), raised: boolean(), tref: reference() | nil } defstruct alarm_module: nil, child_module: nil, interval: Application.get_env(:omg_status, :statsd_reconnect_backoff_ms), pid: nil, raised: false, tref: nil def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init([_ | _] = opts) do _ = Logger.info("Starting #{inspect(__MODULE__)}.") install_alarm_handler() alarm_module = Keyword.fetch!(opts, :alarm_module) child_module = Keyword.fetch!(opts, :child_module) false = Process.flag(:trap_exit, true) {:ok, pid} = apply(child_module, :start_link, []) state = %__MODULE__{ alarm_module: alarm_module, child_module: child_module, pid: pid } _ = raise_clear(alarm_module, state.raised, Process.alive?(pid)) {:ok, state} end # gen_event init def init(_args) do {:ok, %{}} end def handle_info({:EXIT, _, reason}, state) do _ = Logger.error("Monitored datadog connection process from statix died of reason #{inspect(reason)} ") _ = state.alarm_module.set(state.alarm_module.statsd_client_connection(__MODULE__)) _ = :timer.cancel(state.tref) {:ok, tref} = :timer.send_after(state.interval, :connect) {:noreply, %{state | raised: true, tref: tref}} end def handle_info(:connect, state) do {:ok, pid} = apply(state.child_module, :start_link, []) alive = Process.alive?(pid) _ = raise_clear(state.alarm_module, state.raised, alive) {:noreply, %{state | pid: pid}} end def handle_cast(:clear_alarm, state) do {:noreply, %{state | raised: false}} end def handle_cast(:set_alarm, state) do {:noreply, %{state | raised: true}} end def terminate(_, _), do: :ok # # gen_event # def handle_call(_request, state), do: {:ok, :ok, state} def handle_event({:clear_alarm, {:statsd_client_connection, %{reporter: __MODULE__}}}, state) do _ = Logger.warn("Established connection to the client. :statsd_client_connection alarm clearead.") :ok = GenServer.cast(__MODULE__, :clear_alarm) {:ok, state} end def handle_event({:set_alarm, {:statsd_client_connection, %{reporter: __MODULE__}}}, state) do _ = Logger.warn("Connection dropped raising :statsd_client_connection alarm.") :ok = GenServer.cast(__MODULE__, :set_alarm) {:ok, state} end # flush def handle_event(event, state) do _ = Logger.info("#{__MODULE__} got event: #{inspect(event)}. Ignoring.") {:ok, state} end # if an alarm is raised, we don't have to raise it again. # if an alarm is cleared, we don't need to clear it again # we want to avoid pushing events again @spec raise_clear(module(), boolean(), boolean()) :: :ok | :duplicate defp raise_clear(_alarm_module, true, false), do: :ok defp raise_clear(alarm_module, false, false), do: alarm_module.set(alarm_module.statsd_client_connection(__MODULE__)) defp raise_clear(alarm_module, true, _), do: alarm_module.clear(alarm_module.statsd_client_connection(__MODULE__)) defp raise_clear(_alarm_module, false, _), do: :ok defp install_alarm_handler() do case Enum.member?(:gen_event.which_handlers(:alarm_handler), __MODULE__) do true -> :ok _ -> :alarm_handler.add_alarm_handler(__MODULE__) end end end ================================================ FILE: apps/omg_status/lib/omg_status/release_tasks/set_application.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.ReleaseTasks.SetApplication do @moduledoc false @behaviour Config.Provider def init(args) do args end def load(config, release: release, current_version: current_version) do Config.Reader.merge(config, omg_status: [release: release, current_version: current_version]) end end ================================================ FILE: apps/omg_status/lib/omg_status/release_tasks/set_logger.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.ReleaseTasks.SetLogger do @moduledoc false @behaviour Config.Provider require Logger @app :logger @default_backend Ink def init(args) do args end def load(config, _args) do _ = on_load() logger_backends = Application.get_env(@app, :backends, persistent: true) logger_backend = get_logger_backend() remove = case logger_backend do :console -> Ink _ -> :console end backends = logger_backends |> Kernel.--([remove]) |> Enum.concat([logger_backend]) |> Enum.uniq() Config.Reader.merge(config, logger: [backends: backends]) end defp get_logger_backend() do logger = "LOGGER_BACKEND" |> get_env() |> validate_string(@default_backend) _ = Logger.info("CONFIGURATION: App: #{@app} Key: LOGGER_BACKEND Value: #{inspect(logger)}.") logger end defp get_env(key), do: System.get_env(key) defp validate_string(nil, default), do: default defp validate_string(value, default), do: do_validate_string(String.upcase(value), default) defp do_validate_string("CONSOLE", _default), do: :console defp do_validate_string("INK", _default), do: Ink defp do_validate_string(_, default), do: default defp on_load() do _ = Application.load(:logger) end end ================================================ FILE: apps/omg_status/lib/omg_status/release_tasks/set_sentry.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.ReleaseTasks.SetSentry do @moduledoc false @behaviour Config.Provider require Logger @app :sentry def init(args) do args end def load(config, release: release, current_version: current_version) do _ = Application.ensure_all_started(:logger) app_env = get_app_env() sentry_dsn = System.get_env("SENTRY_DSN") case is_binary(sentry_dsn) do true -> hostname = get_hostname() _ = Logger.warn( "Sentry configuration provided. Enabling Sentry with APP ENV #{inspect(app_env)}, with SENTRY_DSN #{ inspect(sentry_dsn) }, with HOSTNAME (server_name) #{inspect(hostname)}" ) Config.Reader.merge( config, sentry: [ dsn: sentry_dsn, environment_name: app_env, included_environments: [app_env], server_name: hostname, tags: %{ application: release, eth_network: get_env("ETHEREUM_NETWORK"), eth_node: get_rpc_client_type(), current_version: "vsn-#{current_version}", app_env: "#{app_env}", hostname: "#{hostname}" } ] ) _ -> _ = Logger.warn( "Sentry configuration not provided. Disabling Sentry. If you want it enabled provide APP_ENV and SENTRY_DSN." ) Config.Reader.merge(config, sentry: [included_environments: []]) end end defp get_app_env() do env = validate_string(get_env("APP_ENV"), Application.get_env(@app, :environment_name)) _ = Logger.info("CONFIGURATION: App: #{@app} Key: APP_ENV Value: #{inspect(env)}.") env end defp get_hostname() do hostname = validate_string(get_env("HOSTNAME"), Application.get_env(@app, :server_name)) _ = Logger.info("CONFIGURATION: App: #{@app} Key: HOSTNAME, server_name Value: #{inspect(hostname)}.") hostname end defp get_rpc_client_type() do rpc_client_type = validate_rpc_client_type(get_env("ETH_NODE"), Application.get_env(@app, :tags)[:eth_node]) _ = Logger.info("CONFIGURATION: App: #{@app} Key: ETH_NODE Value: #{inspect(rpc_client_type)}.") rpc_client_type end defp get_env(key), do: System.get_env(key) defp validate_rpc_client_type(value, _default) when is_binary(value), do: to_rpc_client_type(String.upcase(value)) defp validate_rpc_client_type(_value, default), do: default defp to_rpc_client_type("GETH"), do: :geth defp to_rpc_client_type("PARITY"), do: :parity defp to_rpc_client_type("INFURA"), do: :infura defp to_rpc_client_type(_), do: exit("ETH_NODE must be either GETH, PARITY or INFURA.") defp validate_string(value, _default) when is_binary(value), do: value defp validate_string(_, default), do: default end ================================================ FILE: apps/omg_status/lib/omg_status/release_tasks/set_tracer.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.ReleaseTasks.SetTracer do @moduledoc false @behaviour Config.Provider alias OMG.Status.Metric.Tracer require Logger @app :omg_status def init(args) do args end def load(config, args) do _ = on_load() adapter = Keyword.get(args, :system_adapter, System) _ = Process.put(:system_adapter, adapter) dd_disabled = get_dd_disabled() tracer_config = @app |> Application.get_env(Tracer) |> Keyword.put(:disabled?, dd_disabled) {app_env, tracer_config} = case dd_disabled do false -> app_env = get_app_env() {app_env, Keyword.put(tracer_config, :env, app_env)} true -> app_env = "" {app_env, Keyword.put(tracer_config, :env, app_env)} end release = Keyword.get(args, :release) tags = ["application:#{release}", "app_env:#{app_env}", "hostname:#{get_hostname()}"] spandex_datadog_host = Application.get_env(:spandex_datadog, :host) spandex_datadog_port = Application.get_env(:spandex_datadog, :port) statix_default_port = Application.get_env(:statix, :port) statix_default_hostname = Application.get_env(:statix, :host) batch_size = get_batch_size() sync_threshold = get_sync_threshold() Config.Reader.merge(config, spandex_datadog: [ host: get_dd_hostname(spandex_datadog_host), port: get_dd_spandex_port(spandex_datadog_port), batch_size: batch_size, sync_threshold: sync_threshold ], statix: [ port: get_dd_port(statix_default_port), host: get_dd_hostname(statix_default_hostname), tags: tags ], omg_status: [{Tracer, tracer_config}] ) end defp get_hostname() do hostname = validate_hostname(get_env("HOSTNAME")) _ = Logger.info("CONFIGURATION: App: #{@app} Key: HOSTNAME Value: #{inspect(hostname)}.") hostname end defp get_dd_disabled() do dd_disabled? = validate_bool(get_env("DD_DISABLED"), Application.get_env(@app, Tracer)[:disabled?]) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DD_DISABLED Value: #{inspect(dd_disabled?)}.") dd_disabled? end defp get_app_env() do env = validate_string(get_env("APP_ENV"), Application.get_env(@app, Tracer)[:env]) _ = Logger.info("CONFIGURATION: App: #{@app} Key: APP_ENV Value: #{inspect(env)}.") env end defp get_dd_hostname(default) do dd_hostname = validate_string(get_env("DD_HOSTNAME"), default) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DD_HOSTNAME Value: #{inspect(dd_hostname)}.") dd_hostname end defp get_dd_port(default) do dd_port = validate_integer(get_env("DD_PORT"), default) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DD_PORT Value: #{inspect(dd_port)}.") dd_port end defp get_dd_spandex_port(default) do dd_spandex_port = validate_integer(get_env("DD_APM_PORT"), default) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DD_APM_PORT Value: #{inspect(dd_spandex_port)}.") dd_spandex_port end def get_batch_size() do batch_size = validate_integer(get_env("BATCH_SIZE"), Application.get_env(:spandex_datadog, :batch_size)) _ = Logger.info("CONFIGURATION: App: #{@app} Key: BATCH_SIZE Value: #{inspect(batch_size)}.") batch_size end defp validate_hostname(value) when is_binary(value), do: value defp validate_hostname(_), do: exit("HOSTNAME is not set correctly.") def get_sync_threshold() do sync_threshold = Application.get_env(:spandex_datadog, :sync_threshold) sync_threshold = validate_integer(get_env("SYNC_THRESHOLD"), sync_threshold) _ = Logger.info("CONFIGURATION: App: #{@app} Key: SYNC_THRESHOLD Value: #{inspect(sync_threshold)}.") sync_threshold end defp get_env(key) do Process.get(:system_adapter).get_env(key) end defp validate_bool(value, _default) when is_binary(value), do: to_bool(String.upcase(value)) defp validate_bool(_, default), do: default defp to_bool("TRUE"), do: true defp to_bool("FALSE"), do: false defp to_bool(_), do: exit("DD_DISABLED either true or false.") defp validate_string(value, _default) when is_binary(value), do: value defp validate_string(_, default), do: default defp validate_integer(value, _default) when is_binary(value), do: String.to_integer(value) defp validate_integer(_, default), do: default defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(@app) _ = Application.load(:spandex_datadog) _ = Application.load(:statix) end end ================================================ FILE: apps/omg_status/lib/omg_status/sentry_filter.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.SentryFilter do @moduledoc """ Sentry callback for filtering events. """ @behaviour Sentry.EventFilter # when the development environment restarts it lacks network access # something to do with Cloud DNS def exclude_exception?(%MatchError{term: {:error, :nxdomain}}, _), do: true # Ignoring 406 status code invalid headers exception def exclude_exception?(%Phoenix.NotAcceptableError{plug_status: 406}, _), do: true def exclude_exception?(%Plug.Parsers.RequestTooLargeError{}, _), do: true def exclude_exception?(%CaseClauseError{term: {:error, :econnrefused}}, _), do: true def exclude_exception?(_, _), do: false end ================================================ FILE: apps/omg_status/lib/status.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status do @moduledoc """ An interface towards the node health for RPC requests. For the RPC to work we need Ethereum client connectivity and booting should not be in progress. """ alias OMG.Status.Alert.AlarmHandler # this can be read as # if ETS table has a tuple entry in form of {:boot_in_progress, 0}, return false # if ETS table has a tuple entry in form of {:boot_in_progress, 1}, return true @health_match List.flatten( for n <- [ :boot_in_progress, :ethereum_connection_error, :ethereum_stalled_sync, :main_supervisor_halted ], do: [{{n, 0}, [], [false]}, {{n, 1}, [], [true]}] ) @spec is_healthy() :: boolean() def is_healthy() do # the selector returns true when an alarm is raised # the selector returns false when an alarm is not raised # one alarm is enough to say we're not healthy not Enum.member?(:ets.select(AlarmHandler.table_name(), @health_match), true) end end ================================================ FILE: apps/omg_status/mix.exs ================================================ defmodule OMG.Status.Mixfile do use Mix.Project def project() do [ app: :omg_status, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end # Specifies which paths to compile per environment. defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] def application() do [ mod: {OMG.Status.Application, []}, start_phases: [{:install_alarm_handler, []}], extra_applications: [:logger, :sasl, :os_mon, :statix, :telemetry], included_applications: [:vmstats] ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end defp deps(), do: [ {:telemetry, "~> 0.4.1"}, {:telemetry_metrics, "~> 0.4"}, {:telemetry_metrics_statsd, "~> 0.3.0"}, {:sentry, "~> 8.0"}, {:statix, git: "https://github.com/omgnetwork/statix", branch: "otp-21.3.8.4-support-global-tag-patch"}, {:spandex_datadog, "~> 1.0"}, {:decorator, "~> 1.2"}, {:vmstats, "~> 2.3", runtime: false}, {:ink, "~> 1.1"}, # umbrella apps {:omg_bus, in_umbrella: true} ] end ================================================ FILE: apps/omg_status/test/omg_status/alert/alarm_printer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Alert.AlarmPrinterTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.Status.AlarmPrinter @moduletag :common setup do {:ok, alarm_printer} = AlarmPrinter.start_link(alarm_module: __MODULE__.Alarm, name: String.to_atom("test-#{:rand.uniform(1000)}")) %{alarm_printer: alarm_printer} end test "if the process has a previous backoff set", %{alarm_printer: alarm_printer} do assert capture_log(fn -> :erlang.trace(alarm_printer, true, [:receive]) %{previous_backoff: previous_backoff} = :sys.get_state(alarm_printer) assert is_number(previous_backoff) end) end test "that the process sends itself a message after startup", %{alarm_printer: alarm_printer} do assert capture_log(fn -> %{previous_backoff: previous_backoff} = :sys.get_state(alarm_printer) :erlang.trace(alarm_printer, true, [:send]) :ok = Process.sleep(previous_backoff) assert_receive {:trace, _, :send, {:notify, {:warn, _, {Logger, "An alarm was raised 1", {_, _}, _}}}, Logger} assert_receive {:trace, _, :send, {:notify, {:warn, _, {Logger, "An alarm was raised 2", {_, _}, _}}}, Logger} assert_receive {:trace, _, :send, {:notify, {:warn, _, {Logger, "An alarm was raised 3", {_, _}, _}}}, Logger} end) end test "that the process increases the backoff", %{alarm_printer: alarm_printer} do assert capture_log(fn -> %{previous_backoff: previous_backoff} = :sys.get_state(alarm_printer) :erlang.trace(alarm_printer, true, [:send]) :ok = Process.sleep(previous_backoff) assert_receive {:trace, _, :send, {:notify, {:warn, _, {Logger, "An alarm was raised 1", {_, _}, _}}}, Logger} assert_receive {:trace, _, :send, {:notify, {:warn, _, {Logger, "An alarm was raised 2", {_, _}, _}}}, Logger} assert_receive {:trace, _, :send, {:notify, {:warn, _, {Logger, "An alarm was raised 3", {_, _}, _}}}, Logger} %{previous_backoff: previous_backoff_1} = :sys.get_state(alarm_printer) assert previous_backoff_1 > previous_backoff end) end defmodule Alarm do def all(), do: [1, 2, 3] end end ================================================ FILE: apps/omg_status/test/omg_status/datadog_event/alarm_consumer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.DatadogEvent.AlarmConsumerTest do @moduledoc false use ExUnit.Case, async: true alias OMG.Status.DatadogEvent.AlarmConsumer @alarm_details %{test_pid: :test_case_1} setup_all do {:ok, _pid} = :gen_event.start_link({:local, __MODULE__.DatadogAlarmMock}) :ok end setup do start_supervised( AlarmConsumer.prepare_child( alarm_handler: __MODULE__.DatadogAlarmMock, dd_alarm_handler: __MODULE__.DatadogAlarmHandlerMock, release: "child_chain_or_watcher", current_version: "test-123", publisher: __MODULE__.DatadogEventMock ) ) :ok end test "if a event message put on omg bus is consumed by the event consumer and published on the publisher interface" do %{test_pid: test_pid_name} = @alarm_details true = Process.register(self(), test_pid_name) alarm = {:ethereum_connection_error, @alarm_details} __MODULE__.DatadogAlarmMock.set_alarm(alarm) assert_receive :event end defmodule DatadogEventMock do # we've put the this test process identifieer into the alarm details # message is a binary string "%{test_pid: :test_case_1}" def event(_title, "%{test_pid: :" <> rest = _message, _options) do <> = rest # test_pid_name should now be ":test_case_1" Kernel.send(String.to_existing_atom(test_pid_name), :event) end end defmodule DatadogAlarmHandlerMock do def init([alarm_consumer_process]) do {:ok, alarm_consumer_process} end def handle_event({:set_alarm, {:ethereum_connection_error, _details}} = alarm, alarm_consumer_process) do :ok = GenServer.cast(alarm_consumer_process, alarm) {:ok, alarm_consumer_process} end def handle_event({:clear_alarm, {:ethereum_connection_error, _details}} = alarm, alarm_consumer_process) do :ok = GenServer.cast(alarm_consumer_process, alarm) {:ok, alarm_consumer_process} end end defmodule DatadogAlarmMock do def init(_) do {:ok, []} end def add_alarm_handler(module, args) do :gen_event.add_handler(__MODULE__, module, args) end def set_alarm(alarm) do :gen_event.notify(__MODULE__, {:set_alarm, alarm}) end def clear_alarm(alarm_id) do :gen_event.notify(__MODULE__, {:clear_alarm, alarm_id}) end end end ================================================ FILE: apps/omg_status/test/omg_status/integration/alarms_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Alert.AlarmTest do use ExUnit.Case, async: false alias OMG.Status.Alert.Alarm @moduletag :integration @moduletag :common @moduletag timeout: 240_000 setup_all do {:ok, apps} = Application.ensure_all_started(:omg_status) on_exit(fn -> apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) :ok end setup do Alarm.clear_all() end test "raise and clear alarm based only on id" do alarm = {:id, "details"} :alarm_handler.set_alarm(alarm) assert get_alarms([:id]) == [alarm] :alarm_handler.clear_alarm(alarm) assert get_alarms([:id]) == [] end test "raise and clear alarm based on full alarm" do alarm = {:id5, %{a: 12, b: 34}} :alarm_handler.set_alarm(alarm) assert get_alarms([:id5]) == [alarm] :alarm_handler.clear_alarm({:id5, %{a: 12, b: 666}}) assert get_alarms([:id5]) == [alarm] :alarm_handler.clear_alarm(alarm) assert get_alarms([:id5]) == [] end test "adds and removes alarms" do # we *do* (unifying them under one app) want system alarms (like CPU, memory...) :alarm_handler.set_alarm({:some_system_alarm, "description_1"}) assert not Enum.empty?(get_alarms([:some_system_alarm])) Alarm.clear_all() Alarm.set(Alarm.ethereum_connection_error(__MODULE__)) assert Enum.count(get_alarms([:some_system_alarm, :ethereum_connection_error])) == 1 Alarm.set(Alarm.ethereum_connection_error(__MODULE__.SecondProcess)) assert Enum.count(get_alarms([:some_system_alarm, :ethereum_connection_error])) == 2 Alarm.clear(Alarm.ethereum_connection_error(__MODULE__)) assert Enum.count(get_alarms([:some_system_alarm, :ethereum_connection_error])) == 1 Alarm.clear_all() assert Enum.empty?(get_alarms([:some_system_alarm, :ethereum_connection_error])) == true end test "an alarm raise twice is reported once" do Alarm.set(Alarm.ethereum_connection_error(__MODULE__)) first_count = Enum.count(get_alarms([:ethereum_connection_error])) Alarm.set(Alarm.ethereum_connection_error(__MODULE__)) ^first_count = Enum.count(get_alarms([:ethereum_connection_error])) end test "memsup alarms" do # memsup set alarm :alarm_handler.set_alarm({:system_memory_high_watermark, []}) assert Enum.any?(Alarm.all(), &(elem(&1, 0) == :system_memory_high_watermark)) end # we need to filter them because of unwanted system alarms, like high memory threshold # so we send the alarms we want to find in the args defp get_alarms(ids), do: Enum.filter(Alarm.all(), fn {id, _desc} -> id in ids end) end ================================================ FILE: apps/omg_status/test/omg_status/metric/datadog_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Metric.DatadogTest do use ExUnit.Case, async: true alias OMG.Status.Metric.Datadog test "if exiting process/port sends an exit signal to the parent process" do parent = self() {:ok, _} = Task.start(fn -> {:ok, datadog_pid} = Datadog.start_link() port = Port.open({:spawn, "cat"}, [:binary]) true = Process.link(datadog_pid) send(parent, {:data, port, datadog_pid}) # we want to exit because the port forcefully closes # so this sleep shouldn't happen Process.sleep(10_000) end) receive do {:data, port, datadog_pid} -> :erlang.trace(datadog_pid, true, [:receive]) true = Process.exit(port, :portkill) assert_receive {:trace, ^datadog_pid, :receive, {:EXIT, _port, :portkill}} end end end ================================================ FILE: apps/omg_status/test/omg_status/monitor/memory_monitor_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Monitor.MemoryMonitorTest do use ExUnit.Case, async: true alias OMG.Status.Monitor.MemoryMonitor setup do {:ok, apps} = Application.ensure_all_started(:omg_status) {:ok, _} = __MODULE__.Alarm.start_link(self()) {:ok, _} = __MODULE__.Memsup.start_link() {:ok, monitor_pid} = MemoryMonitor.start_link( alarm_module: __MODULE__.Alarm, memsup_module: __MODULE__.Memsup, interval_ms: 10, threshold: 0.8 ) on_exit(fn -> apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) {:ok, %{monitor_pid: monitor_pid}} end test "raises an alarm if used memory is above the threshold" do set_memsup(total_memory: 1000, free_memory: 100, buffered_memory: 0, cached_memory: 0) assert_receive :got_raise_alarm end test "clears the alarm if used memory is below threshold", context do :sys.replace_state(context.monitor_pid, fn state -> %{state | raised: true} end) set_memsup(total_memory: 1000, free_memory: 201, buffered_memory: 0, cached_memory: 0) assert_receive :got_clear_alarm end test "raises an alarm if combined used memory is above the threshold" do set_memsup(total_memory: 1000, free_memory: 60, buffered_memory: 60, cached_memory: 60) assert_receive :got_raise_alarm end test "clears the alarm if combined used memory is below threshold", context do :sys.replace_state(context.monitor_pid, fn state -> %{state | raised: true} end) set_memsup(total_memory: 1000, free_memory: 70, buffered_memory: 70, cached_memory: 70) assert_receive :got_clear_alarm end test "works with :buffered_memory and :cached_memory values are not provided" do set_memsup(total_memory: 1000, free_memory: 100) assert_receive :got_raise_alarm end defp set_memsup(memory_data) do :sys.replace_state(__MODULE__.Memsup, fn _ -> memory_data end) end defmodule Alarm do use GenServer def start_link(parent) do GenServer.start_link(__MODULE__, [parent], name: __MODULE__) end def init([parent]) do {:ok, %{parent: parent}} end def system_memory_too_high(reporter) do {:system_memory_too_high, %{node: Node.self(), reporter: reporter}} end def set({:system_memory_too_high, _}) do GenServer.call(__MODULE__, :got_raise_alarm) end def clear({:system_memory_too_high, _}) do GenServer.call(__MODULE__, :got_clear_alarm) end def handle_call(:got_raise_alarm, _, state) do {:reply, send(state.parent, :got_raise_alarm), state} end def handle_call(:got_clear_alarm, _, state) do {:reply, send(state.parent, :got_clear_alarm), state} end end defmodule Memsup do use GenServer def start_link() do GenServer.start_link(__MODULE__, [], name: __MODULE__) end def init(_) do memory_data = [ total_memory: 1000, free_memory: 1000, buffered_memory: 0, cached_memory: 0 ] {:ok, memory_data} end def get_system_memory_data() do GenServer.call(__MODULE__, :get_system_memory_data) end def handle_call(:get_system_memory_data, _, state) do {:reply, state, state} end end end ================================================ FILE: apps/omg_status/test/omg_status/monitor/statsd_monitor_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.Monitor.StatsdMonitorTest do use ExUnit.Case, async: true alias OMG.Status.Monitor.StatsdMonitor setup do {:ok, apps} = Application.ensure_all_started(:omg_status) {:ok, alarm_process} = __MODULE__.Alarm.start(self()) {:ok, statsd_monitor} = StatsdMonitor.start_link(alarm_module: __MODULE__.Alarm, child_module: __MODULE__.StasdWrapper) on_exit(fn -> apps |> Enum.reverse() |> Enum.each(&Application.stop/1) Process.exit(alarm_process, :cleanup) Process.exit(statsd_monitor, :cleanup) Process.sleep(10) end) %{ alarm_process: alarm_process, statsd_monitor: statsd_monitor } end test "if exiting process/port sends an exit signal to the parent process", %{alarm_process: alarm_process} do :erlang.trace(alarm_process, true, [:receive]) %{pid: pid} = :sys.get_state(StatsdMonitor) true = Process.exit(pid, :testkill) assert_receive :got_raise_alarm end test "if exiting process/port sends an exit signal to the parent process 2", %{ alarm_process: alarm_process, statsd_monitor: _statsd_monitor } do %{pid: pid} = :sys.get_state(StatsdMonitor) :erlang.trace(alarm_process, true, [:receive]) true = Process.exit(pid, :testkill) assert_receive :got_raise_alarm assert_receive :got_clear_alarm end defmodule Alarm do use GenServer def start(parent) do GenServer.start(__MODULE__, [parent], name: __MODULE__) end def init([parent]) do {:ok, %{parent: parent}} end def statsd_client_connection(reporter), do: {:statsd_client_connection, %{node: Node.self(), reporter: reporter}} def set({:statsd_client_connection, _details}) do GenServer.call(__MODULE__, :got_raise_alarm) end def clear({:statsd_client_connection, _details}) do GenServer.call(__MODULE__, :got_clear_alarm) end def handle_call(:got_raise_alarm, _, state) do {:reply, send(state.parent, :got_raise_alarm), state} end def handle_call(:got_clear_alarm, _, state) do {:reply, send(state.parent, :got_clear_alarm), state} end end defmodule StasdWrapper do use GenServer def start_link() do GenServer.start_link(__MODULE__, [], []) end def init(_) do {:ok, %{}} end end end ================================================ FILE: apps/omg_status/test/omg_status/release_tasks/set_logger_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.ReleaseTasks.SetLoggerTest do use ExUnit.Case, async: true alias OMG.Status.ReleaseTasks.SetLogger @app :logger setup do :ok = System.delete_env("LOGGER_BACKEND") on_exit(fn -> :ok = System.delete_env("LOGGER_BACKEND") end) end test "if environment variables (INK) get applied in the configuration" do :ok = System.put_env("LOGGER_BACKEND", "INK") config = SetLogger.load([], []) backends = config |> Keyword.fetch!(:logger) |> Keyword.fetch!(:backends) assert Enum.member?(backends, Ink) == true end test "if environment variables (CONSOLE) get applied in the configuration" do # env var to console and asserting that Ink gets removed :ok = System.put_env("LOGGER_BACKEND", "conSole") config = SetLogger.load([], []) backends = config |> Keyword.fetch!(:logger) |> Keyword.fetch!(:backends) assert Enum.member?(backends, :console) == true end test "if environment variables are not present the default configuration gets used (INK)" do config = SetLogger.load([], []) backends = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:backends) assert Enum.member?(backends, :console) == false assert Enum.member?(backends, Ink) == true end end ================================================ FILE: apps/omg_status/test/omg_status/release_tasks/set_sentry_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.ReleaseTasks.SetSentryTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.Status.ReleaseTasks.SetSentry setup do on_exit(fn -> :ok = System.delete_env("SENTRY_DSN") :ok = System.delete_env("APP_ENV") :ok = System.delete_env("HOSTNAME") :ok = System.delete_env("ETHEREUM_NETWORK") :ok = System.delete_env("ETH_NODE") end) :ok end test "if environment variables get applied in the configuration" do dsn = "/dsn/dsn/dsn" yolo = "YOLO" server_name = "server name" network = "RINKEBY" current_version = "current_version" :ok = System.put_env("SENTRY_DSN", dsn) :ok = System.put_env("APP_ENV", yolo) :ok = System.put_env("HOSTNAME", server_name) :ok = System.put_env("ETHEREUM_NETWORK", network) capture_log(fn -> config = SetSentry.load([], release: :watcher, current_version: current_version) config_expect = [ sentry: [ dsn: dsn, environment_name: yolo, included_environments: [yolo], server_name: server_name, tags: %{ application: :watcher, eth_network: network, eth_node: :geth, current_version: "vsn-" <> current_version, app_env: yolo, hostname: server_name } ] ] assert config == config_expect end) end test "if sentry is disabled if there's no SENTRY DSN env var set" do capture_log(fn -> config = SetSentry.load([], release: :child_chain, current_version: "current_version") config_expect = [sentry: [included_environments: []]] assert config == config_expect end) end test "if faulty eth node exits" do :ok = System.put_env("ETH_NODE", "random random random") :ok = System.put_env("SENTRY_DSN", "/dsn/dsn/dsn") capture_log(fn -> assert catch_exit(SetSentry.load([], release: :child_chain, current_version: "current_version")) end) end end ================================================ FILE: apps/omg_status/test/omg_status/release_tasks/set_tracer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.ReleaseTasks.SetTracerTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.Status.Metric.Tracer alias OMG.Status.ReleaseTasks.SetTracer @app :omg_status setup do {:ok, pid} = __MODULE__.System.start_link([]) nil = Process.put(__MODULE__.System, pid) :ok end test "if environment variables get applied in the configuration" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUE") :ok = __MODULE__.System.put_env("APP_ENV", "YOLO") :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 3") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) disabled = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:disabled?) env = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:env) assert disabled == true # if it's disabled, env doesn't matter, so we set it to an empty string assert env == "" end) end test "if default configuration is used when there's no environment variables" do :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 3") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) # we set env to an empty string because disabled? is set to true! configuration = @app |> Application.get_env(Tracer) |> Keyword.put(:env, "") |> Enum.sort() tracer_config = config |> Keyword.get(@app) |> Keyword.get(Tracer) |> Enum.sort() assert configuration == tracer_config end) end test "if environment variables get applied in the statix configuration" do set_cluster = "cluster" set_port = "1919" set_hostname = "this is my tracer test 1" set_app_env = "test 1" set_disabled = "false" :ok = __MODULE__.System.put_env("DD_HOSTNAME", set_cluster) :ok = __MODULE__.System.put_env("DD_PORT", set_port) :ok = __MODULE__.System.put_env("HOSTNAME", set_hostname) :ok = __MODULE__.System.put_env("APP_ENV", set_app_env) :ok = __MODULE__.System.put_env("DD_DISABLED", set_disabled) assert capture_log(fn -> config = SetTracer.load([], release: :test_case_1, system_adapter: __MODULE__.System) port = config |> Keyword.fetch!(:statix) |> Keyword.fetch!(:port) host = config |> Keyword.fetch!(:statix) |> Keyword.fetch!(:host) tags = config |> Keyword.fetch!(:statix) |> Keyword.fetch!(:tags) assert host == set_cluster assert port == String.to_integer(set_port) assert Enum.member?(tags, "app_env:test 1") == true end) end test "if default statix configuration is used when there's no environment variables" do app_env = "test 2" hostname = "this is my tracer test 2" disabled = "false" :ok = __MODULE__.System.put_env("HOSTNAME", hostname) :ok = __MODULE__.System.put_env("APP_ENV", app_env) :ok = __MODULE__.System.put_env("DD_DISABLED", disabled) configuration = SetTracer.load([], release: :test_case_2, system_adapter: __MODULE__.System) tags = configuration |> Keyword.fetch!(:statix) |> Keyword.fetch!(:tags) assert tags == ["application:test_case_2", "app_env:#{app_env}", "hostname:#{hostname}"] end test "if environment variables get applied in the spandex_datadog configuration" do :ok = __MODULE__.System.put_env("DD_HOSTNAME", "cluster") :ok = __MODULE__.System.put_env("DD_APM_PORT", "1919") :ok = __MODULE__.System.put_env("BATCH_SIZE", "7000") :ok = __MODULE__.System.put_env("SYNC_THRESHOLD", "900") :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 4") capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) port = config |> Keyword.fetch!(:spandex_datadog) |> Keyword.fetch!(:port) host = config |> Keyword.fetch!(:spandex_datadog) |> Keyword.fetch!(:host) batch_size = config |> Keyword.fetch!(:spandex_datadog) |> Keyword.fetch!(:batch_size) sync_threshold = config |> Keyword.fetch!(:spandex_datadog) |> Keyword.fetch!(:sync_threshold) assert port == 1919 assert host == "cluster" assert batch_size == 7000 assert sync_threshold == 900 end) end test "if default spandex_datadog configuration is used when there's no environment variables" do :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 5") config = SetTracer.load([], system_adapter: __MODULE__.System) configuration = Application.get_all_env(:spandex_datadog) sorted_configuration = configuration |> Enum.sort() |> Keyword.drop([:http]) spandex_datadog_config = Keyword.fetch!(config, :spandex_datadog) assert sorted_configuration == Enum.sort(spandex_datadog_config) end test "if exit is thrown when faulty configuration is used" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUEeee") catch_exit(SetTracer.load([], system_adapter: __MODULE__.System)) end test "if exit is thrown when faulty configuration for hostname is used" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUE") :ok = __MODULE__.System.put_env("APP_ENV", "YOLO") assert catch_exit(SetTracer.load([], system_adapter: __MODULE__.System)) == "HOSTNAME is not set correctly." end defmodule System do def start_link(args), do: GenServer.start_link(__MODULE__, args, []) def get_env(key), do: __MODULE__ |> Process.get() |> GenServer.call({:get_env, key}) def put_env(key, value), do: __MODULE__ |> Process.get() |> GenServer.call({:put_env, key, value}) def init(_), do: {:ok, %{}} def handle_call({:get_env, key}, _, state) do {:reply, state[key], state} end def handle_call({:put_env, key, value}, _, state) do {:reply, :ok, Map.put(state, key, value)} end end end ================================================ FILE: apps/omg_status/test/sentry_filter_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Status.SentryFilterTest do use ExUnit.Case, async: true test "excludes exception properly" do # ignore excluded exception assert_raise( Phoenix.NotAcceptableError, fn -> raise Phoenix.NotAcceptableError, "Could not render \"406.json\" for OMG.WatcherRPC.Web.Views.Error, please define a matching clause for render/2 or define a template at \"lib/omg_watcher_rpc_web/templates/views/error/*\". No templates were compiled for this module." end ) assert Sentry.capture_exception( %Phoenix.NotAcceptableError{plug_status: 406}, event_source: :plug, result: :sync ) == :excluded end end ================================================ FILE: apps/omg_status/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.configure(exclude: [integration: true, property: true, wrappers: true, common: true]) ExUnit.start() ================================================ FILE: apps/omg_utils/lib/omg_utils/app_version.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.AppVersion do @moduledoc false @sha String.replace(elem(System.cmd("git", ["rev-parse", "--short=7", "HEAD"]), 0), "\n", "") @doc """ Derive the running service's version for adding to a response. """ @spec version(Application.app()) :: String.t() def version(app) do {:ok, vsn} = :application.get_key(app, :vsn) List.to_string(vsn) <> "+" <> @sha end end ================================================ FILE: apps/omg_utils/lib/omg_utils/http_rpc/encoding.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.HttpRPC.Encoding do @moduledoc """ Provides binary to HEX and reverse encodings. NOTE: Intentionally wraps see: `OMG.Eth.Encoding` to keep flexibility for change. """ @doc """ Encodes raw binary to '0x'-preceded, lowercase HEX string """ # because https://github.com/rrrene/credo/issues/583, we need to: # credo:disable-for-next-line Credo.Check.Consistency.SpaceAroundOperators @spec to_hex(binary()) :: <<_::16, _::_*8>> def to_hex(binary), do: "0x" <> Base.encode16(binary, case: :lower) @doc """ Decodes '0x'-preceded, lowercase HEX string to raw binary, see `to_hex` """ # because https://github.com/rrrene/credo/issues/583, we need to: # credo:disable-for-next-line Credo.Check.Consistency.SpaceAroundOperators @spec from_hex(<<_::16, _::_*8>>) :: {:ok, binary()} | {:error, :invalid_hex} def from_hex("0x" <> hexstr), do: Base.decode16(hexstr, case: :mixed) def from_hex(_), do: {:error, :invalid_hex} # credo:disable-for-next-line Credo.Check.Consistency.SpaceAroundOperators @spec from_hex!(<<_::16, _::_*8>>) :: binary def from_hex!("0x" <> encoded), do: Base.decode16!(encoded, case: :lower) end ================================================ FILE: apps/omg_utils/lib/omg_utils/http_rpc/error.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.HttpRPC.Error do @moduledoc """ Provides standard data structure for API Error response """ alias OMG.Utils.HttpRPC.Response @doc """ Serializes error's code and description provided in response's data field. """ @spec serialize(atom() | String.t(), String.t() | nil, map() | nil) :: map() def serialize(code, description, messages \\ nil) do %{ object: :error, code: code, description: description } |> add_messages(messages) |> Response.serialize() end defp add_messages(data, nil), do: data defp add_messages(data, messages), do: Map.put_new(data, :messages, messages) end ================================================ FILE: apps/omg_utils/lib/omg_utils/http_rpc/response.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.HttpRPC.Response do @moduledoc """ Serializes the response into expected result/data format. """ alias OMG.Utils.HttpRPC.Encoding @type response_t :: %{version: binary(), success: boolean(), data: map()} def serialize_page(data, data_paging) do data |> serialize() |> Map.put(:data_paging, data_paging) end @doc """ Append result of operation to the response data forming standard api response structure """ @spec serialize(any()) :: response_t() def serialize(%{object: :error} = error) do to_response(error, :error) end def serialize(data) do data |> sanitize() |> to_response(:success) end @doc """ Removes or encodes fields in response that cannot be serialized to api response. By default, it: * encodes to hex all binary values * removes metadata fields Provides standard data structure for API response """ @spec sanitize(any()) :: any() def sanitize(response) # serialize all DateTimes to ISO8601 formatted strings def sanitize(%DateTime{} = datetime) do datetime |> DateTime.truncate(:second) |> DateTime.to_iso8601() end def sanitize(list) when is_list(list) do Enum.map(list, &sanitize/1) end def sanitize(map_or_struct) when is_map(map_or_struct) do map_or_struct |> to_map() |> do_filter() |> sanitize_map() end def sanitize(bin) when is_binary(bin), do: Encoding.to_hex(bin) def sanitize({:skip_hex_encode, bin}), do: bin def sanitize({{key, value}, _}), do: Map.put_new(%{}, key, value) def sanitize({key, value}), do: Map.put_new(%{}, key, value) def sanitize(value), do: value defp do_filter(map_or_struct) do if :code.is_loaded(Ecto) do Enum.filter(map_or_struct, fn {_, %{__struct__: Ecto.Association.NotLoaded}} -> false _ -> true end) |> Map.new() else map_or_struct end end # Allows to skip sanitize on specifies keys provided in list in key :skip_hex_encode defp sanitize_map(map) do {skip_keys, map} = Map.pop(map, :skip_hex_encode, []) skip_keys = MapSet.new(skip_keys) map |> Enum.map(fn {k, v} -> case MapSet.member?(skip_keys, k) do true -> {k, v} false -> {k, sanitize(v)} end end) |> Map.new() end defp to_map(struct), do: Map.drop(struct, [:__struct__, :__meta__]) defp to_response(data, result) do %{ success: result == :success, data: data } end end ================================================ FILE: apps/omg_utils/lib/omg_utils/http_rpc/validators/base.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.HttpRPC.Validator.Base do @moduledoc """ Implements simple validation engine with basic validators provided and allows to chain them to make more comprehensive one. """ alias OMG.Utils.HttpRPC.Encoding @type validation_error_t() :: {:error, {:validation_error, binary(), any()}} # Creates a named chain of basic validators aka alias, for easier to use. # IMPORTANT: Alias can use already defined validators, not other aliases (no backtracking) @aliases %{ address: [:hex, length: 20], currency: [:hex, length: 20], hash: [:hex, length: 32], signature: [:hex, length: 65], pos_integer: [:integer, greater: 0], non_neg_integer: [:integer, greater: -1] } @doc """ Validates value of given key in the map with provided list of validators. First validator list is preprocessed which replaces aliases with its definitions. Then value is fetched from the map and each validator is run passing a tuple where first element is a value and second validation error from previous validator. If all validators succeed on the value the second element is empty list (no validation errors). Last result of the validation is translated to {:ok, value} or error. ## Examples * `expect(args, "arg_name", [:integer, greater: 1000])` Validate and positive integer greater than 1000 * `expect(args, "arg_name", [:integer, :optional])` Validate integer value or when `arg_name` key is missing {:ok, `nil`} is returned * `expect(args, "arg_name", [:optional, :integer])` NOTE: **invalid order** it's the same as just `:integer` To validate optional integer values it should be `:integer, :optional` """ @spec expect(map(), atom() | binary(), atom() | list()) :: {:ok, any()} | validation_error_t() def expect(map, key, atom) when is_atom(atom), do: expect(map, key, [atom]) def expect(map, key, opts) do opts |> replace_aliases() |> Enum.reduce( map |> get(key), &validate/2 ) |> case do {val, []} -> {:ok, val} {_, [err | _]} -> error(key, err) end end @doc """ Creates custom validation error """ @spec error(binary(), any()) :: validation_error_t() def error(parent_name, {:validation_error, child_name, reason}), do: error(parent_name <> "." <> child_name, reason) def error(param_name, reason) when is_binary(param_name), do: {:error, {:validation_error, param_name, reason}} @doc """ Unwraps elements from the results list: `[{:ok, elt} | {:error, any()}]` or returns the first error """ @spec all_success_or_error([{:ok, any()} | {:error, any()}]) :: list() | {:error, any()} def all_success_or_error(result_list) do with nil <- result_list |> Enum.find(&(:error == Kernel.elem(&1, 0))), do: result_list |> Enum.map(fn {:ok, elt} -> elt end) end @doc """ `integer` function is an example of basic validator used by the engine. Validators are passed to the `expect` function in `opts` parameter as a keyword list. Each validator expects a tuple, where first element is value of specified `key` in `map` possibly processed by previous validators in `opts` list. Second element is a validator list which fails on the value. It depends on validator but usually if some previous validator returns error on value, others just pass the error through and do not add themselves to the list. """ @spec integer({any(), list()}) :: {any(), list()} def integer({_, [_ | _]} = err), do: err def integer({val, []} = acc) when is_integer(val), do: acc def integer({val, []}), do: {val, [:integer]} @spec optional({any(), list()}) :: {any(), list()} def optional({val, _}) when val in [:missing, nil], do: {nil, []} def optional(acc), do: acc @spec optional({any(), list()}, atom()) :: {any(), list()} def optional({val, _}, true) when val in [:missing, nil], do: {nil, []} def optional(acc, _), do: acc @spec hex({any(), list()}) :: {any(), list()} def hex({_, [_ | _]} = err), do: err def hex({str, []}) do case Encoding.from_hex(str) do {:ok, bin} -> {bin, []} _ -> {str, [:hex]} end end @spec length({any(), list()}, non_neg_integer()) :: {any(), list()} def length({_, [_ | _]} = err, _len), do: err def length({str, []}, len) when is_binary(str) do if Kernel.byte_size(str) == len, do: {str, []}, else: {str, length: len} end def length({val, []}, len), do: {val, length: len} @spec max_length({any(), list()}, non_neg_integer()) :: {any(), list()} def max_length({_, [_ | _]} = err, _len), do: err def max_length({list, []}, len) when is_list(list) and length(list) <= len, do: {list, []} def max_length({val, []}, len), do: {val, max_length: len} @spec min_length({any(), list()}, non_neg_integer()) :: {any(), list()} def min_length({_, [_ | _]} = err, _len), do: err def min_length({list, []}, len) when is_list(list) and length(list) >= len, do: {list, []} def min_length({val, []}, len), do: {val, min_length: len} @spec greater({any(), list()}, integer()) :: {any(), list()} def greater({_, [_ | _]} = err, _b), do: err def greater({val, []}, bound) when is_integer(val) and val > bound, do: {val, []} def greater({val, []}, _b) when not is_integer(val), do: {val, [:integer]} def greater({val, []}, bound), do: {val, greater: bound} @spec lesser({any(), list()}, integer()) :: {any(), list()} def lesser({_, [_ | _]} = err, _b), do: err def lesser({val, []}, bound) when is_integer(val) and val < bound, do: {val, []} def lesser({val, []}, _b) when not is_integer(val), do: {val, [:integer]} def lesser({val, []}, bound), do: {val, lesser: bound} @spec list({any(), list()}, function() | nil) :: {any(), list()} def list(tuple, mapper \\ nil) def list({_, [_ | _]} = err, _), do: err def list({val, []}, nil) when is_list(val), do: {val, []} def list({val, []}, mapper) when is_list(val), do: list_processor(val, mapper) def list({val, _}, _), do: {val, [:list]} @spec map({any(), list()}, function() | nil) :: {any(), list()} def map(tuple, parser \\ nil) def map({_, [_ | _]} = err, _), do: err def map({val, []}, nil) when is_map(val), do: {val, []} def map({val, []}, parser) when is_map(val), do: (case parser.(val) do {:error, err} -> {val, [err]} {:ok, map} -> {map, []} end) def map({val, _}, _), do: {val, [:map]} defp list_processor(val, mapper) do list_reducer = fn {:error, map_err}, _acc -> {:halt, map_err} {:ok, elt}, acc -> {:cont, [elt | acc]} elt, acc -> {:cont, [elt | acc]} end val |> Enum.reduce_while([], fn elt, acc -> mapper.(elt) |> list_reducer.(acc) end) |> case do list when is_list(list) -> {Enum.reverse(list), []} err -> {val, [err]} end end # provides initial value to the validators reducer, see: `expect` defp get(map, key), do: {Map.get(map, key, :missing), []} defp validate(validator, acc) when is_atom(validator), do: Kernel.apply(__MODULE__, validator, [acc]) defp validate({validator, args}, acc), do: Kernel.apply(__MODULE__, validator, [acc, args]) defp replace_aliases(validators) do validators |> Enum.reduce( [], fn v, acc -> key = validator_name(v) pre = Map.get(@aliases, key, [v]) [_ | _] = acc ++ pre end ) end defp validator_name(v) when is_atom(v), do: v defp validator_name({v, _}), do: v end ================================================ FILE: apps/omg_utils/lib/omg_utils/paginator.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.Paginator do @moduledoc """ Wraps resulted query data along with pagination information used. """ @default_limit 200 @first_page 1 @type t(data_type) :: %__MODULE__{ data: list(data_type), data_paging: %{limit: pos_integer(), page: pos_integer()} } defstruct data: [], data_paging: %{ limit: @default_limit, page: @first_page } @doc """ Creates new paginator from query constraints like [limit: 200, page: 1], none of keys is required. """ @spec from_constraints(Keyword.t(), integer()) :: %__MODULE__{:data => [], :data_paging => map()} def from_constraints(constraints, max_limit) when is_integer(max_limit) do data_paging = constraints |> Keyword.take([:page, :limit]) |> Keyword.put_new(:page, @first_page) |> Keyword.update(:limit, max_limit, &min(&1, max_limit)) |> Map.new() %__MODULE__{data: [], data_paging: data_paging} end @spec set_data(list(), t(%__MODULE__{})) :: t(%__MODULE__{}) def set_data(data, paginator) when is_list(data), do: %__MODULE__{paginator | data: data} end ================================================ FILE: apps/omg_utils/lib/omg_utils/remote_ip.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.RemoteIP do @moduledoc """ This plug sets remote_ip from CF-Connecting-IP header. """ import Plug.Conn @header_name "cf-connecting-ip" def init(options), do: options def call(conn, _opts) do ips = get_req_header(conn, @header_name) parse_and_set_ip(conn, ips) end defp parse_and_set_ip(conn, [forwarded_ips]) when is_binary(forwarded_ips) do left_ip = forwarded_ips |> String.split(",") |> List.first() parse_ip(conn, left_ip) end defp parse_and_set_ip(conn, _ip), do: conn defp parse_ip(conn, ip_string) when is_binary(ip_string) do parsed_ip = ip_string |> String.trim() |> String.to_charlist() |> :inet.parse_address() case parsed_ip do {:ok, ip} -> %{conn | remote_ip: ip} _ -> conn end end defp parse_ip(conn, _), do: conn end ================================================ FILE: apps/omg_utils/lib/utils.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils do @moduledoc false end ================================================ FILE: apps/omg_utils/mix.exs ================================================ defmodule Utils.MixProject do use Mix.Project def project() do [ app: :omg_utils, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: [], test_coverage: [tool: ExCoveralls] ] end def application() do [extra_applications: [:plug]] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end # Specifies which paths to compile per environment. defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] end ================================================ FILE: apps/omg_utils/test/omg_utils/app_version_tet.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.AppVersionTest do use ExUnit.Case, async: true alias OMG.Utils.AppVersion describe "version/1" do test "returns a compliant semver when given an application" do # Using :elixir as the app because it is certain to be running during the test version = AppVersion.version(:elixir) assert {:ok, _} = Version.parse(version) end end end ================================================ FILE: apps/omg_utils/test/omg_utils/http_rpc/encoding_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.HttpRPC.EncodingTest do use ExUnit.Case, async: true alias OMG.Utils.HttpRPC.Encoding test "decodes all up/down/mixed case values" do assert [{:ok, <<222, 173, 190, 239>>}, {:ok, <<222, 173, 190, 239>>}, {:ok, <<222, 173, 190, 239>>}] == Enum.map(["0xdeadbeef", "0xDEADBEEF", "0xDeadBeeF"], &Encoding.from_hex/1) end test "doesn't decode hex without '0x' prefix" do assert {:error, :invalid_hex} == Encoding.from_hex("deadbeef") end test "encodes stuff" do assert "0xdeadbeef" == Encoding.to_hex(<<222, 173, 190, 239>>) end end ================================================ FILE: apps/omg_utils/test/omg_utils/http_rpc/response_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.HttpRPC.ResponseTest do use ExUnit.Case, async: true alias OMG.Utils.HttpRPC.Response alias OMG.WatcherInfo.DB @cleaned_tx %{ blknum: nil, txbytes: nil, txhash: nil, txindex: nil, txtype: nil, metadata: nil, inserted_at: nil, updated_at: nil } setup %{} do load_ecto() :ok end describe "test sanitization without ecto preloaded" do test "cleaning response: simple value list works without ecto loaded" do unload_ecto() value = [nil, 1, "01234", :atom, [], %{}, {:skip_hex_encode, "an arbitrary string"}] expected_value = [nil, 1, "0x3031323334", :atom, [], %{}, "an arbitrary string"] assert expected_value == Response.sanitize(value) end test "cleaning response structure: list of maps when ecto unloaded" do unload_ecto() refute [@cleaned_tx, @cleaned_tx] == Response.sanitize([%DB.Transaction{}, %DB.Transaction{}]) end end test "cleaning response structure: map of maps" do assert %{first: @cleaned_tx, second: @cleaned_tx} == Response.sanitize(%{second: %DB.Transaction{}, first: %DB.Transaction{}}) end test "cleaning response structure: list of maps" do assert [@cleaned_tx, @cleaned_tx] == Response.sanitize([%DB.Transaction{}, %DB.Transaction{}]) end test "cleaning response: simple value list" do value = [nil, 1, "01234", :atom, [], %{}, {:skip_hex_encode, "an arbitrary string"}] expected_value = [nil, 1, "0x3031323334", :atom, [], %{}, "an arbitrary string"] assert expected_value == Response.sanitize(value) end test "cleaning response: remove nested meta keys" do data = %{ address: "0xd5b6e653beec1f8131d2ea4f574b2fd58770d9e0", utxos: [ %{ __meta__: %{context: nil, source: {nil, "txoutputs"}, state: :loaded}, amount: 1, creating_deposit: "hash1", creating_transaction: nil, currency: String.duplicate("00", 20), deposit: %{ __meta__: %{context: nil, source: {nil, "txoutputs"}, state: :loaded}, blknum: 1, txindex: 0, event_type: :deposit, hash: "hash1" }, id: 1 } ] } |> Response.sanitize() assert false == Enum.any?( hd(data.utxos).deposit, &match?({:__meta__, _}, &1) ) end test "sanitize alarm types" do system_alarm = {:system_memory_high_watermark, []} system_disk_alarm = {{:disk_almost_full, "/dev/null"}, []} app_alarm = {:ethereum_connection_error, %{node: Node.self(), reporter: __MODULE__}} assert %{disk_almost_full: "/dev/null"} == Response.sanitize(system_disk_alarm) assert %{ethereum_connection_error: %{node: Node.self(), reporter: __MODULE__}} == Response.sanitize(app_alarm) assert %{system_memory_high_watermark: []} == Response.sanitize(system_alarm) end test "skiping sanitize for specified keys" do # simplified EIP-712 structures serialization where # `types` should be skip entirely # `domain` sanitized partially # `message` fully sanitized address = <<124, 39, 109, 202, 171, 153, 189, 22, 22, 60, 27, 204, 230, 113, 202, 214, 161, 236, 9, 69>> address_hex = "0x7c276dcaab99bd16163c1bcce671cad6a1ec0945" zero20_hex = "0x" <> String.duplicate("00", 20) domain_spec = [ %{name: "name", type: "string"}, %{name: "verifyingContract", type: "address"}, %{name: "chainId", type: "uint256"} ] domain_data = %{ name: {:skip_hex_encode, "OMG Network"}, verifyingContract: address, chainId: 32 } message = %{ input0: %{owner: address, currency: <<0::160>>, amount: 111} } typed_data = %{ types: %{Eip712Domain: domain_spec}, primaryType: "Transaction", domain: domain_data, message: message, # spicifies key to skip during sanitize skip_hex_encode: [:types, :primaryType, :non_existing] } response = Response.sanitize(typed_data) assert %{ domain: %{name: "OMG Network", verifyingContract: ^address_hex, chainId: 32}, message: %{ input0: %{owner: ^address_hex, currency: ^zero20_hex, amount: 111} }, primaryType: "Transaction", types: %{Eip712Domain: ^domain_spec} } = response # meta-key is removed from sanitized response assert response |> Map.get(:skip_hex_encode) |> is_nil() end defp unload_ecto() do :code.purge(Ecto) :code.delete(Ecto) false = :code.is_loaded(Ecto) end defp load_ecto(), do: true = Code.ensure_loaded?(Ecto) end ================================================ FILE: apps/omg_utils/test/omg_utils/http_rpc/validators/base_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Utils.HttpRPC.Validator.BaseTest do alias OMG.Utils.HttpRPC.Encoding use ExUnitFixtures use ExUnit.Case, async: true import OMG.Utils.HttpRPC.Validator.Base @params %{ "int_1" => -1_234_567_890, "int_2" => 0, "int_3" => 1_234_567_890, "nint_1" => "1234567890", "nil" => nil, "opt_1" => true, "hex_1" => "0x1234567890abcdef", "hex_2" => "0x1234567890AbCdEf", "hex_3" => "0x1234567890ABCDEF", "non_hex_1" => "!@#$%^&*().?", "non_hex_2" => "1234567890ABCDE", "non_hex_3" => "1234567890ABCDEZ", "valid_address" => "0x" <> String.duplicate("00", 20), "non_hex_address" => "0x" <> String.duplicate("ZZ", 20), "too_long_address" => "0x" <> String.duplicate("00", 21), "too_short_address" => "0x" <> String.duplicate("00", 19), "valid_signature" => "0x" <> String.duplicate("00", 65), "non_hex_signature" => "0x" <> String.duplicate("ZZ", 65), "too_long_signature" => "0x" <> String.duplicate("00", 66), "too_short_signature" => "0x" <> String.duplicate("00", 64), "valid_hash" => "0x" <> String.duplicate("00", 32), "non_hex_hash" => "0x" <> String.duplicate("ZZ", 32), "too_long_hash" => "0x" <> String.duplicate("00", 33), "too_short_hash" => "0x" <> String.duplicate("00", 31), "len_1" => "1", "len_2" => <<1, 2, 3, 4, 5>>, "max_len_1" => [1, 2, 3, 4, 5], "min_len_1" => [1, 2, 3] } describe "Basic validation:" do test "integer: positive cases" do assert {:ok, -1_234_567_890} == expect(@params, "int_1", :integer) assert {:ok, 0} == expect(@params, "int_2", :integer) assert {:ok, 1_234_567_890} == expect(@params, "int_3", [:integer]) end test "integer: negative cases" do assert {:error, {:validation_error, "nint_1", :integer}} == expect(@params, "nint_1", :integer) assert {:error, {:validation_error, "nil", :integer}} == expect(@params, "nil", :integer) end test "optional: positive cases" do assert {:ok, 0} == expect(@params, "int_2", :optional) assert {:ok, true} == expect(@params, "opt_1", :optional) assert {:ok, nil} == expect(@params, "no_such_key", [:optional]) assert {:ok, nil} == expect(@params, "nil", [:integer, :optional]) assert {:ok, 1_234_567_890} == expect(@params, "int_3", [:integer, :optional]) end test "optional, list" do list = [1, 2, 3] assert {:ok, list} == expect(%{"list" => list}, "list", [:list, :optional]) assert {:ok, nil} == expect(%{}, "list", [:list, :optional]) assert {:ok, nil} == expect(%{}, "list", list: &(&1 * 2), optional: true) assert {:ok, [2, 4, 6]} == expect(%{"list" => list}, "list", list: &(&1 * 2), optional: true) assert {:error, {:validation_error, "list", :list}} == expect(%{}, "list", list: &(&1 * 2), optional: false) end test "optional: negative cases" do assert {:error, {:validation_error, "nil", :integer}} == expect(@params, "nil", [:optional, :integer]) end test "hex: positive cases" do {:ok, hex_1_value} = @params |> Map.get("hex_1") |> Encoding.from_hex() assert {:ok, hex_1_value} == expect(@params, "hex_1", :hex) {:ok, hex_2_value} = @params |> Map.get("hex_2") |> Encoding.from_hex() assert {:ok, hex_2_value} == expect(@params, "hex_2", :hex) {:ok, hex_3_value} = @params |> Map.get("hex_3") |> Encoding.from_hex() assert {:ok, hex_3_value} == expect(@params, "hex_3", :hex) end test "hex: negative cases" do assert {:error, {:validation_error, "non_hex_1", :hex}} == expect(@params, "non_hex_1", :hex) end test "length: positive cases" do assert {:ok, "1"} == expect(@params, "len_1", length: 1) assert {:ok, <<1, 2, 3, 4, 5>>} == expect(@params, "len_2", length: 5) assert {:ok, [1, 2, 3, 4, 5]} == expect(@params, "max_len_1", max_length: 10) assert {:ok, [1, 2, 3, 4, 5]} == expect(@params, "max_len_1", max_length: 5) end test "length: negative cases" do assert {:error, {:validation_error, "len_1", {:length, 5}}} == expect(@params, "len_1", length: 5) assert {:error, {:validation_error, "len_2", {:length, 1}}} == expect(@params, "len_2", length: 1) assert {:error, {:validation_error, "max_len_1", {:max_length, 3}}} == expect(@params, "max_len_1", max_length: 3) end test "max_length: positive cases" do assert {:ok, [1, 2, 3, 4, 5]} == expect(@params, "max_len_1", max_length: 10) assert {:ok, [1, 2, 3, 4, 5]} == expect(@params, "max_len_1", max_length: 5) end test "max_length: negative cases" do assert {:error, {:validation_error, "max_len_1", {:max_length, 3}}} == expect(@params, "max_len_1", max_length: 3) end test "min_length: positive cases" do assert {:ok, [1, 2, 3]} == expect(@params, "min_len_1", min_length: 0) assert {:ok, [1, 2, 3]} == expect(@params, "min_len_1", min_length: 2) assert {:ok, [1, 2, 3]} == expect(@params, "min_len_1", min_length: 3) end test "min_length: negative cases" do assert {:error, {:validation_error, "min_len_1", {:min_length, 4}}} == expect(@params, "min_len_1", min_length: 4) end test "list: positive cases" do list = [1, "a", :b] assert {:ok, list} == expect(%{"list" => list}, "list", :list) end test "list: negative cases" do assert {:error, {:validation_error, "list", :list}} == expect(%{"list" => "[42]"}, "list", :list) end test "map: positive cases" do map = %{"a" => 0, "b" => 1} assert {:ok, map} == expect(%{"map" => map}, "map", :map) end test "map: negative cases" do assert {:error, {:validation_error, "map", :map}} == expect(%{"map" => [42]}, "map", :map) end test "map, missing" do assert {:error, {:validation_error, "map", :map}} == expect(%{}, "map", :map) end end describe "list and map preprocessing:" do test "mapping list elements" do assert {:ok, [2, 4, 6]} == expect(%{"list" => [1, 2, 3]}, "list", list: &(&1 * 2)) end test "validating list elements" do is_even = fn elt when rem(elt, 2) == 0 -> {:ok, elt} _ -> {:error, :odd_number} end assert {:ok, [2, 4, 6]} == expect( %{"all_even" => [2, 4, 6]}, "all_even", list: is_even ) assert {:error, {:validation_error, "all_even", :odd_number}} == expect( %{"all_even" => [2, 3, 6]}, "all_even", list: is_even ) end test "parsing map" do parser = fn map -> with {:ok, currency} <- expect(map, "currency", :address), {:ok, amount} <- expect(map, "amount", :non_neg_integer), do: {:ok, %{currency: currency, amount: amount}} end {:ok, address_value} = @params |> Map.get("valid_address") |> Encoding.from_hex() assert {:ok, %{currency: address_value, amount: 100}} == expect( %{"fee" => %{"currency" => @params["valid_address"], "amount" => 100}}, "fee", map: parser ) assert {:error, {:validation_error, "fee.currency", :hex}} = expect( %{"fee" => %{"currency" => @params["non_hex_address"], "amount" => 100}}, "fee", map: parser ) end test "unwrapping results list" do list = Enum.to_list(0..9) ok_list = Enum.map(list, &{:ok, &1}) assert list == all_success_or_error(ok_list) error = {:error, "bad news"} list_with_err = Enum.shuffle([error | ok_list]) assert error == all_success_or_error(list_with_err) end end describe "Preprocessors:" do test "greater: positive cases" do assert {:ok, 0} == expect(@params, "int_2", greater: -1) assert {:ok, 1_234_567_890} == expect(@params, "int_3", greater: 1_000_000_000) end test "greater: negative cases" do assert {:error, {:validation_error, "int_2", {:greater, 0}}} == expect(@params, "int_2", greater: 0) assert {:error, {:validation_error, "nint_1", :integer}} == expect(@params, "nint_1", greater: 0) end test "lesser: positive cases" do upper_bound = 1000 limit_param = 999 assert {:ok, 999} = expect(%{"limit" => limit_param}, "limit", lesser: upper_bound) end test "lesser: negative cases" do upper_bound = 1000 limit_param = 1001 assert {:error, {:validation_error, "limit", {:lesser, 1000}}} = expect(%{"limit" => limit_param}, "limit", lesser: upper_bound) end test "address should validate both hex value and length" do {:ok, address_value} = @params |> Map.get("valid_address") |> Encoding.from_hex() assert {:ok, address_value} == expect(@params, "valid_address", :address) assert {:error, {:validation_error, "non_hex_address", :hex}} == expect(@params, "non_hex_address", :address) assert {:error, {:validation_error, "too_short_address", {:length, 20}}} == expect(@params, "too_short_address", :address) assert {:error, {:validation_error, "too_long_address", {:length, 20}}} == expect(@params, "too_long_address", :address) end test "signature should validate both hex value and length" do {:ok, signature_value} = @params |> Map.get("valid_signature") |> Encoding.from_hex() assert {:ok, signature_value} == expect(@params, "valid_signature", :signature) assert {:error, {:validation_error, "non_hex_signature", :hex}} == expect(@params, "non_hex_signature", :signature) assert {:error, {:validation_error, "too_short_signature", {:length, 65}}} == expect(@params, "too_short_signature", :signature) assert {:error, {:validation_error, "too_long_signature", {:length, 65}}} == expect(@params, "too_long_signature", :signature) end test "hash should validate both hex value and length" do {:ok, hash_value} = @params |> Map.get("valid_hash") |> Encoding.from_hex() assert {:ok, hash_value} == expect(@params, "valid_hash", :hash) assert {:error, {:validation_error, "non_hex_hash", :hex}} == expect(@params, "non_hex_hash", :hash) assert {:error, {:validation_error, "too_short_hash", {:length, 32}}} == expect(@params, "too_short_hash", :hash) assert {:error, {:validation_error, "too_long_hash", {:length, 32}}} == expect(@params, "too_long_hash", :hash) end end test "positive and non negative integers" do args = %{ "neg" => -1, "zero" => 0, "pos" => 1, "NaN" => true } assert {:ok, 0} == expect(args, "zero", :non_neg_integer) assert {:error, {:validation_error, "neg", {:greater, -1}}} == expect(args, "neg", :non_neg_integer) assert {:ok, 1} == expect(args, "pos", :pos_integer) assert {:error, {:validation_error, "zero", {:greater, 0}}} == expect(args, "zero", :pos_integer) assert {:error, {:validation_error, "NaN", :integer}} == expect(args, "NaN", :pos_integer) end end ================================================ FILE: apps/omg_utils/test/omg_utils/remote_ip_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License defmodule OMG.Utils.RemoteIPTest do use ExUnit.Case, async: true alias OMG.Utils.RemoteIP describe "call/2" do test "sets remote_ip field" do conn = %Plug.Conn{ req_headers: [ {"cf-connecting-ip", "99.99.99.99"} ] } conn_with_remote_ip = RemoteIP.call(conn, %{}) assert conn_with_remote_ip.remote_ip == {99, 99, 99, 99} end test "does not set remote_ip if cf-connecting-ip header is not set" do conn = %Plug.Conn{} conn_with_remote_ip = RemoteIP.call(conn, %{}) assert is_nil(conn_with_remote_ip.remote_ip) end test "does not set remote_ip if cf-connecting-ip header is invalid" do conn = %Plug.Conn{ req_headers: [ {"cf-connecting-ip", "myip"} ] } conn_with_remote_ip = RemoteIP.call(conn, %{}) assert is_nil(conn_with_remote_ip.remote_ip) end test "sets the left-most ip address" do conn = %Plug.Conn{ req_headers: [ {"cf-connecting-ip", "77.77.77.77, 99.99.99.99"} ] } conn_with_remote_ip = RemoteIP.call(conn, %{}) assert conn_with_remote_ip.remote_ip == {77, 77, 77, 77} end end end ================================================ FILE: apps/omg_utils/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.start() ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/account.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.Account do @moduledoc """ Module provides operations related to plasma accounts. """ alias OMG.DB.Models.PaymentExitInfo alias OMG.Watcher.Crypto alias OMG.Watcher.Utxo require OMG.Watcher.Utxo @doc """ Gets all utxos belonging to the given address. Slow operation. """ @spec get_exitable_utxos(Crypto.address_t()) :: list(OMG.Watcher.State.Core.exitable_utxos()) def get_exitable_utxos(address) do # OMG.DB.utxos() takes a while. {:ok, utxos} = OMG.DB.utxos() standard_exitable_utxos = OMG.Watcher.State.Core.standard_exitable_utxos(utxos, address) # PaymentExitInfo.all_exit_infos() takes a while. {:ok, standard_exits} = PaymentExitInfo.all_exit_infos() {:ok, in_flight_exits} = PaymentExitInfo.all_in_flight_exits_infos() # See issue for more details: https://github.com/omgnetwork/private-issues/issues/41 active_exiting_utxos = MapSet.union( OMG.Watcher.ExitProcessor.Core.active_standard_exiting_utxos(standard_exits), OMG.Watcher.ExitProcessor.Core.active_in_flight_exiting_inputs(in_flight_exits) ) # active standard exiting utxos are excluded filter_standard_exiting_utxos(standard_exitable_utxos, active_exiting_utxos) end defp filter_standard_exiting_utxos(standard_exitable_utxos, active_exiting_utxos) do Enum.filter( standard_exitable_utxos, fn %{blknum: blknum, txindex: txindex, oindex: oindex} -> not MapSet.member?(active_exiting_utxos, Utxo.position(blknum, txindex, oindex)) end ) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/alarm.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.Alarm do @moduledoc """ Watcher alarm API """ alias OMG.Status.Alert.Alarm @spec get_alarms() :: {:ok, Alarm.alarms()} def get_alarms(), do: {:ok, Alarm.all()} end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/configuration.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.Configuration do @moduledoc """ Watcher API for retrieving configuration """ alias OMG.Watcher.Configuration @spec get_configuration() :: {:ok, map()} def get_configuration() do configuration = %{ exit_processor_sla_margin: Configuration.exit_processor_sla_margin(), deposit_finality_margin: Configuration.deposit_finality_margin(), contract_semver: OMG.Eth.Configuration.contract_semver(), network: OMG.Eth.Configuration.network() } {:ok, configuration} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/in_flight_exit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.InFlightExit do @moduledoc """ Module provides API for starting, validating and challenging in-flight exits """ alias OMG.Watcher.API alias OMG.Watcher.ExitProcessor alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @type in_flight_exit() :: %{ in_flight_tx: binary(), input_txs: list(binary()), input_txs_inclusion_proofs: list(binary()), in_flight_tx_sigs: list(binary()) } @doc """ Returns arguments for plasma contract function that starts in-flight exit for a given transaction. """ @spec get_in_flight_exit(binary) :: {:ok, in_flight_exit()} | {:error, atom} def get_in_flight_exit(txbytes) do with {:ok, tx} <- Transaction.Signed.decode(txbytes), {:ok, {proofs, input_txs, input_utxos_pos}} <- find_input_data(tx) do %Transaction.Signed{sigs: sigs} = tx {:ok, %{ in_flight_tx: Transaction.raw_txbytes(tx), input_txs: input_txs, input_utxos_pos: input_utxos_pos, input_txs_inclusion_proofs: proofs, in_flight_tx_sigs: sigs }} end end @doc """ Returns arguments for plasma contract function that challenges a non-canonical IFE with a competitor for a given in-flight-exiting transaction. This delegates directly to `OMG.Watcher.ExitProcessor` see there for details """ def get_competitor(txbytes) do ExitProcessor.get_competitor_for_ife(txbytes) end @doc """ Returns arguments for plasma contract function that responds to a challeng to an IFE with an inclusion proof This delegates directly to `OMG.Watcher.ExitProcessor` see there for details """ def prove_canonical(txbytes) do ExitProcessor.prove_canonical_for_ife(txbytes) end @doc """ Returns arguments for plasma contract function proving that input was double-signed in some other IFE. This delegates directly to `OMG.Watcher.ExitProcessor` see there for details """ def get_input_challenge_data(txbytes, input_index) do ExitProcessor.get_input_challenge_data(txbytes, input_index) end @doc """ Returns arguments for plasma contract function proving that output was double-spent in other IFE or block. This delegates directly to `OMG.Watcher.ExitProcessor` see there for details """ def get_output_challenge_data(txbytes, output_index) do ExitProcessor.get_output_challenge_data(txbytes, output_index) end defp find_input_data(tx) do tx |> Transaction.get_inputs() # reversing to preserve the order of inputs, the `reduce_while` builds 3 lists by prepending |> Enum.reverse() |> Enum.reduce_while({:ok, {[], [], []}}, &find_single_input_data/2) end defp find_single_input_data(input_utxo_pos, {:ok, {proofs, txbyteses, utxo_positions}}) do input_utxo_pos |> API.Utxo.compose_utxo_exit() |> case do {:ok, %{proof: proof, txbytes: txbytes}} -> utxo_pos = Utxo.Position.encode(input_utxo_pos) {:cont, {:ok, {[proof | proofs], [txbytes | txbyteses], [utxo_pos | utxo_positions]}}} {:error, :utxo_not_found} -> {:halt, {:error, :tx_for_input_not_found}} {:error, :no_deposit_for_given_blknum} -> {:halt, {:error, :deposit_input_spent_ife_unsupported}} end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/status.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.Status do @moduledoc """ Watcher status API """ alias OMG.Watcher.API.StatusCache @doc """ Returns status of the watcher from the ETS cache. """ @spec get_status() :: {:ok, StatusCache.status()} def get_status() do {:ok, StatusCache.get()} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/status_cache/external.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.StatusCache.External do @moduledoc """ The integration point of the caching process """ alias OMG.Eth alias OMG.Eth.Client alias OMG.Eth.Configuration alias OMG.Eth.EthereumHeight alias OMG.Eth.RootChain alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.BlockGetter alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.RootChainCoordinator alias OMG.Watcher.State @type t() :: %{ last_validated_child_block_number: non_neg_integer(), last_validated_child_block_timestamp: non_neg_integer(), last_mined_child_block_number: non_neg_integer(), last_mined_child_block_timestamp: non_neg_integer(), last_seen_eth_block_number: non_neg_integer(), last_seen_eth_block_timestamp: non_neg_integer(), eth_syncing: boolean(), byzantine_events: list(Event.t()), in_flight_exits: ExitProcessor.Core.in_flight_exits_response_t(), contract_addr: binary, services_synced_heights: RootChainCoordinator.Core.ethereum_heights_result_t() } def get_ethereum_height() do EthereumHeight.get() end # Returns status of the watcher. Status consists of last validated child block number, # last mined child block number and it's timestamp, and a flag indicating if watcher is syncing with Ethereum. # This function calls into a number of services (internal and external), collects the results. # If any of the underlying services are unavailable, it will crash @spec get_status(non_neg_integer()) :: {:ok, t()} def get_status(eth_block_number) do {:ok, eth_block_timestamp} = Eth.get_block_timestamp_by_number(eth_block_number) eth_syncing = syncing?() validated_child_block_number = get_validated_child_block_number() contracts = Configuration.contracts() contract_addr = contract_map_from_hex(contracts) mined_child_block_number = RootChain.get_mined_child_block() {_, mined_child_block_timestamp} = RootChain.blocks(mined_child_block_number) {_, validated_child_block_timestamp} = RootChain.blocks(validated_child_block_number) {:ok, services_synced_heights} = RootChainCoordinator.get_ethereum_heights() {_, events_processor} = ExitProcessor.check_validity() {:ok, in_flight_exits} = ExitProcessor.get_active_in_flight_exits() {:ok, {_, events_block_getter}} = BlockGetter.get_events() status = %{ last_validated_child_block_number: validated_child_block_number, last_validated_child_block_timestamp: validated_child_block_timestamp, last_mined_child_block_number: mined_child_block_number, last_mined_child_block_timestamp: mined_child_block_timestamp, last_seen_eth_block_number: eth_block_number, last_seen_eth_block_timestamp: eth_block_timestamp, eth_syncing: eth_syncing, byzantine_events: events_processor ++ events_block_getter, in_flight_exits: in_flight_exits, contract_addr: contract_addr, services_synced_heights: services_synced_heights } {:ok, status} end # Checks geth syncing status, errors are treated as not synced. # Returns: # * false - geth is synced # * true - geth is still syncing. defp syncing?() do Client.node_ready() != :ok end defp get_validated_child_block_number() do child_block_interval = Configuration.child_block_interval() {state_current_block, _} = State.get_status() state_current_block - child_block_interval end defp contract_map_from_hex(contract_map) do Enum.into(contract_map, %{}, fn {name, addr} -> {name, Encoding.from_hex!(addr)} end) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/status_cache/storage.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.StatusCache.Storage do @moduledoc """ Watcher status API storage """ @doc """ This gets periodically called (defined by Ethereum height change). """ def update_status(ets, key, eth_block_number, integration_module) do {:ok, status} = integration_module.get_status(eth_block_number) :ets.insert(ets, {key, status}) end def ensure_ets_init(status_cache) do case :ets.info(status_cache) do :undefined -> ^status_cache = :ets.new(status_cache, [:set, :public, :named_table, read_concurrency: true]) :ok _ -> :ok end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/status_cache.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.StatusCache do @moduledoc """ Watcher status API cache """ alias OMG.Watcher.API.StatusCache.External alias OMG.Watcher.API.StatusCache.Storage alias OMG.Watcher.SyncSupervisor use GenServer require Logger @type status() :: External.t() defstruct [:ets, :integration_module] @type t :: %__MODULE__{ ets: atom(), integration_module: module() } @spec get() :: status() def get() do :ets.lookup_element(SyncSupervisor.status_cache(), key(), 2) end def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end @spec init(Keyword.t()) :: {:ok, t()} def init(opts) do event_bus = Keyword.fetch!(opts, :event_bus) ets = Keyword.fetch!(opts, :ets) integration_module = Keyword.get(opts, :integration_module, External) :ok = event_bus.subscribe({:root_chain, "ethereum_new_height"}, link: true) {:ok, eth_block_number} = integration_module.get_ethereum_height() Storage.update_status(ets, key(), eth_block_number, integration_module) _ = Logger.info("Started #{inspect(__MODULE__)}.") {:ok, %__MODULE__{ets: ets, integration_module: integration_module}} end @doc """ This gets periodically called (defined by Ethereum height change). """ def handle_info({:internal_event_bus, :ethereum_new_height, eth_block_number}, state) do _ = Storage.update_status(state.ets, key(), eth_block_number, state.integration_module) {:noreply, state} end def handle_info({:ssl_closed, _}, state) do # eat this bug https://github.com/benoitc/hackney/issues/464 {:noreply, state} end defp key() do :status end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.Transaction do @moduledoc """ Module provides API for transactions """ alias OMG.Watcher.HttpRPC.Client alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @doc """ Passes the signed transaction to the child chain. Caution: This function is unaware of the child chain's security status, e.g.: * Watcher is fully synced, * all operator blocks have been verified, * transaction doesn't spend funds not yet mined * etc... """ @spec submit(list(Transaction.Signed.t())) :: Client.response_t() | {:error, atom()} def batch_submit(signed_txs) do url = Application.get_env(:omg_watcher, :child_chain_url) Client.batch_submit(signed_txs, url) end @spec submit(Transaction.Signed.t()) :: Client.response_t() | {:error, atom()} def submit(%Transaction.Signed{} = signed_tx) do url = Application.get_env(:omg_watcher, :child_chain_url) signed_tx |> Transaction.Signed.encode() |> Client.submit(url) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/api/utxo.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.Utxo do @moduledoc """ Module provides API for utxos """ alias OMG.Eth.Configuration alias OMG.Watcher.ExitProcessor alias OMG.Watcher.Utxo alias OMG.Watcher.UtxoExit.Core require Utxo @type exit_t() :: %{ utxo_pos: pos_integer(), txbytes: binary(), proof: binary(), sigs: binary() } @interval Configuration.child_block_interval() # Based on the contract parameters determines whether UTXO position provided was created by a deposit defguardp is_deposit(blknum) when rem(blknum, @interval) != 0 @doc """ Returns a proof that utxo was spent """ @spec create_challenge(Utxo.Position.t()) :: {:ok, ExitProcessor.StandardExit.Challenge.t()} | {:error, :utxo_not_spent} | {:error, :exit_not_found} def create_challenge(utxo) do ExitProcessor.create_challenge(utxo) end @spec compose_utxo_exit(Utxo.Position.t()) :: {:ok, exit_t()} | {:error, :utxo_not_found} | {:error, :no_deposit_for_given_blknum} def compose_utxo_exit(Utxo.position(blknum, _, _) = utxo_pos) when is_deposit(blknum) do utxo_pos |> Utxo.Position.to_input_db_key() |> OMG.DB.utxo() |> Core.compose_deposit_standard_exit() end def compose_utxo_exit(Utxo.position(blknum, _, _) = utxo_pos) do with {:ok, [blk_hash]} <- OMG.DB.block_hashes([blknum]), {:ok, [db_block]} <- OMG.DB.blocks([blk_hash]) do Core.compose_block_standard_exit(db_block, utxo_pos) else _error -> {:error, :utxo_not_found} end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Application do # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications @moduledoc false use Application require Logger def start(_type, _args) do _ = Logger.info("Starting #{inspect(__MODULE__)}") start_root_supervisor() end def start_root_supervisor() do # root supervisor must stop whenever any of its children supervisors goes down (children carry the load of restarts) children = [ %{ id: OMG.Watcher.Supervisor, start: {OMG.Watcher.Supervisor, :start_link, []}, restart: :permanent, type: :supervisor } ] opts = [ strategy: :one_for_one, # whenever any of supervisor's children goes down, so it does max_restarts: 0, name: OMG.Watcher.RootSupervisor ] Supervisor.start_link(children, opts) end def start_phase(:attach_telemetry, :normal, _phase_args) do handlers = [ ["measure-state", OMG.Watcher.State.Measure.supported_events(), &OMG.Watcher.State.Measure.handle_event/4, nil], [ "measure-blockgetter", OMG.Watcher.BlockGetter.Measure.supported_events(), &OMG.Watcher.BlockGetter.Measure.handle_event/4, nil ], [ "measure-ethereum-event-listener", OMG.Watcher.EthereumEventListener.Measure.supported_events(), &OMG.Watcher.EthereumEventListener.Measure.handle_event/4, nil ] ] Enum.each(handlers, fn handler -> case apply(:telemetry, :attach_many, handler) do :ok -> :ok {:error, :already_exists} -> :ok end end) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Block do @moduledoc """ Representation of an OMG network child chain block. """ alias OMG.Watcher.Merkle alias OMG.Watcher.State.Transaction @type block_hash_t() :: <<_::256>> defstruct [:transactions, :hash, :number] @type t() :: %__MODULE__{ transactions: list(Transaction.Signed.tx_bytes()), hash: block_hash_t(), number: pos_integer() } @type db_t() :: %{ transactions: list(binary), hash: block_hash_t(), number: pos_integer() } @doc """ Returns a Block from enumberable of transactions, at a certain child block number, along with a calculated merkle root hash """ @spec hashed_txs_at(list(Transaction.Recovered.t()), non_neg_integer()) :: t() def hashed_txs_at(txs, blknum) do signed_txs_bytes = Enum.map(txs, & &1.signed_tx_bytes) txs_bytes = Enum.map(txs, &Transaction.raw_txbytes/1) %__MODULE__{hash: Merkle.hash(txs_bytes), transactions: signed_txs_bytes, number: blknum} end @doc """ Coerces the block struct to a format more in-line with the external API format """ def to_api_format(%__MODULE__{number: blknum} = struct_block) do struct_block |> Map.from_struct() |> Map.delete(:number) |> Map.put(:blknum, blknum) end # NOTE: we have no migrations, so we handle data compatibility here (make_db_update/1 and from_db_kv/1), OMG-421 def to_db_value(%__MODULE__{transactions: transactions, hash: hash, number: number}) when is_list(transactions) and is_binary(hash) and is_integer(number) do %{transactions: transactions, hash: hash, number: number} end def from_db_value(%{transactions: transactions, hash: hash, number: number}) when is_list(transactions) and is_binary(hash) and is_integer(number) do value = %{transactions: transactions, hash: hash, number: number} struct!(__MODULE__, value) end @doc """ Calculates inclusion proof for the transaction in the block """ @spec inclusion_proof(t() | list(Transaction.Signed.tx_bytes()), non_neg_integer()) :: binary() def inclusion_proof(transactions, txindex) when is_list(transactions) do txs_bytes = transactions |> Enum.map(&Transaction.Signed.decode!/1) |> Enum.map(&Transaction.raw_txbytes/1) Merkle.create_tx_proof(txs_bytes, txindex) end def inclusion_proof(%__MODULE__{transactions: transactions}, txindex), do: inclusion_proof(transactions, txindex) end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block_getter/block_application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockGetter.BlockApplication do @moduledoc """ Contains all the information that `apply_block` and `handle_cast(:apply_block)` would need to apply a statelessly valid, downloaded block """ alias OMG.Watcher.BlockGetter.BlockApplication @type t :: %__MODULE__{ number: pos_integer(), eth_height: non_neg_integer(), eth_height_done: boolean(), hash: binary(), timestamp: pos_integer(), transactions: list() } defstruct [ :number, :eth_height, :eth_height_done, :hash, :timestamp, transactions: [] ] def new(block, recovered_txs, block_timestamp) do struct!( BlockApplication, block |> Map.from_struct() |> Map.put(:transactions, recovered_txs) |> Map.put(:timestamp, block_timestamp) ) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block_getter/core.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockGetter.Core do @moduledoc """ Logic module for the `OMG.Watcher.BlockGetter`. Responsible for: - figuring out the range of child chain blocks needed to be downloaded - tracking the block downloading process and signaling withholding if need be - doing the stateless validation of blocks (and transactions within those blocks) - tracking the progress of stateful validation of blocks - matching up `BlockSubmitted` root chain events with the downloaded blocks, to discover submission `eth_height`. """ alias OMG.Watcher.Block alias OMG.Watcher.BlockGetter.BlockApplication alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.State.Transaction require Logger defmodule Config do @moduledoc false defstruct [ :block_interval, :block_getter_reorg_margin, :block_getter_loops_interval_ms, :child_chain_url, maximum_number_of_pending_blocks: 10, maximum_block_withholding_time_ms: 0, maximum_number_of_unapplied_blocks: 50 ] @type t :: %__MODULE__{ maximum_number_of_pending_blocks: pos_integer, maximum_block_withholding_time_ms: pos_integer, maximum_number_of_unapplied_blocks: pos_integer, block_interval: pos_integer, block_getter_loops_interval_ms: pos_integer, child_chain_url: String.t() } end defmodule PotentialWithholdingReport do @moduledoc """ Represents a downloading error interpreted as a potential block withholding event. """ defstruct [:blknum, :hash, :time] @type t :: %__MODULE__{ blknum: pos_integer, hash: binary, time: pos_integer } end defmodule PotentialWithholding do @moduledoc """ Used to track a recognized potential withholding and track work towards resolving it. """ defstruct time: nil, downloading: false @type t :: %__MODULE__{ time: pos_integer, downloading: boolean } end defstruct [ :synced_height, :last_applied_block, :num_of_highest_block_being_downloaded, :number_of_blocks_being_downloaded, :unapplied_blocks, :potential_block_withholdings, :config, :events, :chain_status ] @type t() :: %__MODULE__{ synced_height: pos_integer(), last_applied_block: non_neg_integer, num_of_highest_block_being_downloaded: non_neg_integer, number_of_blocks_being_downloaded: non_neg_integer, unapplied_blocks: %{non_neg_integer => BlockApplication.t()}, potential_block_withholdings: %{ non_neg_integer => PotentialWithholding.t() }, config: Config.t(), events: block_getter_events_t(), chain_status: chain_status_t() } @type block_getter_event_t() :: Event.InvalidBlock.t() | Event.BlockWithholding.t() @type block_getter_events_t() :: list(block_getter_event_t()) @type chain_status_t() :: :ok | :error @type chain_ok_response_t() :: {chain_status_t(), block_getter_events_t()} @type block_error() :: :incorrect_hash | :bad_returned_hash | :withholding | Transaction.Recovered.recover_tx_error() @type init_error() :: :not_at_block_beginning @type validate_download_response_result_t() :: {:ok, BlockApplication.t() | PotentialWithholdingReport.t()} | {:error, {block_error(), binary(), pos_integer()}} @doc """ Initializes a fresh instance of BlockGetter's state, having `block_number` as last consumed child block, using `child_block_interval` when progressing from one child block to another, `synced_height` as the root chain height up to witch all published blocked were processed and `block_getter_reorg_margin` as number of root chain blocks that may change during an reorg. Opts can be: - `:maximum_number_of_pending_blocks` - how many block should be pulled from the child chain at once (10) - `:maximum_block_withholding_time_ms` - how much time should we wait after the first failed pull until we call it a block withholding byzantine condition of the child chain (0 ms). """ @spec init( non_neg_integer, pos_integer, non_neg_integer, non_neg_integer, boolean, ExitProcessor.Core.check_validity_result_t(), Keyword.t() ) :: {:ok, %__MODULE__{}} | {:error, init_error()} def init( block_number, child_block_interval, synced_height, block_getter_reorg_margin, state_at_block_beginning, exit_processor_results, opts \\ [] ) do with true <- state_at_block_beginning || {:error, :not_at_block_beginning}, true <- init_opts_valid?(opts) do state = %__MODULE__{ synced_height: synced_height, last_applied_block: block_number, num_of_highest_block_being_downloaded: block_number, number_of_blocks_being_downloaded: 0, unapplied_blocks: %{}, potential_block_withholdings: %{}, config: struct( Config, Keyword.merge(opts, block_interval: child_block_interval, block_getter_reorg_margin: block_getter_reorg_margin ) ), events: [], chain_status: :ok } state = consider_exits(state, exit_processor_results) {:ok, state} end end @doc """ Returns: 1. `chain_status` which is based on BlockGetter events and ExitProcessor events 2. BlockGetter events. """ @spec chain_ok(t()) :: chain_ok_response_t() def chain_ok(%__MODULE__{chain_status: chain_status, events: events}), do: {chain_status, events} @doc """ Marks that child chain block published on `eth_height` was processed """ @spec apply_block(t(), BlockApplication.t()) :: {t(), non_neg_integer(), list()} def apply_block(%__MODULE__{} = state, %BlockApplication{ number: blknum, eth_height: eth_height, eth_height_done: eth_height_done }) do _ = Logger.debug("\##{inspect(blknum)}, from: #{inspect(eth_height)}, eth height done: #{inspect(eth_height_done)}") if eth_height_done do # final - we need to mark this eth height as processed state = %{state | synced_height: eth_height} {state, eth_height, [{:put, :last_block_getter_eth_height, eth_height}]} else # not final - this applied child block doesn't wrap up any eth height {state, state.synced_height, []} end end @doc """ Produces root chain block height range to search for events of block submission. If the range is not empty it spans from current synced root chain height to `coordinator_height`. Empty range case is solved naturally with {a, b}, a > b. """ @spec get_eth_range_for_block_submitted_events(t(), non_neg_integer()) :: {pos_integer(), pos_integer()} def get_eth_range_for_block_submitted_events( %__MODULE__{synced_height: synced_height, config: config}, coordinator_height ) do {max(0, synced_height - config.block_getter_reorg_margin), coordinator_height} end @doc """ Returns blocks that can be pushed to state or updates the `synced_height` if no new blocks` submissions are found in a range. That is the longest continuous range of blocks downloaded from child chain, contained in `block_submitted_events`, published on ethereum height not exceeding `coordinator_height` and not pushed to state before. """ @spec get_blocks_to_apply(t(), list(), non_neg_integer()) :: {list(BlockApplication.t()), non_neg_integer(), list(), t()} def get_blocks_to_apply( %__MODULE__{last_applied_block: last_applied} = state, block_submitted_events, coordinator_height ) do # this ensures that we don't take submissions of already applied blocks into account **at all** filtered_submissions = block_submitted_events |> Enum.filter(fn %{blknum: blknum} -> blknum > last_applied end) do_get_blocks_to_apply(state, filtered_submissions, coordinator_height) end @doc """ Returns additional blocks number on which the Core will be waiting. The number of expected block is limited by maximum_number_of_pending_blocks. """ @spec get_numbers_of_blocks_to_download(%__MODULE__{}, non_neg_integer) :: {%__MODULE__{}, list(non_neg_integer)} def get_numbers_of_blocks_to_download( %__MODULE__{ unapplied_blocks: unapplied_blocks, num_of_highest_block_being_downloaded: num_of_highest_block_being_downloaded, number_of_blocks_being_downloaded: number_of_blocks_being_downloaded, potential_block_withholdings: potential_block_withholdings, config: config, chain_status: :ok } = state, next_child ) do first_block_number = num_of_highest_block_being_downloaded + config.block_interval number_of_empty_slots = config.maximum_number_of_pending_blocks - number_of_blocks_being_downloaded potential_block_withholding_numbers = potential_block_withholdings |> Enum.filter(fn {_, %PotentialWithholding{downloading: downloading}} -> !downloading end) |> Enum.map(fn {key, _} -> key end) potential_next_block_numbers = first_block_number |> Stream.iterate(&(&1 + config.block_interval)) |> Stream.take_while(&(&1 < next_child)) |> Enum.to_list() number_of_blocks_to_download = min( number_of_empty_slots, max( 0, config.maximum_number_of_unapplied_blocks - number_of_blocks_being_downloaded - Kernel.map_size(unapplied_blocks) ) ) blocks_numbers = (potential_block_withholding_numbers ++ potential_next_block_numbers) |> Enum.take(number_of_blocks_to_download) [num_of_highest_block_being_downloaded | _] = ([num_of_highest_block_being_downloaded] ++ blocks_numbers) |> Enum.sort(&(&1 > &2)) _ = log_downloading_blocks(next_child, blocks_numbers) update_for_witholding = potential_block_withholdings |> Map.take(blocks_numbers) |> Enum.map(fn {key, value} -> {key, Map.put(value, :downloading, true)} end) |> Map.new() {%{ state | number_of_blocks_being_downloaded: length(blocks_numbers) + number_of_blocks_being_downloaded, num_of_highest_block_being_downloaded: num_of_highest_block_being_downloaded, potential_block_withholdings: Map.merge(potential_block_withholdings, update_for_witholding) }, blocks_numbers} end def get_numbers_of_blocks_to_download(state, _next_child) do {state, []} end @doc """ Statelessly decodes and validates a downloaded block, does all the checks before handing off to State.exec-checking. Requested_hash is given to compare to always have a consistent data structure coming out. Requested_number is given to _override_ since we're getting by hash, we can have empty blocks with same hashes! """ @spec validate_download_response( {:ok, map()} | {:error, block_error()}, binary(), pos_integer(), pos_integer(), pos_integer() ) :: validate_download_response_result_t() def validate_download_response( {:ok, %{hash: returned_hash, transactions: transactions, number: number}}, requested_hash, requested_number, block_timestamp, _time ) do with true <- returned_hash == requested_hash || {:error, :bad_returned_hash}, true <- number == requested_number || {:error, :bad_returned_number}, {:ok, recovered_txs} <- recover_all_txs(transactions), # hash the block yourself and compare %Block{hash: calculated_hash} = block = Block.hashed_txs_at(recovered_txs, number), true <- calculated_hash == requested_hash || {:error, :incorrect_hash} do {:ok, BlockApplication.new(block, recovered_txs, block_timestamp)} else {:error, reason} -> {:error, {reason, requested_hash, requested_number}} end end def validate_download_response({:error, _} = error, requested_hash, requested_number, _block_timestamp, time) do _ = Logger.info("Potential block withholding #{inspect(error)}, number: \##{inspect(requested_number)}") {:ok, %PotentialWithholdingReport{blknum: requested_number, hash: requested_hash, time: time}} end @doc """ First scenario: Add block to \"block to consume\" tick off the block from pending blocks. Returns the consumable, contiguous list of ordered blocks Second scenario: In case of invalid block detection Returns InvalidBlock event. Third scenario: In case of potential withholding block detection Returns same state, state with new potential_block_withholding or BlockWithHolding event """ @spec handle_downloaded_block( %__MODULE__{}, {:ok, BlockApplication.t() | PotentialWithholdingReport.t()} | {:error, {block_error(), binary(), pos_integer()}} ) :: {:ok | {:error, block_error()}, %__MODULE__{}} | {:error, :duplicate | :unexpected_block} def handle_downloaded_block( %__MODULE__{ number_of_blocks_being_downloaded: number_of_blocks_being_downloaded, potential_block_withholdings: potential_block_withholdings } = state, response ) do blknum = get_blknum(response) # if there was a potential withholding registered - mark it as non-downloading. Otherwise noop potential_block_withholdings = case potential_block_withholdings[blknum] do nil -> potential_block_withholdings potential_block_withholding -> Map.put(potential_block_withholdings, blknum, %PotentialWithholding{ potential_block_withholding | downloading: false }) end state = %{ state | number_of_blocks_being_downloaded: number_of_blocks_being_downloaded - 1, potential_block_withholdings: potential_block_withholdings } validate_downloaded_block(state, response) end @spec validate_executions( list({Transaction.tx_hash(), pos_integer, pos_integer}), map, t() ) :: {:ok, t()} | {{:error, {:tx_execution, any()}}, t()} def validate_executions(tx_execution_results, %{hash: hash, number: blknum}, state) do case all_tx_executions_ok?(tx_execution_results) do true -> {:ok, state} {:error, reason} -> event = %Event.InvalidBlock{error_type: :tx_execution, hash: hash, blknum: blknum} state = state |> set_chain_status(:error) |> add_distinct_event(event) {{:error, {:tx_execution, reason}}, state} end end @doc """ Takes results from `ExitProcessor.check_validity` into account, to potentially stop getting blocks """ @spec consider_exits(t(), ExitProcessor.Core.check_validity_result_t()) :: t() def consider_exits(%__MODULE__{} = state, {:ok, _}), do: state def consider_exits(%__MODULE__{} = state, {{:error, :unchallenged_exit} = error, _}) do _ = Logger.warn("Chain invalid when taking exits into account, because of #{inspect(error)}") set_chain_status(state, :error) end # # Private functions # defp init_opts_valid?(opts) do maximum_number_of_pending_blocks = Keyword.get(opts, :maximum_number_of_pending_blocks, 1) maximum_number_of_pending_blocks >= 1 || {:error, :maximum_number_of_pending_blocks_too_low} end # height served as syncable from the `OMG.Watcher.RootChainCoordinator` is older, nothing we can do about it, so noop defp do_get_blocks_to_apply( %__MODULE__{synced_height: synced_height} = state, _block_submitted_events, coordinator_height ) when coordinator_height <= synced_height do {[], synced_height, [], state} end # there are no **non-applied** submissions in the prescribed range of eth-blocks, so let's as much as we can defp do_get_blocks_to_apply(%__MODULE__{} = state, [], coordinator_height) do db_updates = [{:put, :last_block_getter_eth_height, coordinator_height}] {[], coordinator_height, db_updates, %{state | synced_height: coordinator_height}} end # there are blocks to apply, so let's schedule that. This clause defers advancing the synced_height until apply_block defp do_get_blocks_to_apply( %__MODULE__{unapplied_blocks: blocks, config: config} = state, block_submissions, _coordinator_height ) do eth_height_done_by_blknum = final_blknums(block_submissions) block_submissions = Enum.into(block_submissions, %{}, fn %{blknum: blknum, eth_height: eth_height} -> {blknum, eth_height} end) first_blknum_to_apply = state.last_applied_block + config.block_interval blknums_to_apply = first_blknum_to_apply |> Stream.iterate(&(&1 + config.block_interval)) |> Enum.take_while(fn blknum -> Map.has_key?(block_submissions, blknum) and Map.has_key?(blocks, blknum) end) blocks_to_keep = Map.drop(blocks, blknums_to_apply) last_applied_block = List.last([state.last_applied_block] ++ blknums_to_apply) blocks_to_apply = blknums_to_apply |> Enum.map(fn blknum -> Map.get(blocks, blknum) |> Map.put(:eth_height, Map.get(block_submissions, blknum)) |> Map.put(:eth_height_done, Map.has_key?(eth_height_done_by_blknum, blknum)) |> struct!() end) {blocks_to_apply, state.synced_height, [], %{ state | unapplied_blocks: blocks_to_keep, last_applied_block: last_applied_block }} end # goes through new submissions and figures out a mapping from blknum to eth_height, where blknum # is the **last** child block number submitted at the root chain height it maps to # this is later used to sign eth heights off as synced (`apply_block`) defp final_blknums(new_submissions) do new_submissions |> Enum.group_by(fn %{eth_height: eth_height} -> eth_height end, fn %{blknum: blknum} -> blknum end) |> Enum.into(%{}, fn {eth_height, blknums} -> last_blknum = Enum.max(blknums) {last_blknum, eth_height} end) end defp log_downloading_blocks(_next_child, []), do: :ok defp log_downloading_blocks(next_child, blocks_numbers) do Logger.info("Child chain seen at block \##{inspect(next_child)}. Downloading blocks #{inspect(blocks_numbers)}") end defp get_blknum({:ok, %{number: number}}), do: number defp get_blknum({:ok, %PotentialWithholdingReport{blknum: blknum}}), do: blknum defp get_blknum({:error, {_error_type, _hash, number}}), do: number defp validate_downloaded_block( %__MODULE__{ unapplied_blocks: unapplied_blocks, potential_block_withholdings: potential_block_withholdings } = state, {:ok, %BlockApplication{number: number} = to_apply} ) do with true <- not_queued_up_yet?(number, unapplied_blocks) || {{:error, :duplicate}, state}, true <- expected_to_queue_up?(number, state) || {{:error, :unexpected_block}, state} do state = %{ state | unapplied_blocks: Map.put(unapplied_blocks, number, to_apply), potential_block_withholdings: Map.delete(potential_block_withholdings, number) } {:ok, state} end end defp validate_downloaded_block( %__MODULE__{} = state, {:error, {error_type, hash, blknum}} ) do event = %Event.InvalidBlock{error_type: error_type, hash: hash, blknum: blknum} state = state |> set_chain_status(:error) |> add_distinct_event(event) {{:error, error_type}, state} end defp validate_downloaded_block( %__MODULE__{ potential_block_withholdings: potential_block_withholdings, config: config } = state, {:ok, %PotentialWithholdingReport{blknum: blknum, hash: hash, time: time}} ) do %{time: blknum_time} = Map.get(potential_block_withholdings, blknum, %PotentialWithholding{}) cond do blknum_time == nil -> potential_block_withholdings = Map.put(potential_block_withholdings, blknum, %PotentialWithholding{time: time}) state = %{state | potential_block_withholdings: potential_block_withholdings} {:ok, state} time - blknum_time >= config.maximum_block_withholding_time_ms -> event = %Event.BlockWithholding{blknum: blknum, hash: hash} state = state |> set_chain_status(:error) |> add_distinct_event(event) {{:error, :withholding}, state} true -> {:ok, state} end end defp not_queued_up_yet?(number, unapplied_blocks), do: not Map.has_key?(unapplied_blocks, number) defp expected_to_queue_up?(number, %{num_of_highest_block_being_downloaded: highest, last_applied_block: last}), do: last < number and number <= highest defp recover_all_txs(transactions) do transactions |> Enum.reverse() |> Enum.reduce_while({:ok, []}, fn tx, {:ok, recovered_so_far} -> case Transaction.Recovered.recover_from(tx) do {:ok, recovered} -> {:cont, {:ok, [recovered | recovered_so_far]}} other -> {:halt, other} end end) end defp all_tx_executions_ok?(tx_execution_results) do Enum.find(tx_execution_results, &(!match?({:ok, {_, _, _}}, &1))) |> case do nil -> true other -> other end end defp add_distinct_event(%__MODULE__{events: events} = state, event) do if Enum.member?(events, event), do: state, else: %{state | events: [event | events]} end defp set_chain_status(state, status), do: %{state | chain_status: status} end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block_getter/measure.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockGetter.Measure do @moduledoc """ Counting business metrics sent to Datadog """ import OMG.Status.Metric.Event, only: [name: 1] alias OMG.Status.Metric.Datadog alias OMG.Watcher.BlockGetter @supported_events [[:process, BlockGetter]] def supported_events(), do: @supported_events def handle_event([:process, BlockGetter], _, _state, _config) do value = self() |> Process.info(:message_queue_len) |> elem(1) _ = Datadog.gauge(name(:block_getter_message_queue_len), value) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block_getter/status.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockGetter.Status do @moduledoc """ Keeps track and exposes the current status of `OMG.Watcher.BlockGetter`. The reason to have this is that the parent `OMG.Watcher.BlockGetter` is doing a lot of synchronous heavy lifting and can easily become unresponsive when syncing (especially when catching up a lot of blocks). Expects current status to be eagerly pushed to it. """ alias OMG.Watcher.BlockGetter.Core def start_link() do Agent.start_link(fn -> nil end, name: __MODULE__) end @doc """ Overwrites the currently stored status with the provided one """ @spec update(Core.chain_ok_response_t()) :: :ok def update(status), do: Agent.update(__MODULE__, fn _old_status -> status end) @doc """ Retrieves the freshest information about `OMG.Watcher.BlockGetter`'s status. Prefer `OMG.Watcher.BlockGetter.get_events/0` to this """ @spec get_events() :: {:ok, Core.chain_ok_response_t()} def get_events(), do: Agent.get(__MODULE__, fn status -> {:ok, status} end) end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block_getter/supervisor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockGetter.Supervisor do @moduledoc """ This supervisor takes care of BlockGetter and State processes. In case one process fails, this supervisor's role is to restore consistent state """ use Supervisor require Logger alias OMG.Watcher.BlockGetter alias OMG.Watcher.Configuration def start_link(args) do Supervisor.start_link(__MODULE__, args, name: __MODULE__) end def init(args) do contract_deployment_height = Keyword.fetch!(args, :contract_deployment_height) block_getter_reorg_margin = Configuration.block_getter_reorg_margin() maximum_block_withholding_time_ms = Configuration.maximum_block_withholding_time_ms() maximum_number_of_unapplied_blocks = Configuration.maximum_number_of_unapplied_blocks() metrics_collection_interval = Configuration.metrics_collection_interval() child_chain_url = Configuration.child_chain_url() child_block_interval = OMG.Eth.Configuration.child_block_interval() contracts = OMG.Eth.Configuration.contracts() block_getter_loops_interval_ms = Configuration.ethereum_events_check_interval_ms() fee_claimer_address = Base.decode16!("DEAD000000000000000000000000000000000000") # State and Block Getter are linked, because they must restore their state to the last stored state # If Block Getter fails, it starts from the last checkpoint while State might have had executed some transactions # such a situation will cause error when trying to execute already executed transaction children = [ # NOTE: Watcher doesn't need the actual fee claimer address {OMG.Watcher.State, [ fee_claimer_address: fee_claimer_address, child_block_interval: child_block_interval, metrics_collection_interval: metrics_collection_interval ]}, %{ id: BlockGetter, start: {BlockGetter, :start_link, [ [ child_block_interval: child_block_interval, block_getter_reorg_margin: block_getter_reorg_margin, maximum_block_withholding_time_ms: maximum_block_withholding_time_ms, maximum_number_of_unapplied_blocks: maximum_number_of_unapplied_blocks, metrics_collection_interval: metrics_collection_interval, block_getter_loops_interval_ms: block_getter_loops_interval_ms, child_chain_url: child_chain_url, contract_deployment_height: contract_deployment_height, contracts: contracts ] ]}, restart: :transient } ] opts = [strategy: :one_for_all] _ = Logger.info("Starting #{inspect(__MODULE__)}") Supervisor.init(children, opts) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block_getter.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockGetter do @moduledoc """ Downloads blocks from child chain, validates them and updates watcher state. Manages concurrent downloading and stateless-validation of blocks. Detects byzantine behaviors like invalid blocks and block withholding and exposes those events. Responsible for processing all block submissions and processing them once, regardless of the reorg situation. Note that `BlockGetter` shouldn't have any finality margin configured, i.e. it should be prepared to be served events from zero-confirmation Ethereum blocks from the `OMG.Watcher.RootChainCoordinator`. The flow of getting blocks is as follows: - `BlockGetter` tracks the top child block number mined in the root chain contract (by doing `eth_call` on the ethereum node) - if this is newer than local state, it gets the hash of the block from the contract (another `eth_call`) - with the hash it calls `block.get` on the child chain server - if this succeeds it continues to statelessly validate the block (recover transactions, calculate Merkle root) - if this fails (e.g. timeout) it goes into a `PotentialWithholding` state and tries to see if the problem resolves. If not it ends up reporting a `block_withholding` byzantine event - it holds such downloaded block until `OMG.Watcher.RootChainCoordinator` allows the blocks submitted at given Ethereum heights to be applied - Applies the block by statefully validating and executing the txs on `OMG.Watcher.State` - after the block is fully validated it gathers all the updates to `OMG.DB` and executes them. This includes marking a respective Ethereum height (that contained the `BlockSubmitted` event) as processed - checks in to `OMG.Watcher.RootChainCoordinator` to let other services know about progress The process of downloading and stateless validation of blocks is done in `Task`s for concurrency. See `OMG.Watcher.BlockGetter.Core` for the implementation of the business logic for the getter. """ use GenServer require Logger use Spandex.Decorators alias OMG.Eth.RootChain alias OMG.Watcher.BlockGetter.BlockApplication alias OMG.Watcher.BlockGetter.Core alias OMG.Watcher.BlockGetter.Status alias OMG.Watcher.ExitProcessor alias OMG.Watcher.HttpRPC.Client alias OMG.Watcher.RootChainCoordinator alias OMG.Watcher.RootChainCoordinator.SyncGuide alias OMG.Watcher.State @doc """ Retrieves the freshest information about `OMG.Watcher.BlockGetter`'s status, as stored by the slave process `Status`. """ @spec get_events() :: {:ok, Core.chain_ok_response_t()} def get_events(), do: __MODULE__.Status.get_events() def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end @doc """ Reads the status of block getting and application from `OMG.DB`, reads the current state of the contract and root chain and starts the pollers that will take care of getting blocks. """ def init(args) do child_block_interval = Keyword.fetch!(args, :child_block_interval) # how many eth blocks backward can change during an reorg block_getter_reorg_margin = Keyword.fetch!(args, :block_getter_reorg_margin) maximum_block_withholding_time_ms = Keyword.fetch!(args, :maximum_block_withholding_time_ms) maximum_number_of_unapplied_blocks = Keyword.fetch!(args, :maximum_number_of_unapplied_blocks) block_getter_loops_interval_ms = Keyword.fetch!(args, :block_getter_loops_interval_ms) child_chain_url = Keyword.fetch!(args, :child_chain_url) contract_deployment_height = Keyword.fetch!(args, :contract_deployment_height) # TODO rethink posible solutions see issue #724 # if we do not wait here, `ExitProcessor.check_validity()` may timeouts, # which causes State and BlockGetter to reboot, fetches entire UTXO set again, and then timeout... exit_processor_initial_results = ExitProcessor.check_validity(10 * 60_000) # State treats current as the next block to be executed or a block that is being executed # while top block number is a block that has been formed (they differ by the interval) {current_block_height, state_at_block_beginning} = State.get_status() child_top_block_number = current_block_height - child_block_interval {:ok, last_synced_height} = OMG.DB.get_single_value(:last_block_getter_eth_height) synced_height = max(contract_deployment_height, last_synced_height) {:ok, state} = Core.init( child_top_block_number, child_block_interval, synced_height, block_getter_reorg_margin, state_at_block_beginning, exit_processor_initial_results, maximum_block_withholding_time_ms: maximum_block_withholding_time_ms, maximum_number_of_unapplied_blocks: maximum_number_of_unapplied_blocks, # NOTE: not elegant, but this should limit the number of heavy-lifting workers and chance to starve the rest maximum_number_of_pending_blocks: System.schedulers(), block_getter_loops_interval_ms: block_getter_loops_interval_ms, child_chain_url: child_chain_url ) :ok = check_in_to_coordinator(synced_height) {:ok, _} = schedule_sync_height(block_getter_loops_interval_ms) {:ok, _} = schedule_producer(block_getter_loops_interval_ms) {:ok, _} = __MODULE__.Status.start_link() :ok = update_status(state) metrics_collection_interval = Keyword.fetch!(args, :metrics_collection_interval) {:ok, _} = :timer.send_interval(metrics_collection_interval, self(), :send_metrics) _ = Logger.info( "Started #{inspect(__MODULE__)}, synced_height: #{inspect(synced_height)} maximum_block_withholding_time_ms: #{ maximum_block_withholding_time_ms }" ) {:ok, state} end # :apply_block pipeline of steps @doc """ Read top down: - (execute_transactions) Stateful validation and execution of transactions on `OMG.Watcher.State`. Reacts in case that returns any failed transactions. - (run_block_download_task) Schedules more blocks to download in case some work downloading is finished and we want to progress. - (close_and_apply_block) Marks a block as applied and updates `OMG.DB` values. Also commits the updates to `OMG.DB` that `OMG.Watcher.State` handed off containing the data coming from the newly applied block. - (check_validity) Updates its view of validity of the chain. """ def handle_continue({:apply_block_step, :execute_transactions, block_application}, state) do tx_exec_results = for(tx <- block_application.transactions, do: OMG.Watcher.State.exec(tx, :ignore_fees)) case Core.validate_executions(tx_exec_results, block_application, state) do {:ok, state} -> if Code.ensure_loaded?(OMG.WatcherInfo.DB.Block), do: Kernel.apply(OMG.WatcherInfo.BlockApplicator, :insert_block!, [block_application]) {:noreply, state, {:continue, {:apply_block_step, :run_block_download_task, block_application}}} {{:error, _} = error, new_state} -> :ok = update_status(new_state) _ = Logger.error("Invalid block #{inspect(block_application.number)}, because of #{inspect(error)}") {:noreply, new_state} end end def handle_continue({:apply_block_step, :run_block_download_task, block_application}, state) do {:noreply, run_block_download_task(state), {:continue, {:apply_block_step, :close_and_apply_block, block_application}}} end def handle_continue({:apply_block_step, :close_and_apply_block, block_application}, state) do {:ok, db_updates_from_state} = OMG.Watcher.State.close_block() {state, synced_height, db_updates} = Core.apply_block(state, block_application) _ = Logger.debug("Synced height update: #{inspect(db_updates)}") :ok = OMG.DB.multi_update(db_updates ++ db_updates_from_state) :ok = check_in_to_coordinator(synced_height) _ = Logger.info( "Applied block: \##{inspect(block_application.number)}, from eth height: #{ inspect(block_application.eth_height) } " <> "with #{inspect(length(block_application.transactions))} txs" ) {:noreply, state, {:continue, {:apply_block_step, :check_validity}}} end def handle_continue({:apply_block_step, :check_validity}, state) do exit_processor_results = ExitProcessor.check_validity() state = Core.consider_exits(state, exit_processor_results) :ok = update_status(state) {:noreply, state} end @doc """ Statefully apply a statelessly validated block, coming in as a `BlockApplication` structure. """ def handle_cast({:apply_block, %BlockApplication{} = block_application}, state) do case Core.chain_ok(state) do {:ok, _} -> {:noreply, state, {:continue, {:apply_block_step, :execute_transactions, block_application}}} error -> :ok = update_status(state) _ = Logger.warn( "Chain already invalid before applying block #{inspect(block_application.number)} because of #{ inspect(error) }" ) {:noreply, state} end end @spec handle_info( :producer | {reference(), {:downloaded_block, {:ok, map}}} | {reference(), {:downloaded_block, {:error, Core.block_error()}}} | {:DOWN, reference(), :process, pid, :normal}, Core.t() ) :: {:noreply, Core.t()} | {:stop, :normal, Core.t()} def handle_info(msg, state) def handle_info(:producer, state), do: do_producer(state) def handle_info({_ref, {:downloaded_block, response}}, state), do: do_downloaded_block(response, state) def handle_info({:DOWN, _ref, :process, _pid, :normal} = _process, state), do: {:noreply, state} def handle_info(:sync, state), do: do_sync(state) def handle_info(:send_metrics, state) do :ok = :telemetry.execute([:process, __MODULE__], %{}, state) {:noreply, state} end def handle_info({:ssl_closed, _}, state) do # eat this bug https://github.com/benoitc/hackney/issues/464 {:noreply, state} end # # Private functions # defp do_producer(state) do case Core.chain_ok(state) do {:ok, _} -> new_state = run_block_download_task(state) {:ok, _} = schedule_producer(state.config.block_getter_loops_interval_ms) :ok = update_status(new_state) {:noreply, new_state} {:error, _} = error -> :ok = update_status(state) _ = Logger.warn("Chain invalid when trying to download blocks, because of #{inspect(error)}, won't try again") {:noreply, state} end end defp do_downloaded_block(response, state) do # 1/ process the block that arrived and consume case Core.handle_downloaded_block(state, response) do {:ok, state} -> state = run_block_download_task(state) :ok = update_status(state) {:noreply, state} {{:error, _} = error, state} -> :ok = update_status(state) _ = Logger.error("Error while handling downloaded block because of #{inspect(error)}") {:noreply, state} end end defp do_sync(state) do with {:ok, _} <- Core.chain_ok(state), %SyncGuide{sync_height: next_synced_height} <- RootChainCoordinator.get_sync_info() do {block_from, block_to} = Core.get_eth_range_for_block_submitted_events(state, next_synced_height) {:ok, submissions} = get_block_submitted_events(block_from, block_to) {blocks_to_apply, synced_height, db_updates, state} = Core.get_blocks_to_apply(state, submissions, next_synced_height) _ = Logger.debug("Synced height is #{inspect(synced_height)}, got #{length(blocks_to_apply)} blocks to apply") Enum.each(blocks_to_apply, &GenServer.cast(__MODULE__, {:apply_block, &1})) :ok = OMG.DB.multi_update(db_updates) :ok = check_in_to_coordinator(synced_height) {:ok, _} = schedule_sync_height(state.config.block_getter_loops_interval_ms) :ok = update_status(state) :ok = publish_events(submissions) {:noreply, state} else :nosync -> :ok = check_in_to_coordinator(state.synced_height) :ok = update_status(state) {:ok, _} = schedule_sync_height(state.config.block_getter_loops_interval_ms) {:noreply, state} {:error, _} = error -> :ok = update_status(state) _ = Logger.warn("Chain invalid when trying to sync, because of #{inspect(error)}, won't try again") {:noreply, state} end end @decorate trace(tracer: OMG.Watcher.Tracer, type: :backend, service: :block_getter) defp get_block_submitted_events(block_from, block_to) do RootChain.get_block_submitted_events(block_from, block_to) end defp run_block_download_task(state) do next_child = RootChain.next_child_block() {new_state, blocks_numbers} = Core.get_numbers_of_blocks_to_download(state, next_child) Enum.each( blocks_numbers, # captures the result in handle_info/2 with the atom: downloaded_block &Task.async(fn -> {:downloaded_block, download_block(&1, state.config.child_chain_url)} end) ) new_state end defp schedule_sync_height(block_getter_loops_interval_ms) do :timer.send_after(block_getter_loops_interval_ms, self(), :sync) end defp schedule_producer(block_getter_loops_interval_ms) do :timer.send_after(block_getter_loops_interval_ms, self(), :producer) end @spec download_block(pos_integer(), String.t()) :: Core.validate_download_response_result_t() defp download_block(requested_number, child_chain_url) do {requested_hash, block_timestamp} = RootChain.blocks(requested_number) response = Client.get_block(requested_hash, child_chain_url) Core.validate_download_response( response, requested_hash, requested_number, block_timestamp, :os.system_time(:millisecond) ) end defp check_in_to_coordinator(synced_height), do: RootChainCoordinator.check_in(synced_height, :block_getter) defp update_status(%Core{} = state), do: Status.update(Core.chain_ok(state)) defp publish_events([%{event_signature: event_signature} | _] = data) do # event signature is string with a method name with arguments, # for example: BlockSubmitted(uint256) [event_signature, _] = String.split(event_signature, "(") {:root_chain, event_signature} |> OMG.Bus.Event.new(:data, data) |> OMG.Bus.direct_local_broadcast() end defp publish_events([]), do: :ok end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/block_validator.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockValidator do @moduledoc """ Operations related to block validation. """ alias OMG.Watcher.Block alias OMG.Watcher.Merkle alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo.Position @transaction_upper_limit 2 |> :math.pow(16) |> Kernel.trunc() @doc """ Executes stateless validation of a submitted block: - Verifies that the number of transactions falls within the accepted range. - Verifies that (payment and fee) transactions are correctly formed. - Verifies that fee transactions are correctly placed and unique per currency. - Verifies that there are no duplicate inputs at the block level. - Verifies that given Merkle root matches reconstructed Merkle root. """ @spec stateless_validate(Block.t()) :: {:ok, boolean()} | {:error, atom()} def stateless_validate(submitted_block) do with :ok <- number_of_transactions_within_limit(submitted_block.transactions), {:ok, recovered_transactions} <- verify_transactions(submitted_block.transactions), {:ok, _fee_transactions} <- verify_fee_transactions(recovered_transactions), {:ok, _inputs} <- verify_no_duplicate_inputs(recovered_transactions), {:ok, _block} <- verify_merkle_root(submitted_block, recovered_transactions) do {:ok, true} end end @spec verify_merkle_root(Block.t(), list(Transaction.Recovered.t())) :: {:ok, Block.t()} | {:error, :mismatched_merkle_root} defp verify_merkle_root(block, transactions) do reconstructed_merkle_hash = transactions |> Enum.map(&Transaction.raw_txbytes/1) |> Merkle.hash() case block.hash do ^reconstructed_merkle_hash -> {:ok, block} _ -> {:error, :invalid_merkle_root} end end @spec verify_transactions(transactions :: list(Transaction.Signed.tx_bytes())) :: {:ok, list(Transaction.Recovered.t())} | {:error, Transaction.Recovered.recover_tx_error()} defp verify_transactions(transactions) do transactions |> Enum.reverse() |> Enum.reduce_while({:ok, []}, fn tx, {:ok, already_recovered} -> case Transaction.Recovered.recover_from(tx) do {:ok, recovered} -> {:cont, {:ok, [recovered | already_recovered]}} error -> {:halt, error} end end) end @spec number_of_transactions_within_limit([Transaction.Signed.tx_bytes()]) :: :ok | {:error, atom()} defp number_of_transactions_within_limit([]), do: {:error, :empty_block} defp number_of_transactions_within_limit(transactions) when length(transactions) > @transaction_upper_limit do {:error, :transactions_exceed_block_limit} end defp number_of_transactions_within_limit(_transactions), do: :ok @spec verify_no_duplicate_inputs([Transaction.Recovered.t()]) :: {:ok, [map()]} | {:error, :block_duplicate_inputs} defp verify_no_duplicate_inputs(transactions) do all_inputs = Enum.flat_map(transactions, &Transaction.get_inputs/1) uniq_inputs = Enum.uniq_by(all_inputs, &Position.encode/1) case length(all_inputs) == length(uniq_inputs) do true -> {:ok, all_inputs} false -> {:error, :block_duplicate_inputs} end end @spec verify_fee_transactions([Transaction.Recovered.t()]) :: {:ok, [Transaction.Recovered.t()]} | {:error, atom()} defp verify_fee_transactions(transactions) do identified_fee_transactions = Enum.filter(transactions, &is_fee/1) with :ok <- expected_index(transactions, identified_fee_transactions), :ok <- unique_fee_transaction_per_currency(identified_fee_transactions) do {:ok, identified_fee_transactions} end end @spec expected_index([Transaction.Recovered.t()], [Transaction.Recovered.t()]) :: :ok | {:error, atom()} defp expected_index(transactions, identified_fee_transactions) do number_of_fee_txs = length(identified_fee_transactions) tail = Enum.slice(transactions, -number_of_fee_txs, number_of_fee_txs) case identified_fee_transactions do ^tail -> :ok _ -> {:error, :unexpected_transaction_type_at_fee_index} end end @spec unique_fee_transaction_per_currency([Transaction.Recovered.t()]) :: :ok | {:error, atom()} defp unique_fee_transaction_per_currency(identified_fee_transactions) do identified_fee_transactions |> Enum.uniq_by(fn fee_transaction -> fee_transaction |> get_fee_output() |> Map.get(:currency) end) |> case do ^identified_fee_transactions -> :ok _ -> {:error, :duplicate_fee_transaction_for_ccy} end end defp is_fee(%Transaction.Recovered{signed_tx: %Transaction.Signed{raw_tx: %Transaction.Fee{}}}) do true end defp is_fee(_), do: false defp get_fee_output(fee_transaction) do fee_transaction |> Transaction.get_outputs() |> Enum.at(0) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/child_manager.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ChildManager do @moduledoc """ Reports it's health to the Monitor after start or restart and shutsdown. """ use GenServer, restart: :transient require Logger @timer 100 def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init(args) do monitor = Keyword.fetch!(args, :monitor) {:ok, _tref} = :timer.send_after(@timer, :health_checkin) {:ok, %{timer: @timer, monitor: monitor}} end def handle_info(:health_checkin, state) do :ok = state.monitor.health_checkin() {:stop, :normal, state} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/configuration.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Configuration do @moduledoc """ Provides access to applications configuration """ @app :omg_watcher def exit_processor_sla_margin() do Application.fetch_env!(@app, :exit_processor_sla_margin) end def exit_processor_sla_margin_forced() do Application.fetch_env!(@app, :exit_processor_sla_margin_forced) end def metrics_collection_interval() do Application.fetch_env!(@app, :metrics_collection_interval) end def block_getter_reorg_margin() do Application.fetch_env!(@app, :block_getter_reorg_margin) end def maximum_block_withholding_time_ms() do Application.fetch_env!(@app, :maximum_block_withholding_time_ms) end def maximum_number_of_unapplied_blocks() do Application.fetch_env!(@app, :maximum_number_of_unapplied_blocks) end def child_chain_url() do Application.get_env(@app, :child_chain_url) end def exit_finality_margin() do Application.get_env(@app, :exit_finality_margin) end @spec deposit_finality_margin() :: pos_integer() | no_return def deposit_finality_margin() do Application.get_env(@app, :deposit_finality_margin) end @spec fee_claimer_address() :: binary() | no_return def fee_claimer_address() do Application.fetch_env!(@app, :fee_claimer_address) end @spec ethereum_events_check_interval_ms() :: pos_integer() | no_return def ethereum_events_check_interval_ms() do Application.fetch_env!(@app, :ethereum_events_check_interval_ms) end @spec coordinator_eth_height_check_interval_ms() :: pos_integer() | no_return def coordinator_eth_height_check_interval_ms() do Application.fetch_env!(@app, :coordinator_eth_height_check_interval_ms) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/coordinator_setup.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.CoordinatorSetup do @moduledoc """ The setup of `OMG.Watcher.RootChainCoordinator` for the Watcher - configures the relations between different event listeners """ @doc """ The `OMG.Watcher.RootChainCoordinator` setup for the `OMG.Watcher` app. Summary of the configuration: - deposits are recognized after `deposit_finality_margin`. Should take child chain server's setting into account - exit-related events are recognized after `exit_finality_margin` - exit-related events wait for deposits and themselves respectively, in case of the inter-dependent IFE events - exit finalization-related events wait for deposits and blocks to never finalize not-yet created UTXOs - blocks wait for deposits _BUT_ they advance by the finality margin of the `depositor`. In practice this means that blocks wait for deposits when syncing, but don't when processing fresh events. This allows for 0-confirmation finality of child chain transaction (the user is responsible for deciding on finality and confirmations) """ def coordinator_setup( metrics_collection_interval, coordinator_eth_height_check_interval_ms, finality_margin, deposit_finality_margin ) do {[ metrics_collection_interval: metrics_collection_interval, coordinator_eth_height_check_interval_ms: coordinator_eth_height_check_interval_ms ], %{ depositor: [finality_margin: deposit_finality_margin], block_getter: [ waits_for: [depositor: :no_margin], finality_margin: 0 ], exit_processor: [waits_for: :depositor, finality_margin: finality_margin], exit_finalizer: [ waits_for: [:depositor, :block_getter, :exit_processor], finality_margin: finality_margin ], exit_challenger: [waits_for: :exit_processor, finality_margin: finality_margin], in_flight_exit_processor: [waits_for: :depositor, finality_margin: finality_margin], in_flight_exit_deleted_processor: [waits_for: :in_flight_exit_processor, finality_margin: finality_margin], piggyback_processor: [waits_for: :in_flight_exit_processor, finality_margin: finality_margin], competitor_processor: [waits_for: :in_flight_exit_processor, finality_margin: finality_margin], challenges_responds_processor: [waits_for: :competitor_processor, finality_margin: finality_margin], piggyback_challenges_processor: [waits_for: :piggyback_processor, finality_margin: finality_margin], ife_exit_finalizer: [ waits_for: [:depositor, :block_getter, :in_flight_exit_processor, :piggyback_processor], finality_margin: finality_margin ] }} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/crypto.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Crypto do @moduledoc """ Signs and validates signatures. Constructed signatures can be used directly in Ethereum with `ecrecover` call. For unsafe code, limited to `:test` and `:dev` environments and related to private key handling refer to: `OMG.Watcher.DevCrypto` in `test/support` """ alias ExPlasma.Crypto alias OMG.Watcher.Signature @type sig_t() :: <<_::520>> @type pub_key_t() :: <<_::512>> @type priv_key_t() :: <<_::256>> | <<>> @type address_t() :: <<_::160>> @type hash_t() :: <<_::256>> @type domain_separator_t() :: <<_::256>> | nil @doc """ Produces a KECCAK digest for the message. see https://hexdocs.pm/exth_crypto/ExthCrypto.Hash.html#kec/0 ## Example iex> OMG.Watcher.Crypto.hash("omg!") <<241, 85, 204, 147, 187, 239, 139, 133, 69, 248, 239, 233, 219, 51, 189, 54, 171, 76, 106, 229, 69, 102, 203, 7, 21, 134, 230, 92, 23, 209, 187, 12>> """ @spec hash(binary) :: hash_t() def hash(message), do: Crypto.keccak_hash(message) @doc """ Recovers the address of the signer from a binary-encoded signature. """ @spec recover_address(hash_t(), sig_t()) :: {:ok, address_t()} | {:error, atom()} def recover_address(<>, <>) do case Signature.recover_public(digest, packed_signature) do {:ok, pub} -> generate_address(pub) {:error, :recovery_id_not_u8} -> {:error, :signature_corrupt} {:error, _} = error -> error end end @doc """ Given public key, returns an address. """ @spec generate_address(pub_key_t()) :: {:ok, address_t()} def generate_address(<>) do <<_::binary-size(12), address::binary-size(20)>> = hash(pub) {:ok, address} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/datadog_event/contract_event_consumer.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.DatadogEvent.ContractEventConsumer do @moduledoc """ Subscribes to new events from EthereumEventListeners and pushes them to Datadog Integrated with the help of: https://docs.datadoghq.com/api/?lang=bash#post-an-event Most things from the doc doesn't work. Either because Statix doesn't work or Datadog. Date_happened, aggregation_key, source_type_name doesn't seem to appear in Events list. Hence we transform everything into a tag. """ require Logger alias OMG.Watcher.DatadogEvent.Encode @doc """ Returns child_specs for the given `EventConsumer`, to be included e.g. in Supervisor's children. """ # sobelow_skip ["DOS.StringToAtom"] @spec prepare_child(keyword()) :: %{id: atom(), start: tuple()} def prepare_child(opts \\ []) do {:root_chain, topic_name} = Keyword.fetch!(opts, :topic) %{ id: String.to_atom("root_chain:#{topic_name}_worker"), start: {__MODULE__, :start_link, [opts]}, shutdown: :brutal_kill, type: :worker } end def start_link(args) do GenServer.start_link(__MODULE__, args) end ### Server use GenServer def init(args) do publisher = Keyword.fetch!(args, :publisher) {:root_chain, event_name} = topic = Keyword.fetch!(args, :topic) release = Keyword.fetch!(args, :release) current_version = Keyword.fetch!(args, :current_version) :ok = OMG.Bus.subscribe(topic, link: true) _ = Logger.info("Started #{inspect(__MODULE__)} for event root_chain:#{event_name}") {:ok, %{publisher: publisher, release: release, current_version: current_version}} end def handle_info({:internal_event_bus, :enqueue_block, _omg_block}, state) do # ignore for now {:noreply, state} end @doc """ Listens to events via OMG BUS and send them off the assumption is all events are of the same type """ def handle_info({:internal_event_bus, :data, data}, state) do aggregation_key = :root_chain timestamp = DateTime.to_unix(DateTime.utc_now(), :millisecond) options = tags(aggregation_key, state.release, state.current_version, timestamp) Enum.each(data, fn ev -> %{event_signature: event_signature} = ev [event_name, _] = String.split(event_signature, "(") title = "#{event_name}" message = "[#{inspect(Encode.make_it_readable!(ev))}] - Timestamp: #{timestamp}" :ok = apply(state.publisher, :event, create_event_data(title, message, options)) end) {:noreply, state} end defp create_event_data(title, message, options) do [title, message, options] end # https://docs.datadoghq.com/api/?lang=bash#api-reference defp tags(aggregation_key, release, current_version, _timestamp) do [ {:aggregation_key, aggregation_key}, {:tags, ["#{aggregation_key}", "#{release}", "vsn-#{current_version}"]}, {:alert_type, "success"} # {:timestamp, timestamp} ] end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/datadog_event/encode.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.DatadogEvent.Encode do @moduledoc """ Iterates the input and hex encodes binaries """ def make_it_readable!(event) when is_map(event) do compactor = fn {k, v}, acc -> cond do is_map(v) and Enum.empty?(v) -> acc is_list(v) -> Map.put_new(acc, k, make_it_readable!(v)) is_map(v) -> Map.put_new(acc, k, make_it_readable!(v)) k == :event_signature -> Map.put_new(acc, k, v) is_binary(v) -> Map.put_new(acc, k, "0x" <> Base.encode16(v, case: :lower)) true -> Map.put_new(acc, k, v) end end Enum.reduce(event, %{}, compactor) end def make_it_readable!(event) when is_list(event) do compactor = fn v, acc -> cond do is_map(v) and Enum.empty?(v) -> acc is_integer(v) -> [v | acc] is_map(v) -> [make_it_readable!(v) | acc] is_binary(v) -> ["0x" <> Base.encode16(v, case: :lower) | acc] true -> [v | acc] end end Enum.reduce(event, [], compactor) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/ethereum_event_aggregator.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.EthereumEventAggregator do @moduledoc """ This process combines all plasma contract events we're interested in and does eth_getLogs + enriches them if needed for all Ethereum Event Listener processes. """ use GenServer require Logger use Spandex.Decorators alias OMG.Eth.RootChain.Abi alias OMG.Eth.RootChain.Event alias OMG.Eth.RootChain.Rpc @timeout 55_000 @type result() :: {:ok, list(map())} | {:error, :check_range} @spec deposit_created(GenServer.server(), pos_integer(), pos_integer()) :: result() def deposit_created(server \\ __MODULE__, from_block, to_block) do forward_call(server, :deposit_created, from_block, to_block, @timeout) end @spec exit_started(GenServer.server(), pos_integer(), pos_integer()) :: result() def exit_started(server \\ __MODULE__, from_block, to_block) do forward_call(server, :exit_started, from_block, to_block, @timeout) end @spec exit_finalized(GenServer.server(), pos_integer(), pos_integer()) :: result() def exit_finalized(server \\ __MODULE__, from_block, to_block) do forward_call(server, :exit_finalized, from_block, to_block, @timeout) end @spec exit_challenged(GenServer.server(), pos_integer(), pos_integer()) :: result() def exit_challenged(server \\ __MODULE__, from_block, to_block) do forward_call(server, :exit_challenged, from_block, to_block, @timeout) end @spec in_flight_exit_started(GenServer.server(), pos_integer(), pos_integer()) :: result() def in_flight_exit_started(server \\ __MODULE__, from_block, to_block) do forward_call(server, :in_flight_exit_started, from_block, to_block, @timeout) end @spec in_flight_exit_deleted(GenServer.server(), pos_integer(), pos_integer()) :: result() def in_flight_exit_deleted(server \\ __MODULE__, from_block, to_block) do forward_call(server, :in_flight_exit_deleted, from_block, to_block, @timeout) end @spec in_flight_exit_piggybacked(GenServer.server(), pos_integer(), pos_integer()) :: result() def in_flight_exit_piggybacked(server \\ __MODULE__, from_block, to_block) do # input and output forward_call(server, :in_flight_exit_piggybacked, from_block, to_block, @timeout) end @spec in_flight_exit_challenged(GenServer.server(), pos_integer(), pos_integer()) :: result() def in_flight_exit_challenged(server \\ __MODULE__, from_block, to_block) do forward_call(server, :in_flight_exit_challenged, from_block, to_block, @timeout) end @spec in_flight_exit_challenge_responded(GenServer.server(), pos_integer(), pos_integer()) :: result() def in_flight_exit_challenge_responded(server \\ __MODULE__, from_block, to_block) do forward_call(server, :in_flight_exit_challenge_responded, from_block, to_block, @timeout) end @spec in_flight_exit_blocked(GenServer.server(), pos_integer(), pos_integer()) :: result() def in_flight_exit_blocked(server \\ __MODULE__, from_block, to_block) do forward_call(server, :in_flight_exit_blocked, from_block, to_block, @timeout) end @spec in_flight_exit_withdrawn(GenServer.server(), pos_integer(), pos_integer()) :: result() def in_flight_exit_withdrawn(server \\ __MODULE__, from_block, to_block) do forward_call(server, :in_flight_exit_withdrawn, from_block, to_block, @timeout) end defstruct delete_events_threshold_ethereum_block_height: 1000, ets_bucket: nil, event_signatures: [], events: [], contracts: [], rpc: nil def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, __MODULE__)) end def init(opts) do contracts = opts |> Keyword.fetch!(:contracts) |> Map.values() |> Enum.map(&from_hex(&1)) # events = [[signature: "ExitStarted(address,uint160)", name: :exit_started, enrich: true],..] events = opts |> Keyword.fetch!(:events) |> Enum.map(&Keyword.fetch!(&1, :name)) |> Event.get_events() |> Enum.zip(Keyword.fetch!(opts, :events)) |> Enum.reduce([], fn {signature, event}, acc -> [Keyword.put(event, :signature, signature) | acc] end) events_signatures = opts |> Keyword.fetch!(:events) |> Enum.map(&Keyword.fetch!(&1, :name)) |> Event.get_events() ets_bucket = Keyword.fetch!(opts, :ets_bucket) rpc = Keyword.get(opts, :rpc, Rpc) {:ok, %__MODULE__{ # 1000 blocks of events will be kept in memory delete_events_threshold_ethereum_block_height: 1000, ets_bucket: ets_bucket, event_signatures: events_signatures, events: events, contracts: contracts, rpc: rpc }} end @decorate trace(tracer: OMG.Watcher.Tracer, type: :backend, service: __MODULE__, name: "handle_call/3") def handle_call({:in_flight_exit_withdrawn, from_block, to_block}, _, state) do names = [:in_flight_exit_input_withdrawn, :in_flight_exit_output_withdrawn] logs = Enum.reduce(names, [], fn name, acc -> signature = state.events |> Enum.find(fn event -> Keyword.fetch!(event, :name) == name end) |> Keyword.fetch!(:signature) logs = retrieve_log(signature, from_block, to_block, state) logs ++ acc end) {:reply, {:ok, logs}, state, {:continue, from_block}} end @decorate trace(tracer: OMG.Watcher.Tracer, type: :backend, service: __MODULE__, name: "handle_call/3") def handle_call({:in_flight_exit_blocked, from_block, to_block}, _, state) do names = [:in_flight_exit_input_blocked, :in_flight_exit_output_blocked] logs = names |> Enum.reduce([], fn name, acc -> signature = state.events |> Enum.find(fn event -> Keyword.fetch!(event, :name) == name end) |> Keyword.fetch!(:signature) logs = retrieve_log(signature, from_block, to_block, state) logs ++ acc end) {:reply, {:ok, logs}, state, {:continue, from_block}} end @decorate trace(tracer: OMG.Watcher.Tracer, type: :backend, service: __MODULE__, name: "handle_call/3") def handle_call({:in_flight_exit_piggybacked, from_block, to_block}, _, state) do names = [:in_flight_exit_output_piggybacked, :in_flight_exit_input_piggybacked] logs = names |> Enum.reduce([], fn name, acc -> signature = state.events |> Enum.find(fn event -> Keyword.fetch!(event, :name) == name end) |> Keyword.fetch!(:signature) logs = retrieve_log(signature, from_block, to_block, state) logs ++ acc end) {:reply, {:ok, logs}, state, {:continue, from_block}} end @decorate trace(tracer: OMG.Watcher.Tracer, type: :backend, service: __MODULE__, name: "handle_call/3") def handle_call({name, from_block, to_block}, _, state) do signature = state.events |> Enum.find(fn event -> Keyword.fetch!(event, :name) == name end) |> Keyword.fetch!(:signature) logs = retrieve_log(signature, from_block, to_block, state) {:reply, {:ok, logs}, state, {:continue, from_block}} end defp forward_call(server, event, from_block, to_block, timeout) when from_block <= to_block do GenServer.call(server, {event, from_block, to_block}, timeout) end defp forward_call(_, _, from_block, to_block, _) when from_block > to_block do _ = Logger.error("From block #{from_block} was bigger than to_block #{to_block}") {:error, :check_range} end def handle_continue(new_height_blknum, state) do _num_deleted = delete_old_logs(new_height_blknum, state) {:noreply, state} end defp retrieve_and_store_logs(from_block, to_block, state) do from_block |> get_logs(to_block, state) |> enrich_logs_with_call_data(state) |> store_logs(from_block, to_block, state) end defp get_logs(from_height, to_height, state) do {:ok, logs} = state.rpc.get_ethereum_events(from_height, to_height, state.event_signatures, state.contracts) Enum.map(logs, &Abi.decode_log(&1)) end # we get the logs from RPC and we cross check with the event definition if we need to enrich them defp enrich_logs_with_call_data(decoded_logs, state) do events = state.events rpc = state.rpc Enum.map(decoded_logs, fn decoded_log -> decoded_log_signature = decoded_log.event_signature event = Enum.find(events, fn event -> Keyword.fetch!(event, :signature) == decoded_log_signature end) case Keyword.fetch!(event, :enrich) do true -> {:ok, enriched_data} = rpc.get_call_data(decoded_log.root_chain_txhash) enriched_data_decoded = enriched_data |> from_hex |> Abi.decode_function() Map.put(decoded_log, :call_data, enriched_data_decoded) _ -> decoded_log end end) end defp store_logs(decoded_logs, from_block, to_block, state) do event_signatures = state.event_signatures # all logs come in a list of maps # we want to group them by blknum and signature: # [{286, "InFlightExitChallengeResponded(address,bytes32,uint256)", [event]}, # {287, "ExitChallenged(uint256)",[event, event]] decoded_logs_in_keypair = decoded_logs |> Enum.group_by( fn decoded_log -> {decoded_log.eth_height, decoded_log.event_signature} end, fn decoded_log -> decoded_log end ) |> Enum.map(fn {{blknum, signature}, logs} -> {blknum, signature, logs} end) # if we visited a particular range of blknum (from, to) we want to # insert empty data in the DB, so that clients know we've been there and that blocks are # empty of logs. # for the whole from, to range and signatures we create group pairs like so: # from = 286, to = 287 signatures = ["Exit", "Deposit"] # [{286, "Exit", []},{286, "Deposit", []},{287, "Exit", []},{287, "Deposit", []}] empty_blknum_signature_events = from_block..to_block |> Enum.to_list() |> Enum.map(fn blknum -> Enum.map(event_signatures, fn signature -> {blknum, signature, []} end) end) |> List.flatten() # we now merge the two lists # it is important that logs we got from RPC are first # because uniq_by takes the first occurance of {blknum, signature} # so that we don't overwrite retrieved logs data = decoded_logs_in_keypair |> Enum.concat(empty_blknum_signature_events) |> Enum.uniq_by(fn {blknum, signature, _data} -> {blknum, signature} end) true = :ets.insert(state.ets_bucket, data) :ok end # delete everything older then (current block - delete_events_threshold) defp delete_old_logs(new_height_blknum, state) do # :ets.fun2ms(fn {block_number, _event_signature, _event} when # block_number <= new_height - delete_events_threshold -> true end) match_spec = [ {{:"$1", :"$2", :"$3"}, [ {:"=<", :"$1", {:-, {:const, new_height_blknum}, {:const, state.delete_events_threshold_ethereum_block_height}}} ], [true]} ] :ets.select_delete(state.ets_bucket, match_spec) end # allow ethereum event listeners to retrieve logs from ETS in bulk defp retrieve_log(signature, from_block, to_block, state) do # :ets.fun2ms(fn {block_number, event_signature, event} when # block_number >= from_block and block_number <= to_block # and event_signature == signature -> event # end) event_match_spec = [ {{:"$1", :"$2", :"$3"}, [ {:andalso, {:andalso, {:>=, :"$1", {:const, from_block}}, {:"=<", :"$1", {:const, to_block}}}, {:==, :"$2", {:const, signature}}} ], [:"$3"]} ] block_range = [ {{:"$1", :"$2", :"$3"}, [ {:andalso, {:andalso, {:>=, :"$1", {:const, from_block}}, {:"=<", :"$1", {:const, to_block}}}, {:==, :"$2", {:const, signature}}} ], [:"$1"]} ] events = state.ets_bucket |> :ets.select(event_match_spec) |> List.flatten() blknum_list = :ets.select(state.ets_bucket, block_range) # we may not have all the block information the ethereum event listener wants # so we check for that and find all logs for missing blocks # in one RPC call for all signatures case Enum.to_list(from_block..to_block) -- blknum_list do [] -> events missing_blocks -> missing_blocks = Enum.sort(missing_blocks) missing_from_block = List.first(missing_blocks) missing_to_block = List.last(missing_blocks) _ = Logger.debug( "Missing block information (#{missing_from_block}, #{missing_to_block}) in event fetcher. Retrieving from RPC." ) :ok = retrieve_and_store_logs(missing_from_block, missing_to_block, state) retrieve_log(signature, from_block, to_block, state) end end defp from_hex("0x" <> encoded), do: Base.decode16!(encoded, case: :lower) end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/ethereum_event_listener/core.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.EthereumEventListener.Core do @moduledoc """ Logic module for the `OMG.Watcher.EthereumEventListener` Responsible for: - deciding what ranges of Ethereum events should be fetched from the Ethereum node - deciding the right size of event batches to read (too little means many RPC requests, too big can timeout) - deciding what to check in into the `OMG.Watcher.RootChainCoordinator` - deciding what to put into the `OMG.DB` in terms of Ethereum height till which the events are already processed Leverages a rudimentary in-memory cache for events, to be able to ask for right-sized batches of events """ alias OMG.Watcher.RootChainCoordinator.SyncGuide use Spandex.Decorators # synced_height is what's being exchanged with `RootChainCoordinator`. # The point in root chain until where it processed defstruct synced_height_update_key: nil, service_name: nil, synced_height: 0, ethereum_events_check_interval_ms: nil, request_max_size: 1000 @type event :: %{eth_height: non_neg_integer()} @type t() :: %__MODULE__{ synced_height_update_key: atom(), service_name: atom(), synced_height: integer(), ethereum_events_check_interval_ms: non_neg_integer(), request_max_size: pos_integer() } @doc """ Initializes the listener logic based on its configuration and the last persisted Ethereum height, till which events were processed """ @spec init(atom(), atom(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: {t(), non_neg_integer()} def init( update_key, service_name, last_synced_ethereum_height, ethereum_events_check_interval_ms, request_max_size \\ 1000 ) do initial_state = %__MODULE__{ synced_height_update_key: update_key, synced_height: last_synced_ethereum_height, service_name: service_name, request_max_size: request_max_size, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms } {initial_state, last_synced_ethereum_height} end @doc """ Returns the events range - - from (inclusive!), - to (inclusive!) that needs to be scraped and sets synced_height in the state. """ @decorate span(service: :ethereum_event_listener, type: :backend, name: "calc_events_range_set_height/2") @spec calc_events_range_set_height(t(), SyncGuide.t()) :: {:dont_fetch_events, t()} | {{non_neg_integer, non_neg_integer}, t()} def calc_events_range_set_height(state, sync_guide) do case sync_guide.sync_height <= state.synced_height do true -> {:dont_fetch_events, state} _ -> # if sync_guide.sync_height has applied margin (reorg protection) # the only thing we need to be aware of is that we don't go pass that! # but we want to move as fast as possible so we try to fetch as much as we can (request_max_size) first_not_visited = state.synced_height + 1 # if first not visited = 1, and request max size is 10 # it means we can scrape AT MOST request_max_size events max_height = state.request_max_size - 1 upper_bound = min(sync_guide.sync_height, first_not_visited + max_height) {{first_not_visited, upper_bound}, %{state | synced_height: upper_bound}} end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/ethereum_event_listener/measure.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.EthereumEventListener.Measure do @moduledoc """ Counting business metrics sent to Datadog. We don't want to pattern match on :ok to Datadog because the connection towards the statsd client can be intermittent and sending would be unsuccessful and that would trigger the removal of telemetry handler. But because we have monitors in place, that eventually recover the connection to Statsd handlers wouldn't exist anymore and metrics wouldn't be published. """ import OMG.Status.Metric.Event, only: [name: 2] alias OMG.Status.Metric.Datadog alias OMG.Status.Metric.Tracer @supported_events [ [:process, OMG.Watcher.EthereumEventListener], [:trace, OMG.Watcher.EthereumEventListener], [:trace, OMG.Watcher.EthereumEventListener.Core] ] def supported_events(), do: @supported_events def handle_event([:process, OMG.Watcher.EthereumEventListener], %{events: events}, state, _config) do _ = Datadog.gauge(name(state.service_name, :events), length(events)) end def handle_event([:process, OMG.Watcher.EthereumEventListener], %{}, state, _config) do value = self() |> Process.info(:message_queue_len) |> elem(1) _ = Datadog.gauge(name(state.service_name, :message_queue_len), value) end def handle_event([:trace, _], %{}, state, _config) do Tracer.update_top_span(service: state.service_name, tags: []) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/ethereum_event_listener.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.EthereumEventListener do @moduledoc """ GenServer running the listener. Periodically fetches events made on dynamically changing block range from the root chain contract and feeds them to a callback. It is **not** responsible for figuring out which ranges of Ethereum blocks are eligible to scan and when, see `OMG.Watcher.RootChainCoordinator` for that. The `OMG.Watcher.RootChainCoordinator` provides the `SyncGuide` that indicates what's eligible to scan, taking into account: - finality margin - mutual ordering and dependencies of various types of Ethereum events to be respected. It **is** responsible for processing all events from all blocks and processing them only once. It accomplishes that by keeping a persisted value in `OMG.DB` and its state that reflects till which Ethereum height the events were processed (`synced_height`). This `synced_height` is updated after every batch of Ethereum events get successfully consumed by `callbacks.process_events_callback`, as called in `sync_height/2`, together with all the `OMG.DB` updates this callback returns, atomically. The key in `OMG.DB` used to persist `synced_height` is defined by the value of `synced_height_update_key`. What specific Ethereum events it fetches, and what it does with them is up to predefined `callbacks`. See `OMG.Watcher.EthereumEventListener.Core` for the implementation of the business logic for the listener. """ use GenServer use Spandex.Decorators require Logger alias OMG.Watcher.EthereumEventListener.Core alias OMG.Watcher.RootChainCoordinator @type config() :: %{ block_finality_margin: non_neg_integer, synced_height_update_key: atom, service_name: atom, # maps a pair denoting eth height range to a list of ethereum events get_events_callback: (non_neg_integer, non_neg_integer -> {:ok, [map]}), # maps a list of ethereum events to a list of `db_updates` to send to `OMG.DB` process_events_callback: ([any] -> {:ok, [tuple]}) } ### Client @spec start_link(config()) :: GenServer.on_start() def start_link(config) do %{service_name: name} = config GenServer.start_link(__MODULE__, config, name: name) end @doc """ Returns child_specs for the given `EthereumEventListener` setup, to be included e.g. in Supervisor's children. See `handle_continue/2` for the required keyword arguments. """ @spec prepare_child(keyword()) :: %{id: atom(), start: tuple()} def prepare_child(opts \\ []) do name = Keyword.fetch!(opts, :service_name) %{ id: name, start: {OMG.Watcher.EthereumEventListener, :start_link, [Map.new(opts)]}, shutdown: :brutal_kill, type: :worker } end ### Server @doc """ Initializes the GenServer state, most work done in `handle_continue/2`. """ def init(init) do {:ok, init, {:continue, :setup}} end @doc """ Reads the status of listening (till which Ethereum height were the events processed) from the `OMG.DB` and initializes the logic `OMG.Watcher.EthereumEventListener.Core` with it. Does an initial `OMG.Watcher.RootChainCoordinator.check_in` with the Ethereum height it last stopped on. Next, it continues to monitor and fetch the events as usual. """ def handle_continue( :setup, %{ contract_deployment_height: contract_deployment_height, synced_height_update_key: update_key, service_name: service_name, get_events_callback: get_events_callback, process_events_callback: process_events_callback, metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms } ) do _ = Logger.info("Starting #{inspect(__MODULE__)} for #{service_name}.") {:ok, last_event_block_height} = OMG.DB.get_single_value(update_key) # we don't need to ever look at earlier than contract deployment last_event_block_height = max(last_event_block_height, contract_deployment_height) {initial_state, height_to_check_in} = Core.init(update_key, service_name, last_event_block_height, ethereum_events_check_interval_ms) callbacks = %{ get_ethereum_events_callback: get_events_callback, process_events_callback: process_events_callback } {:ok, _} = schedule_get_events(ethereum_events_check_interval_ms) :ok = RootChainCoordinator.check_in(height_to_check_in, service_name) {:ok, _} = :timer.send_interval(metrics_collection_interval, self(), :send_metrics) _ = Logger.info("Started #{inspect(__MODULE__)} for #{service_name}, synced_height: #{inspect(height_to_check_in)}") {:noreply, {initial_state, callbacks}} end def handle_info(:send_metrics, {state, callbacks}) do :ok = :telemetry.execute([:process, __MODULE__], %{}, state) {:noreply, {state, callbacks}} end @doc """ Main worker function, called on a cadence as initialized in `handle_continue/2`. Does the following: - asks `OMG.Watcher.RootChainCoordinator` about how to sync, with respect to other services listening to Ethereum - (`sync_height/2`) figures out what is the suitable range of Ethereum blocks to download events for - (`sync_height/2`) if necessary fetches those events to the in-memory cache in `OMG.Watcher.EthereumEventListener.Core` - (`sync_height/2`) executes the related event-consuming callback with events as arguments - (`sync_height/2`) does `OMG.DB` updates that persist the processes Ethereum height as well as whatever the callbacks returned to persist - (`sync_height/2`) `OMG.Watcher.RootChainCoordinator.check_in` to tell the rest what Ethereum height was processed. """ @decorate trace(service: :ethereum_event_listener, type: :backend) def handle_info(:sync, {state, callbacks}) do :ok = :telemetry.execute([:trace, __MODULE__], %{}, state) case RootChainCoordinator.get_sync_info() do :nosync -> :ok = RootChainCoordinator.check_in(state.synced_height, state.service_name) {:ok, _} = schedule_get_events(state.ethereum_events_check_interval_ms) {:noreply, {state, callbacks}} sync_info -> new_state = sync_height(state, callbacks, sync_info) {:ok, _} = schedule_get_events(state.ethereum_events_check_interval_ms) {:noreply, {new_state, callbacks}} end end # see `handle_info/2`, clause for `:sync` @decorate span(service: :ethereum_event_listener, type: :backend, name: "sync_height/3") defp sync_height(state, callbacks, sync_guide) do {events, new_state} = state |> Core.calc_events_range_set_height(sync_guide) |> get_events(callbacks.get_ethereum_events_callback) db_update = [{:put, new_state.synced_height_update_key, new_state.synced_height}] :ok = :telemetry.execute([:process, __MODULE__], %{events: events}, new_state) {:ok, db_updates_from_callback} = callbacks.process_events_callback.(events) :ok = publish_events(events) :ok = OMG.DB.multi_update(db_update ++ db_updates_from_callback) :ok = RootChainCoordinator.check_in(new_state.synced_height, new_state.service_name) new_state end defp get_events({{from, to}, state}, get_events_callback) do {:ok, new_events} = get_events_callback.(from, to) {new_events, state} end defp get_events({:dont_fetch_events, state}, _callback) do {[], state} end defp schedule_get_events(ethereum_events_check_interval_ms) do :timer.send_after(ethereum_events_check_interval_ms, self(), :sync) end defp publish_events([%{event_signature: event_signature} | _] = data) do [event_signature, _] = String.split(event_signature, "(") {:root_chain, event_signature} |> OMG.Bus.Event.new(:data, data) |> OMG.Bus.direct_local_broadcast() end defp publish_events([]), do: :ok end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/event.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Event do @moduledoc """ Definitions of structures representing various events delivered by the Watcher This module is agnostic of mode of delivery of events - both push and poll events go here """ alias OMG.Watcher.Block alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction @type byzantine_t :: OMG.Watcher.Event.InvalidBlock.t() | OMG.Watcher.Event.BlockWithholding.t() | OMG.Watcher.Event.InvalidExit.t() | OMG.Watcher.Event.UnchallengedExit.t() | OMG.Watcher.Event.NonCanonicalIFE.t() | OMG.Watcher.Event.UnchallengedNonCanonicalIFE.t() | OMG.Watcher.Event.InvalidIFEChallenge.t() | OMG.Watcher.Event.PiggybackAvailable.t() | OMG.Watcher.Event.InvalidPiggyback.t() | OMG.Watcher.Event.UnchallengedPiggyback.t() @type t :: OMG.Watcher.Event.AddressReceived.t() | OMG.Watcher.Event.ExitFinalized.t() | byzantine_t() @type module_t :: OMG.Watcher.Event.InvalidBlock | OMG.Watcher.Event.BlockWithholding | OMG.Watcher.Event.InvalidExit | OMG.Watcher.Event.UnchallengedExit | OMG.Watcher.Event.NonCanonicalIFE | OMG.Watcher.Event.UnchallengedNonCanonicalIFE.t() | OMG.Watcher.Event.InvalidIFEChallenge | OMG.Watcher.Event.PiggybackAvailable | OMG.Watcher.Event.InvalidPiggyback | OMG.Watcher.Event.UnchallengedPiggyback | OMG.Watcher.Event.AddressReceived | OMG.Watcher.Event.ExitFinalized # TODO The reason why events have name as String and byzantine events as atom is that # Phoniex websockets requires topics as strings + currently we treat Strings and binaries in # the same way in `OMG.Watcher.Web.Serializers.Response` defmodule AddressReceived do @moduledoc """ Notifies about received funds by particular address """ defstruct [:tx, :child_blknum, :child_txindex, :child_block_hash, :submited_at_ethheight] @type t :: %__MODULE__{ tx: Transaction.Recovered.t(), child_blknum: pos_integer(), child_txindex: non_neg_integer(), child_block_hash: Block.block_hash_t(), submited_at_ethheight: pos_integer() } end defmodule AddressSpent do @moduledoc """ Notifies about spent funds by particular address """ defstruct [:tx, :child_blknum, :child_txindex, :child_block_hash, :submited_at_ethheight] @type t :: %__MODULE__{ tx: Transaction.Recovered.t(), child_blknum: pos_integer(), child_txindex: non_neg_integer(), child_block_hash: Block.block_hash_t(), submited_at_ethheight: pos_integer() } end defmodule ExitFinalized do @moduledoc """ Notifies about finalized exit """ defstruct [:currency, :amount, :child_blknum, :child_txindex, :child_oindex] @type t :: %__MODULE__{ currency: Crypto.address_t(), amount: non_neg_integer(), child_blknum: non_neg_integer(), child_txindex: non_neg_integer(), child_oindex: non_neg_integer() } end defmodule InvalidBlock do @moduledoc """ Notifies about invalid block """ defstruct [:hash, :blknum, :error_type, name: :invalid_block] @type t :: %__MODULE__{ hash: Block.block_hash_t(), blknum: integer(), error_type: atom(), name: atom() } end defmodule BlockWithholding do @moduledoc """ Notifies about block-withholding """ defstruct [:blknum, :hash, name: :block_withholding] @type t :: %__MODULE__{ blknum: pos_integer(), hash: Block.block_hash_t(), name: atom() } end defmodule InvalidExit do @moduledoc """ Notifies about invalid exit """ defstruct [ :amount, :currency, :eth_height, :owner, :root_chain_txhash, :scheduled_finalization_time, :utxo_pos, :spending_txhash, name: :invalid_exit ] @type t :: %__MODULE__{ amount: pos_integer(), currency: binary(), owner: binary(), utxo_pos: pos_integer(), eth_height: pos_integer(), name: atom(), root_chain_txhash: Transaction.tx_hash() | nil, scheduled_finalization_time: pos_integer() | nil, spending_txhash: Transaction.tx_hash() | nil, root_chain_txhash: Transaction.tx_hash() | nil, name: atom() } end defmodule UnchallengedExit do @moduledoc """ Notifies about an invalid exit, that is dangerously approaching finalization, without being challenged It is a prompt to exit """ defstruct [ :amount, :currency, :eth_height, :owner, :root_chain_txhash, :scheduled_finalization_time, :utxo_pos, :spending_txhash, name: :unchallenged_exit ] @type t :: %__MODULE__{ amount: pos_integer(), currency: binary(), owner: binary(), utxo_pos: pos_integer(), eth_height: pos_integer(), name: atom(), root_chain_txhash: Transaction.tx_hash() | nil, scheduled_finalization_time: pos_integer() | nil, root_chain_txhash: Transaction.tx_hash() | nil, spending_txhash: Transaction.tx_hash() | nil, name: atom() } end defmodule NonCanonicalIFE do @moduledoc """ Notifies about an in-flight exit which has a competitor """ defstruct [:txbytes, name: :non_canonical_ife] @type t :: %__MODULE__{ txbytes: binary(), name: atom() } end defmodule UnchallengedNonCanonicalIFE do @moduledoc """ Notifies about an in-flight exit which has a competitor but is dangerously close to finalization. It is a prompt to exit """ defstruct [:txbytes, name: :unchallenged_non_canonical_ife] @type t :: %__MODULE__{ txbytes: binary(), name: atom() } end defmodule InvalidIFEChallenge do @moduledoc """ Notifies that a canonical in-flight exit has been challenged. The challenge should be responded to. """ defstruct [:txbytes, name: :invalid_ife_challenge] @type t :: %__MODULE__{ txbytes: binary(), name: atom() } end defmodule PiggybackAvailable do @moduledoc """ Notifies about an available piggyback. It is only fired, when the transaction hasn't been seen included. """ defstruct [:txbytes, :available_outputs, :available_inputs, name: :piggyback_available] @type available_output :: %{index: pos_integer(), address: binary()} @type t :: %__MODULE__{ txbytes: binary(), available_outputs: list(available_output()), available_inputs: list(available_output()), name: atom() } end defmodule InvalidPiggyback do @moduledoc """ Notifies about invalid piggyback. Piggyback is invalid if it is on input and that particular input was double-spend in other transaction (or other in-flight exit) or if it is on output that was spent on plasma chain. """ defstruct [:txbytes, :inputs, :outputs, name: :invalid_piggyback] @type t :: %__MODULE__{ txbytes: binary(), inputs: [non_neg_integer()], outputs: [non_neg_integer()], name: atom() } end defmodule UnchallengedPiggyback do @moduledoc """ Notifies about invalid piggyback, that is dangerously approaching finalization, without being challenged It is a prompt to exit """ defstruct [:txbytes, :inputs, :outputs, name: :unchallenged_piggyback] @type t :: %__MODULE__{ txbytes: binary(), inputs: [non_neg_integer()], outputs: [non_neg_integer()], name: atom() } end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/canonicity.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Canonicity do @moduledoc """ Encapsulates managing and executing the behaviors related to treating exits by the child chain and watchers Keeps a state of exits that are in progress, updates it with news from the root chain, compares to the state of the ledger (`OMG.Watcher.State`), issues notifications as it finds suitable. Should manage all kinds of exits allowed in the protocol and handle the interactions between them. This is the functional, zero-side-effect part of the exit processor. Logic should go here: - orchestrating the persistence of the state - finding invalid exits, disseminating them as events according to rules - enabling to challenge invalid exits - figuring out critical failure of invalid exit challenging (aka `:unchallenged_exit` event) - MoreVP protocol managing in general For the imperative shell, see `OMG.Watcher.ExitProcessor` """ alias OMG.Watcher.Block alias OMG.Watcher.Crypto alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.ExitProcessor.DoubleSpend alias OMG.Watcher.ExitProcessor.InFlightExitInfo alias OMG.Watcher.ExitProcessor.KnownTx alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo import OMG.Watcher.ExitProcessor.Tools require Utxo require Logger @type competitor_data_t :: %{ input_txbytes: binary(), input_utxo_pos: Utxo.Position.t(), in_flight_txbytes: binary(), in_flight_input_index: non_neg_integer(), competing_txbytes: binary(), competing_input_index: non_neg_integer(), competing_sig: Crypto.sig_t(), competing_tx_pos: nil | Utxo.Position.t(), competing_proof: binary() } @type prove_canonical_data_t :: %{ in_flight_txbytes: binary(), in_flight_tx_pos: Utxo.Position.t(), in_flight_proof: binary() } @doc """ Returns a tuple with byzantine events: first element is a list of events for ifes with competitor and the second is the same list filtered for late ifes past sla margin """ @spec get_ife_txs_with_competitors(Core.t(), KnownTx.known_txs_by_input_t(), pos_integer()) :: {list(Event.NonCanonicalIFE.t()), list(Event.UnchallengedNonCanonicalIFE.t())} def get_ife_txs_with_competitors(state, known_txs_by_input, eth_height_now) do non_canonical_ifes = state.in_flight_exits |> Map.values() |> Stream.map(fn ife -> {ife, DoubleSpend.find_competitor(known_txs_by_input, ife.tx)} end) |> Stream.filter(fn {_ife, maybe_competitor} -> !is_nil(maybe_competitor) end) |> Stream.filter(fn {ife, %DoubleSpend{known_tx: %KnownTx{utxo_pos: utxo_pos}}} -> InFlightExitInfo.is_viable_competitor?(ife, utxo_pos) end) non_canonical_ife_events = non_canonical_ifes |> Stream.map(fn {ife, _double_spend} -> Transaction.raw_txbytes(ife.tx) end) |> Enum.uniq() |> Enum.map(fn txbytes -> %Event.NonCanonicalIFE{txbytes: txbytes} end) past_sla_margin = fn {ife, _double_spend} -> ife.eth_height + state.sla_margin <= eth_height_now end late_non_canonical_ife_events = non_canonical_ifes |> Stream.filter(past_sla_margin) |> Stream.map(fn {ife, _double_spend} -> Transaction.raw_txbytes(ife.tx) end) |> Enum.uniq() |> Enum.map(fn txbytes -> %Event.UnchallengedNonCanonicalIFE{txbytes: txbytes} end) {non_canonical_ife_events, late_non_canonical_ife_events} end @doc """ Returns byzantine events for open IFEs that were challenged with an invalid challenge """ @spec get_invalid_ife_challenges(Core.t()) :: list(Event.InvalidIFEChallenge.t()) def get_invalid_ife_challenges(%Core{in_flight_exits: ifes}) do ifes |> Map.values() |> Stream.filter(&InFlightExitInfo.is_invalidly_challenged?/1) |> Stream.map(&Transaction.raw_txbytes(&1.tx)) |> Enum.uniq() |> Enum.map(fn txbytes -> %Event.InvalidIFEChallenge{txbytes: txbytes} end) end @doc """ Gets the root chain contract-required set of data to challenge a non-canonical ife """ @spec get_competitor_for_ife(ExitProcessor.Request.t(), Core.t(), binary()) :: {:ok, competitor_data_t()} | {:error, :competitor_not_found} | {:error, :ife_not_known_for_tx} | {:error, :no_viable_competitor_found} | {:error, Transaction.decode_error()} def get_competitor_for_ife( %ExitProcessor.Request{blocks_result: blocks}, %Core{} = state, ife_txbytes ) do known_txs_by_input = KnownTx.get_all_from_blocks_appendix(blocks, state) # find its competitor and use it to prepare the requested data with {:ok, ife_tx} <- Transaction.decode(ife_txbytes), {:ok, ife} <- get_ife(ife_tx, state.in_flight_exits), {:ok, double_spend} <- get_competitor(known_txs_by_input, ife.tx), %DoubleSpend{known_tx: %KnownTx{utxo_pos: utxo_pos}} = double_spend, true <- check_viable_competitor(ife, utxo_pos), do: {:ok, prepare_competitor_response(double_spend, ife, blocks)} end @doc """ Gets the root chain contract-required set of data to challenge an ife appearing as non-canonical in the root chain contract but which is known to be canonical locally because included in one of the blocks """ @spec prove_canonical_for_ife(Core.t(), binary()) :: {:ok, prove_canonical_data_t()} | {:error, :no_viable_canonical_proof_found} def prove_canonical_for_ife(%Core{} = state, ife_txbytes) do with {:ok, raw_ife_tx} <- Transaction.decode(ife_txbytes), {:ok, ife} <- get_ife(raw_ife_tx, state.in_flight_exits), true <- check_is_invalidly_challenged(ife), do: {:ok, prepare_canonical_response(ife)} end defp prepare_competitor_response( %DoubleSpend{ index: in_flight_input_index, known_spent_index: competing_input_index, known_tx: %KnownTx{signed_tx: known_signed_tx, utxo_pos: known_tx_utxo_pos} }, %InFlightExitInfo{tx: signed_ife_tx} = ife, blocks ) do {:ok, input_witnesses} = Transaction.Signed.get_witnesses(signed_ife_tx) owner = input_witnesses[in_flight_input_index] %{ input_tx: Enum.at(ife.input_txs, in_flight_input_index), input_utxo_pos: Enum.at(ife.input_utxos_pos, in_flight_input_index), in_flight_txbytes: signed_ife_tx |> Transaction.raw_txbytes(), in_flight_input_index: in_flight_input_index, competing_txbytes: known_signed_tx |> Transaction.raw_txbytes(), competing_input_index: competing_input_index, competing_sig: find_sig!(known_signed_tx, owner), competing_tx_pos: known_tx_utxo_pos || Utxo.position(0, 0, 0), competing_proof: maybe_calculate_proof(known_tx_utxo_pos, blocks) } end defp prepare_canonical_response(%InFlightExitInfo{tx: tx, tx_seen_in_blocks_at: {pos, proof}}), do: %{in_flight_txbytes: Transaction.raw_txbytes(tx), in_flight_tx_pos: pos, in_flight_proof: proof} defp maybe_calculate_proof(nil, _), do: <<>> defp maybe_calculate_proof(Utxo.position(blknum, txindex, _), blocks) do blocks |> Enum.find(fn %Block{number: number} -> blknum == number end) |> Block.inclusion_proof(txindex) end defp get_competitor(known_txs_by_input, signed_ife_tx) do known_txs_by_input |> DoubleSpend.find_competitor(signed_ife_tx) |> case do nil -> {:error, :competitor_not_found} value -> {:ok, value} end end defp check_viable_competitor(ife, utxo_pos), do: if(InFlightExitInfo.is_viable_competitor?(ife, utxo_pos), do: true, else: {:error, :no_viable_competitor_found}) defp check_is_invalidly_challenged(ife), do: if(InFlightExitInfo.is_invalidly_challenged?(ife), do: true, else: {:error, :no_viable_canonical_proof_found}) end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/competitor_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.CompetitorInfo do @moduledoc """ Represents the bulk of information about a competitor to an IFE. Internal stuff of `OMG.Watcher.ExitProcessor` """ alias OMG.Watcher.Crypto alias OMG.Watcher.ExitProcessor.InFlightExitInfo alias OMG.Watcher.State.Transaction # mapped by tx_hash defstruct [ :tx, # TODO: what if someone does challenges once more but with another input? :competing_input_index, :competing_input_signature ] # NOTE: Although `Transaction.Signed` is used here, not all inputs will have signatures in this construct # Still, we do use it, because it is formally correct - it is just not a valid transaction from the POV of # the ledger @type t :: %__MODULE__{ tx: Transaction.Signed.t(), competing_input_index: Transaction.input_index_t(), competing_input_signature: Crypto.sig_t() } # NOTE: we have no migrations, so we handle data compatibility here (make_db_update/1 and from_db_kv/1), OMG-421 def make_db_update( {tx_hash, %__MODULE__{ tx: tx = %Transaction.Signed{}, competing_input_index: input_index, competing_input_signature: signature }} ) when is_integer(input_index) and is_binary(signature) do value = %{ tx: InFlightExitInfo.to_db_value(tx), competing_input_index: input_index, competing_input_signature: signature } {:put, :competitor_info, {tx_hash, value}} end def from_db_kv({tx_hash, %{tx: signed_tx_map, competing_input_index: index, competing_input_signature: signature}}) when is_map(signed_tx_map) and is_integer(index) and is_binary(signature) do tx = InFlightExitInfo.from_db_signed_tx(signed_tx_map) competitor_map = %{ tx: tx, competing_input_index: index, competing_input_signature: signature } {tx_hash, struct!(__MODULE__, competitor_map)} end def new(%{call_data: %{competing_tx: tx_bytes, competing_tx_input_index: index, competing_tx_sig: sig}}), do: do_new(tx_bytes, index, sig) defp do_new(tx_bytes, competing_input_index, competing_input_signature) do with {:ok, %Transaction.Payment{} = raw_tx} <- Transaction.decode(tx_bytes) do {Transaction.raw_txhash(raw_tx), %__MODULE__{ tx: %Transaction.Signed{ raw_tx: raw_tx, sigs: [competing_input_signature] }, competing_input_index: competing_input_index, competing_input_signature: competing_input_signature }} end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/core.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Core do @moduledoc """ Logic related to treating exits by the Watcher. This is the functional, zero-side-effect part of the exit processor. Logic should go here: - orchestrating the persistence of the state - finding invalid exits, disseminating them as events according to rules - enabling to challenge invalid exits - figuring out critical failure of invalid exit challenging (aka `:unchallenged_exit` event) - MoreVP protocol managing in general This is the functional logic driving the `GenServer` in `OMG.Watcher.ExitProcessor` """ alias OMG.Watcher.Block alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.CompetitorInfo alias OMG.Watcher.ExitProcessor.ExitInfo alias OMG.Watcher.ExitProcessor.InFlightExitInfo alias OMG.Watcher.ExitProcessor.KnownTx alias OMG.Watcher.ExitProcessor.StandardExit alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo import OMG.Watcher.ExitProcessor.Tools require Utxo require Transaction.Payment require Logger @default_sla_margin 10 @zero_address <<0::160>> @max_inputs Transaction.Payment.max_inputs() @max_outputs Transaction.Payment.max_outputs() @type new_in_flight_exit_status_t() :: {tuple(), pos_integer()} @type piggyback_input_index_t() :: 0..unquote(@max_inputs - 1) @type piggyback_output_index_t() :: 0..unquote(@max_outputs - 1) @type new_piggyback_input_event_t() :: %{ tx_hash: Transaction.tx_hash(), output_index: piggyback_input_index_t(), omg_data: %{piggyback_type: :input} } @type new_piggyback_output_event_t() :: %{ tx_hash: Transaction.tx_hash(), output_index: piggyback_output_index_t(), omg_data: %{piggyback_type: :output} } @type new_piggyback_event_t() :: new_piggyback_input_event_t() | new_piggyback_output_event_t() defstruct [ :sla_margin, :min_exit_period_seconds, :child_block_interval, exits: %{}, in_flight_exits: %{}, exit_ids: %{}, competitors: %{} ] @type t :: %__MODULE__{ sla_margin: non_neg_integer(), exits: %{Utxo.Position.t() => ExitInfo.t()}, in_flight_exits: %{Transaction.tx_hash() => InFlightExitInfo.t()}, # NOTE: maps only standard exit_ids to the natural keys of standard exits (input pointers/utxo_pos) # rethink the approach to the keys in the data structures - how to manage exit_ids? should the contract # serve more data (e.g. input pointers/tx hashes) where it would normally only serve exit_ids? exit_ids: %{non_neg_integer() => Utxo.Position.t()}, competitors: %{Transaction.tx_hash() => CompetitorInfo.t()}, min_exit_period_seconds: non_neg_integer(), child_block_interval: non_neg_integer() } @type check_validity_result_t :: {:ok | {:error, :unchallenged_exit}, list(Event.byzantine_t())} @type spent_blknum_result_t() :: {:ok, pos_integer} | :not_found @type in_flight_exit_response_t() :: %{ txhash: binary(), txbytes: binary(), eth_height: non_neg_integer(), piggybacked_inputs: list(non_neg_integer()), piggybacked_outputs: list(non_neg_integer()) } @type in_flight_exits_response_t() :: %{binary() => in_flight_exit_response_t()} @doc """ Reads database-specific list of exits and turns them into current state """ @spec init( db_exits :: [{{pos_integer, non_neg_integer, non_neg_integer}, map}], db_in_flight_exits :: [{Transaction.tx_hash(), InFlightExitInfo.t()}], db_competitors :: [{Transaction.tx_hash(), CompetitorInfo.t()}], min_exit_period_seconds :: non_neg_integer(), child_block_interval :: non_neg_integer, sla_margin :: non_neg_integer ) :: {:ok, t()} def init( db_exits, db_in_flight_exits, db_competitors, min_exit_period_seconds, child_block_interval, sla_margin \\ @default_sla_margin ) do exits = db_exits |> Enum.map(&ExitInfo.from_db_kv/1) |> Map.new() exit_ids = Enum.into(exits, %{}, fn {utxo_pos, %ExitInfo{exit_id: exit_id}} -> {exit_id, utxo_pos} end) {:ok, %__MODULE__{ exits: exits, in_flight_exits: db_in_flight_exits |> Enum.map(&InFlightExitInfo.from_db_kv/1) |> Map.new(), exit_ids: exit_ids, competitors: db_competitors |> Enum.map(&CompetitorInfo.from_db_kv/1) |> Map.new(), sla_margin: sla_margin, min_exit_period_seconds: min_exit_period_seconds, child_block_interval: child_block_interval }} end @doc """ Use to check if the settings regarding the `:exit_processor_sla_margin` config of `:omg_watcher` are OK. Since there are combinations of our configuration that may lead to a dangerous setup of the Watcher (in particular - muting the reports of unchallenged_exits), we're enforcing that the `exit_processor_sla_margin` be not larger than `min_exit_period`. """ @spec check_sla_margin(pos_integer(), boolean(), pos_integer(), pos_integer()) :: :ok | {:error, :sla_margin_too_big} def check_sla_margin(sla_margin, sla_margin_forced, min_exit_period_seconds, ethereum_block_time_seconds) def check_sla_margin(sla_margin, true, min_exit_period_seconds, ethereum_block_time_seconds) do _ = if !sla_margin_safe?(sla_margin, min_exit_period_seconds, ethereum_block_time_seconds), do: Logger.warn("Allowing unsafe sla margin of #{sla_margin} blocks") :ok end def check_sla_margin(sla_margin, false, min_exit_period_seconds, ethereum_block_time_seconds) do if sla_margin_safe?(sla_margin, min_exit_period_seconds, ethereum_block_time_seconds), do: :ok, else: {:error, :sla_margin_too_big} end def exit_key_by_exit_id(%__MODULE__{exit_ids: exit_ids}, exit_id), do: exit_ids[exit_id] @doc """ Add new exits from Ethereum events into tracked state. The list of `exit_contract_statuses` is used to track current (as in wall-clock "now", not syncing "now") status. This is to prevent spurious invalid exit events being fired during syncing for exits that were challenged/finalized Still we do want to track these exits when syncing, to have them spend from `OMG.Watcher.State` on their finalization """ @spec new_exits(t(), list(map()), list(map)) :: {t(), list()} | {:error, :unexpected_events} def new_exits(state, new_exits, exit_contract_statuses) def new_exits(_, new_exits, exit_contract_statuses) when length(new_exits) != length(exit_contract_statuses) do {:error, :unexpected_events} end def new_exits(%__MODULE__{exits: exits, exit_ids: exit_ids} = state, new_exits, exit_contract_statuses) do new_exits_kv_pairs = new_exits |> Enum.zip(exit_contract_statuses) |> Enum.map(fn {event, contract_status} -> {ExitInfo.new_key(contract_status, event), ExitInfo.new(contract_status, event)} end) db_updates = new_exits_kv_pairs |> Enum.map(&ExitInfo.make_db_update/1) new_exits_map = Map.new(new_exits_kv_pairs) new_exit_ids_map = new_exits_map |> Enum.into(%{}, fn {utxo_pos, %ExitInfo{exit_id: exit_id}} -> {exit_id, utxo_pos} end) {%{state | exits: Map.merge(exits, new_exits_map), exit_ids: Map.merge(exit_ids, new_exit_ids_map)}, db_updates} end defdelegate finalize_exits(state, validities), to: ExitProcessor.Finalizations defdelegate prepare_utxo_exits_for_in_flight_exit_finalizations(state, finalizations), to: ExitProcessor.Finalizations defdelegate finalize_in_flight_exits(state, finalizations, validities), to: ExitProcessor.Finalizations @spec challenge_exits(t(), list(map)) :: {t(), list} def challenge_exits(%__MODULE__{exits: exits} = state, challenges) do challenged_positions = get_positions_from_events(challenges) new_exits_kv_pairs = exits |> Map.take(challenged_positions) |> Enum.into(%{}, fn {utxo_pos, exit_info} -> {utxo_pos, %ExitInfo{exit_info | is_active: false}} end) new_state = %{state | exits: Map.merge(exits, new_exits_kv_pairs)} db_updates = new_exits_kv_pairs |> Enum.map(&ExitInfo.make_db_update/1) {new_state, db_updates} end defp get_positions_from_events(exits) do exits |> Enum.map(fn %{utxo_pos: utxo_pos} = _finalization_info -> Utxo.Position.decode!(utxo_pos) end) end @doc """ Add new in flight exits from Ethereum events into tracked state. """ @spec new_in_flight_exits(t(), list(map()), list(new_in_flight_exit_status_t())) :: {t(), list()} | {:error, :unexpected_events} def new_in_flight_exits(state, new_ifes_events, contract_statuses) def new_in_flight_exits(_state, new_ifes_events, contract_statuses) when length(new_ifes_events) != length(contract_statuses), do: {:error, :unexpected_events} def new_in_flight_exits(%__MODULE__{in_flight_exits: ifes} = state, new_ifes_events, contract_statuses) do new_ifes = new_ifes_events |> Enum.zip(contract_statuses) |> Enum.map(fn {event, contract_status} -> InFlightExitInfo.new_kv(event, contract_status) end) |> Map.new() updated_state = %{state | in_flight_exits: Map.merge(ifes, new_ifes)} updated_ife_keys = new_ifes |> Enum.unzip() |> elem(0) db_updates = ife_db_updates(updated_state, updated_ife_keys) {updated_state, db_updates} end defp ife_db_updates(%__MODULE__{in_flight_exits: ifes}, updated_ife_keys) do ifes |> Map.take(Enum.to_list(updated_ife_keys)) |> Enum.map(&InFlightExitInfo.make_db_update/1) end @doc """ Add piggybacks from Ethereum events into tracked state. """ @spec new_piggybacks(t(), list(new_piggyback_event_t())) :: {t(), list()} def new_piggybacks(%__MODULE__{} = state, piggyback_events) when is_list(piggyback_events) do event_field_f = fn event -> {event[:omg_data][:piggyback_type], event[:output_index]} end consume_events(state, piggyback_events, event_field_f, &InFlightExitInfo.piggyback/2) end @spec new_ife_challenges(t(), [map()]) :: {t(), list()} def new_ife_challenges(%__MODULE__{} = state, challenges_events) do {updated_state, ife_db_updates} = consume_events(state, challenges_events, & &1[:competitor_position], &InFlightExitInfo.challenge/2) {updated_state2, competitors_db_updates} = append_new_competitors(updated_state, challenges_events) {updated_state2, competitors_db_updates ++ ife_db_updates} end defp append_new_competitors(%__MODULE__{competitors: competitors} = state, challenges_events) do new_competitors = challenges_events |> Enum.map(&CompetitorInfo.new/1) db_updates = new_competitors |> Enum.map(&CompetitorInfo.make_db_update/1) {%{state | competitors: Map.merge(competitors, Map.new(new_competitors))}, db_updates} end @spec respond_to_in_flight_exits_challenges(t(), [map()]) :: {t(), list()} def respond_to_in_flight_exits_challenges(%__MODULE__{} = state, responds_events) do consume_events(state, responds_events, & &1[:challenge_position], &InFlightExitInfo.respond_to_challenge/2) end @spec challenge_piggybacks(t(), [map()]) :: {t(), list()} def challenge_piggybacks(%__MODULE__{} = state, challenges) do event_field_f = fn event -> {event[:omg_data][:piggyback_type], event[:output_index]} end consume_events(state, challenges, event_field_f, &InFlightExitInfo.challenge_piggyback/2) end # produces new state and some db_updates based on # - an enumerable of Ethereum events with tx_hash and some field # - name of that other field # - a function operating on a single IFE structure and that fields value # Leverages the fact, that operating on various IFE-related events follows the same pattern defp consume_events(%__MODULE__{} = state, events, event_field_f, ife_f) do processing_f = process_reducing_events_f(event_field_f, ife_f) {updated_state, updated_ife_keys} = Enum.reduce(events, {state, MapSet.new()}, processing_f) db_updates = ife_db_updates(updated_state, updated_ife_keys) {updated_state, db_updates} end # produces an `Enum.reduce`-able function that: grabs an IFE by tx_hash from the event, invokes a function on that # using the value of a different field from the event, returning updated state and mapset of modified keys. # Pseudocode: # `event |> get IFE by tx_hash |> ife_f.(event[event_field])` defp process_reducing_events_f(event_field_f, ife_f) do fn event, {%__MODULE__{in_flight_exits: ifes} = state, updated_ife_keys} -> tx_hash = event.tx_hash event_field_value = event_field_f.(event) updated_ife = ifes |> Map.fetch!(tx_hash) |> ife_f.(event_field_value) updated_state = %{state | in_flight_exits: Map.put(ifes, tx_hash, updated_ife)} {updated_state, MapSet.put(updated_ife_keys, tx_hash)} end end @doc """ Only for the active in-flight exits, based on the current tracked state. Only for IFEs which transactions where included into the chain and whose outputs were potentially spent. Compare with determine_utxo_existence_to_get/2. """ @spec determine_ife_input_utxos_existence_to_get(ExitProcessor.Request.t(), t()) :: ExitProcessor.Request.t() def determine_ife_input_utxos_existence_to_get( %ExitProcessor.Request{blknum_now: blknum_now} = request, %__MODULE__{in_flight_exits: ifes} ) when not is_nil(blknum_now) do ife_input_positions = ifes |> Map.values() |> Enum.filter(&InFlightExitInfo.should_be_seeked_in_blocks?/1) |> Enum.filter(&InFlightExitInfo.is_relevant?(&1, blknum_now)) |> Enum.flat_map(&Transaction.get_inputs(&1.tx)) |> :lists.usort() %{request | ife_input_utxos_to_check: ife_input_positions} end @doc """ All the active exits, in-flight exits, exiting output piggybacks etc., based on the current tracked state """ @spec determine_utxo_existence_to_get(ExitProcessor.Request.t(), t()) :: ExitProcessor.Request.t() def determine_utxo_existence_to_get( %ExitProcessor.Request{blknum_now: blknum_now} = request, %__MODULE__{} = state ) when not is_nil(blknum_now) do %{request | utxos_to_check: do_determine_utxo_existence_to_get(state, blknum_now)} end defp do_determine_utxo_existence_to_get(%__MODULE__{in_flight_exits: ifes} = state, blknum_now) do standard_exits_pos = StandardExit.exiting_positions(state) |> Enum.filter(fn Utxo.position(blknum, _, _) -> blknum < blknum_now end) active_relevant_ifes = ifes |> Map.values() |> Enum.filter(& &1.is_active) |> Enum.filter(&InFlightExitInfo.is_relevant?(&1, blknum_now)) ife_inputs_pos = active_relevant_ifes |> Enum.flat_map(&Transaction.get_inputs(&1.tx)) ife_outputs_pos = active_relevant_ifes |> Enum.flat_map(&InFlightExitInfo.get_active_output_piggybacks_positions/1) (ife_outputs_pos ++ ife_inputs_pos ++ standard_exits_pos) |> :lists.usort() end @doc """ Figures out which numbers of "spending transaction blocks" to get for the utxos, based on the existence reported by `OMG.Watcher.State` and possibly other factors, eg. only take the non-existent UTXOs spends (naturally) and ones that pertain to IFE transaction inputs. Assumes that UTXOs that haven't been checked (i.e. not a key in `utxo_exists?` map) **exist** To proceed with validation/proof building, this function must ask for blocks that satisfy following criteria: 1/ blocks where any input to any IFE was spent 2/ blocks where any output to any IFE was spent """ @spec determine_spends_to_get(ExitProcessor.Request.t(), __MODULE__.t()) :: ExitProcessor.Request.t() def determine_spends_to_get( %ExitProcessor.Request{ utxos_to_check: utxos_to_check, utxo_exists_result: utxo_exists_result } = request, %__MODULE__{in_flight_exits: ifes} ) do utxo_exists? = Enum.zip(utxos_to_check, utxo_exists_result) |> Map.new() spends_to_get = ifes |> Map.values() |> Enum.flat_map(fn %{tx: tx} = ife -> InFlightExitInfo.get_active_output_piggybacks_positions(ife) ++ Transaction.get_inputs(tx) end) |> only_utxos_checked_and_missing(utxo_exists?) |> :lists.usort() %{request | spends_to_get: spends_to_get} end @doc """ Figures out which numbers of "spending transaction blocks" to get for the outputs on IFEs utxos. To proceed with validation/proof building, this function must ask for blocks that satisfy following criteria: 1/ blocks, where any output from an IFE tx might have been created, by including such IFE tx Similar to `determine_spends_to_get`, otherwise. """ @spec determine_ife_spends_to_get(ExitProcessor.Request.t(), __MODULE__.t()) :: ExitProcessor.Request.t() def determine_ife_spends_to_get( %ExitProcessor.Request{ ife_input_utxos_to_check: utxos_to_check, ife_input_utxo_exists_result: utxo_exists_result } = request, %__MODULE__{in_flight_exits: ifes} ) do utxo_exists? = Enum.zip(utxos_to_check, utxo_exists_result) |> Map.new() spends_to_get = ifes |> Map.values() |> Enum.flat_map(&Transaction.get_inputs(&1.tx)) |> only_utxos_checked_and_missing(utxo_exists?) |> :lists.usort() %{request | ife_input_spends_to_get: spends_to_get} end @doc """ Filters out all the spends that have not been found (`:not_found` instead of a block) This might occur if a UTXO is exited by exit finalization. A block spending such UTXO will not exist. """ @spec handle_spent_blknum_result(list(spent_blknum_result_t()), list(Utxo.Position.t())) :: list(pos_integer()) def handle_spent_blknum_result(spent_blknum_result, spent_positions_to_get) do {not_founds, founds} = Stream.zip(spent_positions_to_get, spent_blknum_result) |> Enum.split_with(fn {_utxo_pos, result} -> result == :not_found end) blknums_to_get = founds |> Enum.unzip() |> elem(1) |> Enum.map(fn {:ok, blknum} -> blknum end) warn? = !Enum.empty?(not_founds) _ = if warn?, do: Logger.warn("UTXO doesn't exists but no spend registered (spent in exit?) #{inspect(not_founds)}") Enum.uniq(blknums_to_get) end @doc """ Based on the result of exit validity (utxo existence), return invalid exits or appropriate notifications NOTE: We're using `ExitStarted`-height with `sla_exit_margin` added on top, to determine old, unchallenged invalid exits. This is different than documented, according to what we ought to be using `exitable_at - sla_exit_margin_s` to determine such exits. NOTE: If there were any exits unchallenged for some time in chain history, this might detect breach of SLA, even if the exits were eventually challenged (e.g. during syncing) """ @spec check_validity(ExitProcessor.Request.t(), t()) :: check_validity_result_t() def check_validity( %ExitProcessor.Request{ eth_height_now: eth_height_now, utxos_to_check: utxos_to_check, utxo_exists_result: utxo_exists_result, blocks_result: blocks }, %__MODULE__{} = state ) when not is_nil(eth_height_now) do utxo_exists? = Enum.zip(utxos_to_check, utxo_exists_result) |> Map.new() {invalid_exits, late_invalid_exits} = StandardExit.get_invalid(state, utxo_exists?, eth_height_now) invalid_exit_events = invalid_exits |> Enum.map(fn {position, exit_info} -> ExitInfo.make_event_data(Event.InvalidExit, position, exit_info) end) late_invalid_exits_events = late_invalid_exits |> Enum.map(fn {position, late_exit} -> ExitInfo.make_event_data(Event.UnchallengedExit, position, late_exit) end) known_txs_by_input = KnownTx.get_all_from_blocks_appendix(blocks, state) {non_canonical_ife_events, late_non_canonical_ife_events} = ExitProcessor.Canonicity.get_ife_txs_with_competitors(state, known_txs_by_input, eth_height_now) invalid_ife_challenges_events = ExitProcessor.Canonicity.get_invalid_ife_challenges(state) {invalid_piggybacks_events, late_invalid_piggybacks_events} = ExitProcessor.Piggyback.get_invalid_piggybacks_events(state, known_txs_by_input, eth_height_now) available_piggybacks_events = state |> get_ifes_to_piggyback() |> Enum.flat_map(&prepare_available_piggyback/1) unchallenged_exit_events = late_non_canonical_ife_events ++ late_invalid_exits_events ++ late_invalid_piggybacks_events chain_validity = if Enum.empty?(unchallenged_exit_events), do: :ok, else: {:error, :unchallenged_exit} events = Enum.concat([ unchallenged_exit_events, invalid_exit_events, invalid_piggybacks_events, non_canonical_ife_events, invalid_ife_challenges_events, available_piggybacks_events ]) {chain_validity, events} end defdelegate get_competitor_for_ife(request, state, ife_txbytes), to: ExitProcessor.Canonicity defdelegate prove_canonical_for_ife(state, ife_txbytes), to: ExitProcessor.Canonicity defdelegate get_input_challenge_data(request, state, txbytes, input_index), to: ExitProcessor.Piggyback defdelegate get_output_challenge_data(request, state, txbytes, output_index), to: ExitProcessor.Piggyback defdelegate determine_standard_challenge_queries(request, state, exiting_utxo_exists), to: ExitProcessor.StandardExit defdelegate create_challenge(request, state), to: ExitProcessor.StandardExit @spec get_ifes_to_piggyback(t()) :: list(InFlightExitInfo.t()) defp get_ifes_to_piggyback(%__MODULE__{in_flight_exits: ifes}) do ifes |> Map.values() |> Stream.filter(fn %InFlightExitInfo{is_active: is_active, tx_seen_in_blocks_at: seen} -> is_active && !seen end) |> Enum.uniq_by(fn %InFlightExitInfo{tx: signed_tx} -> signed_tx end) end @spec prepare_available_piggyback(InFlightExitInfo.t()) :: list(Event.PiggybackAvailable.t()) defp prepare_available_piggyback(%InFlightExitInfo{tx: signed_tx} = ife) do outputs = Transaction.get_outputs(signed_tx) {:ok, input_witnesses} = Transaction.Signed.get_witnesses(signed_tx) available_inputs = input_witnesses |> Enum.filter(fn {index, _} -> not InFlightExitInfo.is_piggybacked?(ife, {:input, index}) end) |> Enum.map(fn {index, owner} -> %{index: index, address: owner} end) available_outputs = outputs |> Enum.filter(fn %{owner: owner} -> zero_address?(owner) end) |> Enum.with_index() |> Enum.filter(fn {_, index} -> not InFlightExitInfo.is_piggybacked?(ife, {:output, index}) end) |> Enum.map(fn {%{owner: owner}, index} -> %{index: index, address: owner} end) if Enum.empty?(available_inputs) and Enum.empty?(available_outputs) do [] else [ %Event.PiggybackAvailable{ txbytes: Transaction.raw_txbytes(signed_tx), available_outputs: available_outputs, available_inputs: available_inputs } ] end end @doc """ Returns a map of active in flight exits, where keys are IFE hashes and values are IFES """ @spec get_active_in_flight_exits(__MODULE__.t()) :: list(map) def get_active_in_flight_exits(%__MODULE__{in_flight_exits: ifes}) do ifes |> Enum.filter(fn {_, %InFlightExitInfo{is_active: is_active}} -> is_active end) |> Enum.map(&prepare_in_flight_exit/1) end @doc """ Returns a set of utxo positions for standard exiting utxos """ @spec active_standard_exiting_utxos(list(map)) :: MapSet.t(Utxo.Position.t()) def active_standard_exiting_utxos(db_exits) do db_exits |> Stream.map(&ExitInfo.from_db_kv/1) |> Stream.filter(fn {_, exit_info} -> exit_info.is_active end) |> Enum.map(&Kernel.elem(&1, 0)) |> MapSet.new() end @doc """ Returns a set of input's utxo positions for in-flight exiting transactions """ @spec active_in_flight_exiting_inputs(list(map)) :: MapSet.t(Utxo.Position.t()) def active_in_flight_exiting_inputs(db_exits) do db_exits |> Stream.map(&InFlightExitInfo.from_db_kv/1) |> Stream.filter(fn {_, exit_info} -> exit_info.is_active end) |> Enum.flat_map(fn {_, exit_info} -> exit_info.input_utxos_pos end) |> MapSet.new() end defp prepare_in_flight_exit({txhash, ife_info}) do %{tx: tx, eth_height: eth_height} = ife_info %{ txhash: txhash, txbytes: Transaction.raw_txbytes(tx), eth_height: eth_height, piggybacked_inputs: InFlightExitInfo.actively_piggybacked_inputs(ife_info), piggybacked_outputs: InFlightExitInfo.actively_piggybacked_outputs(ife_info) } end @doc """ If IFE's spend is in blocks, find its txpos and update the IFE. Note: this change is not persisted later! """ def find_ifes_in_blocks( %__MODULE__{in_flight_exits: ifes} = state, %ExitProcessor.Request{ife_input_spending_blocks_result: blocks} ) do # precompute some useful maps first blocks = Enum.filter(blocks, &(&1 != :not_found)) positions_by_tx_hash = KnownTx.get_positions_by_txhash(blocks) blocks_by_blknum = KnownTx.get_blocks_by_blknum(blocks) new_ifes = ifes |> Enum.filter(fn {_, ife} -> InFlightExitInfo.should_be_seeked_in_blocks?(ife) end) |> Enum.map(fn {hash, ife} -> {hash, ife, KnownTx.find_tx_in_blocks(hash, positions_by_tx_hash, blocks_by_blknum)} end) |> Enum.filter(fn {_hash, _ife, maybepos} -> maybepos != nil end) |> Enum.into(ifes, fn {hash, ife, {block, position}} -> Utxo.position(_, txindex, _) = position proof = Block.inclusion_proof(block, txindex) {hash, %InFlightExitInfo{ife | tx_seen_in_blocks_at: {position, proof}}} end) %{state | in_flight_exits: new_ifes} end defp zero_address?(address) do address != @zero_address end defp sla_margin_safe?(exit_processor_sla_margin, min_exit_period_seconds, ethereum_block_time_seconds), do: exit_processor_sla_margin * ethereum_block_time_seconds < min_exit_period_seconds @doc """ Deletes in-flight exits from state and returns deleted exits """ @spec delete_in_flight_exits(__MODULE__.t(), list(map)) :: {__MODULE__.t(), list(InFlightExitInfo.t()), list(any())} def delete_in_flight_exits(state, deletions) do exit_ids = deletions |> Enum.map(fn %{exit_id: exit_id} -> InFlightExitInfo.to_contract_id(exit_id) end) |> MapSet.new() deleted_ifes_by_key = state.in_flight_exits |> Enum.filter(fn {_, ife} -> MapSet.member?(exit_ids, ife.contract_id) end) |> Map.new() deleted_keys = Map.keys(deleted_ifes_by_key) updated_ifes = Map.drop(state.in_flight_exits, deleted_keys) deleted_utxos = deleted_ifes_by_key |> Map.values() |> InFlightExitInfo.get_input_utxos() db_updates = Enum.map(deleted_keys, fn key -> {:delete, :in_flight_exit_info, key} end) {%{state | in_flight_exits: updated_ifes}, deleted_utxos, db_updates} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/double_spend.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.DoubleSpend do @moduledoc """ Wraps information about a single double spend occuring between a verified transaction and a known transaction """ defstruct [:index, :utxo_pos, :known_spent_index, :known_tx] alias OMG.Watcher.ExitProcessor.KnownTx alias OMG.Watcher.ExitProcessor.Tools alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo @type t() :: %__MODULE__{ index: non_neg_integer(), utxo_pos: Utxo.Position.t(), known_spent_index: non_neg_integer, known_tx: KnownTx.t() } @doc """ Finds the single, oldest competitor from a set of known transactions grouped by input. `nil` if there's none `known_txs_by_input` are assumed to hold _the oldest_ transaction spending given input for every input """ @spec find_competitor(KnownTx.known_txs_by_input_t(), Transaction.any_flavor_t()) :: nil | t() def find_competitor(known_txs_by_input, tx) do inputs = Transaction.get_inputs(tx) known_txs_by_input |> all_distinct_spends_of_inputs(inputs, tx) # need to sort, to get the oldest transaction (double-) spending for _all the_ inputs of `tx` |> Enum.sort(&KnownTx.is_older?/2) |> Enum.at(0) |> case do nil -> nil known_tx -> inputs |> Enum.with_index() |> Tools.double_spends_from_known_tx(known_tx) |> hd() end end @doc """ Gets all the double spends found in an `known_txs_by_input`, following an indexed breakdown of particular utxo_positions of `tx`. This is useful if the interesting utxo positions aren't just inputs of `tx` (e.g. piggybacking, tx's outputs, etc.) """ @spec all_double_spends_by_index( list({Utxo.Position.t(), non_neg_integer}), map(), Transaction.any_flavor_t() ) :: %{non_neg_integer => t()} def all_double_spends_by_index(indexed_utxo_positions, known_txs_by_input, tx) do {inputs, _indices} = Enum.unzip(indexed_utxo_positions) # Will find all spenders of provided indexed inputs. known_txs_by_input |> all_distinct_spends_of_inputs(inputs, tx) |> Stream.flat_map(&Tools.double_spends_from_known_tx(indexed_utxo_positions, &1)) |> Enum.group_by(& &1.index) end # filters all the transactions, spending any of the inputs, distinct from `tx` - to find all the double-spending txs defp all_distinct_spends_of_inputs(known_txs_by_input, inputs, tx) do known_txs_by_input |> Map.take(inputs) |> Stream.flat_map(fn {_input, spending_txs} -> spending_txs end) |> Stream.filter(&Tools.txs_different(tx, &1.signed_tx)) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/exit_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.ExitInfo do @moduledoc """ Represents the bulk of information about a tracked exit. Internal stuff of `OMG.Watcher.ExitProcessor` """ alias OMG.Watcher.Crypto alias OMG.Watcher.Event alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @enforce_keys [ :amount, :currency, :owner, :exit_id, :exiting_txbytes, :is_active, :eth_height, :root_chain_txhash, :scheduled_finalization_time, :block_timestamp, :spending_txhash ] defstruct @enforce_keys @type t :: %__MODULE__{ amount: non_neg_integer(), currency: Crypto.address_t(), owner: Crypto.address_t(), exit_id: non_neg_integer(), # the transaction creating the exiting output exiting_txbytes: Transaction.tx_bytes(), # this means the exit has been first seen active. If false, it won't be considered harmful is_active: boolean(), eth_height: pos_integer(), root_chain_txhash: Transaction.tx_hash() | nil, scheduled_finalization_time: pos_integer() | nil, block_timestamp: pos_integer() | nil, spending_txhash: Transaction.tx_hash() | nil } @spec new(map(), map()) :: t() def new( contract_status, %{ eth_height: eth_height, call_data: %{output_tx: txbytes}, exit_id: exit_id, root_chain_txhash: root_chain_txhash, scheduled_finalization_time: scheduled_finalization_time, block_timestamp: block_timestamp } = exit_event ) do Utxo.position(_, _, oindex) = utxo_pos_for(exit_event) {:ok, raw_tx} = Transaction.decode(txbytes) %{amount: amount, currency: currency, owner: owner} = raw_tx |> Transaction.get_outputs() |> Enum.at(oindex) do_new(contract_status, amount: amount, currency: currency, owner: owner, exit_id: exit_id, exiting_txbytes: txbytes, eth_height: eth_height, root_chain_txhash: root_chain_txhash, scheduled_finalization_time: scheduled_finalization_time, block_timestamp: block_timestamp, spending_txhash: nil ) end def new_key(_contract_status, exit_info), do: utxo_pos_for(exit_info) defp utxo_pos_for(%{call_data: %{utxo_pos: utxo_pos_enc}} = _exit_info), do: Utxo.Position.decode!(utxo_pos_enc) @spec do_new(map(), list(keyword())) :: t() defp do_new(contract_status, fields) do fields = Keyword.put_new(fields, :is_active, parse_contract_exit_status(contract_status)) struct!(__MODULE__, fields) end @spec make_event_data(Event.module_t(), Utxo.Position.t(), t()) :: struct() def make_event_data(type, position, exit_info) do struct( type, exit_info |> Map.from_struct() |> Map.put(:utxo_pos, Utxo.Position.encode(position)) ) end # NOTE: we have no migrations, so we handle data compatibility here (make_db_update/1 and from_db_kv/1), OMG-421 @spec make_db_update({Utxo.Position.t(), t()}) :: {:put, :exit_info, {Utxo.Position.db_t(), map()}} def make_db_update({position, exit_info}) do value = %{ amount: exit_info.amount, currency: exit_info.currency, owner: exit_info.owner, exit_id: exit_info.exit_id, exiting_txbytes: exit_info.exiting_txbytes, is_active: exit_info.is_active, eth_height: exit_info.eth_height, root_chain_txhash: exit_info.root_chain_txhash, scheduled_finalization_time: exit_info.scheduled_finalization_time, block_timestamp: exit_info.block_timestamp } {:put, :exit_info, {Utxo.Position.to_db_key(position), value}} end @spec from_db_kv({Utxo.Position.db_t(), map()}) :: {Utxo.Position.t(), t()} def from_db_kv({db_utxo_pos, exit_info}) do # mapping is used in case of changes in data structure value = %{ amount: exit_info.amount, currency: exit_info.currency, owner: exit_info.owner, exit_id: exit_info.exit_id, exiting_txbytes: exit_info.exiting_txbytes, is_active: exit_info.is_active, eth_height: exit_info.eth_height, spending_txhash: nil, # defaults value to nil if non-existent in the DB. root_chain_txhash: Map.get(exit_info, :root_chain_txhash), scheduled_finalization_time: Map.get(exit_info, :scheduled_finalization_time), block_timestamp: Map.get(exit_info, :block_timestamp) } {Utxo.Position.from_db_key(db_utxo_pos), struct!(__MODULE__, value)} end # processes the return value of `Eth.get_standard_exit_structs(exit_ids)` # `exitable` will be `false` if the exit was challenged # `exitable` will be `false` ALONG WITH the whole tuple holding zeroees, if the exit was processed successfully # **NOTE** one can only rely on the zero-nonzero of this data, since for processed exits this data will be all zeros defp parse_contract_exit_status({exitable, _, _, _, _, _}), do: exitable # Based on the block number determines whether UTXO was created by a deposit. defguardp is_deposit(blknum, child_block_interval) when rem(blknum, child_block_interval) != 0 @doc """ Calculates the time at which an exit can be processed and released if not challenged successfully. See https://docs.omg.network/challenge-period for calculation logic. """ @spec calculate_sft( blknum :: pos_integer(), exit_block_timestamp :: pos_integer(), utxo_creation_timestamp :: pos_integer(), min_exit_period :: pos_integer(), child_block_interval :: pos_integer() ) :: {:ok, pos_integer()} def calculate_sft(blknum, exit_block_timestamp, utxo_creation_timestamp, min_exit_period, child_block_interval) do case is_deposit(blknum, child_block_interval) do true -> {:ok, max(exit_block_timestamp + min_exit_period, utxo_creation_timestamp + min_exit_period)} false -> {:ok, max(exit_block_timestamp + min_exit_period, utxo_creation_timestamp + 2 * min_exit_period)} end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/finalizations.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Finalizations do @moduledoc """ Encapsulates managing and executing the behaviors related to treating exits by the child chain and watchers Keeps a state of exits that are in progress, updates it with news from the root chain, compares to the state of the ledger (`OMG.Watcher.State`), issues notifications as it finds suitable. Should manage all kinds of exits allowed in the protocol and handle the interactions between them. This is the functional, zero-side-effect part of the exit processor. Logic should go here: - orchestrating the persistence of the state - finding invalid exits, disseminating them as events according to rules - enabling to challenge invalid exits - figuring out critical failure of invalid exit challenging (aka `:unchallenged_exit` event) - MoreVP protocol managing in general For the imperative shell, see `OMG.Watcher.ExitProcessor` """ alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.ExitProcessor.ExitInfo alias OMG.Watcher.ExitProcessor.InFlightExitInfo alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Logger require Utxo @doc """ Finalize exits based on Ethereum events, removing from tracked state if valid. Invalid finalizing exits should continue being tracked as `is_active`, to continue emitting events. This includes non-`is_active` exits that finalize invalid, which are turned to be `is_active` now. """ @spec finalize_exits(Core.t(), validities :: {list(Utxo.Position.t()), list(Utxo.Position.t())}) :: {Core.t(), list(), list()} def finalize_exits(%Core{exits: exits} = state, {valid_finalizations, invalid}) do # handling valid finalizations new_exits_kv_pairs = exits |> Map.take(valid_finalizations) |> Enum.into(%{}, fn {utxo_pos, exit_info} -> {utxo_pos, %ExitInfo{exit_info | is_active: false}} end) new_state1 = %{state | exits: Map.merge(exits, new_exits_kv_pairs)} db_updates = new_exits_kv_pairs |> Enum.map(&ExitInfo.make_db_update/1) # invalid ones - activating, in case they were inactive, to keep being invalid forever {new_state2, activating_db_updates} = activate_on_invalid_finalization(new_state1, invalid) {new_state2, db_updates ++ activating_db_updates} end defp activate_on_invalid_finalization(%Core{exits: exits} = state, invalid_finalizations) do exits_to_activate = exits |> Map.take(invalid_finalizations) |> Enum.map(fn {k, v} -> {k, Map.update!(v, :is_active, fn _ -> true end)} end) |> Map.new() activating_db_updates = exits_to_activate |> Enum.map(&ExitInfo.make_db_update/1) state = %{state | exits: Map.merge(exits, exits_to_activate)} {state, activating_db_updates} end @doc """ Returns a tuple of `{:ok, %{ife_exit_id => {finalized_input_exits | finalized_output_exits}}, list(events_exits)}`. Finalized input exits and finalized output exits structures both fit into `OMG.Watcher.State.exit_utxos/1`. Events exits list contains Ethereum's finalization events paired with utxos they exits. This data is needed to broadcast information to the consumers about utxos that needs to marked as spend as the result of finalization. When there are invalid finalizations, returns one of the following: - {:inactive_piggybacks_finalizing, list of piggybacks that exit processor state is not aware of} - {:unknown_in_flight_exit, set of in-flight exit ids that exit processor is not aware of} """ @spec prepare_utxo_exits_for_in_flight_exit_finalizations(Core.t(), [map()]) :: {:ok, map(), list()} | {:inactive_piggybacks_finalizing, list()} | {:unknown_in_flight_exit, MapSet.t(non_neg_integer())} def prepare_utxo_exits_for_in_flight_exit_finalizations(%Core{in_flight_exits: ifes}, finalizations) do finalizations = finalizations |> Enum.map(&ife_id_to_binary/1) with {:ok, ifes_by_id} <- get_all_finalized_ifes_by_ife_contract_id(finalizations, ifes), {:ok, []} <- known_piggybacks?(finalizations, ifes_by_id) do {exiting_positions_by_ife_id, events_with_positions} = finalizations |> Enum.reverse() |> Enum.reduce({%{}, []}, &combine_utxo_exits_with_finalization(&1, &2, ifes_by_id)) { :ok, exiting_positions_by_ife_id, Enum.reject(events_with_positions, &Kernel.match?({_, []}, &1)) } end end # converts from int, which is how the contract serves it defp ife_id_to_binary(finalization), do: Map.update!(finalization, :in_flight_exit_id, fn id -> <> end) defp get_all_finalized_ifes_by_ife_contract_id(finalizations, ifes) do finalizations_ids = finalizations |> Enum.map(fn %{in_flight_exit_id: id} -> id end) |> MapSet.new() by_contract_id = ifes |> Enum.map(fn {_tx_hash, %InFlightExitInfo{contract_id: id} = ife} -> {id, ife} end) |> Map.new() known_ifes = by_contract_id |> Map.keys() |> MapSet.new() unknown_ifes = MapSet.difference(finalizations_ids, known_ifes) if Enum.empty?(unknown_ifes) do {:ok, by_contract_id} else {:unknown_in_flight_exit, unknown_ifes} end end defp known_piggybacks?(finalizations, ifes_by_id) do finalizations |> Enum.filter(&finalization_not_piggybacked?(&1, ifes_by_id)) |> case do [] -> {:ok, []} not_piggybacked -> {:inactive_piggybacks_finalizing, not_piggybacked} end end defp finalization_not_piggybacked?( %{in_flight_exit_id: ife_id, output_index: output_index, omg_data: %{piggyback_type: piggyback_type}}, ifes_by_id ), do: not InFlightExitInfo.is_active?(ifes_by_id[ife_id], {piggyback_type, output_index}) defp combine_utxo_exits_with_finalization( %{in_flight_exit_id: ife_id, output_index: output_index, omg_data: %{piggyback_type: piggyback_type}} = event, {exiting_positions, events_with_positions}, ifes_by_id ) do ife = ifes_by_id[ife_id] # a runtime sanity check - if this were false it would mean all piggybacks finalized so contract wouldn't allow that true = InFlightExitInfo.is_active?(ife, {piggyback_type, output_index}) # figure out if there's any UTXOs really exiting from the `OMG.Watcher.State` # from this IFE's piggybacked input/output exiting_positions_for_piggyback = get_exiting_positions(ife, output_index, piggyback_type) { Map.update(exiting_positions, ife_id, exiting_positions_for_piggyback, &(exiting_positions_for_piggyback ++ &1)), [{event, exiting_positions_for_piggyback} | events_with_positions] } end defp get_exiting_positions(ife, output_index, :input) do %InFlightExitInfo{tx: %Transaction.Signed{raw_tx: tx}} = ife input_position = tx |> Transaction.get_inputs() |> Enum.at(output_index) [input_position] end defp get_exiting_positions(ife, output_index, :output) do case ife.tx_seen_in_blocks_at do nil -> [] {Utxo.position(blknum, txindex, _), _proof} -> [Utxo.position(blknum, txindex, output_index)] end end @doc """ Finalizes in-flight exits. Returns a tuple of {:ok, updated state, database updates}. When there are invalid finalizations, returns one of the following: - {:inactive_piggybacks_finalizing, list of piggybacks that exit processor state is not aware of} - {:unknown_in_flight_exit, set of in-flight exit ids that exit processor is not aware of} """ @spec finalize_in_flight_exits(Core.t(), [map()], map()) :: {:ok, Core.t(), list()} | {:inactive_piggybacks_finalizing, list()} | {:unknown_in_flight_exit, MapSet.t(non_neg_integer())} def finalize_in_flight_exits(%Core{in_flight_exits: ifes} = state, finalizations, invalidities_by_ife_id) do # convert ife_id from int (given by contract) to a binary finalizations = Enum.map(finalizations, &ife_id_to_binary/1) with {:ok, ifes_by_id} <- get_all_finalized_ifes_by_ife_contract_id(finalizations, ifes), {:ok, []} <- known_piggybacks?(finalizations, ifes_by_id) do {ifes_by_id, updated_ifes} = finalizations |> Enum.reduce({ifes_by_id, MapSet.new()}, &finalize_single_exit/2) |> activate_on_invalid_utxo_exits(invalidities_by_ife_id) db_updates = ifes_by_id |> Map.take(Enum.to_list(updated_ifes)) |> Map.values() # re-key those IFEs by tx_hash as how they are originally stored |> Enum.map(&{Transaction.raw_txhash(&1.tx), &1}) |> Enum.map(&InFlightExitInfo.make_db_update/1) ifes = ifes_by_id # re-key those IFEs by tx_hash as how they are originally stored |> Map.values() |> Enum.into(%{}, &{Transaction.raw_txhash(&1.tx), &1}) {:ok, %{state | in_flight_exits: ifes}, db_updates} end end defp finalize_single_exit( %{in_flight_exit_id: ife_id, output_index: output_index, omg_data: %{piggyback_type: piggyback_type}}, {ifes_by_id, updated_ifes} ) do combined_index = {piggyback_type, output_index} ife = ifes_by_id[ife_id] if InFlightExitInfo.is_active?(ife, combined_index) do {:ok, finalized_ife} = InFlightExitInfo.finalize(ife, combined_index) ifes_by_id = Map.put(ifes_by_id, ife_id, finalized_ife) updated_ifes = MapSet.put(updated_ifes, ife_id) {ifes_by_id, updated_ifes} else {ifes_by_id, updated_ifes} end end defp activate_on_invalid_utxo_exits({ifes_by_id, updated_ifes}, invalidities_by_ife_id) do ids_to_activate = invalidities_by_ife_id |> Enum.filter(fn {_ife_id, invalidities} -> not Enum.empty?(invalidities) end) |> Enum.map(fn {ife_id, _invalidities} -> ife_id end) |> MapSet.new() # iterates over the ifes that are spotted with invalid finalizing (their `ife_ids`) and activates the ifes new_ifes_by_id = Enum.reduce(ids_to_activate, ifes_by_id, fn id, ifes -> Map.update!(ifes, id, &InFlightExitInfo.activate/1) end) {new_ifes_by_id, MapSet.union(ids_to_activate, updated_ifes)} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/in_flight_exit_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.InFlightExitInfo do @moduledoc """ Represents the bulk of information about a tracked in-flight exit. Internal stuff of `OMG.Watcher.ExitProcessor` """ alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require OMG.Watcher.Utxo require Transaction require Transaction.Payment @max_inputs Transaction.Payment.max_inputs() @max_outputs Transaction.Payment.max_outputs() @inputs_index_range 0..(@max_inputs - 1) @outputs_index_range 0..(@max_outputs - 1) @type combined_index_t() :: {:input, 0..unquote(@max_inputs - 1)} | {:output, 0..unquote(@max_outputs - 1)} # TODO: divide into inputs and outputs: prevent contract's implementation from leaking into watcher # https://github.com/omgnetwork/elixir-omg/pull/361#discussion_r247926222 @enforce_keys [ :tx, :timestamp, :contract_id, :eth_height, :is_active ] defstruct [ :tx, :contract_tx_pos, :tx_seen_in_blocks_at, :timestamp, :contract_id, :oldest_competitor, :eth_height, :input_txs, :input_utxos_pos, :relevant_from_blknum, # piggybacking & finalization exit_map: Map.new(), is_canonical: true, is_active: true ] @type blknum() :: pos_integer() @type tx_index() :: non_neg_integer() @type ife_contract_id() :: <<_::192>> @type exit_map_t() :: %{ {:input | :output, non_neg_integer()} => %{ is_piggybacked: boolean(), is_finalized: boolean(), is_challenged: boolean() } } @type t :: %__MODULE__{ tx: Transaction.Signed.t(), # if not nil, position was proven in contract contract_tx_pos: Utxo.Position.t() | nil, # nil value means that it was not included # OR we haven't processed it yet # OR we have found and filled this data, but haven't persisted it later tx_seen_in_blocks_at: {Utxo.Position.t(), inclusion_proof :: binary()} | nil, timestamp: non_neg_integer(), contract_id: ife_contract_id(), # includes a special value denoting "age" of a non-included transaction being a competitor oldest_competitor: Utxo.Position.t() | :no_position | nil, eth_height: pos_integer(), input_txs: list(Transaction.Protocol.t()), input_utxos_pos: list(Utxo.Position.t()), relevant_from_blknum: pos_integer(), exit_map: exit_map_t(), is_canonical: boolean(), is_active: boolean() } @doc """ Creates a new instance of the key-value pair for the respective `InFlightExitInfo`, from the Ethereum event map """ @spec new_kv(map(), {tuple(), non_neg_integer()}) :: t() def new_kv( %{ eth_height: eth_height, call_data: %{ in_flight_tx: tx_bytes, in_flight_tx_sigs: signatures, input_txs: input_txs, input_utxos_pos: input_utxos_pos } }, {contract_status, contract_ife_id} ) do do_new(tx_bytes, signatures, contract_status, contract_id: <>, eth_height: eth_height, input_txs: input_txs, input_utxos_pos: Enum.map(input_utxos_pos, &Utxo.Position.decode!/1) ) end defp do_new(tx_bytes, tx_signatures, contract_status, fields) do {timestamp, is_active} = parse_contract_in_flight_exit_status(contract_status) with {:ok, tx} <- prepare_tx(tx_bytes, tx_signatures) do # NOTE: in case of using output_id as the input pointer, getting the youngest will be entirely different Utxo.position(youngest_input_blknum, _, _) = tx |> Transaction.get_inputs() |> Enum.sort_by(&Utxo.Position.encode/1, &>=/2) |> hd() fields = fields |> Keyword.put_new(:tx, tx) |> Keyword.put_new(:is_active, is_active) |> Keyword.put_new(:relevant_from_blknum, youngest_input_blknum) |> Keyword.put_new(:timestamp, timestamp) {Transaction.raw_txhash(tx), struct!(__MODULE__, fields)} end end defp parse_contract_in_flight_exit_status({_, timestamp, _, _, _, _, _}), do: {timestamp, timestamp != 0} defp prepare_tx(tx_bytes, tx_signatures) do with {:ok, raw_tx} <- Transaction.decode(tx_bytes) do tx = %Transaction.Signed{raw_tx: raw_tx, sigs: tx_signatures} {:ok, tx} end end # NOTE: we have no migrations, so we handle data compatibility here (make_db_update/1 and from_db_kv/1), OMG-421 def make_db_update( {ife_hash, %__MODULE__{ tx: %Transaction.Signed{} = tx, contract_tx_pos: tx_pos, timestamp: timestamp, contract_id: contract_id, oldest_competitor: oldest_competitor, eth_height: eth_height, input_txs: input_txs, input_utxos_pos: input_utxos_pos, relevant_from_blknum: relevant_from_blknum, exit_map: exit_map, is_canonical: is_canonical, is_active: is_active }} ) when is_binary(contract_id) and is_integer(timestamp) and is_integer(eth_height) and is_list(input_txs) and is_list(input_utxos_pos) and is_integer(relevant_from_blknum) and is_map(exit_map) and is_boolean(is_canonical) and is_boolean(is_active) do :ok = assert_utxo_pos_type(tx_pos) :ok = assert_utxo_pos_type(oldest_competitor) # mapping is used in case of changes in data structure value = %{ tx: to_db_value(tx), tx_pos: tx_pos, timestamp: timestamp, contract_id: contract_id, oldest_competitor: oldest_competitor, eth_height: eth_height, input_txs: input_txs, input_utxos_pos: input_utxos_pos, relevant_from_blknum: relevant_from_blknum, exit_map: exit_map, is_canonical: is_canonical, is_active: is_active } {:put, :in_flight_exit_info, {ife_hash, value}} end @doc """ Returns all input utxos for given in-flight exits """ @spec get_input_utxos(list(t())) :: list(Utxo.Position.t()) def get_input_utxos(in_flight_exits) do in_flight_exits |> Enum.map(& &1.input_utxos_pos) |> List.flatten() end defp assert_utxo_pos_type(Utxo.position(blknum, txindex, oindex)) when is_integer(blknum) and is_integer(txindex) and is_integer(oindex), do: :ok defp assert_utxo_pos_type(nil), do: :ok # a special value denoting position ("age") of a non-included transaction is ok too defp assert_utxo_pos_type(:no_position), do: :ok def from_db_kv({ife_hash, fields}) do # TODO: this got really horrible. Instead of tidying up/maintaining maybe go `Ecto` and use `Ecto.x` facilities # on this here and elsewhere assert_types(fields, [:tx_pos, :oldest_competitor], fn value -> :ok = assert_utxo_pos_type(value) end) assert_types(fields, [:tx, :exit_map], fn value -> true = is_map(value) end) assert_types(fields, [:contract_id], fn value -> true = is_binary(value) end) assert_types(fields, [:timestamp, :eth_height, :relevant_from_blknum], fn value -> true = is_integer(value) end) assert_types(fields, [:input_txs, :input_utxos_pos], fn value -> true = is_list(value) end) assert_types(fields, [:is_canonical, :is_active], fn value -> true = is_boolean(value) end) # mapping is used in case of changes in data structure ife_map = %{ tx: from_db_signed_tx(fields[:tx]), contract_tx_pos: fields[:tx_pos], timestamp: fields[:timestamp], contract_id: fields[:contract_id], oldest_competitor: fields[:oldest_competitor], eth_height: fields[:eth_height], input_txs: fields[:input_txs], input_utxos_pos: fields[:input_utxos_pos], relevant_from_blknum: fields[:relevant_from_blknum], exit_map: fields[:exit_map], is_canonical: fields[:is_canonical], is_active: fields[:is_active] } {ife_hash, struct!(__MODULE__, ife_map)} end defp assert_types(fields, keys, assertion) do fields |> Map.take(keys) |> Map.values() |> Enum.each(assertion) end # NOTE: non-private because `CompetitorInfo` holds `Transaction.Signed` objects too def from_db_signed_tx(%{raw_tx: raw_tx_map, sigs: sigs}) when is_map(raw_tx_map) and is_list(sigs) do value = %{raw_tx: from_db_raw_tx(raw_tx_map), sigs: sigs} struct!(Transaction.Signed, value) end def from_db_raw_tx(%{tx_type: tx_type, inputs: inputs, outputs: outputs, metadata: metadata}) when is_list(inputs) and is_list(outputs) and Transaction.is_metadata(metadata) do value = %{tx_type: tx_type, inputs: inputs, outputs: outputs, metadata: metadata} struct!(Transaction.Payment, value) end def to_db_value(%Transaction.Signed{raw_tx: raw_tx, sigs: sigs}) when is_list(sigs) do %{raw_tx: to_db_value(raw_tx), sigs: sigs} end def to_db_value(%Transaction.Payment{tx_type: tx_type, inputs: inputs, outputs: outputs, metadata: metadata}) when is_list(inputs) and is_list(outputs) and Transaction.is_metadata(metadata) do %{tx_type: tx_type, inputs: inputs, outputs: outputs, metadata: metadata} end @spec piggyback(t(), combined_index_t()) :: t() | {:error, :non_existent_exit | :cannot_piggyback} def piggyback(ife, index) def piggyback(%__MODULE__{exit_map: exit_map} = ife, combined_index) when is_tuple(combined_index) do with exit <- exit_map_get(exit_map, combined_index), {:ok, updated_exit} <- piggyback_exit(exit) do %{ife | exit_map: Map.put(exit_map, combined_index, updated_exit)} end end def piggyback(%__MODULE__{}, _), do: {:error, :non_existent_exit} defp piggyback_exit(%{is_piggybacked: false, is_finalized: false, is_challenged: false} = exit_map_entry), do: {:ok, %{exit_map_entry | is_piggybacked: true}} defp piggyback_exit(_), do: {:error, :cannot_piggyback} @spec challenge(t(), non_neg_integer()) :: t() | {:error, :competitor_too_young} def challenge(ife, competitor_position) def challenge(%__MODULE__{oldest_competitor: nil} = ife, competitor_position), do: %{ife | is_canonical: false, oldest_competitor: decode_position_possibly_exceeding(competitor_position)} def challenge(%__MODULE__{oldest_competitor: current_oldest} = ife, competitor_position) do with decoded_competitor_pos <- Utxo.Position.decode!(competitor_position), true <- is_older?(decoded_competitor_pos, current_oldest) do %{ife | is_canonical: false, oldest_competitor: decoded_competitor_pos} else _ -> {:error, :competitor_too_young} end end @spec challenge_piggyback(t(), combined_index_t()) :: t() def challenge_piggyback(%__MODULE__{exit_map: exit_map} = ife, combined_index) when is_tuple(combined_index) do %{is_piggybacked: true, is_finalized: false, is_challenged: false} = exit_map_entry = exit_map_get(exit_map, combined_index) %{ife | exit_map: Map.replace!(exit_map, combined_index, %{exit_map_entry | is_challenged: true})} end @spec respond_to_challenge(t(), Utxo.Position.t()) :: t() | {:error, :responded_with_too_young_tx | :cannot_respond} def respond_to_challenge(ife, tx_position) def respond_to_challenge(%__MODULE__{oldest_competitor: current_oldest} = ife, tx_position) do decoded = Utxo.Position.decode!(tx_position) if is_nil(current_oldest) or is_older?(decoded, current_oldest) do %{ife | oldest_competitor: decoded, is_canonical: true, contract_tx_pos: decoded} else {:error, :responded_with_too_young_tx} end end def respond_to_challenge(%__MODULE__{}, _), do: {:error, :cannot_respond} @spec finalize(t(), combined_index_t()) :: {:ok, t()} | :unknown_output_index def finalize(%__MODULE__{exit_map: exit_map} = ife, combined_index) when is_tuple(combined_index) do case exit_map_get(exit_map, combined_index) do nil -> :unknown_output_index output_exit -> output_exit = %{output_exit | is_finalized: true} exit_map = Map.put(exit_map, combined_index, output_exit) ife = %{ife | exit_map: exit_map} is_active = exit_map |> Map.keys() |> Enum.any?(&is_active?(ife, &1)) ife = %{ife | is_active: is_active} {:ok, ife} end end @spec get_active_output_piggybacks_positions(t()) :: [Utxo.Position.t()] def get_active_output_piggybacks_positions(%__MODULE__{tx_seen_in_blocks_at: nil}), do: [] def get_active_output_piggybacks_positions( %__MODULE__{tx_seen_in_blocks_at: {Utxo.position(blknum, txindex, _), _}} = ife ) do @outputs_index_range |> Enum.filter(&is_unchallenged?(ife, {:output, &1})) |> Enum.map(&Utxo.position(blknum, txindex, &1)) end def unchallenged_piggybacks_by_ife(%__MODULE__{tx: tx} = ife, :input) do indexed_piggybacked_inputs = tx |> Transaction.get_inputs() |> Enum.with_index() |> Enum.filter(fn {_input, index} -> is_unchallenged?(ife, {:input, index}) end) {ife, indexed_piggybacked_inputs} end def unchallenged_piggybacks_by_ife(%__MODULE__{} = ife, :output) do indexed_piggybacked_outputs = ife |> get_active_output_piggybacks_positions() |> Enum.map(&index_output_position/1) {ife, indexed_piggybacked_outputs} end defp index_output_position(position) do Utxo.position(_, _, oindex) = position {position, oindex} end def actively_piggybacked_inputs(ife) do @inputs_index_range |> Enum.filter(&is_active?(ife, {:input, &1})) end def actively_piggybacked_outputs(ife) do @outputs_index_range |> Enum.filter(&is_active?(ife, {:output, &1})) end @spec is_active?(t(), combined_index_t()) :: boolean() def is_active?(%__MODULE__{} = ife, combined_index) do is_piggybacked?(ife, combined_index) and !is_finalized?(ife, combined_index) and !is_challenged?(ife, combined_index) end @spec is_unchallenged?(t(), combined_index_t()) :: boolean() def is_unchallenged?(%__MODULE__{} = ife, combined_index) do is_piggybacked?(ife, combined_index) and !is_challenged?(ife, combined_index) end def activate(%__MODULE__{} = ife) do %{ife | is_active: true} end def should_be_seeked_in_blocks?(%__MODULE__{} = ife), do: ife.is_active && ife.tx_seen_in_blocks_at == nil @doc """ First, it determines if it is challenged at all - if it isn't returns false. Second, If the tx hasn't been seen at all then it will be false If it is challenged (hence non-canonical) and seen it will figure out if the IFE tx has been seen in an older than oldest competitor's position. """ @spec is_invalidly_challenged?(t()) :: boolean() def is_invalidly_challenged?(%__MODULE__{is_canonical: true}), do: false def is_invalidly_challenged?(%__MODULE__{tx_seen_in_blocks_at: nil}), do: false def is_invalidly_challenged?(%__MODULE__{ tx_seen_in_blocks_at: {Utxo.position(_, _, _) = seen_in_pos, _proof}, oldest_competitor: oldest_competitor_pos }), do: is_older?(seen_in_pos, oldest_competitor_pos) @doc """ Converts integer to contract's in-flight exit id """ @spec to_contract_id(non_neg_integer) :: <<_::192>> def to_contract_id(id), do: <> @doc """ Checks if the competitor being seen at `competitor_pos` (`nil` if unseen) is viable to challenge with, considering the current state of the IFE - that is, only if it is older than IFE tx's inclusion and other competitors """ @spec is_viable_competitor?(t(), Utxo.Position.t() | nil) :: boolean() def is_viable_competitor?( %__MODULE__{tx_seen_in_blocks_at: nil, oldest_competitor: oldest_competitor_pos}, competitor_pos ), do: do_is_viable_competitor?(nil, oldest_competitor_pos, competitor_pos) def is_viable_competitor?( %__MODULE__{tx_seen_in_blocks_at: {seen_at_pos, _proof}, oldest_competitor: oldest_competitor_pos}, competitor_pos ), do: do_is_viable_competitor?(seen_at_pos, oldest_competitor_pos, competitor_pos) def is_relevant?(%__MODULE__{relevant_from_blknum: relevant_from_blknum}, blknum_now), do: relevant_from_blknum < blknum_now @spec is_piggybacked?(t(), combined_index_t()) :: boolean() def is_piggybacked?(%__MODULE__{exit_map: map}, combined_index) when is_tuple(combined_index) do if exit = exit_map_get(map, combined_index) do Map.get(exit, :is_piggybacked, false) else false end end @spec is_finalized?(t(), combined_index_t()) :: boolean() defp is_finalized?(%__MODULE__{exit_map: map}, combined_index) do if exit = exit_map_get(map, combined_index) do Map.get(exit, :is_finalized, false) else false end end @spec is_challenged?(t(), combined_index_t()) :: boolean() defp is_challenged?(%__MODULE__{exit_map: map}, combined_index) do if exit = exit_map_get(map, combined_index) do Map.get(exit, :is_challenged, false) else false end end # there's nothing with any position, so there's nothing older than competitor, so it's good to challenge with defp do_is_viable_competitor?(nil, nil, _competitor_pos), do: true # there's something with position and the competitor doesn't have any - not good to challenge with defp do_is_viable_competitor?(_seen_at_pos, _oldest_pos, nil), do: false # there already is a competitor reported in the contract, if the competitor is older then good to challenge with defp do_is_viable_competitor?(nil, oldest_pos, competitor_pos), do: is_older?(competitor_pos, oldest_pos) # this IFE tx has been already seen at some position, if the competitor is older then good to challenge with defp do_is_viable_competitor?(seen_at_pos, nil, competitor_pos), do: is_older?(competitor_pos, seen_at_pos) # the competitor must be older than anything else to be good to challenge with defp do_is_viable_competitor?(seen_at_pos, oldest_pos, competitor_pos), do: is_older?(competitor_pos, seen_at_pos) and is_older?(competitor_pos, oldest_pos) # no position is older than any real position defp is_older?(Utxo.position(_, _, _), :no_position), do: true # no position is younger than any real position defp is_older?(:no_position, Utxo.position(_, _, _)), do: false # for real positions, the smaller it is the older it is defp is_older?(Utxo.position(tx1_blknum, tx1_index, _), Utxo.position(tx2_blknum, tx2_index, _)), do: tx1_blknum < tx2_blknum or (tx1_blknum == tx2_blknum and tx1_index < tx2_index) # to cater for utxo positions coming from the contract, that represent non-included transactions defp decode_position_possibly_exceeding(encoded_position) do case Utxo.Position.decode(encoded_position) do {:ok, Utxo.position(_, _, _) = decoded} -> decoded # The position was huge so it denoted a non-included transaction. # Use a special value denoting "age" of a non-included transaction {:error, :encoded_utxo_position_too_low} -> :no_position end end @spec exit_map_get(exit_map_t(), combined_index_t()) :: %{ is_piggybacked: boolean(), is_finalized: boolean(), is_challenged: boolean() } defp exit_map_get(exit_map, {type, index} = combined_index) when (type == :input and index < @max_inputs) or (type == :output and index < @max_outputs), do: Map.get(exit_map, combined_index, %{is_piggybacked: false, is_finalized: false, is_challenged: false}) end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/known_tx.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.KnownTx do @moduledoc """ Wrapps information about a particular signed transaction known from somewhere, optionally with its UTXO position Private """ defstruct [:signed_tx, :utxo_pos] alias OMG.Watcher.Block alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.ExitProcessor.TxAppendix alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @type t() :: %__MODULE__{ signed_tx: Transaction.Signed.t(), utxo_pos: Utxo.Position.t() | nil } @type known_txs_by_input_t() :: %{Utxo.Position.t() => list(__MODULE__.t())} def new(%Transaction.Signed{} = signed_tx, Utxo.position(_, _, _) = utxo_pos), do: %__MODULE__{signed_tx: signed_tx, utxo_pos: utxo_pos} def new(%Transaction.Signed{} = signed_tx), do: %__MODULE__{signed_tx: signed_tx} def get_positions_by_txhash(blocks) do blocks |> get_all_from() # cannot simply `Enum.into` here, because for every position the tx might have been included, we need the oldest |> Enum.group_by(&Transaction.raw_txhash(&1.signed_tx), & &1.utxo_pos) |> Enum.into(%{}, fn {txhash, positions} -> {txhash, hd(positions)} end) end def get_blocks_by_blknum(blocks), do: blocks |> Enum.into(%{}, fn %Block{number: blknum} = block -> {blknum, block} end) def find_tx_in_blocks(txhash, positions_by_tx_hash, blocks_by_blknum) do txhash |> (&Map.get(positions_by_tx_hash, &1)).() |> case do nil -> nil Utxo.position(blknum, _, _) = position -> {blocks_by_blknum[blknum], position} end end @doc """ Groups the spending transactions by the input spent, preserves the sorting for every input. Expects an `Enumberable` of `KnownTx`s Duplicates are possible. """ @spec group_txs_by_input(Enumerable.t()) :: known_txs_by_input_t def group_txs_by_input(all_known_txs) do all_known_txs |> Stream.map(&{&1, Transaction.get_inputs(&1.signed_tx)}) |> Stream.flat_map(fn {known_tx, inputs} -> for input <- inputs, do: {input, known_tx} end) |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) end # returns the known transactions in the proper order - first ones from blocks sorted from oldest then ones # from outside of blocks (TxAppendix) def get_all_from_blocks_appendix(blocks, %Core{} = processor) do [get_all_from(blocks), get_all_from(processor)] |> Stream.concat() |> group_txs_by_input() end defp get_all_from(%Core{} = processor) do TxAppendix.get_all(processor) |> Stream.map(&new/1) end defp get_all_from(%Block{transactions: txs, number: blknum}) do txs |> Stream.map(&Transaction.Signed.decode!/1) |> Stream.with_index() |> Stream.map(fn {signed, txindex} -> new(signed, Utxo.position(blknum, txindex, 0)) end) end defp get_all_from(blocks) when is_list(blocks), do: blocks |> sort_blocks() |> Stream.flat_map(&get_all_from/1) # we're sorting the blocks by their blknum here, because we wan't oldest (best) competitors first always defp sort_blocks(blocks), do: blocks |> Enum.sort_by(fn %Block{number: number} -> number end) def is_older?(%__MODULE__{utxo_pos: utxo_pos1}, %__MODULE__{utxo_pos: utxo_pos2}) do cond do is_nil(utxo_pos1) -> false is_nil(utxo_pos2) -> true true -> Utxo.Position.encode(utxo_pos1) < Utxo.Position.encode(utxo_pos2) end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/measure.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Measure do @moduledoc """ Counting business metrics sent to Datadog """ import OMG.Status.Metric.Event, only: [name: 1] alias OMG.Status.Metric.Datadog alias OMG.Watcher.ExitProcessor def handle_event([:process, ExitProcessor], _, _state, _config) do value = self() |> Process.info(:message_queue_len) |> elem(1) _ = Datadog.gauge(name(:watcher_exit_processor_message_queue_len), value) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/piggyback.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Piggyback do @moduledoc """ Encapsulates managing and executing the behaviors related to treating exits by the child chain and watchers Keeps a state of exits that are in progress, updates it with news from the root chain, compares to the state of the ledger (`OMG.Watcher.State`), issues notifications as it finds suitable. Should manage all kinds of exits allowed in the protocol and handle the interactions between them. This is the functional, zero-side-effect part of the exit processor. Logic should go here: - orchestrating the persistence of the state - finding invalid exits, disseminating them as events according to rules - enabling to challenge invalid exits - figuring out critical failure of invalid exit challenging (aka `:unchallenged_exit` event) - MoreVP protocol managing in general For the imperative shell, see `OMG.Watcher.ExitProcessor` """ alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.ExitProcessor.DoubleSpend alias OMG.Watcher.ExitProcessor.InFlightExitInfo alias OMG.Watcher.ExitProcessor.KnownTx alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo import OMG.Watcher.ExitProcessor.Tools require Transaction.Payment require Logger @type piggyback_type_t() :: :input | :output @type piggyback_t() :: {piggyback_type_t(), non_neg_integer()} @type input_challenge_data :: %{ in_flight_txbytes: Transaction.tx_bytes(), in_flight_input_index: 0..3, spending_txbytes: Transaction.tx_bytes(), spending_input_index: 0..3, spending_sig: <<_::520>>, input_tx: Transaction.tx_bytes(), input_utxo_pos: Utxo.Position.t() } @type output_challenge_data :: %{ in_flight_txbytes: Transaction.tx_bytes(), in_flight_output_pos: pos_integer(), in_flight_input_index: 4..7, spending_txbytes: Transaction.tx_bytes(), spending_input_index: 0..3, spending_sig: <<_::520>> } @type piggyback_challenge_data_error() :: :ife_not_known_for_tx | Transaction.decode_error() | :no_double_spend_on_particular_piggyback def get_input_challenge_data(request, state, txbytes, input_index) do case input_index in 0..(Transaction.Payment.max_inputs() - 1) do true -> get_piggyback_challenge_data(request, state, txbytes, {:input, input_index}) false -> {:error, :piggybacked_index_out_of_range} end end def get_output_challenge_data(request, state, txbytes, output_index) do case output_index in 0..(Transaction.Payment.max_outputs() - 1) do true -> get_piggyback_challenge_data(request, state, txbytes, {:output, output_index}) false -> {:error, :piggybacked_index_out_of_range} end end @doc """ Returns a tuple of ivalid piggybacks and invalid piggybacks that are past SLA margin. This is inclusive, invalid piggybacks past SLA margin are included in the invalid piggybacks list. """ @spec get_invalid_piggybacks_events(Core.t(), KnownTx.known_txs_by_input_t(), pos_integer()) :: {list(Event.InvalidPiggyback.t()), list(Event.UnchallengedPiggyback.t())} def get_invalid_piggybacks_events( %Core{sla_margin: sla_margin, in_flight_exits: ifes}, known_txs_by_input, eth_height_now ) do invalid_piggybacks_by_ife = ifes |> Map.values() |> all_invalid_piggybacks_by_ife(known_txs_by_input) invalid_piggybacks_events = to_events(invalid_piggybacks_by_ife, &to_invalid_piggyback_event/1) past_sla_margin = fn {ife, _type, _materials} -> ife.eth_height + sla_margin <= eth_height_now end unchallenged_piggybacks_events = invalid_piggybacks_by_ife |> Enum.filter(past_sla_margin) |> to_events(&to_unchallenged_piggyback_event/1) {invalid_piggybacks_events, unchallenged_piggybacks_events} end defp all_invalid_piggybacks_by_ife(ifes_values, known_txs_by_input) do [:input, :output] |> Enum.flat_map(fn pb_type -> invalid_piggybacks_by_ife(known_txs_by_input, pb_type, ifes_values) end) end defp to_events(piggybacks_by_ife, to_event) do piggybacks_by_ife |> group_by_txbytes() |> Enum.map(to_event) end defp to_invalid_piggyback_event({txbytes, type_materials_pairs}) do %Event.InvalidPiggyback{ txbytes: txbytes, inputs: invalid_piggyback_indices(type_materials_pairs, :input), outputs: invalid_piggyback_indices(type_materials_pairs, :output) } end defp to_unchallenged_piggyback_event({txbytes, type_materials_pairs}) do %Event.UnchallengedPiggyback{ txbytes: txbytes, inputs: invalid_piggyback_indices(type_materials_pairs, :input), outputs: invalid_piggyback_indices(type_materials_pairs, :output) } end # we need to produce only one event per IFE, with both piggybacks on inputs and outputs defp group_by_txbytes(invalid_piggybacks) do invalid_piggybacks |> Enum.map(fn {ife, type, materials} -> {Transaction.raw_txbytes(ife.tx), type, materials} end) |> Enum.group_by(&elem(&1, 0), fn {_, type, materials} -> {type, materials} end) end defp invalid_piggyback_indices(type_materials_pairs, pb_type) do # here we need to additionally group the materials found by type input/output # then we gut just the list of indices to present to the user in the event type_materials_pairs |> Enum.filter(fn {type, _materials} -> type == pb_type end) |> Enum.flat_map(fn {_type, materials} -> Map.keys(materials) end) end @spec invalid_piggybacks_by_ife(KnownTx.known_txs_by_input_t(), piggyback_type_t(), list(InFlightExitInfo.t())) :: list({InFlightExitInfo.t(), piggyback_type_t(), %{non_neg_integer => DoubleSpend.t()}}) defp invalid_piggybacks_by_ife(known_txs_by_input, pb_type, ifes) do ifes |> Enum.map(&InFlightExitInfo.unchallenged_piggybacks_by_ife(&1, pb_type)) |> Enum.filter(&ife_has_something?/1) |> Enum.map(fn {ife, indexed_piggybacked_utxo_positions} -> proof_materials = DoubleSpend.all_double_spends_by_index(indexed_piggybacked_utxo_positions, known_txs_by_input, ife.tx) {ife, pb_type, proof_materials} end) |> Enum.filter(&ife_has_something?/1) end defp ife_has_something?({_ife, finds_for_ife}), do: !Enum.empty?(finds_for_ife) defp ife_has_something?({_ife, _, finds_for_ife}), do: !Enum.empty?(finds_for_ife) @spec get_piggyback_challenge_data(ExitProcessor.Request.t(), Core.t(), binary(), piggyback_t()) :: {:ok, input_challenge_data() | output_challenge_data()} | {:error, piggyback_challenge_data_error()} defp get_piggyback_challenge_data(%ExitProcessor.Request{blocks_result: blocks}, state, txbytes, piggyback) do with {:ok, tx} <- Transaction.decode(txbytes), {:ok, ife} <- get_ife(tx, state.in_flight_exits) do known_txs_by_input = KnownTx.get_all_from_blocks_appendix(blocks, state) produce_invalid_piggyback_proof(ife, known_txs_by_input, piggyback) end end @spec produce_invalid_piggyback_proof(InFlightExitInfo.t(), KnownTx.known_txs_by_input_t(), piggyback_t()) :: {:ok, input_challenge_data() | output_challenge_data()} | {:error, :no_double_spend_on_particular_piggyback} defp produce_invalid_piggyback_proof(ife, known_txs_by_input, {pb_type, pb_index} = piggyback) do with {:ok, proof_materials} <- get_proofs_for_particular_ife(ife, pb_type, known_txs_by_input), {:ok, proof} <- get_proof_for_particular_piggyback(pb_index, proof_materials) do {:ok, prepare_piggyback_challenge_response(ife, piggyback, proof)} end end # gets all proof materials for all possibly invalid piggybacks for a single ife, for a determined type (input/output) defp get_proofs_for_particular_ife(ife, pb_type, known_txs_by_input) do invalid_piggybacks_by_ife(known_txs_by_input, pb_type, [ife]) |> case do [] -> {:error, :no_double_spend_on_particular_piggyback} # ife and pb_type are pinned here for a runtime sanity check - we got what we explicitly asked for [{^ife, ^pb_type, proof_materials}] -> {:ok, proof_materials} end end # gets any proof of a particular invalid piggyback, after we have figured the exact piggyback index affected defp get_proof_for_particular_piggyback(pb_index, proof_materials) do proof_materials |> Map.get(pb_index) |> case do nil -> {:error, :no_double_spend_on_particular_piggyback} # any challenging tx will do, taking the very first [proof | _] -> {:ok, proof} end end @spec prepare_piggyback_challenge_response(InFlightExitInfo.t(), piggyback_t(), DoubleSpend.t()) :: input_challenge_data() | output_challenge_data() defp prepare_piggyback_challenge_response(ife, {:input, input_index}, proof) do %{ in_flight_txbytes: Transaction.raw_txbytes(ife.tx), in_flight_input_index: input_index, spending_txbytes: Transaction.raw_txbytes(proof.known_tx.signed_tx), spending_input_index: proof.known_spent_index, spending_sig: Enum.at(proof.known_tx.signed_tx.sigs, proof.known_spent_index), input_tx: Enum.at(ife.input_txs, input_index), input_utxo_pos: Enum.at(ife.input_utxos_pos, input_index) } end defp prepare_piggyback_challenge_response(ife, {:output, _output_index}, proof) do {_, inclusion_proof} = ife.tx_seen_in_blocks_at %{ in_flight_txbytes: Transaction.raw_txbytes(ife.tx), in_flight_output_pos: proof.utxo_pos, in_flight_proof: inclusion_proof, spending_txbytes: Transaction.raw_txbytes(proof.known_tx.signed_tx), spending_input_index: proof.known_spent_index, spending_sig: Enum.at(proof.known_tx.signed_tx.sigs, proof.known_spent_index) } end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/request.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Request do @moduledoc """ Encapsulates the state of processing of `OMG.Watcher.ExitProcessor` pipelines Holds all the necessary query date and the respective response """ alias OMG.Watcher.Block alias OMG.Watcher.Utxo defstruct [ :eth_height_now, :blknum_now, utxos_to_check: [], spends_to_get: [], blknums_to_get: [], ife_input_utxos_to_check: [], ife_input_spends_to_get: [], piggybacked_blknums_to_get: [], utxo_exists_result: [], blocks_result: [], ife_input_utxo_exists_result: [], ife_input_spending_blocks_result: [], se_exiting_pos: nil, se_spending_blocks_to_get: [], se_spending_blocks_result: [] ] @type t :: %__MODULE__{ eth_height_now: nil | pos_integer, blknum_now: nil | pos_integer, utxos_to_check: list(Utxo.Position.t()), spends_to_get: list(Utxo.Position.t()), blknums_to_get: list(pos_integer), ife_input_utxos_to_check: list(Utxo.Position.t()), ife_input_spends_to_get: list(Utxo.Position.t()), piggybacked_blknums_to_get: list(pos_integer), utxo_exists_result: list(boolean), blocks_result: list(Block.t()), ife_input_utxo_exists_result: list(boolean), ife_input_spending_blocks_result: list(Block.t()), se_exiting_pos: nil | Utxo.Position.t(), se_spending_blocks_to_get: list(Utxo.Position.t()), se_spending_blocks_result: list(Block.t()) } end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/standard_exit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.StandardExit do @moduledoc """ Part of Core to handle SE challenges & invalid exit detection. Treat as private helper submodule of `OMG.Watcher.ExitProcessor.Core`, test and call via that """ defmodule Challenge do @moduledoc """ Represents a challenge to a standard exit as returned by the `ExitProcessor` """ @enforce_keys [:exit_id, :exiting_tx, :txbytes, :input_index, :sig] defstruct @enforce_keys alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction @type t() :: %__MODULE__{ exit_id: pos_integer(), exiting_tx: Transaction.tx_bytes(), txbytes: Transaction.tx_bytes(), input_index: non_neg_integer(), sig: Crypto.sig_t() } end alias OMG.Watcher.Block alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.ExitProcessor.DoubleSpend alias OMG.Watcher.ExitProcessor.ExitInfo alias OMG.Watcher.ExitProcessor.KnownTx alias OMG.Watcher.ExitProcessor.TxAppendix alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo import OMG.Watcher.ExitProcessor.Tools require Utxo @doc """ Gets all utxo positions exiting via active standard exits """ @spec exiting_positions(Core.t()) :: list(Utxo.Position.t()) def exiting_positions(%Core{} = state) do state |> active_exits() |> Enum.map(fn {utxo_pos, _value} -> utxo_pos end) end @doc """ Gets all standard exits that are invalid, all and late ones separately, also adds their :spending_txhash """ @spec get_invalid(Core.t(), %{Utxo.Position.t() => boolean}, pos_integer()) :: {%{Utxo.Position.t() => ExitInfo.t()}, %{Utxo.Position.t() => ExitInfo.t()}} def get_invalid(%Core{sla_margin: sla_margin} = state, utxo_exists?, eth_height_now) do active_exits = active_exits(state) exits_invalid_by_ife = state |> TxAppendix.get_all() |> get_invalid_exits_based_on_ifes(active_exits) invalid_exit_positions = active_exits |> Enum.map(fn {utxo_pos, _value} -> utxo_pos end) |> only_utxos_checked_and_missing(utxo_exists?) standard_invalid_exits = active_exits |> Map.take(invalid_exit_positions) |> Enum.map(fn {utxo_pos, invalid_exit} -> spending_txhash = spending_txhash_for_exit_at(utxo_pos) {utxo_pos, %{invalid_exit | spending_txhash: spending_txhash}} end) invalid_exits = standard_invalid_exits |> Enum.concat(exits_invalid_by_ife) |> Enum.uniq() # get exits which are still invalid and after the SLA margin late_invalid_exits = Enum.filter(invalid_exits, fn {_, %ExitInfo{eth_height: eth_height}} -> eth_height + sla_margin <= eth_height_now end) {Map.new(invalid_exits), Map.new(late_invalid_exits)} end defp spending_txhash_for_exit_at(utxo_pos) do utxo_pos |> Utxo.Position.to_input_db_key() |> OMG.DB.spent_blknum() |> List.wrap() |> Core.handle_spent_blknum_result([utxo_pos]) |> do_get_blocks() |> case do [block] -> %DoubleSpend{known_tx: %KnownTx{signed_tx: spending_tx}} = get_double_spend_for_standard_exit(block, utxo_pos) Transaction.raw_txhash(spending_tx) _ -> nil end end defp do_get_blocks(blknums) do {:ok, hashes} = OMG.DB.block_hashes(blknums) {:ok, blocks} = OMG.DB.blocks(hashes) Enum.map(blocks, &Block.from_db_value/1) end @doc """ Determines the utxo-creating and utxo-spending blocks to get from `OMG.DB` `se_spending_blocks_to_get` are requested by the UTXO position they spend """ @spec determine_standard_challenge_queries(ExitProcessor.Request.t(), Core.t(), boolean()) :: {:ok, ExitProcessor.Request.t()} | {:error, :exit_not_found | :utxo_not_spent} def determine_standard_challenge_queries( %ExitProcessor.Request{se_exiting_pos: Utxo.position(_, _, _) = exiting_pos} = request, %Core{exits: exits} = state, exiting_utxo_exists ) do with {:ok, _exit_info} <- get_exit(exits, exiting_pos), # once figured out the exit exists, check if it is spent in an IFE? ife_based_on_utxo = get_ife_based_on_utxo(exiting_pos, state), # To be challengable, the exit utxo must be spent in either an IFE or missing from the `OMG.Watcher.State`. # In the latter case we'll go on looking for the spending tx in the `OMG.DB` true <- !is_nil(ife_based_on_utxo) || !exiting_utxo_exists || {:error, :utxo_not_spent} do # if the exit utxo is spent in an IFE no need to bother with looking for the spending tx in the blocks spending_blocks_to_get = if ife_based_on_utxo, do: [], else: [exiting_pos] {:ok, %ExitProcessor.Request{request | se_spending_blocks_to_get: spending_blocks_to_get}} end end @doc """ Creates the final challenge response, if possible """ @spec create_challenge(ExitProcessor.Request.t(), Core.t()) :: {:ok, Challenge.t()} | {:error, :utxo_not_spent} def create_challenge( %ExitProcessor.Request{se_exiting_pos: exiting_pos, se_spending_blocks_result: spending_blocks_result}, %Core{exits: exits} = state ) when not is_nil(exiting_pos) do %ExitInfo{owner: owner, exit_id: exit_id, exiting_txbytes: exiting_txbytes} = exits[exiting_pos] ife_result = get_ife_based_on_utxo(exiting_pos, state) with {:ok, spending_tx_or_block} <- ensure_challengeable(spending_blocks_result, ife_result) do %DoubleSpend{known_spent_index: input_index, known_tx: %KnownTx{signed_tx: challenging_signed}} = get_double_spend_for_standard_exit(spending_tx_or_block, exiting_pos) {:ok, %Challenge{ exit_id: exit_id, input_index: input_index, exiting_tx: exiting_txbytes, txbytes: challenging_signed |> Transaction.raw_txbytes(), sig: find_sig!(challenging_signed, owner) }} end end defp ensure_challengeable(spending_blknum_response, ife_response) defp ensure_challengeable([%Block{} = block], _), do: {:ok, block} defp ensure_challengeable(_, ife_response) when not is_nil(ife_response), do: {:ok, ife_response} defp ensure_challengeable(_, _), do: {:error, :utxo_not_spent} @spec get_ife_based_on_utxo(Utxo.Position.t(), Core.t()) :: KnownTx.t() | nil defp get_ife_based_on_utxo(Utxo.position(_, _, _) = utxo_pos, %Core{} = state) do state |> TxAppendix.get_all() |> get_ife_txs_by_spent_input() |> Map.get(utxo_pos) |> case do nil -> nil some -> Enum.at(some, 0) end end # finds transaction in given block and input index spending given utxo @spec get_double_spend_for_standard_exit(Block.t() | KnownTx.t(), Utxo.Position.t()) :: DoubleSpend.t() | nil defp get_double_spend_for_standard_exit(%Block{transactions: txs}, utxo_pos) do txs |> Enum.map(&Transaction.Signed.decode!/1) |> Enum.find_value(fn tx -> get_double_spend_for_standard_exit(%KnownTx{signed_tx: tx}, utxo_pos) end) end defp get_double_spend_for_standard_exit(%KnownTx{} = known_tx, utxo_pos) do Enum.at(get_double_spends_by_utxo_pos(utxo_pos, known_tx), 0) end # Gets all standard exits invalidated by IFEs exiting their utxo positions and append the spending_txhash @spec get_invalid_exits_based_on_ifes(TxAppendix.t(), %{Utxo.Position.t() => ExitInfo.t()}) :: list({Utxo.Position.t(), ExitInfo.t()}) defp get_invalid_exits_based_on_ifes(tx_appendix, active_exits) do known_txs_by_input = get_ife_txs_by_spent_input(tx_appendix) active_exits |> Enum.filter(fn {utxo_pos, _exit_info} -> Map.has_key?(known_txs_by_input, utxo_pos) end) |> Enum.map(fn {utxo_pos, exit_info} -> spending_txhash = known_txs_by_input |> Map.get(utxo_pos) |> Enum.at(0) |> Map.get(:signed_tx) |> Transaction.raw_txhash() {utxo_pos, %{exit_info | spending_txhash: spending_txhash}} end) end @spec get_double_spends_by_utxo_pos(Utxo.Position.t(), KnownTx.t()) :: list(DoubleSpend.t()) defp get_double_spends_by_utxo_pos(Utxo.position(_, _, oindex) = utxo_pos, known_tx), # the function used expects positions with an index (either input index or oindex), hence the oindex added do: [{utxo_pos, oindex}] |> double_spends_from_known_tx(known_tx) defp get_ife_txs_by_spent_input(tx_appendix) do tx_appendix |> Enum.map(fn signed -> %KnownTx{signed_tx: signed} end) |> KnownTx.group_txs_by_input() end defp get_exit(exits, exiting_pos) do case Map.get(exits, exiting_pos) do nil -> {:error, :exit_not_found} other -> {:ok, other} end end defp active_exits(%Core{exits: exits}), do: exits |> Enum.filter(fn {_key, %ExitInfo{is_active: is_active}} -> is_active end) |> Map.new() end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/tools.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Tools do @moduledoc """ Private tools that various components of the `ExitProcessor` share """ alias OMG.Watcher.Crypto alias OMG.Watcher.ExitProcessor.DoubleSpend alias OMG.Watcher.ExitProcessor.KnownTx alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash alias OMG.Watcher.Utxo require OMG.Watcher.Utxo @typep eth_event_t() :: %{root_chain_txhash: Crypto.hash_t(), log_index: non_neg_integer()} @typep eth_event_with_exiting_positions_t() :: {eth_event_t(), list(Utxo.Position.t())} | eth_event_t() # Intersects utxos, looking for duplicates. Gives full list of double-spends with indexes for # a pair of transactions. @spec double_spends_from_known_tx(list({Utxo.Position.t(), non_neg_integer()}), KnownTx.t()) :: list(DoubleSpend.t()) def double_spends_from_known_tx(inputs, %KnownTx{signed_tx: signed} = known_tx) when is_list(inputs) do known_spent_inputs = signed |> Transaction.get_inputs() |> Enum.with_index() # NOTE: possibly ineffective if Transaction.Payment.max_inputs >> 4, BUT we're calling it seldom so no biggie for {left, left_index} <- inputs, {right, right_index} <- known_spent_inputs, left == right, do: %DoubleSpend{index: left_index, utxo_pos: left, known_spent_index: right_index, known_tx: known_tx} end # based on an enumberable of `Utxo.Position` and a mapping that tells whether one exists it will pick # only those that **were checked** and were missing # (i.e. those not checked are assumed to be present) def only_utxos_checked_and_missing(utxo_positions, utxo_exists?) do # the default value below is true, so that the assumption is that utxo not checked is **present** Enum.filter(utxo_positions, fn utxo_pos -> !Map.get(utxo_exists?, utxo_pos, true) end) end @doc """ Finds the exact signature which signed the particular transaction for the given owner address """ @spec find_sig(Transaction.Signed.t(), Crypto.address_t()) :: {:ok, Crypto.sig_t()} | nil def find_sig(%Transaction.Signed{sigs: sigs, raw_tx: raw_tx}, owner) do tx_hash = TypedDataHash.hash_struct(raw_tx) Enum.find(sigs, fn sig -> {:ok, owner} == Crypto.recover_address(tx_hash, sig) end) |> case do nil -> nil other -> {:ok, other} end end @doc """ Throwing version of `find_sig/2` At some point having a tx that wasn't actually signed is an error, hence pattern match if `find_sig/2` returns nil it means somethings very wrong - the owner taken (effectively) from the contract doesn't appear to have signed the potential competitor, which means that some prior signature checking was skipped """ def find_sig!(tx, owner) do {:ok, sig} = find_sig(tx, owner) sig end def txs_different(tx1, tx2), do: Transaction.raw_txhash(tx1) != Transaction.raw_txhash(tx2) def get_ife(ife_tx, ifes) do case ifes[Transaction.raw_txhash(ife_tx)] do nil -> {:error, :ife_not_known_for_tx} value -> {:ok, value} end end @doc """ Transforms Ethereum events like InFlightExitStarted or InFlightExitOutputWithdrawn to form that can be consumed by subscribers """ @spec to_bus_events_data(list(eth_event_with_exiting_positions_t())) :: list(%{ call_data: map(), root_chain_txhash: charlist(), log_index: non_neg_integer(), eth_height: pos_integer() }) def to_bus_events_data(eth_events_with_exiting_utxos) do Enum.reduce(eth_events_with_exiting_utxos, [], &to_bus_events_reducer/2) end defp to_bus_events_reducer( {%{root_chain_txhash: root_chain_txhash, log_index: log_index, eth_height: eth_height}, utxo_positions}, bus_events ) do utxo_pos_transform = fn Utxo.position(_, _, _) = u -> Utxo.Position.encode(u) encoded when is_integer(encoded) -> encoded end utxo_positions |> Enum.map( &%{ call_data: %{utxo_pos: utxo_pos_transform.(&1)}, root_chain_txhash: root_chain_txhash, eth_height: eth_height, log_index: log_index } ) |> Enum.concat(bus_events) end defp to_bus_events_reducer(%{omg_data: %{piggyback_type: :input}}, bus_events) do # In-flight transaction's inputs are spend when IFE is started we are not interested with input piggybacks bus_events end defp to_bus_events_reducer( %{ root_chain_txhash: root_chain_txhash, log_index: log_index, eth_height: eth_height, omg_data: %{piggyback_type: :output}, tx_hash: txhash, output_index: oindex }, bus_events ) do # Note: It cannot be deposit as it is piggyback to output, so output is created by in-flight transaction # If transaction was included in plasma block, output is created and could be spend by this event [ %{ call_data: %{txhash: txhash, oindex: oindex}, root_chain_txhash: root_chain_txhash, log_index: log_index, eth_height: eth_height } | bus_events ] end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor/tx_appendix.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.TxAppendix do @moduledoc """ Part of the exit processor serving as the API to the transaction appendix Transaction appendix (TxAppendix) serves the transactions that were witnessed, but aren't included in the blocks """ alias OMG.Watcher.ExitProcessor @doc """ Enumerable of `Transaction.Signed.t()` """ @type t() :: Enumerable.t() @spec get_all(ExitProcessor.Core.t()) :: t() def get_all(%ExitProcessor.Core{in_flight_exits: ifes, competitors: competitors}) do ifes |> Map.values() |> Stream.concat(Map.values(competitors)) |> Stream.map(&Map.get(&1, :tx)) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/exit_processor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor do @moduledoc """ Tracks and handles the exits from the child chain, their validity and challenges. Keeps a state of exits that are in progress, updates it with news from the root chain contract, compares to the state of the ledger (`OMG.Watcher.State`), issues notifications as it finds suitable. Should manage all kinds of exits allowed in the protocol and handle the interactions between them. For functional logic and more info see `OMG.Watcher.ExitProcessor.Core` NOTE: Note that all calls return `db_updates` and relay on the caller to do persistence. """ alias OMG.DB alias OMG.DB.Models.PaymentExitInfo alias OMG.Eth alias OMG.Eth.EthereumHeight alias OMG.Eth.RootChain alias OMG.Watcher.Block alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.ExitProcessor.ExitInfo alias OMG.Watcher.ExitProcessor.StandardExit alias OMG.Watcher.ExitProcessor.Tools alias OMG.Watcher.State alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Logger require Utxo @timeout 60_000 ### Client @doc """ Starts the `GenServer` process with options. For documentation of the options see `init/1` """ def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end @doc """ Accepts events and processes them in the state - new exits are tracked. Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of standard exit start events from the contract - fetches the currently observed exit status in the contract (to decide if exits are "inactive on recognition", which helps cover the case when the Watcher is syncing up) - updates the `ExitProcessor`'s state - returns `db_updates` """ def new_exits([]), do: {:ok, []} def new_exits(exits) do GenServer.call(__MODULE__, {:new_exits, exits}, @timeout) end @doc """ Accepts events and processes them in the state - new in flight exits are tracked. Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of IFE exit start events from the contract - fetches the currently observed exit status in the contract (to decide if exits are "inactive on recognition", which helps cover the case when the Watcher is syncing up) - updates the `ExitProcessor`'s state - returns `db_updates` """ # empty list clause to not block the server for no-ops def new_in_flight_exits([]), do: {:ok, []} def new_in_flight_exits(in_flight_exit_started_events) do GenServer.call(__MODULE__, {:new_in_flight_exits, in_flight_exit_started_events}, @timeout) end @doc """ Accepts events and processes them in the state - new in flight exits are tracked. Returns `db_updates` to be sent to `OMG.DB` by the caller - spends input utxos - deletes in-flight exits from state """ def delete_in_flight_exits([]), do: {:ok, []} def delete_in_flight_exits(in_flight_exit_deleted_events) do GenServer.call(__MODULE__, {:delete_in_flight_exits, in_flight_exit_deleted_events}, @timeout) end @doc """ Accepts events and processes them in the state - finalized exits are untracked _if valid_ otherwise raises alert Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of standard exit finalization events from the contract - discovers the `OMG.Watcher.State`'s native key for the finalizing exits (`utxo_pos`) (`Core.exit_key_by_exit_id/2`) - marks as spent these UTXOs in `OMG.Watcher.State` expecting it to tell which of those were valid finalizations (UTXOs exist) - reflects this result in the `ExitProcessor`'s state - returns `db_updates`, concatenated with those related to the call to `OMG.Watcher.State` """ def finalize_exits([]), do: {:ok, []} def finalize_exits(finalizations) do GenServer.call(__MODULE__, {:finalize_exits, finalizations}, @timeout) end @doc """ Accepts events and processes them in the state - new piggybacks are tracked, if invalid raises an alert Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of IFE piggybacking events from the contract - updates the `ExitProcessor`'s state - returns `db_updates` """ def piggyback_exits([]), do: {:ok, []} def piggyback_exits(piggybacks) do GenServer.call(__MODULE__, {:piggyback_exits, piggybacks}, @timeout) end @doc """ Accepts events and processes them in the state - challenged exits are untracked Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of standard exit challenge events from the contract - updates the `ExitProcessor`'s state - returns `db_updates` """ def challenge_exits([]), do: {:ok, []} def challenge_exits(challenges) do GenServer.call(__MODULE__, {:challenge_exits, challenges}, @timeout) end @doc """ Accepts events and processes them in the state. Marks the challenged IFE as non-canonical and persists information about the competitor and its age. Competitors are stored for future use (i.e. to challenge an in flight exit). Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of IFE exit canonicity challenge events from the contract - updates the `ExitProcessor`'s state - returns `db_updates` """ def new_ife_challenges([]), do: {:ok, []} def new_ife_challenges(challenges) do GenServer.call(__MODULE__, {:new_ife_challenges, challenges}, @timeout) end @doc """ Accepts events and processes them in state. Marks the IFE as canonical and perists information about the inclusion age as responded with in the contract. Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of IFE exit canonicity challenge response events from the contract - updates the `ExitProcessor`'s state - returns `db_updates` """ def respond_to_in_flight_exits_challenges([]), do: {:ok, []} def respond_to_in_flight_exits_challenges(responds) do GenServer.call(__MODULE__, {:respond_to_in_flight_exits_challenges, responds}, @timeout) end @doc """ Accepts events and processes them in state. Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of IFE piggyback challenge events from the contract - updates the `ExitProcessor`'s state - returns `db_updates` """ def challenge_piggybacks([]), do: {:ok, []} def challenge_piggybacks(challenges) do GenServer.call(__MODULE__, {:challenge_piggybacks, challenges}, @timeout) end @doc """ Accepts events and processes them in state - finalized outputs are applied to the state. Returns `db_updates` to be sent to `OMG.DB` by the caller - takes a list of IFE exit finalization events from the contract - pulls current information on IFE transaction inclusion - discovers the `OMG.Watcher.State`'s native key for the finalizing exits (`utxo_pos`) (`Core.prepare_utxo_exits_for_in_flight_exit_finalizations/2`) - marks as spent these UTXOs in `OMG.Watcher.State` expecting it to tell which of those were valid finalizations (UTXOs exist) - reflects this result in the `ExitProcessor`'s state - returns `db_updates`, concatenated with those related to the call to `OMG.Watcher.State` """ def finalize_in_flight_exits([]), do: {:ok, []} def finalize_in_flight_exits(finalizations) do GenServer.call(__MODULE__, {:finalize_in_flight_exits, finalizations}, @timeout) end @doc """ Checks validity of all exit-related events and returns the list of actionable items. Works with `OMG.Watcher.State` to discern validity. This function may also update some internal caches to make subsequent calls not redo the work, but under unchanged conditions, it should have unchanged behavior from POV of an outside caller. - pulls current information on IFE transaction inclusion - gets a list of interesting UTXOs to check for existence in `OMG.Watcher.State` - combines this information to discover the state of all the exits to report (mainly byzantine events) """ def check_validity() do GenServer.call(__MODULE__, :check_validity, @timeout) end def check_validity(timeout) do GenServer.call(__MODULE__, :check_validity, timeout) end @doc """ Returns a map of requested in flight exits, keyed by transaction hash """ @spec get_active_in_flight_exits() :: {:ok, Core.in_flight_exits_response_t()} def get_active_in_flight_exits() do GenServer.call(__MODULE__, :get_active_in_flight_exits, @timeout) end @doc """ Returns all information required to produce a transaction to the root chain contract to present a competitor for a non-canonical in-flight exit - pulls current information on IFE transaction inclusion - gets a list of interesting UTXOs to check for existence in `OMG.Watcher.State` - combines this information to compose the challenge data """ @spec get_competitor_for_ife(binary()) :: {:ok, ExitProcessor.Canonicity.competitor_data_t()} | {:error, :competitor_not_found} | {:error, :no_viable_competitor_found} def get_competitor_for_ife(txbytes) do GenServer.call(__MODULE__, {:get_competitor_for_ife, txbytes}, @timeout) end @doc """ Returns all information required to produce a transaction to the root chain contract to present a proof of canonicity for a challenged in-flight exit - pulls current information on IFE transaction inclusion - gets a list of interesting UTXOs to check for existence in `OMG.Watcher.State` - combines this information to compose the challenge data """ @spec prove_canonical_for_ife(binary()) :: {:ok, ExitProcessor.Canonicity.prove_canonical_data_t()} | {:error, :no_viable_canonical_proof_found} def prove_canonical_for_ife(txbytes) do GenServer.call(__MODULE__, {:prove_canonical_for_ife, txbytes}, @timeout) end @doc """ Returns all information required to challenge an invalid input piggyback - gets a list of interesting UTXOs to check for existence in `OMG.Watcher.State` - combines this information to compose the challenge data """ @spec get_input_challenge_data(Transaction.Signed.tx_bytes(), Transaction.input_index_t()) :: {:ok, ExitProcessor.Piggyback.input_challenge_data()} | {:error, ExitProcessor.Piggyback.piggyback_challenge_data_error()} def get_input_challenge_data(txbytes, input_index) do GenServer.call(__MODULE__, {:get_input_challenge_data, txbytes, input_index}, @timeout) end @doc """ Returns all information required to challenge an invalid output piggyback - pulls current information on IFE transaction inclusion - gets a list of interesting UTXOs to check for existence in `OMG.Watcher.State` - combines this information to compose the challenge data """ @spec get_output_challenge_data(Transaction.Signed.tx_bytes(), Transaction.input_index_t()) :: {:ok, ExitProcessor.Piggyback.output_challenge_data()} | {:error, ExitProcessor.Piggyback.piggyback_challenge_data_error()} def get_output_challenge_data(txbytes, output_index) do GenServer.call(__MODULE__, {:get_output_challenge_data, txbytes, output_index}, @timeout) end @doc """ Returns challenge for an invalid standard exit - leverages `OMG.Watcher.State` to quickly learn if the exiting UTXO exists or was spent - pulls some additional data from `OMG.DB`, if needed - combines this information to compose the challenge data """ @spec create_challenge(Utxo.Position.t()) :: {:ok, StandardExit.Challenge.t()} | {:error, :utxo_not_spent | :exit_not_found} def create_challenge(exiting_utxo_pos) do GenServer.call(__MODULE__, {:create_challenge, exiting_utxo_pos}, @timeout) end ### Server use GenServer @doc """ Initializes the state of the `ExitProcessor`'s `GenServer`. Reads the exit data from `OMG.DB`. Options: - `exit_processor_sla_margin`: number of blocks after exit start before it's considered late (and potentially: unchallenged) - `exit_processor_sla_margin_forced`: if `true` will override the check of `exit_processor_sla_margin` against `min_exit_period_seconds` - `min_exit_period_seconds`: should reflect the value of this parameter for the specific child chain watched, - `ethereum_block_time_seconds`: just to relate blocks to seconds for the `exit_processor_sla_margin` check - `metrics_collection_interval`: how often are the metrics sent to `telemetry` (in milliseconds) """ def init( exit_processor_sla_margin: exit_processor_sla_margin, exit_processor_sla_margin_forced: exit_processor_sla_margin_forced, metrics_collection_interval: metrics_collection_interval, min_exit_period_seconds: min_exit_period_seconds, ethereum_block_time_seconds: ethereum_block_time_seconds, child_block_interval: child_block_interval ) do {:ok, db_exits} = PaymentExitInfo.all_exit_infos() {:ok, db_ifes} = PaymentExitInfo.all_in_flight_exits_infos() {:ok, db_competitors} = DB.competitors_info() :ok = Core.check_sla_margin( exit_processor_sla_margin, exit_processor_sla_margin_forced, min_exit_period_seconds, ethereum_block_time_seconds ) {:ok, processor} = Core.init( db_exits, db_ifes, db_competitors, min_exit_period_seconds, child_block_interval, exit_processor_sla_margin ) {:ok, _} = :timer.send_interval(metrics_collection_interval, self(), :send_metrics) _ = Logger.info("Initializing with: #{inspect(processor)}") {:ok, processor} end def handle_call({:new_exits, exits}, _from, state) do _ = if not Enum.empty?(exits), do: Logger.info("Recognized #{Enum.count(exits)} exits: #{inspect(exits)}") {:ok, exit_contract_statuses} = Eth.RootChain.get_standard_exit_structs(get_in(exits, [Access.all(), :exit_id])) exit_maps = exits |> Task.async_stream( fn exit_event -> put_timestamp_and_sft(exit_event, state.min_exit_period_seconds, state.child_block_interval) end, timeout: 50_000, on_timeout: :exit, max_concurrency: System.schedulers_online() * 2 ) |> Enum.map(fn {:ok, result} -> result end) if Code.ensure_loaded?(OMG.WatcherInfo.DB.EthEvent), do: Kernel.apply(OMG.WatcherInfo.DB.EthEvent, :insert_exits!, [exits, :standard_exit, nil]) {new_state, db_updates} = Core.new_exits(state, exit_maps, exit_contract_statuses) {:reply, {:ok, db_updates}, new_state} end def handle_call({:new_in_flight_exits, exits}, _from, state) do _ = if not Enum.empty?(exits), do: Logger.info("Recognized #{Enum.count(exits)} in-flight exits: #{inspect(exits)}") contract_ife_ids = Enum.map(exits, fn %{call_data: %{in_flight_tx: txbytes}} -> ExPlasma.InFlightExit.tx_bytes_to_id(txbytes) end) # Prepare events data for internal bus events = exits |> Enum.map(fn %{call_data: %{input_utxos_pos: inputs}} = event -> {event, inputs} end) |> Tools.to_bus_events_data() :ok = publish_internal_bus_events(events, :InFlightExitStarted) if Code.ensure_loaded?(OMG.WatcherInfo.DB.EthEvent), do: Kernel.apply(OMG.WatcherInfo.DB.EthEvent, :insert_exits!, [events, :in_flight_exit, :InFlightExitStarted]) {:ok, statuses} = Eth.RootChain.get_in_flight_exit_structs(contract_ife_ids) ife_contract_statuses = Enum.zip(statuses, contract_ife_ids) {new_state, db_updates} = Core.new_in_flight_exits(state, exits, ife_contract_statuses) {:reply, {:ok, db_updates}, new_state} end def handle_call({:delete_in_flight_exits, deletions}, _from, state) do _ = if not Enum.empty?(deletions), do: Logger.info("Recognized #{Enum.count(deletions)} deletions: #{inspect(deletions)}") {new_state, deleted_utxos, db_updates} = Core.delete_in_flight_exits(state, deletions) {:ok, db_updates_from_state, _validities} = State.exit_utxos(deleted_utxos) {:reply, {:ok, db_updates ++ db_updates_from_state}, new_state} end def handle_call({:finalize_exits, exits}, _from, state) do _ = if not Enum.empty?(exits), do: Logger.info("Recognized #{Enum.count(exits)} finalizations: #{inspect(exits)}") {:ok, db_updates_from_state, validities} = exits |> Enum.map(&Core.exit_key_by_exit_id(state, &1.exit_id)) |> State.exit_utxos() {new_state, db_updates} = Core.finalize_exits(state, validities) {:reply, {:ok, db_updates ++ db_updates_from_state}, new_state} end def handle_call({:piggyback_exits, exits}, _from, state) do _ = if not Enum.empty?(exits), do: Logger.info("Recognized #{Enum.count(exits)} piggybacks: #{inspect(exits)}") {new_state, db_updates} = Core.new_piggybacks(state, exits) events = Tools.to_bus_events_data(exits) :ok = publish_internal_bus_events(events, :InFlightTxOutputPiggybacked) if Code.ensure_loaded?(OMG.WatcherInfo.DB.EthEvent), do: Kernel.apply( OMG.WatcherInfo.DB.EthEvent, :insert_exits!, [events, :in_flight_exit, :InFlightTxOutputPiggybacked] ) {:reply, {:ok, db_updates}, new_state} end def handle_call({:challenge_exits, exits}, _from, state) do _ = if not Enum.empty?(exits), do: Logger.info("Recognized #{Enum.count(exits)} challenges: #{inspect(exits)}") {new_state, db_updates} = Core.challenge_exits(state, exits) {:reply, {:ok, db_updates}, new_state} end def handle_call({:new_ife_challenges, challenges}, _from, state) do _ = if not Enum.empty?(challenges), do: Logger.info("Recognized #{Enum.count(challenges)} ife challenges: #{inspect(challenges)}") {new_state, db_updates} = Core.new_ife_challenges(state, challenges) {:reply, {:ok, db_updates}, new_state} end def handle_call({:challenge_piggybacks, challenges}, _from, state) do _ = if not Enum.empty?(challenges), do: Logger.info("Recognized #{Enum.count(challenges)} piggyback challenges: #{inspect(challenges)}") {new_state, db_updates} = Core.challenge_piggybacks(state, challenges) {:reply, {:ok, db_updates}, new_state} end def handle_call({:respond_to_in_flight_exits_challenges, responds}, _from, state) do _ = if not Enum.empty?(responds), do: Logger.info("Recognized #{Enum.count(responds)} response to IFE challenge: #{inspect(responds)}") {new_state, db_updates} = Core.respond_to_in_flight_exits_challenges(state, responds) {:reply, {:ok, db_updates}, new_state} end def handle_call({:finalize_in_flight_exits, finalizations}, _from, state) do _ = Logger.info("Recognized #{Enum.count(finalizations)} ife finalizations: #{inspect(finalizations)}") # necessary, so that the processor knows the current state of inclusion of exiting IFE txs state2 = update_with_ife_txs_from_blocks(state) {:ok, exiting_positions, events_with_utxos} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(state2, finalizations) # NOTE: it's not straightforward to track from utxo position returned when exiting utxo in State to ife id # See issue #671 https://github.com/omgnetwork/elixir-omg/issues/671 {invalidities, state_db_updates} = Enum.reduce(exiting_positions, {%{}, []}, &collect_invalidities_and_state_db_updates/2) {:ok, state3, db_updates} = Core.finalize_in_flight_exits(state2, finalizations, invalidities) events = Tools.to_bus_events_data(events_with_utxos) :ok = publish_internal_bus_events(events, :InFlightExitOutputWithdrawn) if Code.ensure_loaded?(OMG.WatcherInfo.DB.EthEvent), do: Kernel.apply( OMG.WatcherInfo.DB.EthEvent, :insert_exits!, [events, :in_flight_exit, :InFlightExitOutputWithdrawn] ) {:reply, {:ok, state_db_updates ++ db_updates}, state3} end def handle_call(:check_validity, _from, state) do new_state = update_with_ife_txs_from_blocks(state) response = %ExitProcessor.Request{} |> fill_request_with_spending_data(new_state) |> Core.check_validity(new_state) {:reply, response, new_state} end def handle_call(:get_active_in_flight_exits, _from, state) do {:reply, {:ok, Core.get_active_in_flight_exits(state)}, state} end def handle_call({:get_competitor_for_ife, txbytes}, _from, state) do # TODO: run_status_gets and getting all non-existent UTXO positions imaginable can be optimized out heavily # only the UTXO positions being inputs to `txbytes` must be looked at, but it becomes problematic as # txbytes can be invalid so we'd need a with here... new_state = update_with_ife_txs_from_blocks(state) competitor_result = %ExitProcessor.Request{} |> fill_request_with_spending_data(new_state) |> Core.get_competitor_for_ife(new_state, txbytes) {:reply, competitor_result, new_state} end def handle_call({:prove_canonical_for_ife, txbytes}, _from, state) do new_state = update_with_ife_txs_from_blocks(state) canonicity_result = Core.prove_canonical_for_ife(new_state, txbytes) {:reply, canonicity_result, new_state} end def handle_call({:get_input_challenge_data, txbytes, input_index}, _from, state) do response = %ExitProcessor.Request{} |> fill_request_with_spending_data(state) |> Core.get_input_challenge_data(state, txbytes, input_index) {:reply, response, state} end def handle_call({:get_output_challenge_data, txbytes, output_index}, _from, state) do new_state = update_with_ife_txs_from_blocks(state) response = %ExitProcessor.Request{} |> fill_request_with_spending_data(new_state) |> Core.get_output_challenge_data(new_state, txbytes, output_index) {:reply, response, new_state} end def handle_call({:create_challenge, exiting_utxo_pos}, _from, state) do request = %ExitProcessor.Request{se_exiting_pos: exiting_utxo_pos} exiting_utxo_exists = State.utxo_exists?(exiting_utxo_pos) response = with {:ok, request} <- Core.determine_standard_challenge_queries(request, state, exiting_utxo_exists), do: request |> fill_request_with_standard_challenge_data() |> Core.create_challenge(state) {:reply, response, state} end def handle_info(:send_metrics, state) do :ok = :telemetry.execute([:process, __MODULE__], %{}, state) {:noreply, state} end defp fill_request_with_standard_challenge_data(%ExitProcessor.Request{se_spending_blocks_to_get: positions} = request) do %ExitProcessor.Request{request | se_spending_blocks_result: do_get_spending_blocks(positions)} end # based on the exits being processed, fills the request structure with data required to process queries @spec fill_request_with_spending_data(ExitProcessor.Request.t(), Core.t()) :: ExitProcessor.Request.t() defp fill_request_with_spending_data(request, state) do request |> run_status_gets() |> Core.determine_utxo_existence_to_get(state) |> get_utxo_existence() |> Core.determine_spends_to_get(state) |> get_spending_blocks() end # based on in-flight exiting transactions, updates the state with witnesses of those transactions' inclusions in block @spec update_with_ife_txs_from_blocks(Core.t()) :: Core.t() defp update_with_ife_txs_from_blocks(state) do prepared_request = %ExitProcessor.Request{} |> run_status_gets() # To find if IFE was included, see first if its inputs were spent. |> Core.determine_ife_input_utxos_existence_to_get(state) |> get_ife_input_utxo_existence() # Next, check by what transactions they were spent. |> Core.determine_ife_spends_to_get(state) |> get_ife_input_spending_blocks() # Compare found txes with ife.tx. # If equal, persist information about position. Core.find_ifes_in_blocks(state, prepared_request) end defp run_status_gets(%ExitProcessor.Request{eth_height_now: nil, blknum_now: nil} = request) do {:ok, eth_height_now} = EthereumHeight.get() {blknum_now, _} = State.get_status() _ = Logger.debug("eth_height_now: #{inspect(eth_height_now)}, blknum_now: #{inspect(blknum_now)}") %{request | eth_height_now: eth_height_now, blknum_now: blknum_now} end defp get_utxo_existence(%ExitProcessor.Request{utxos_to_check: positions} = request), do: %{request | utxo_exists_result: do_utxo_exists?(positions)} defp get_ife_input_utxo_existence(%ExitProcessor.Request{ife_input_utxos_to_check: positions} = request), do: %{request | ife_input_utxo_exists_result: do_utxo_exists?(positions)} defp do_utxo_exists?(positions) do result = Enum.map(positions, &State.utxo_exists?/1) _ = Logger.debug("utxos_to_check: #{inspect(positions)}, utxo_exists_result: #{inspect(result)}") result end defp get_spending_blocks(%ExitProcessor.Request{spends_to_get: positions} = request) do %{request | blocks_result: do_get_spending_blocks(positions)} end defp get_ife_input_spending_blocks(%ExitProcessor.Request{ife_input_spends_to_get: positions} = request) do %{request | ife_input_spending_blocks_result: do_get_spending_blocks(positions)} end defp do_get_spending_blocks(spent_positions_to_get) do blknums = Enum.map(spent_positions_to_get, &do_get_spent_blknum/1) _ = Logger.debug("spends_to_get: #{inspect(spent_positions_to_get)}, spent_blknum_result: #{inspect(blknums)}") blknums |> Core.handle_spent_blknum_result(spent_positions_to_get) |> do_get_blocks() end defp do_get_blocks(blknums) do {:ok, hashes} = OMG.DB.block_hashes(blknums) _ = Logger.debug("blknums: #{inspect(blknums)}, hashes: #{inspect(hashes)}") {:ok, blocks} = OMG.DB.blocks(hashes) _ = Logger.debug("blocks_result: #{inspect(blocks)}") Enum.map(blocks, &Block.from_db_value/1) end defp do_get_spent_blknum(position) do position |> Utxo.Position.to_input_db_key() |> OMG.DB.spent_blknum() end defp collect_invalidities_and_state_db_updates( {ife_id, exiting_positions}, {invalidities_by_ife_id, state_db_updates} ) do {:ok, exits_state_updates, {_, invalidities}} = State.exit_utxos(exiting_positions) _ = if not Enum.empty?(invalidities), do: Logger.warn("Invalid in-flight exit finalization: #{inspect(invalidities)}") invalidities_by_ife_id = Map.put(invalidities_by_ife_id, ife_id, invalidities) state_db_updates = exits_state_updates ++ state_db_updates {invalidities_by_ife_id, state_db_updates} end @spec put_timestamp_and_sft(map(), pos_integer(), pos_integer()) :: map() defp put_timestamp_and_sft( %{eth_height: eth_height, call_data: %{utxo_pos: utxo_pos_enc}} = exit_event, min_exit_period_seconds, child_block_interval ) do {:utxo_position, blknum, _, _} = Utxo.Position.decode!(utxo_pos_enc) {_block_hash, utxo_creation_block_timestamp} = RootChain.blocks(blknum) {:ok, exit_block_timestamp} = Eth.get_block_timestamp_by_number(eth_height) {:ok, scheduled_finalization_time} = ExitInfo.calculate_sft( blknum, exit_block_timestamp, utxo_creation_block_timestamp, min_exit_period_seconds, child_block_interval ) exit_event |> Map.put(:scheduled_finalization_time, scheduled_finalization_time) |> Map.put(:block_timestamp, exit_block_timestamp) end defp publish_internal_bus_events([], _), do: :ok defp publish_internal_bus_events(events_data, topic) when is_list(events_data) and is_atom(topic) do {:watcher, topic} |> OMG.Bus.Event.new(:data, events_data) |> OMG.Bus.direct_local_broadcast() end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/fees/fee_filter.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Fees.FeeFilter do @moduledoc """ Filtering of fees. """ alias OMG.Watcher.Fees @doc ~S""" Returns a filtered map of fees given a list of transaction types and currencies. Passing a nil value or an empty array skip the filtering. ## Examples iex> OMG.Watcher.Fees.FeeFilter.filter( ...> %{ ...> 1 => %{ ...> "eth" => %{ ...> amount: 1, ...> subunit_to_unit: 1_000_000_000_000_000_000, ...> pegged_amount: 4, ...> pegged_currency: "USD", ...> pegged_subunit_to_unit: 100, ...> updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") ...> }, ...> "omg" => %{ ...> amount: 3, ...> subunit_to_unit: 1_000_000_000_000_000_000, ...> pegged_amount: 4, ...> pegged_currency: "USD", ...> pegged_subunit_to_unit: 100, ...> updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") ...> } ...> }, ...> 2 => %{ ...> "omg" => %{ ...> amount: 3, ...> subunit_to_unit: 1_000_000_000_000_000_000, ...> pegged_amount: 4, ...> pegged_currency: "USD", ...> pegged_subunit_to_unit: 100, ...> updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") ...> } ...> }, ...> 3 => %{ ...> "omg" => %{ ...> amount: 3, ...> subunit_to_unit: 1_000_000_000_000_000_000, ...> pegged_amount: 4, ...> pegged_currency: "USD", ...> pegged_subunit_to_unit: 100, ...> updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") ...> } ...> } ...> }, ...> [1,2], ...> ["eth"] ...> ) {:ok, %{ 1 => %{ "eth" => %{ amount: 1, subunit_to_unit: 1_000_000_000_000_000_000, pegged_amount: 4, pegged_currency: "USD", pegged_subunit_to_unit: 100, updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") } }, 2 => %{} } } """ @spec filter(Fees.full_fee_t(), list(non_neg_integer()), list(String.t()) | nil) :: {:ok, Fees.full_fee_t()} | {:error, :currency_fee_not_supported} | {:error, :tx_type_not_supported} # empty list = no filter def filter(fees, []), do: {:ok, fees} def filter(fees, nil), do: {:ok, fees} def filter(fees, tx_types, currencies) do with {:ok, fees} <- filter_tx_type(fees, tx_types) do filter_currency(fees, currencies) end end defp filter_tx_type(fees, []), do: {:ok, fees} defp filter_tx_type(fees, nil), do: {:ok, fees} defp filter_tx_type(fees, tx_types) do with :ok <- validate_tx_types(tx_types, fees), do: {:ok, Map.take(fees, tx_types)} end defp validate_tx_types(tx_types, fees) do tx_types |> Enum.all?(&Map.has_key?(fees, &1)) |> case do true -> :ok false -> {:error, :tx_type_not_supported} end end defp filter_currency(fees, []), do: {:ok, fees} defp filter_currency(fees, nil), do: {:ok, fees} defp filter_currency(fees, currencies) do with :ok <- validate_currencies(currencies, fees) do {:ok, do_filter_currencies(currencies, fees)} end end defp validate_currencies(currencies, fees) do currencies |> Enum.all?(fn currency -> Enum.any?(fees, &Map.has_key?(elem(&1, 1), currency)) end) |> case do true -> :ok false -> {:error, :currency_fee_not_supported} end end defp do_filter_currencies(currencies, fees) do fees |> Enum.map(fn {tx_type, fees_for_tx_type} -> {tx_type, Map.take(fees_for_tx_type, currencies)} end) |> Enum.into(%{}) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/fees.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Fees do @moduledoc """ Transaction's fee validation functions. """ alias OMG.Watcher.Crypto alias OMG.Watcher.MergeTransactionValidator alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo require Logger @typedoc "A map of token addresses to a single fee spec" @type fee_t() :: %{Crypto.address_t() => fee_spec_t()} @typedoc """ A map of transaction types to fees where fees is itself a map of token to fee spec """ @type full_fee_t() :: %{non_neg_integer() => fee_t()} @type optional_fee_t() :: merged_fee_t() | :ignore_fees | :no_fees_required @typedoc "A map representing a single fee" @type fee_spec_t() :: %{ amount: pos_integer(), subunit_to_unit: pos_integer(), pegged_amount: pos_integer(), pegged_currency: String.t(), pegged_subunit_to_unit: pos_integer(), updated_at: DateTime.t() } @typedoc """ A map of currency to amounts used internally where amounts is a list of supported fee amounts. """ @type typed_merged_fee_t() :: %{non_neg_integer() => merged_fee_t()} @type merged_fee_t() :: %{Crypto.address_t() => list(pos_integer())} @doc ~S""" Checks whether the surplus of tokens sent in a transaction (inputs - outputs) covers the fees depending on the fee model. ## Examples iex> Fees.check_if_covered(%{"eth" => 1, "omg" => 0}, %{"eth" => [1], "omg" => [3]}) :ok iex> Fees.check_if_covered(%{"eth" => 1, "omg" => 0}, %{"eth" => [2, 1], "omg" => [1, 3]}) :ok iex> Fees.check_if_covered(%{"eth" => 1}, %{"eth" => [2]}) {:error, :fees_not_covered} iex> Fees.check_if_covered(%{"eth" => 2}, %{"eth" => [3, 1]}) {:error, :fees_not_covered} iex> Fees.check_if_covered(%{"eth" => 1, "omg" => 1}, %{"eth" => [1]}) {:error, :multiple_potential_currency_fees} iex> Fees.check_if_covered(%{"eth" => 2}, %{"eth" => [1]}) {:error, :overpaying_fees} iex> Fees.check_if_covered(%{"eth" => 2}, %{"eth" => [1, 3]}) {:error, :overpaying_fees} iex> Fees.check_if_covered(%{"eth" => 1}, :no_fees_required) {:error, :overpaying_fees} iex> Fees.check_if_covered(%{"eth" => 1}, :ignore_fees) :ok """ @spec check_if_covered(implicit_paid_fee_by_currency :: map(), accepted_fees :: optional_fee_t()) :: :ok | {:error, :fees_not_covered} | {:error, :overpaying_fees} | {:error, :multiple_potential_currency_fees} # If :ignore_fees is given, we don't require any surplus of tokens. If surplus exists, it will be collected. def check_if_covered(_, :ignore_fees), do: :ok def check_if_covered(_, accepted_fees) when map_size(accepted_fees) == 0, do: :ok # Otherwise we remove all non positive tokens from the map and process it def check_if_covered(implicit_paid_fee_by_currency, accepted_fees) do implicit_paid_fee_by_currency |> remove_zero_fees() |> check_positive_amounts(accepted_fees) end # With :no_fees_required, we ensure that no surplus of token is given # meaning that input amount == output amount. This is used for merge transactions. defp check_positive_amounts([], :no_fees_required), do: :ok defp check_positive_amounts(_, :no_fees_required), do: {:error, :overpaying_fees} # When accepting fees, we ensure that only one fee token is given defp check_positive_amounts([], _), do: {:error, :fees_not_covered} # When accepting fees, we ensure that the paid amount matches exactly the required amount and that # the given surplus token is accepted as a fee token defp check_positive_amounts([{currency, paid_fee}], accepted_fees) do case Map.get(accepted_fees, currency) do nil -> {:error, :fees_not_covered} amounts -> check_if_exact_match(amounts, paid_fee) end end defp check_positive_amounts(_, _), do: {:error, :multiple_potential_currency_fees} defp remove_zero_fees(implicit_paid_fee_by_currency) do Enum.filter(implicit_paid_fee_by_currency, fn {_currency, paid_fee} -> paid_fee > 0 end) end defp check_if_exact_match([current_amount | _] = amounts, paid_fee) do cond do paid_fee in amounts -> :ok current_amount > paid_fee -> {:error, :fees_not_covered} current_amount < paid_fee -> {:error, :overpaying_fees} end end @doc ~S""" Returns the fees to pay for a particular transaction, and under particular fee specs listed in `fee_map`. ## Examples iex> OMG.Watcher.Fees.for_transaction( ...> %OMG.Watcher.State.Transaction.Recovered{ ...> signed_tx: %OMG.Watcher.State.Transaction.Signed{raw_tx: OMG.Watcher.State.Transaction.Payment.new([], [], <<0::256>>)} ...> }, ...> %{ ...> 1 => %{ ...> "eth" => [1], ...> "omg" => [3] ...> }, ...> 2 => %{ ...> "eth" => [4], ...> "omg" => [5] ...> } ...> } ...> ) %{ "eth" => [1], "omg" => [3] } """ @spec for_transaction(Transaction.Recovered.t(), merged_fee_t()) :: optional_fee_t() def for_transaction(transaction, fee_map) do case MergeTransactionValidator.is_merge_transaction?(transaction) do true -> :no_fees_required false -> get_fee_for_type(transaction, fee_map) end end defp get_fee_for_type(%Transaction.Recovered{signed_tx: %Transaction.Signed{raw_tx: %{tx_type: type}}}, fee_map) do case type do nil -> %{} type -> Map.get(fee_map, type, %{}) end end defp get_fee_for_type(_, _fee_map), do: %{} end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/http_rpc/adapter.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.HttpRPC.Adapter do @moduledoc """ Provides functions to communicate with Child Chain API """ alias OMG.Utils.AppVersion require Logger @doc """ Makes HTTP POST request to the API """ def rpc_post(body, path, url) do addr = "#{url}/#{path}" headers = [{"content-type", "application/json"}, {"X-Watcher-Version", AppVersion.version(:omg_watcher)}] with {:ok, body} <- Jason.encode(body), {:ok, %HTTPoison.Response{} = response} <- HTTPoison.post(addr, body, headers) do _ = Logger.debug("rpc post #{inspect(addr)} completed successfully") response else err -> _ = Logger.warn("rpc post #{inspect(addr)} failed with #{inspect(err)}") err end end @doc """ Retrieves body from response structure but don't deserialize it. """ def get_unparsed_response_body({:ok, %HTTPoison.Response{} = response}), do: get_unparsed_response_body(response) def get_unparsed_response_body(%HTTPoison.Response{status_code: 200, body: body}), do: {:ok, body} def get_unparsed_response_body(%HTTPoison.Response{body: error}), do: {:error, {:client_error, error}} def get_unparsed_response_body({:error, %HTTPoison.Error{reason: :econnrefused}}) do {:error, :host_unreachable} end def get_unparsed_response_body({:error, %HTTPoison.Error{reason: reason}}) do {:error, {:server_error, reason}} end def get_unparsed_response_body(error), do: error @doc """ Retrieves body from response structure. When response is successful the structure in body is known, so we can try to deserialize it. """ @spec get_response_body(HTTPoison.Response.t() | {:error, HTTPoison.Error.t()}) :: {:ok, map()} | {:ok, list(map())} | {:error, atom() | tuple() | HTTPoison.Error.t()} def get_response_body(http_response) do with {:ok, body} <- get_unparsed_response_body(http_response), {:ok, response} <- Jason.decode(body), %{"success" => true, "data" => data} <- response do {:ok, convert_keys_to_atoms(data)} else %{"success" => false, "data" => data} -> {:error, {:client_error, data}} error -> error end end defp convert_keys_to_atoms(data) when is_list(data) do Enum.map(data, &convert_keys_to_atoms/1) end defp convert_keys_to_atoms(data) when is_map(data) do data |> Stream.map(fn {k, v} -> {String.to_existing_atom(k), v} end) |> Map.new() end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/http_rpc/client.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.HttpRPC.Client do @moduledoc """ Provides functions to communicate with Child Chain API """ alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.HttpRPC.Adapter require Logger @type response_t() :: list() | {:ok, %{required(atom()) => any()}} | {:error, {:client_error | :server_error, any()} | {:malformed_response, any() | {:error, :invalid}}} @doc """ Gets Block of given hash """ @spec get_block(binary(), binary()) :: response_t() def get_block(hash, url), do: call(%{hash: Encoding.to_hex(hash)}, "block.get", url) @doc """ Submits transaction """ @spec submit(binary(), binary()) :: response_t() def submit(tx, url), do: call(%{transaction: Encoding.to_hex(tx)}, "transaction.submit", url) @doc """ Submits a batch of transactions """ @spec batch_submit(list(binary()), binary()) :: response_t() def batch_submit(txs, url) do call(%{transactions: Enum.map(txs, &Encoding.to_hex(&1))}, "transaction.batch_submit", url) end defp call(params, path, url) do params |> Adapter.rpc_post(path, url) |> Adapter.get_response_body() |> decode_response() end # Translates response's body to known elixir structure, either block or tx submission response or error. defp decode_response({:ok, %{transactions: transactions, blknum: number, hash: hash}}) do {:ok, %{ number: number, hash: decode16!(hash), transactions: Enum.map(transactions, &decode16!/1) }} end defp decode_response({:ok, %{txhash: _hash} = response}) do {:ok, Map.update!(response, :txhash, &decode16!/1)} end defp decode_response({:ok, response}) when is_list(response) do decode_response(response, []) end defp decode_response(error), do: error defp decode_response([], acc) do Enum.reverse(acc) end defp decode_response([%{txhash: _hash} = transaction_response | response], acc) do decode_response(response, [Map.update!(transaction_response, :txhash, &decode16!/1) | acc]) end # all error tuples defp decode_response([%{error: error} | response], acc) do decode_response(response, [%{error: {:skip_hex_encode, error}} | acc]) end defp decode16!(hexstr) do {:ok, bin} = Encoding.from_hex(hexstr) bin end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/merge_transaction_validator.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.MergeTransactionValidator do @moduledoc """ Decides whether transactions qualify as "merge" transactions that use a single currency, single recipient address and have fewer outputs than inputs. This decision is necessary to know by the child chain to not require the transaction fees. """ alias OMG.Output alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo require Logger @spec is_merge_transaction?(%Transaction.Recovered{}) :: boolean() def is_merge_transaction?(recovered_transaction) do [ &is_payment?/1, &only_fungible_tokens?/1, &has_less_outputs_than_inputs?/1, &has_single_currency?/1, &has_same_account?/1 ] |> Enum.all?(fn predicate -> predicate.(recovered_transaction) end) end defp is_payment?(%Transaction.Recovered{signed_tx: %{raw_tx: %Transaction.Payment{}}}), do: true defp is_payment?(_), do: false defp only_fungible_tokens?(tx), do: tx |> Transaction.get_outputs() |> Enum.all?(&match?(%Output{}, &1)) defp has_same_account?(%Transaction.Recovered{witnesses: witnesses} = tx) do spenders = Map.values(witnesses) tx |> Transaction.get_outputs() |> Enum.map(& &1.owner) |> Enum.concat(spenders) |> single?() end defp has_single_currency?(tx) do tx |> Transaction.get_outputs() |> Enum.map(& &1.currency) |> single?() end defp has_less_outputs_than_inputs?(tx) do has_less_outputs_than_inputs?( Transaction.get_inputs(tx), Transaction.get_outputs(tx) ) end defp has_less_outputs_than_inputs?(inputs, outputs), do: length(inputs) >= 1 and length(inputs) > length(outputs) defp single?(list), do: 1 == list |> Enum.uniq() |> length() end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/merkle.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Merkle do @moduledoc """ Encapsulates all the interactions with the MerkleTree library. """ alias OMG.Watcher.Crypto @transaction_merkle_tree_height 16 @default_leaf <<0::256>> # Creates a Merkle proof that transaction under a given transaction index # is included in block consisting of hashed transactions @spec create_tx_proof(list(String.t()), non_neg_integer()) :: binary() def create_tx_proof(txs_bytes, txindex) do build(txs_bytes) |> prove(txindex) |> Enum.reverse() |> Enum.join() end @spec hash(list(String.t())) :: binary() def hash(hashed_txs) do MerkleTree.fast_root(hashed_txs, hash_function: &Crypto.hash/1, height: @transaction_merkle_tree_height, default_data_block: @default_leaf ) end defp build(txs_bytes) do MerkleTree.build(txs_bytes, hash_function: &Crypto.hash/1, height: @transaction_merkle_tree_height, default_data_block: @default_leaf ) end defp prove(tx_bytes, txindex) do MerkleTree.Proof.prove(tx_bytes, txindex) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/monitor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Monitor do @moduledoc """ This module restarts it's children if the Ethereum client connectivity is dropped. It subscribes to alarms and when an alarm is cleared it restarts it children if they're dead. """ defmodule Child do @moduledoc false @type t :: %__MODULE__{ pid: pid(), spec: {module(), term()} | map() } defstruct pid: nil, spec: nil end use GenServer require Logger @type t :: %__MODULE__{ alarm_module: module(), child: Child.t() } defstruct alarm_module: nil, child: nil def health_checkin() do GenServer.cast(__MODULE__, :health_checkin) end def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init([alarm_module, child_spec]) do subscribe_to_alarms() Process.flag(:trap_exit, true) # we raise the alarms first, because we get a health checkin when all # sub processes of the supervisor are ready to go _ = alarm_module.set(alarm_module.main_supervisor_halted(__MODULE__)) {:ok, %__MODULE__{alarm_module: alarm_module, child: start_child(child_spec)}} end # gen_event boot def init(_args) do {:ok, %{}} end # # gen_event # def handle_call(_request, state), do: {:ok, :ok, state} def handle_event({:clear_alarm, {:ethereum_connection_error, _}}, state) do _ = Logger.warn(":ethereum_connection_error alarm was cleared. Beginning to restart processes.") :ok = GenServer.cast(__MODULE__, :start_child) {:ok, state} end # flush def handle_event(event, state) do _ = Logger.info("#{__MODULE__} got event: #{inspect(event)}. Ignoring.") {:ok, state} end # There's a supervisor below us that did the needed restarts for it's children # so we do not attempt to restart the exit from the supervisor, if the alarm clears, we restart it then. # We declare the sytem unhealthy def handle_info({:EXIT, _from, reason}, state) do _ = Logger.warn("Watcher supervisor crashed. Raising alarm. Reason #{inspect(reason)}") state.alarm_module.set(state.alarm_module.main_supervisor_halted(__MODULE__)) {:noreply, state} end # alarm has cleared, we can now begin restarting supervisor child def handle_cast(:health_checkin, state) do _ = Logger.info("Got a health checkin... clearing alarm main_supervisor_halted.") _ = state.alarm_module.clear(state.alarm_module.main_supervisor_halted(__MODULE__)) {:noreply, state} end # alarm has cleared, we can now begin restarting supervisor child def handle_cast(:start_child, state) do child = state.child _ = Logger.info("Monitor is restarting child #{inspect(child)}.") {:noreply, %{state | child: start_child(child)}} end defp start_child(%{id: _name, start: {child_module, function, args}} = spec) do {:ok, pid} = apply(child_module, function, args) %Child{pid: pid, spec: spec} end defp start_child(%Child{pid: pid, spec: spec} = child) do case Process.alive?(pid) do true -> child false -> %{id: _name, start: {child_module, function, args}} = spec {:ok, pid} = apply(child_module, function, args) %Child{pid: pid, spec: spec} end end defp subscribe_to_alarms() do case Enum.member?(:gen_event.which_handlers(:alarm_handler), __MODULE__) do true -> :ok _ -> :alarm_handler.add_alarm_handler(__MODULE__) end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/output.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Output do @moduledoc """ `OMG.Output` and `OMG.Output.Protocol` represent the outputs of transactions, i.e. the valuables or other pieces of data spendable via transactions on the child chain, and/or exitable to the root chain. This module specificially dispatches generic calls to the various specific types THIS IS WHAT HAPPENS IF YOU STORE ELIXIR STRUCTS IN A DATABASE This struct is binary encoded into RocksDB, so when we call `:erlang.binary_to_term(encoded, [:safe])` for old outputs stored, it expects this struct to exist. Just make sure they do the same thing!!! """ alias OMG.Watcher.Crypto alias OMG.Watcher.RawData @output_types Map.keys(OMG.Watcher.WireFormatTypes.output_type_modules()) @type t :: %__MODULE__{ output_type: binary(), owner: Crypto.address_t(), currency: Crypto.address_t(), amount: non_neg_integer() } @type error_t() :: {:error, atom()} defstruct [:output_type, :owner, :currency, :amount] @doc """ Reconstructs the structure from a list of RLP items """ @spec reconstruct(any()) :: t() | error_t() def reconstruct(_rlp_data) def reconstruct([raw_type, [_owner, _currency, _amount]] = rlp_data) when is_binary(raw_type) do with {:ok, type, owner, currency, amont} <- clean_and_validate_data(rlp_data), do: %__MODULE__{ output_type: type, owner: owner, currency: currency, amount: amont } end def reconstruct([_raw_type, [_owner, _currency, _amount]]), do: {:error, :unrecognized_output_type} def reconstruct(_), do: {:error, :malformed_outputs} def from_db_value(%{owner: owner, currency: currency, amount: amount, output_type: output_type}) when is_binary(owner) and is_binary(currency) and is_integer(amount) and is_integer(output_type) do %__MODULE__{owner: owner, currency: currency, amount: amount, output_type: output_type} end def to_db_value(%__MODULE__{owner: owner, currency: currency, amount: amount, output_type: output_type}) when is_binary(owner) and is_binary(currency) and is_integer(amount) and is_integer(output_type) do %{owner: owner, currency: currency, amount: amount, output_type: output_type} end def get_data_for_rlp(%__MODULE__{owner: owner, currency: currency, amount: amount, output_type: output_type}), do: [output_type, [owner, currency, amount]] # TODO(achiurizo) # remove the validation here and port the error tuple response handling into ex_plasma. defp clean_and_validate_data([raw_type, [owner, currency, amount]]) do with {:ok, parsed_type} <- RawData.parse_uint256(raw_type), {:ok, _} <- valid_output_type?(parsed_type), {:ok, parsed_owner} <- RawData.parse_address(owner), {:ok, _} <- non_zero_owner?(owner), {:ok, parsed_currency} <- RawData.parse_address(currency), {:ok, parsed_amount} <- RawData.parse_amount(amount), do: {:ok, parsed_type, parsed_owner, parsed_currency, parsed_amount} end defp non_zero_owner?(<<0::160>>), do: {:error, :output_guard_cant_be_zero} defp non_zero_owner?(_), do: {:ok, :valid} defp valid_output_type?(type) when type in @output_types, do: {:ok, :valid} defp valid_output_type?(_), do: {:error, :unrecognized_output_type} end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/raw_data.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RawData do @moduledoc """ Provides functions to decode various data types from RLP raw format """ alias OMG.Watcher.Crypto @doc """ Parses amount, where 0 < amount < 2^256 """ @spec parse_amount(binary()) :: {:ok, pos_integer()} | {:error, :amount_cant_be_zero | :leading_zeros_in_encoded_uint | :encoded_uint_too_big} def parse_amount(binary) when is_binary(binary) do case parse_uint256(binary) do {:ok, 0} -> {:error, :amount_cant_be_zero} {:ok, amount} -> {:ok, amount} error -> error end end @doc """ Parses 20-bytes address Case `<<>>` is necessary, because RLP handles empty string equally to integer 0 """ @spec parse_address(<<>> | Crypto.address_t()) :: {:ok, Crypto.address_t()} | {:error, :malformed_address} def parse_address(binary) def parse_address(<<_::160>> = address_bytes), do: {:ok, address_bytes} def parse_address(_), do: {:error, :malformed_address} @doc """ Parses unsigned at-most 32-bytes integer. Leading zeros are disallowed """ @spec parse_uint256(binary()) :: {:ok, non_neg_integer()} | {:error, :encoded_uint_too_big | :leading_zeros_in_encoded_uint} def parse_uint256(<<0>> <> _binary), do: {:error, :leading_zeros_in_encoded_uint} def parse_uint256(binary) when byte_size(binary) <= 32, do: {:ok, :binary.decode_unsigned(binary, :big)} def parse_uint256(binary) when byte_size(binary) > 32, do: {:error, :encoded_uint_too_big} def parse_uint256(_), do: {:error, :malformed_uint256} end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/release_tasks/set_application.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ReleaseTasks.SetApplication do @moduledoc false @behaviour Config.Provider def init(args) do args end def load(config, release: release, current_version: current_version) do Config.Reader.merge(config, omg_watcher: [release: release, current_version: current_version]) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/release_tasks/set_ethereum_events_check_interval.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ReleaseTasks.SetEthereumEventsCheckInterval do @moduledoc """ Configures the interval to check for new events from Ethereum. This is essentially the same as `OMG.Watcher.Eth.ReleaseTasks.SetEthereumEventsCheckInterval` but for a different subapp. """ @behaviour Config.Provider require Logger @app :omg_watcher @env_key "ETHEREUM_EVENTS_CHECK_INTERVAL_MS" def init(args) do args end def load(config, _args) do _ = on_load() interval_ms = get_interval_ms() Config.Reader.merge(config, omg_watcher: [ethereum_events_check_interval_ms: interval_ms]) end defp get_interval_ms() do ethereum_events_check_interval_ms = Application.get_env(@app, :ethereum_events_check_interval_ms) interval_ms = validate_integer(get_env(@env_key), ethereum_events_check_interval_ms) _ = Logger.info("CONFIGURATION: App: #{@app} Key: ethereum_events_check_interval_ms Value: #{inspect(interval_ms)}.") interval_ms end defp get_env(key), do: System.get_env(key) defp validate_integer(value, _default) when is_binary(value), do: String.to_integer(value) defp validate_integer(_, default), do: default def on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(:omg_watcher) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/release_tasks/set_exit_processor_sla_margin.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ReleaseTasks.SetExitProcessorSLAMargin do @moduledoc false @behaviour Config.Provider require Logger @app :omg_watcher @system_env_name_margin "EXIT_PROCESSOR_SLA_MARGIN" @app_env_name_margin :exit_processor_sla_margin @system_env_name_force "EXIT_PROCESSOR_SLA_MARGIN_FORCED" @app_env_name_force :exit_processor_sla_margin_forced def init(args) do args end def load(config, _args) do _ = Application.ensure_all_started(:logger) Config.Reader.merge(config, omg_watcher: [ exit_processor_sla_margin: get_exit_processor_sla_margin(), exit_processor_sla_margin_forced: get_exit_processor_sla_forced() ] ) end defp get_exit_processor_sla_margin() do config_value = validate_int(get_env(@system_env_name_margin), Application.get_env(@app, @app_env_name_margin)) _ = Logger.info("CONFIGURATION: App: #{@app} Key: #{@system_env_name_margin} Value: #{inspect(config_value)}.") config_value end defp get_exit_processor_sla_forced() do config_value = validate_bool(get_env(@system_env_name_force), Application.get_env(@app, @app_env_name_force)) _ = Logger.info("CONFIGURATION: App: #{@app} Key: #{@system_env_name_force} Value: #{inspect(config_value)}.") config_value end defp get_env(key), do: System.get_env(key) defp validate_int(value, _default) when is_binary(value), do: to_int(value) defp validate_int(_, default), do: default defp validate_bool(value, _default) when is_binary(value), do: to_bool(String.upcase(value)) defp validate_bool(_, default), do: default defp to_bool("TRUE"), do: true defp to_bool("FALSE"), do: false defp to_bool(_), do: exit("#{@system_env_name_force} either true or false.") defp to_int(value) do case Integer.parse(value) do {result, ""} -> result _ -> exit("#{@system_env_name_margin} must be an integer.") end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/release_tasks/set_tracer.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ReleaseTasks.SetTracer do @moduledoc false @behaviour Config.Provider require Logger @app :omg_watcher def init(args) do args end def load(config, args) do _ = on_load() adapter = Keyword.get(args, :system_adapter, System) _ = Process.put(:system_adapter, adapter) dd_disabled = get_dd_disabled() tracer_config = @app |> Application.get_env(OMG.Watcher.Tracer) |> Keyword.put(:disabled?, dd_disabled) tracer_config = case dd_disabled do false -> app_env = get_app_env() Keyword.put(tracer_config, :env, app_env) true -> Keyword.put(tracer_config, :env, "") end Config.Reader.merge(config, omg_watcher: [{OMG.Watcher.Tracer, tracer_config}]) end defp get_dd_disabled() do disabled = Application.get_env(@app, OMG.Watcher.Tracer)[:disabled?] dd_disabled? = validate_bool(get_env("DD_DISABLED"), disabled) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DD_DISABLED Value: #{inspect(dd_disabled?)}.") dd_disabled? end defp get_app_env() do env = validate_app_env(get_env("APP_ENV")) _ = Logger.info("CONFIGURATION: App: #{@app} Key: APP_ENV Value: #{inspect(env)}.") env end defp get_env(key) do Process.get(:system_adapter).get_env(key) end defp validate_bool(value, _default) when is_binary(value), do: to_bool(String.upcase(value)) defp validate_bool(_, default), do: default defp to_bool("TRUE"), do: true defp to_bool("FALSE"), do: false defp to_bool(_), do: exit("DD_DISABLED either true or false.") defp validate_app_env(value) when is_binary(value), do: value defp validate_app_env(nil), do: exit("APP_ENV must be set.") defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(@app) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/root_chain_coordinator/core.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RootChainCoordinator.Core do @moduledoc """ Synchronizes multiple log-reading services on root chain height. Each synchronized service must have a unique name. Service reports its height by calling 'check_in'. After all the services are checked in, coordinator returns currently synchronizable height, for every service which asks by calling `RootChainCoordinator.get_height()` In case a service fails, it is checked out and coordinator does not resume until the missing service checks_in again. Coordinator periodically updates root chain height, looks after finality margins and ensures geth-queries aren't huge. Coordinator is forgiving in terms of height backoffs: - if the root chain's height backs off, it will treat it as an interim state and ignore the back off (noop) - if any of the coordinated services backs off, it will register the backed off height and coordinate acordingly. All services must accept a `SyncGuide` that tells them they should back off. All services must ensure this doesn't cause them to process any events twice! All services must ensure they process everything! """ alias OMG.Watcher.RootChainCoordinator.Service alias OMG.Watcher.RootChainCoordinator.SyncGuide require Logger defstruct configs_services: %{}, root_chain_height: 0, services: %{} @type config_t :: keyword() @type configs_services :: %{required(atom()) => config_t()} @type t() :: %__MODULE__{ configs_services: configs_services, root_chain_height: non_neg_integer(), services: %{required(atom()) => Service.t()} } @type check_in_error_t :: {:error, :service_not_allowed} @type ethereum_heights_result_t() :: %{atom() => non_neg_integer()} # RootChainCoordinator is also checking if queries to Ethereum client don't get huge @maximum_leap_forward 2_500 @doc """ Initializes the state of the logic module. - `configs_services` - configs of services that are being synchronized. A map of the form `%{service_name => config}`. The `config`s are keyword lists with the following options: - `:finality_margin` - number of Ethereum block confirmations to count before recognizing an event - `:waits_for` - a list of other services, which should sync first. Each service in this list can be an atom, being the name of the service, or a `{service_name, :no_margin}` pair, if the waiting should bypass the finality margin of the awaited process. An example config can be seen in `OMG.Watcher.Watcher.CoordinatorSetup` - `root_chain_height` - current root chain height """ @spec init(map(), non_neg_integer()) :: t() def init(configs_services, root_chain_height) do %__MODULE__{configs_services: configs_services, root_chain_height: root_chain_height} end @doc """ Updates Ethereum height on which a service is synchronized. """ @spec check_in(t(), pid(), pos_integer(), atom()) :: {:ok, t()} | check_in_error_t() def check_in(state, pid, service_height, service_name) when is_integer(service_height) do if allowed?(state.configs_services, service_name) do update_service_synced_height(state, pid, service_height, service_name) else {:error, :service_not_allowed} end end @doc """ Sets root chain height, only allowing to progress, in case Ethereum RPC reports an earlier height """ @spec update_root_chain_height(t(), pos_integer()) :: {:ok, t()} def update_root_chain_height(%__MODULE__{root_chain_height: old_height} = state, new_height) when is_integer(new_height) do {:ok, %{state | root_chain_height: max(old_height, new_height)}} end @doc """ Provides synchronization guide to a service which asks """ @spec get_synced_info(t(), atom() | pid()) :: SyncGuide.t() | :nosync def get_synced_info(state, pid) when is_pid(pid) do service = Enum.find(state.services, fn service -> match?({_, %Service{pid: ^pid}}, service) end) case service do {service_name, _} -> get_synced_info(state, service_name) nil -> :nosync end end def get_synced_info( %__MODULE__{root_chain_height: root_chain_height, configs_services: configs, services: services} = state, service_name ) when is_atom(service_name) do if all_services_checked_in?(state) do config = configs[service_name] current_sync_height = services[service_name].synced_height next_sync_height = config |> Keyword.get(:waits_for, []) |> get_height_of_awaited(state) |> consider_finality(configs[service_name], root_chain_height) |> min(current_sync_height + @maximum_leap_forward) |> min(root_chain_height) |> max(0) finality_bearing_root = max(0, root_chain_height - finality_margin_for(config)) %SyncGuide{sync_height: next_sync_height, root_chain_height: finality_bearing_root} else :nosync end end @doc """ Gets all the ethereum heights reported as synced to by the services (and the main root chain height acknowledged) """ @spec get_ethereum_heights(t()) :: ethereum_heights_result_t() def get_ethereum_heights(%__MODULE__{root_chain_height: root_chain_height, services: services}) do base_result_map = %{root_chain_height: root_chain_height} Enum.into(services, base_result_map, fn {name, %Service{synced_height: height}} -> {name, height} end) end defp finality_margin_for(config), do: Keyword.get(config, :finality_margin, 0) defp finality_margin_for!(config), do: Keyword.fetch!(config, :finality_margin) # ensures we don't exceed the allowed finality margin applied to the root_chain_height defp consider_finality(sync_height, config, root_chain_height), do: min(sync_height, root_chain_height - finality_margin_for(config)) # get the earliest-synced of all of the services we're waiting for, if any, if none then root chain height defp get_height_of_awaited([], %__MODULE__{root_chain_height: root_chain_height}), # wait for nothing so root chain is the limit do: root_chain_height defp get_height_of_awaited(single_awaited, %__MODULE__{services: services}) when is_atom(single_awaited), # we wait for a single service so get that do: services[single_awaited].synced_height defp get_height_of_awaited({single_awaited, :no_margin}, %__MODULE__{configs_services: configs} = state), # in this clause we're waiting on a service, but skipping ahead its particular finality margin do: get_height_of_awaited(single_awaited, state) + finality_margin_for!(configs[single_awaited]) defp get_height_of_awaited(awaited, state), # we're waiting for multiple services, so iterate the list and get the least synced height do: Enum.map(awaited, &get_height_of_awaited(&1, state)) |> Enum.min() defp all_services_checked_in?(%__MODULE__{configs_services: configs_services, services: services}) do sort = fn map -> map |> Map.keys() |> Enum.sort() end sort.(configs_services) == sort.(services) end defp allowed?(configs_services, service_name), do: Map.has_key?(configs_services, service_name) defp update_service_synced_height(state, pid, new_reported_sync_height, service_name) do new_service_state = %Service{synced_height: new_reported_sync_height, pid: pid} {:ok, %{state | services: Map.put(state.services, service_name, new_service_state)}} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/root_chain_coordinator/measure.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RootChainCoordinator.Measure do @moduledoc """ Counting business metrics sent to Datadog """ import OMG.Status.Metric.Event, only: [name: 2] alias OMG.Status.Metric.Datadog alias OMG.Watcher.RootChainCoordinator def handle_event([:process, RootChainCoordinator], _, state, _config) do value = self() |> Process.info(:message_queue_len) |> elem(1) _ = Datadog.gauge(name(state.service_name, :message_queue_len), value) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/root_chain_coordinator/service.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RootChainCoordinator.Service do @moduledoc """ Represents a state of a service that is coordinated by `RootChainCoordinator.Core` """ defstruct synced_height: nil, pid: nil @type t() :: %__MODULE__{ synced_height: pos_integer(), pid: pid() } end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/root_chain_coordinator.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RootChainCoordinator do @moduledoc """ Synchronizes services on root chain height, see `OMG.Watcher.RootChainCoordinator.Core` """ alias OMG.Eth.EthereumHeight alias OMG.Watcher.RootChainCoordinator.Core use GenServer use Spandex.Decorators require Logger defmodule SyncGuide do @moduledoc """ A guiding message to a coordinated service. Tells until which root chain height it is safe to advance syncing to. `sync_height` - until where it is safe to process the root chain `root_chain_height` - until where it is safe to pre-fetch and cache the events from the root chain """ defstruct [:root_chain_height, :sync_height] @type t() :: %__MODULE__{ root_chain_height: non_neg_integer(), sync_height: non_neg_integer() } end @spec start_link(Core.configs_services()) :: GenServer.on_start() def start_link(configs_services) do GenServer.start_link(__MODULE__, configs_services, name: __MODULE__) end @doc """ Notifies that calling service with name `service_name` is synced up to height `synced_height`. `synced_height` is the height that the service is synced when calling this function. """ @decorate span(service: :ethereum_event_listener, type: :backend, name: "check_in/2") @spec check_in(non_neg_integer(), atom()) :: :ok def check_in(synced_height, service_name) do GenServer.call(__MODULE__, {:check_in, synced_height, service_name}) end @doc """ Gets Ethereum height that services can synchronize up to. """ @decorate span(service: :ethereum_event_listener, type: :backend, name: "get_sync_info/0") @spec get_sync_info() :: SyncGuide.t() | :nosync def get_sync_info() do GenServer.call(__MODULE__, :get_sync_info) end @doc """ Gets all the current synced height for all the services checked in """ @spec get_ethereum_heights() :: {:ok, Core.ethereum_heights_result_t()} def get_ethereum_heights() do GenServer.call(__MODULE__, :get_ethereum_heights) end def init({args, configs_services}) do _ = Logger.info("Starting #{__MODULE__} service. #{inspect({args, configs_services})}") metrics_collection_interval = Keyword.fetch!(args, :metrics_collection_interval) coordinator_eth_height_check_interval_ms = Keyword.fetch!(args, :coordinator_eth_height_check_interval_ms) {:ok, rootchain_height} = EthereumHeight.get() {:ok, _} = schedule_get_ethereum_height(coordinator_eth_height_check_interval_ms) state = Core.init(configs_services, rootchain_height) configs_services |> Map.keys() |> request_sync() {:ok, _} = :timer.send_interval(metrics_collection_interval, self(), :send_metrics) _ = Logger.info("Started #{inspect(__MODULE__)}") {:ok, state} end def handle_info(:send_metrics, state) do :ok = :telemetry.execute([:process, __MODULE__], %{}, state) {:noreply, state} end def handle_info(:update_root_chain_height, state) do {:ok, root_chain_height} = EthereumHeight.get() {:ok, state} = Core.update_root_chain_height(state, root_chain_height) {:noreply, state} end def handle_call({:check_in, synced_height, service_name}, {pid, _ref}, state) do _ = Logger.debug("#{inspect(service_name)} checks in on height #{inspect(synced_height)}") {:ok, state} = Core.check_in(state, pid, synced_height, service_name) {:reply, :ok, state} end def handle_call(:get_sync_info, {pid, _}, state) do {:reply, Core.get_synced_info(state, pid), state} end def handle_call(:get_ethereum_heights, _from, state) do {:reply, {:ok, Core.get_ethereum_heights(state)}, state} end defp schedule_get_ethereum_height(interval) do :timer.send_interval(interval, self(), :update_root_chain_height) end defp request_sync(services) do Enum.each(services, fn service -> safe_send(service, :sync) end) end defp safe_send(registered_name_or_pid, msg) do send(registered_name_or_pid, msg) rescue ArgumentError -> msg end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/signature.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Signature do @moduledoc """ Adapted from https://github.com/exthereum/blockchain. Defines helper functions for signing and getting the signature of a transaction, as defined in Appendix F of the Yellow Paper. For any of the following functions, if chain_id is specified, it's assumed that we're post-fork and we should follow the specification EIP-155 from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md """ @base_recovery_id 27 @base_recovery_id_eip_155 35 @signature_len 32 @type keccak_hash :: binary() @type public_key :: <<_::512>> @type private_key :: <<_::256>> @type hash_v :: integer() @type hash_r :: integer() @type hash_s :: integer() @type signature_len :: unquote(@signature_len) @doc """ Recovers a public key from a signed hash. This implements Eq.(208) of the Yellow Paper, adapted from https://stackoverflow.com/a/20000007 ## Example iex(1)> OMG.Watcher.Signature.recover_public(<<2::256>>, ...(1)> 28, ...(1)> 38_938_543_279_057_362_855_969_661_240_129_897_219_713_373_336_787_331_739_561_340_553_100_525_404_231, ...(1)> 23_772_455_091_703_794_797_226_342_343_520_955_590_158_385_983_376_086_035_257_995_824_653_222_457_926 ...(1)> ) {:ok, <<121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23, 152, 72, 58, 218, 119, 38, 163, 196, 101, 93, 164, 251, 252, 14, 17, 8, 168, 253, 23, 180, 72, 166, 133, 84, 25, 156, 71, 208, 143, 251, 16, 212, 184>>} """ @spec recover_public(keccak_hash(), hash_v, hash_r, hash_s, integer() | nil) :: {:ok, public_key} | {:error, atom()} def recover_public(hash, v, r, s, chain_id \\ nil) do signature = pad(:binary.encode_unsigned(r), @signature_len) <> pad(:binary.encode_unsigned(s), @signature_len) # Fork Ψ EIP-155 recovery_id = if not is_nil(chain_id) and uses_chain_id?(v) do v - chain_id * 2 - @base_recovery_id_eip_155 else v - @base_recovery_id end case ExSecp256k1.recover_compact(hash, signature, recovery_id) do {:ok, <<_byte::8, public_key::binary()>>} -> {:ok, public_key} {:error, reason} -> {:error, reason} end end @doc """ Recovers a public key from a signed hash. This implements Eq.(208) of the Yellow Paper, adapted from https://stackoverflow.com/a/20000007 ## Example iex(1)> OMG.Watcher.Signature.recover_public(<<2::256>>, <<168, 39, 110, 198, 11, 113, 141, 8, 168, 151, 22, 210, 198, 150, 24, 111, 23, ...(1)> 173, 42, 122, 59, 152, 143, 224, 214, 70, 96, 204, 31, 173, 154, 198, 97, 94, ...(1)> 203, 172, 169, 136, 182, 131, 11, 106, 54, 190, 96, 128, 227, 222, 248, 231, ...(1)> 75, 254, 141, 233, 113, 49, 74, 28, 189, 73, 249, 32, 89, 165, 27>>) {:ok, <<233, 102, 200, 175, 51, 251, 139, 85, 204, 181, 94, 133, 233, 88, 251, 156, 123, 157, 146, 192, 53, 73, 125, 213, 245, 12, 143, 102, 54, 70, 126, 35, 34, 167, 2, 255, 248, 68, 210, 117, 183, 156, 4, 185, 77, 27, 53, 239, 10, 57, 140, 63, 81, 87, 133, 241, 241, 210, 250, 35, 76, 232, 2, 153>>} """ def recover_public(hash, <>, chain_id \\ nil) do recover_public(hash, v, r, s, chain_id) end @spec uses_chain_id?(hash_v) :: boolean() defp uses_chain_id?(v) do v >= @base_recovery_id_eip_155 end @spec pad(binary(), signature_len()) :: binary() defp pad(binary, desired_length) do desired_bits = desired_length * 8 case byte_size(binary) do 0 -> <<0::size(desired_bits)>> x when x <= desired_length -> padding_bits = (desired_length - x) * 8 <<0::size(padding_bits)>> <> binary _ -> raise "Binary too long for padding" end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/core.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Core do @moduledoc """ The state meant here is the state of the ledger (UTXO set), that determines spendability of coins and forms blocks. All spend transactions, deposits and exits should sync on this for validity of moving funds. ### Notes on loading of the UTXO set We experienced long startup times on large UTXO set, which in some case caused timeouts and lethal `OMG.Watcher.State` restart loop. To mitigate this issue we introduced loading UTXO set on demand (see GH#1103) instead of full load on process startup. During OMG.Watcher.State startup no UTXOs are fetched from DB, which is no longer blocking significantly. Then during each of 6 utxo-related operations (see below) UTXO set is extended with UTXOs from DB to ensure operation behavior hasn't been changed. Transaction processing is populating the in-memory UTXO set and once block is formed newly created UTXO are inserted to DB, but are also kept in process State. Service restart looses all UTXO created by transactions processed as well as mempool transactions therefore DB content stays block-by-block consistent. Operations that require full ledger information are: - utxo_exists? - exec - form_block (and `close_block`) - deposit - exit_utxos These operations assume that passed `OMG.Watcher.State.Core` struct instance contains sufficient UTXO information to proceed. Therefore the UTXOs that in-memory state is unaware of are fetched from the `OMG.DB` and then merged into state. As not every operation updates `OMG.DB` immediately additional `recently_spent` collection was added to in-memory state to defend against double spends in transactions within the same block. After block is formed `OMG.DB` contains full information up to the current block so we could waste in-memory info about utxos and spends. If the process gets restarted before form_block all mempool transactions along with created and spent utxos are lost and the ledger state basically resets to the previous block. """ defstruct [ :height, :fee_claimer_address, :child_block_interval, utxos: %{}, pending_txs: [], tx_index: 0, utxo_db_updates: [], recently_spent: MapSet.new(), fees_paid: %{}, fee_claiming_started: false ] alias OMG.Output alias OMG.Watcher.Block alias OMG.Watcher.Crypto alias OMG.Watcher.Fees alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.State.Transaction.Validator alias OMG.Watcher.State.UtxoSet alias OMG.Watcher.Utxo require Logger require Utxo @type fee_summary_t() :: %{Transaction.Payment.currency() => pos_integer()} @type t() :: %__MODULE__{ height: non_neg_integer(), utxos: utxos, pending_txs: list(Transaction.Recovered.t()), tx_index: non_neg_integer(), # NOTE: that this list is being build reverse, in some cases it may matter. It is reversed just before # it leaves this module in `form_block/3` utxo_db_updates: list(db_update()), # NOTE: because UTXO set is not loaded from DB entirely, we need to remember the UTXOs spent in already # processed transaction before they get removed from DB on form_block. recently_spent: MapSet.t(OMG.Watcher.Utxo.Position.t()), # Summarizes fees paid by pending transactions that will be formed into current block. Fees will be claimed # by appending `Transaction.Fee` txs after pending txs in current block. fees_paid: fee_summary_t(), # fees can be claimed at the end of the block, no other payments can be processed until next block fee_claiming_started: boolean(), fee_claimer_address: Crypto.address_t(), child_block_interval: non_neg_integer() } @type deposit() :: %{ root_chain_txhash: Crypto.hash_t(), log_index: non_neg_integer(), blknum: non_neg_integer(), currency: Crypto.address_t(), owner: Crypto.address_t(), amount: pos_integer(), eth_height: pos_integer() } @type exit_t() :: %{utxo_pos: pos_integer()} @type exit_finalization_t() :: %{utxo_pos: pos_integer()} @type exiting_utxo_triggers_t() :: [Utxo.Position.t()] | [non_neg_integer()] | [exit_t()] | [exit_finalization_t()] | [piggyback()] | [in_flight_exit()] @type in_flight_exit() :: %{in_flight_tx: binary()} @type piggyback() :: %{tx_hash: Transaction.tx_hash(), output_index: non_neg_integer} @type validities_t() :: {list(Utxo.Position.t()), list(Utxo.Position.t() | piggyback())} @type utxos() :: %{Utxo.Position.t() => Utxo.t()} @type db_update :: {:put, :utxo, {Utxo.Position.db_t(), map()}} | {:delete, :utxo, Utxo.Position.db_t()} | {:put, :child_top_block_number, pos_integer()} | {:put, :block, Block.db_t()} @type exitable_utxos :: %{ owner: Crypto.address_t(), currency: Crypto.address_t(), amount: non_neg_integer(), blknum: pos_integer(), txindex: non_neg_integer(), oindex: non_neg_integer() } @doc """ Initializes the state from the values stored in `OMG.DB` """ @spec extract_initial_state( height_query_result :: non_neg_integer() | :not_found, child_block_interval :: pos_integer(), fee_claimer_address :: Crypto.address_t() ) :: {:ok, t()} | {:error, :top_block_number_not_found} def extract_initial_state(height_query_result, child_block_interval, fee_claimer_address) when is_integer(height_query_result) and is_integer(child_block_interval) do state = %__MODULE__{ height: height_query_result + child_block_interval, fee_claimer_address: fee_claimer_address, child_block_interval: child_block_interval } {:ok, state} end def extract_initial_state(:not_found, _child_block_interval, _fee_claimer_address) do {:error, :top_block_number_not_found} end @doc """ Tell whether utxo position was created or spent by current state. """ @spec utxo_processed?(OMG.Watcher.Utxo.Position.t(), t()) :: boolean() def utxo_processed?(utxo_pos, %Core{utxos: utxos, recently_spent: recently_spent}) do Map.has_key?(utxos, utxo_pos) or MapSet.member?(recently_spent, utxo_pos) end @doc """ Extends in-memory utxo set with needed utxos loaded from DB See also: State.init_utxos_from_db/2 """ @spec with_utxos(t(), utxos()) :: t() def with_utxos(%Core{utxos: utxos} = state, db_utxos) do %{state | utxos: UtxoSet.apply_effects(utxos, [], db_utxos)} end @doc """ Includes the transaction into the state when valid, rejects otherwise. NOTE that tx is assumed to have distinct inputs, that should be checked in prior state-less validation See docs/transaction_validation.md for more information about stateful and stateless validation. """ @spec exec(state :: t(), tx :: Transaction.Recovered.t(), fees :: Fees.optional_fee_t()) :: {:ok, {Transaction.tx_hash(), pos_integer, non_neg_integer}, t()} | {{:error, Validator.can_process_tx_error()}, t()} def exec(%Core{} = state, %Transaction.Recovered{} = tx, fees) do tx_hash = Transaction.raw_txhash(tx) case Validator.can_process_tx(state, tx, fees) do {:ok, fees_paid} -> {:ok, {tx_hash, state.height, state.tx_index}, state |> apply_tx(tx) |> add_pending_tx(tx) |> handle_fees(tx, fees_paid)} {{:error, _reason}, _state} = error -> error end end @doc """ Filter user utxos from db response. It may take a while for a large response from db """ @spec standard_exitable_utxos(UtxoSet.query_result_t(), Crypto.address_t()) :: list(exitable_utxos) def standard_exitable_utxos(utxos_query_result, address) do utxos_query_result |> UtxoSet.init() |> UtxoSet.filter_owned_by(address) |> UtxoSet.zip_with_positions() |> Enum.map(fn {{_, utxo}, position} -> utxo_to_exitable_utxo_map(utxo, position) end) end @doc """ - Generates block and calculates it's root hash for submission - generates requests to the persistence layer for a block - processes pending txs gathered, updates height etc - clears `recently_spent` collection """ @spec form_block(state :: t()) :: {:ok, {Block.t(), [db_update]}, new_state :: t()} def form_block(state) do txs = Enum.reverse(state.pending_txs) block = Block.hashed_txs_at(txs, state.height) db_updates_block = {:put, :block, Block.to_db_value(block)} db_updates_top_block_number = {:put, :child_top_block_number, state.height} db_updates = [db_updates_top_block_number, db_updates_block | state.utxo_db_updates] |> Enum.reverse() new_state = %Core{ state | tx_index: 0, height: state.height + state.child_block_interval, pending_txs: [], utxo_db_updates: [], recently_spent: MapSet.new(), fees_paid: %{}, fee_claiming_started: false } _ = :telemetry.execute([:block_transactions, __MODULE__], %{txs: txs}, %{}) {:ok, {block, db_updates}, new_state} end @doc """ Processes a deposit event, introducing a UTXO into the ledger's state. From then on it is spendable on the child chain **NOTE** this expects that each deposit event is fed to here exactly once, so this must be ensured elsewhere. There's no double-checking of this constraint done here. """ @spec deposit(deposits :: [deposit()], state :: t()) :: {:ok, [db_update], new_state :: t()} def deposit(deposits, %Core{utxos: utxos} = state) do new_utxos_map = Enum.into(deposits, %{}, &deposit_to_utxo/1) new_utxos = UtxoSet.apply_effects(utxos, [], new_utxos_map) db_updates = UtxoSet.db_updates([], new_utxos_map) _ = if deposits != [], do: Logger.info("Recognized deposits #{inspect(deposits)}") new_state = %Core{state | utxos: new_utxos} {:ok, db_updates, new_state} end @doc """ Retrieves exitable utxo positions from variety of exit events. Accepts either - a list of utxo positions (decoded) - a list of utxo positions (encoded) - a list of full exit infos containing the utxo positions - a list of full exit events (from ethereum listeners) containing the utxo positions - a list of IFE started events - a list of IFE input/output piggybacked events NOTE: It is done like this to accommodate different clients of this function as they can either be bare `EthereumEventListener` or `ExitProcessor`. Hence different forms it can get the exiting utxos delivered """ @spec extract_exiting_utxo_positions(exiting_utxo_triggers_t(), t()) :: list(Utxo.Position.t()) def extract_exiting_utxo_positions(exit_infos, state) def extract_exiting_utxo_positions([], %Core{}), do: [] # list of full exit infos (from events) containing the utxo positions def extract_exiting_utxo_positions([%{utxo_pos: _} | _] = utxo_position_events, state), do: utxo_position_events |> Enum.map(& &1.utxo_pos) |> extract_exiting_utxo_positions(state) # list of full exit events (from ethereum listeners) def extract_exiting_utxo_positions([%{call_data: %{utxo_pos: _}} | _] = utxo_position_events, state), do: utxo_position_events |> Enum.map(& &1.call_data) |> extract_exiting_utxo_positions(state) # list of utxo positions (encoded) def extract_exiting_utxo_positions([encoded_utxo_pos | _] = encoded_utxo_positions, %Core{}) when is_integer(encoded_utxo_pos), do: Enum.map(encoded_utxo_positions, &Utxo.Position.decode!/1) # list of IFE input/output piggybacked events def extract_exiting_utxo_positions([%{call_data: %{in_flight_tx: _}} | _] = start_ife_events, %Core{}) do _ = Logger.info("Recognized exits from IFE starts #{inspect(start_ife_events)}") Enum.flat_map(start_ife_events, fn %{call_data: %{in_flight_tx: tx_bytes}} -> {:ok, tx} = Transaction.decode(tx_bytes) Transaction.get_inputs(tx) end) end # list of IFE input piggybacked events (they're ignored) def extract_exiting_utxo_positions( [%{tx_hash: _, omg_data: %{piggyback_type: :input}} | _] = piggyback_events, %Core{} ) do _ = Logger.info("Ignoring input piggybacks #{inspect(piggyback_events)}") [] end # list of IFE output piggybacked events. This is used by the child chain only. `OMG.Watcher.ExitProcessor` figures out # the utxo positions to exit on its own def extract_exiting_utxo_positions( [%{tx_hash: _, omg_data: %{piggyback_type: :output}} | _] = piggyback_events, %Core{} = state ) do _ = Logger.info("Recognized exits from piggybacks #{inspect(piggyback_events)}") piggyback_events |> Enum.map(&find_utxo_matching_piggyback(&1, state)) |> Enum.filter(fn utxo -> utxo != nil end) |> Enum.map(fn {position, _} -> position end) end # list of utxo positions (decoded) def extract_exiting_utxo_positions([Utxo.position(_, _, _) | _] = utxo_positions, %Core{}), do: utxo_positions @doc """ Spends exited utxos. Note: state passed here is already extended with DB. """ @spec exit_utxos(exiting_utxos :: list(Utxo.Position.t()), state :: t()) :: {:ok, {[db_update], validities_t()}, new_state :: t()} def exit_utxos([], %Core{} = state), do: {:ok, {[], {[], []}}, state} def exit_utxos( [Utxo.position(_, _, _) | _] = exiting_utxos, %Core{utxos: utxos, recently_spent: recently_spent} = state ) do _ = Logger.info("Recognized exits #{inspect(exiting_utxos)}") {valid, _invalid} = validities = Enum.split_with(exiting_utxos, &utxo_exists?(&1, state)) new_utxos = UtxoSet.apply_effects(utxos, valid, %{}) new_spends = MapSet.union(recently_spent, MapSet.new(valid)) db_updates = UtxoSet.db_updates(valid, %{}) new_state = %{state | utxos: new_utxos, recently_spent: new_spends} {:ok, {db_updates, validities}, new_state} end @doc """ Checks whether utxo exists in UTXO set. Note: state passed here is already extended with DB. """ @spec utxo_exists?(Utxo.Position.t(), t()) :: boolean() def utxo_exists?(Utxo.position(_blknum, _txindex, _oindex) = utxo_pos, %Core{utxos: utxos}) do UtxoSet.exists?(utxos, utxo_pos) end @doc """ Gets the current block's height and whether at the beginning of the block """ @spec get_status(t()) :: {current_block_height :: non_neg_integer(), is_block_beginning :: boolean()} def get_status(%__MODULE__{height: height, tx_index: tx_index, pending_txs: pending}) do is_beginning = tx_index == 0 && Enum.empty?(pending) {height, is_beginning} end defp add_pending_tx(%Core{pending_txs: pending_txs, tx_index: tx_index} = state, %Transaction.Recovered{} = new_tx) do _ = :telemetry.execute([:pending_transactions, __MODULE__], %{new_tx: new_tx}, %{}) %Core{ state | tx_index: tx_index + 1, pending_txs: [new_tx | pending_txs] } end defp apply_tx( %Core{ height: blknum, tx_index: tx_index, utxos: utxos, recently_spent: recently_spent, utxo_db_updates: db_updates } = state, %Transaction.Recovered{signed_tx: %{raw_tx: tx}} ) do {spent_input_pointers, new_utxos_map} = get_effects(tx, blknum, tx_index) new_utxos = UtxoSet.apply_effects(utxos, spent_input_pointers, new_utxos_map) new_db_updates = UtxoSet.db_updates(spent_input_pointers, new_utxos_map) # NOTE: child chain mode don't need 'spend' data for now. Consider to add only in Watcher's modes - OMG-382 spent_blknum_updates = Enum.map(spent_input_pointers, &{:put, :spend, {Utxo.Position.to_input_db_key(&1), blknum}}) %Core{ state | utxos: new_utxos, recently_spent: MapSet.union(recently_spent, MapSet.new(spent_input_pointers)), utxo_db_updates: new_db_updates ++ spent_blknum_updates ++ db_updates } end # Post-processing step of transaction execution. It either claim for Transaction.Fee and collect for the rest. @spec handle_fees(state :: t(), Transaction.Recovered.t(), map()) :: t() defp handle_fees(state, %Transaction.Recovered{signed_tx: %{raw_tx: %Transaction.Fee{}}} = tx, _fees_paid) do [output] = Transaction.get_outputs(tx) state |> flush_collected_fees_for_token(output) |> disallow_payments() end defp handle_fees(state, _tx, fees_paid) do collect_fees(state, fees_paid) end # attempts to build a standard response data about a single UTXO, based on an abstract `output` structure # so that the data can be useful to discover exitable UTXOs defp utxo_to_exitable_utxo_map(%Utxo{output: %{output_type: otype} = output}, Utxo.position(blknum, txindex, oindex)) do output |> Map.from_struct() |> Map.take([:owner, :currency, :amount]) |> Map.put(:otype, otype) |> Map.put(:blknum, blknum) |> Map.put(:txindex, txindex) |> Map.put(:oindex, oindex) end defp collect_fees(%Core{fees_paid: fees_paid} = state, token_surpluses) do fees_paid_with_new = token_surpluses |> Enum.reject(fn {_token, amount} -> amount == 0 end) |> Map.new() |> Map.merge(fees_paid, fn _token, collected, tx_surplus -> collected + tx_surplus end) %Core{state | fees_paid: fees_paid_with_new} end defp disallow_payments(state), do: %Core{state | fee_claiming_started: true} defp flush_collected_fees_for_token(state, %Output{currency: token}) do %Core{state | fees_paid: Map.delete(state.fees_paid, token)} end # Effects of a payment transaction - spends all inputs and creates all outputs # Relies on the polymorphic `get_inputs` and `get_outputs` of `Transaction` defp get_effects(tx, blknum, tx_index) do {Transaction.get_inputs(tx), utxos_from(tx, blknum, tx_index)} end defp utxos_from(tx, blknum, tx_index) do hash = Transaction.raw_txhash(tx) tx |> Transaction.get_outputs() |> Enum.with_index() |> Enum.map(fn {output, oindex} -> {Utxo.position(blknum, tx_index, oindex), output} end) |> Enum.into(%{}, fn {input_pointer, output} -> {input_pointer, %Utxo{output: output, creating_txhash: hash}} end) end defp deposit_to_utxo(deposit) do %{blknum: blknum, currency: cur, owner: owner, amount: amount} = deposit Transaction.Payment.new([], [{owner, cur, amount}]) |> utxos_from(blknum, 0) |> Enum.map(& &1) |> hd() end # We're looking for a UTXO that a piggyback of an in-flight IFE is referencing. # This is useful when trying to do something with the outputs that are piggybacked (like exit them), without their # position. # Only relevant for output piggybacks defp find_utxo_matching_piggyback(piggyback_events, state) do %{omg_data: %{piggyback_type: :output}, tx_hash: tx_hash, output_index: oindex} = piggyback_events UtxoSet.find_matching_utxo(state.utxos, tx_hash, oindex) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/measure.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Measure do @moduledoc """ Counting business metrics sent to Datadog """ import OMG.Status.Metric.Event, only: [name: 1] alias OMG.Status.Metric.Datadog alias OMG.Watcher.State alias OMG.Watcher.State.Core alias OMG.Watcher.State.MeasurementCalculation @supported_events [ [:process, State], [:pending_transactions, Core], [:block_transactions, Core] ] def supported_events(), do: @supported_events def handle_event([:process, State], _, %Core{} = state, _config) do execute = fn -> try do Enum.each(MeasurementCalculation.calculate(state), fn {key, value} -> _ = Datadog.gauge(name(key), value) {key, value, metadata} -> _ = Datadog.gauge(name(key), value, tags: [metadata]) end) rescue _e in ArgumentError -> # This exception occurs when we run without datadog (statix). # In normal scenarios, telemetry would get detached but because this is a spawned proces... :ok end end # TODO proper fix! this is a very hackish approach to get measurements off the back # of OMG State _ = Task.start(execute) :ok end def handle_event([:pending_transactions, Core], %{new_tx: _new_tx}, _, _config) do _ = Datadog.increment(name(:pending_transactions), 1) end def handle_event([:block_transactions, Core], %{txs: txs}, _, _config) do _ = Datadog.gauge(name(:block_transactions), Enum.count(txs)) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/measurement_calculation.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.MeasurementCalculation do @moduledoc """ Calculations based on OMG State that are sent to monitoring service. """ alias OMG.Eth.Encoding alias OMG.Watcher.State.Core # TODO: functions here reach uncleanly into the UtxoSet (not going through `OMG.Watcher.State.UtxoSet`) - is this bad? def calculate(%Core{utxos: utxos}) do balance = Enum.map( balance(utxos), fn {currency, amount} -> {:balance, amount, "currency:#{Encoding.to_hex(currency)}"} end ) unique_users = {:unique_users, unique_users(utxos)} List.flatten([unique_users, balance]) end defp unique_users(utxos) do utxos |> Enum.map(fn {_, %OMG.Watcher.Utxo{output: output}} -> output end) # NOTE: we're counting only outputs that define an owner, so that this remains an owner-counting metric. # For anything, where the owner isn't well defined, careful rethinking would be required |> Enum.map(&Map.get(&1, :owner)) |> Enum.filter(& &1) |> Enum.uniq() |> Enum.count() end defp balance(utxos) do utxos |> Enum.map(fn {_, %OMG.Watcher.Utxo{output: output}} -> output end) # NOTE: we're counting only outputs that define a currency and amount, so that this remains a balance-counting # metric. For anything, where the balance isn't well defined, careful rethinking would be required |> Enum.map(&{Map.get(&1, :currency), Map.get(&1, :amount, 0)}) |> Enum.filter(fn {currency, _} -> currency end) |> Enum.reduce(%{}, fn {currency, amount}, acc -> Map.update(acc, currency, amount, &(&1 + amount)) end) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/fee.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Fee do @moduledoc """ Internal representation of a fee claiming transaction in plasma chain. """ alias OMG.Output alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction require Transaction @fee_token_claim_tx_type OMG.Watcher.WireFormatTypes.tx_type_for(:tx_fee_token_claim) @fee_token_claim_output_type OMG.Watcher.WireFormatTypes.output_type_for(:output_fee_token_claim) defstruct [:tx_type, :outputs, :nonce] @type t() :: %__MODULE__{ tx_type: non_neg_integer(), outputs: [Output.t()], nonce: Crypto.hash_t() } @doc """ Creates new fee claiming transaction """ @spec new( blknum :: non_neg_integer(), {Crypto.address_t(), Transaction.Payment.currency(), pos_integer} ) :: t() def new(blknum, {owner, currency, amount}) do %__MODULE__{ tx_type: @fee_token_claim_tx_type, outputs: [new_output(owner, currency, amount)], nonce: to_nonce(blknum, currency) } end @doc """ Creates output for fee transaction """ @spec new_output(owner :: Crypto.address_t(), currency :: Transaction.Payment.currency(), amount :: pos_integer()) :: Output.t() def new_output(owner, currency, amount) do %Output{ owner: owner, currency: currency, amount: amount, output_type: @fee_token_claim_output_type } end @doc """ Transforms the structure of RLP items after a successful RLP decode of a raw transaction, into a structure instance """ def reconstruct([tx_type, outputs_rlp, nonce_rlp]) do with {:ok, outputs} <- reconstruct_outputs(outputs_rlp), {:ok, nonce} <- reconstruct_nonce(nonce_rlp), do: {:ok, %__MODULE__{tx_type: tx_type, outputs: outputs, nonce: nonce}} end def reconstruct(_), do: {:error, :malformed_transaction} defp reconstruct_outputs(outputs_rlp) do outputs = Enum.map(outputs_rlp, &Output.reconstruct/1) with nil <- Enum.find(outputs, &match?({:error, _}, &1)), true <- only_allowed_output_types?(outputs) || {:error, :tx_cannot_create_output_type}, do: {:ok, outputs} rescue _ -> {:error, :malformed_outputs} end defp reconstruct_nonce(nonce) when is_binary(nonce) and byte_size(nonce) == 32, do: {:ok, nonce} defp reconstruct_nonce(_), do: {:error, :malformed_nonce} defp only_allowed_output_types?([%Output{}]), do: true defp only_allowed_output_types?(_), do: false @spec to_nonce(non_neg_integer(), Transaction.Payment.currency()) :: Crypto.hash_t() defp to_nonce(blknum, token) do blknum_bytes = ABI.TypeEncoder.encode_raw([blknum], [{:uint, 256}]) token_bytes = ABI.TypeEncoder.encode_raw([token], [:address]) Crypto.hash(blknum_bytes <> token_bytes) end end defimpl OMG.Watcher.State.Transaction.Protocol, for: OMG.Watcher.State.Transaction.Fee do alias OMG.Output alias OMG.Watcher.State.Transaction @doc """ Turns a structure instance into a structure of RLP items, ready to be RLP encoded, for a raw transaction """ @spec get_data_for_rlp(Transaction.Fee.t()) :: list(any()) def get_data_for_rlp(%Transaction.Fee{tx_type: tx_type, outputs: outputs, nonce: nonce}) do [ tx_type, Enum.map(outputs, &Output.get_data_for_rlp/1), nonce ] end @doc """ Fee claiming transaction spends single pseudo-output from collected fees. """ @spec get_outputs(Transaction.Fee.t()) :: list(Output.t()) def get_outputs(%Transaction.Fee{outputs: outputs}), do: outputs @doc """ Fee claiming transaction does not contain any inputs. """ @spec get_inputs(Transaction.Fee.t()) :: list(OMG.Watcher.Utxo.Position.t()) def get_inputs(%Transaction.Fee{}), do: [] @doc """ Tells whether Fee claiming transaction is valid """ @spec valid?(Transaction.Fee.t(), Transaction.Signed.t()) :: {:error, :wrong_number_of_fee_outputs | :fee_output_amount_has_to_be_positive} def valid?(%Transaction.Fee{} = fee_tx, _signed_tx) do # we're able to check structure validity => single output with amount > 0 outputs = Transaction.get_outputs(fee_tx) with true <- length(outputs) == 1 || {:error, :wrong_number_of_fee_outputs}, [output] = outputs, true <- output.amount > 0 || {:error, :fee_output_amount_has_to_be_positive}, do: true end @doc """ Fee claiming transaction is not used to transfer funds """ @spec can_apply?(Transaction.Fee.t(), list(Output.t())) :: {:ok, map()} | {:error, :surplus_in_token_not_collected | :claimed_and_collected_amounts_mismatch} def can_apply?(%Transaction.Fee{outputs: [claimed]}, outputs) do with %Output{} = collected <- find_output_by_currency(outputs, claimed.currency), true <- amounts_equal?(collected.amount, claimed.amount), do: {:ok, %{}} end defp find_output_by_currency(outputs, currency), do: Enum.find(outputs, {:error, :surplus_in_token_not_collected}, fn o -> o.currency == currency end) defp amounts_equal?(collected, claimed) when collected == claimed, do: true defp amounts_equal?(_, _), do: {:error, :claimed_and_collected_amounts_mismatch} end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/payment.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Payment do @moduledoc """ Internal representation of a raw payment transaction done on Plasma chain. This module holds the representation of a "raw" transaction, i.e. without signatures nor recovered input spenders """ alias OMG.Watcher.Crypto alias OMG.Output alias OMG.Watcher.RawData alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Transaction require Utxo @zero_metadata <<0::256>> @payment_tx_type OMG.Watcher.WireFormatTypes.tx_type_for(:tx_payment_v1) @payment_output_type OMG.Watcher.WireFormatTypes.output_type_for(:output_payment_v1) defstruct [:tx_type, :inputs, :outputs, metadata: @zero_metadata] @type t() :: %__MODULE__{ tx_type: non_neg_integer(), inputs: list(OMG.Watcher.Utxo.Position.t()), outputs: list(Output.t()), metadata: Transaction.metadata() } @type currency() :: Crypto.address_t() @max_inputs 4 @max_outputs 4 defmacro max_inputs() do quote do unquote(@max_inputs) end end defmacro max_outputs() do quote do unquote(@max_outputs) end end @doc """ Creates a new raw transaction structure from a list of inputs and a list of outputs, given in a succinct tuple form. assumptions: ``` length(inputs) <= @max_inputs length(outputs) <= @max_outputs ``` """ @spec new( list({pos_integer, pos_integer, 0..unquote(@max_outputs - 1)}), list({Crypto.address_t(), currency(), pos_integer}), Transaction.metadata() ) :: t() def new(inputs, outputs, metadata \\ @zero_metadata) def new(inputs, outputs, metadata) when Transaction.is_metadata(metadata) and length(inputs) <= @max_inputs and length(outputs) <= @max_outputs do inputs = Enum.map(inputs, &new_input/1) outputs = Enum.map(outputs, &new_output/1) %__MODULE__{tx_type: @payment_tx_type, inputs: inputs, outputs: outputs, metadata: metadata} end @doc """ Transforms the structure of RLP items after a successful RLP decode of a raw transaction, into a structure instance """ def reconstruct([tx_type, inputs_rlp, outputs_rlp, tx_data_rlp, metadata_rlp]) do with {:ok, inputs} <- reconstruct_inputs(inputs_rlp), {:ok, outputs} <- reconstruct_outputs(outputs_rlp), {:ok, tx_data} <- RawData.parse_uint256(tx_data_rlp), :ok <- check_tx_data(tx_data), {:ok, metadata} <- reconstruct_metadata(metadata_rlp), do: {:ok, %__MODULE__{tx_type: tx_type, inputs: inputs, outputs: outputs, metadata: metadata}} end def reconstruct(_), do: {:error, :malformed_transaction} # `new_input/1` and `new_output/1` are here to just help interpret the short-hand form of inputs outputs when doing # `new/3` defp new_input({blknum, txindex, oindex}), do: Utxo.position(blknum, txindex, oindex) defp new_output({owner, currency, amount}) do %Output{ owner: owner, currency: currency, amount: amount, output_type: @payment_output_type } end defp reconstruct_inputs(inputs_rlp) do with {:ok, inputs} <- parse_inputs(inputs_rlp), do: {:ok, inputs} end defp reconstruct_outputs([]), do: {:error, :empty_outputs} defp reconstruct_outputs(outputs_rlp) do with {:ok, outputs} <- parse_outputs(outputs_rlp), do: {:ok, outputs} end # txData is required to be zero in the contract defp check_tx_data(0), do: :ok defp check_tx_data(_), do: {:error, :malformed_tx_data} defp reconstruct_metadata(metadata) when Transaction.is_metadata(metadata), do: {:ok, metadata} defp reconstruct_metadata(_), do: {:error, :malformed_metadata} defp parse_inputs(inputs_rlp) do with true <- Enum.count(inputs_rlp) <= @max_inputs || {:error, :too_many_inputs}, # NOTE: workaround for https://github.com/omgnetwork/ex_plasma/issues/19. # remove, when this is blocked on `ex_plasma` end true <- Enum.all?(inputs_rlp, &(&1 != <<0::256>>)) || {:error, :malformed_inputs}, do: {:ok, Enum.map(inputs_rlp, &parse_input!/1)} rescue _ -> {:error, :malformed_inputs} end defp parse_outputs(outputs_rlp) do outputs = Enum.map(outputs_rlp, &Output.reconstruct/1) with true <- Enum.count(outputs) <= @max_outputs || {:error, :too_many_outputs}, nil <- Enum.find(outputs, &match?({:error, _}, &1)), true <- only_allowed_output_types?(outputs) || {:error, :tx_cannot_create_output_type}, do: {:ok, outputs} rescue _ -> {:error, :malformed_outputs} end defp only_allowed_output_types?(outputs), do: Enum.all?(outputs, &match?(%Output{}, &1)) defp parse_input!(encoded), do: OMG.Watcher.Utxo.Position.decode!(encoded) end defimpl OMG.Watcher.State.Transaction.Protocol, for: OMG.Watcher.State.Transaction.Payment do alias OMG.Output alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Transaction require Utxo @empty_signature <<0::size(520)>> @doc """ Turns a structure instance into a structure of RLP items, ready to be RLP encoded, for a raw transaction """ @spec get_data_for_rlp(Transaction.Payment.t()) :: list(any()) def get_data_for_rlp(%Transaction.Payment{tx_type: tx_type, inputs: inputs, outputs: outputs, metadata: metadata}) when Transaction.is_metadata(metadata), do: [ tx_type, Enum.map(inputs, &OMG.Watcher.Utxo.Position.get_data_for_rlp/1), Enum.map(outputs, &Output.get_data_for_rlp/1), # used to be optional and as such was `if`-appended if not null here # When it is not optional, and there's the if, dialyzer complains about the if 0, metadata ] @spec get_outputs(Transaction.Payment.t()) :: list(Output.t()) def get_outputs(%Transaction.Payment{outputs: outputs}), do: outputs @spec get_inputs(Transaction.Payment.t()) :: list(OMG.Watcher.Utxo.Position.t()) def get_inputs(%Transaction.Payment{inputs: inputs}), do: inputs @doc """ True if the witnessses provided follow some extra custom validation. Currently this covers the requirement for all the inputs to be signed on predetermined positions """ @spec valid?(Transaction.Payment.t(), Transaction.Signed.t()) :: true | {:error, atom} def valid?(%Transaction.Payment{}, %Transaction.Signed{sigs: sigs} = tx) do tx |> Transaction.get_inputs() |> all_inputs_signed?(sigs) end @doc """ True if a payment can be applied, given a set of input UTXOs is present in the ledger. Involves the checking of balancing of inputs and outputs for currencies Returns the fees that this transaction is paying, mapped by currency """ @spec can_apply?(Transaction.Payment.t(), list(Output.t())) :: {:ok, map()} | {:error, :amounts_do_not_add_up} def can_apply?(%Transaction.Payment{} = tx, outputs_spent) do outputs = Transaction.get_outputs(tx) input_amounts_by_currency = get_amounts_by_currency(outputs_spent) output_amounts_by_currency = get_amounts_by_currency(outputs) with :ok <- amounts_add_up?(input_amounts_by_currency, output_amounts_by_currency), do: {:ok, fees_paid(input_amounts_by_currency, output_amounts_by_currency)} end defp all_inputs_signed?(non_zero_inputs, sigs) do count_non_zero_signatures = Enum.count(sigs, &(&1 != @empty_signature)) count_non_zero_inputs = length(non_zero_inputs) cond do count_non_zero_signatures > count_non_zero_inputs -> {:error, :superfluous_signature} count_non_zero_signatures < count_non_zero_inputs -> {:error, :missing_signature} true -> true end end defp fees_paid(input_amounts_by_currency, output_amounts_by_currency) do Enum.into( input_amounts_by_currency, %{}, fn {input_currency, input_amount} -> # fee is implicit - it's the difference between funds owned and spend implicit_paid_fee = input_amount - Map.get(output_amounts_by_currency, input_currency, 0) {input_currency, implicit_paid_fee} end ) end defp get_amounts_by_currency(outputs) do outputs |> Enum.group_by(fn %{currency: currency} -> currency end, fn %{amount: amount} -> amount end) |> Enum.map(fn {currency, amounts} -> {currency, Enum.sum(amounts)} end) |> Map.new() end defp amounts_add_up?(input_amounts, output_amounts) do for {output_currency, output_amount} <- Map.to_list(output_amounts) do input_amount = Map.get(input_amounts, output_currency, 0) input_amount >= output_amount end |> Enum.all?() |> if(do: :ok, else: {:error, :amounts_do_not_add_up}) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/recovered.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Recovered do @moduledoc """ Representation of a signed transaction, with addresses recovered from signatures (from `OMG.Watcher.State.Transaction.Signed`) Intent is to allow concurrent processing of signatures outside of serial processing in `OMG.Watcher.State`. `Transaction.Recovered` represents a transaction that can be sent to `OMG.Watcher.State.exec/1` """ alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @type tx_bytes() :: binary() @type recover_tx_error() :: :duplicate_inputs | :malformed_transaction | :malformed_transaction_rlp | :signature_corrupt | :missing_signature defstruct [:signed_tx, :tx_hash, :signed_tx_bytes, :witnesses] @type t() :: %__MODULE__{ tx_hash: Transaction.tx_hash(), witnesses: %{non_neg_integer => Transaction.Witness.t()}, signed_tx: Transaction.Signed.t(), signed_tx_bytes: tx_bytes() } @doc """ Transforms a RLP-encoded child chain transaction (binary) into a: - decoded - statelessly valid (mainly inputs logic) - recovered (i.e. signatures get recovered into spenders) transaction See docs/transaction_validation.md for more information about stateful and stateless validation. """ @spec recover_from(binary) :: {:ok, Transaction.Recovered.t()} | {:error, recover_tx_error()} def recover_from(encoded_signed_tx) do with {:ok, signed_tx} <- Transaction.Signed.decode(encoded_signed_tx), true <- valid?(signed_tx), do: recover_from_struct(signed_tx, encoded_signed_tx) end @doc """ Throwing version of `recover_from/1` """ @spec recover_from!(binary) :: Transaction.Recovered.t() def recover_from!(encoded_signed_tx) do {:ok, recovered} = Transaction.Recovered.recover_from(encoded_signed_tx) recovered end @spec recover_from_struct(Transaction.Signed.t(), tx_bytes()) :: {:ok, t()} | {:error, recover_tx_error()} defp recover_from_struct(%Transaction.Signed{} = signed_tx, signed_tx_bytes) do with {:ok, witnesses} <- Transaction.Signed.get_witnesses(signed_tx), do: {:ok, %__MODULE__{ tx_hash: Transaction.raw_txhash(signed_tx), witnesses: witnesses, signed_tx: signed_tx, signed_tx_bytes: signed_tx_bytes }} end defp valid?(%Transaction.Signed{raw_tx: raw_tx} = tx) do with true <- generic_valid?(tx), true <- Transaction.Protocol.valid?(raw_tx, tx), do: true end defp generic_valid?(%Transaction.Signed{raw_tx: raw_tx}) do inputs = Transaction.get_inputs(raw_tx) with true <- no_duplicate_inputs?(inputs) || {:error, :duplicate_inputs}, do: true end defp no_duplicate_inputs?(inputs) do number_of_unique_inputs = inputs |> Enum.uniq() |> Enum.count() inputs_length = Enum.count(inputs) inputs_length == number_of_unique_inputs end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/signed.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Signed do @moduledoc """ Representation of a signed transaction. NOTE: before you use this, make sure you shouldn't use `Transaction` or `Transaction.Recovered` """ alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.State.Transaction.Witness alias OMG.Watcher.TypedDataHash @type tx_bytes() :: binary() defstruct [:raw_tx, :sigs] @type t() :: %__MODULE__{ raw_tx: Transaction.Protocol.t(), sigs: [Crypto.sig_t()] } @doc """ Produce a binary form of a signed transaction - coerces into RLP-encodeable structure and RLP encodes """ @spec encode(t()) :: tx_bytes() def encode(%__MODULE__{raw_tx: %{} = raw_tx, sigs: sigs}) do ExRLP.encode([sigs | Transaction.Protocol.get_data_for_rlp(raw_tx)]) end @doc """ Produces a struct from the binary encoded form of a signed transactions - RLP decodes to structure of RLP-items and then produces an Elixir struct """ @spec decode(tx_bytes()) :: {:ok, t()} | {:error, atom} def decode(signed_tx_bytes) do with {:ok, tx_rlp_decoded_chunks} <- generic_decode(signed_tx_bytes), do: reconstruct(tx_rlp_decoded_chunks) end @doc """ See `decode/1` """ @spec decode!(tx_bytes()) :: t() def decode!(signed_tx_bytes) do {:ok, decoded} = decode(signed_tx_bytes) decoded end @doc """ Recovers the witnesses for non-empty signatures, in the order they appear in transaction's signatures """ @spec get_witnesses(Transaction.Signed.t()) :: {:ok, %{(index :: non_neg_integer) => Crypto.address_t()}} | {:error, atom} def get_witnesses(%Transaction.Signed{sigs: []}), do: {:ok, %{}} def get_witnesses(%Transaction.Signed{raw_tx: raw_tx, sigs: raw_witnesses}) do raw_txhash = TypedDataHash.hash_struct(raw_tx) with {:ok, reversed_witnesses} <- get_reversed_witnesses(raw_txhash, raw_tx, raw_witnesses), do: {:ok, reversed_witnesses |> Enum.reverse() |> Enum.with_index() |> Enum.into(%{}, fn {witness, idx} -> {idx, witness} end)} end defp get_reversed_witnesses(raw_txhash, raw_tx, raw_witnesses) do Enum.reduce_while(raw_witnesses, {:ok, []}, fn raw_witness, acc -> get_witness(raw_txhash, raw_tx, raw_witness, acc) end) end defp get_witness(raw_txhash, raw_tx, raw_witness, {:ok, witnesses}) do raw_witness |> Witness.recover(raw_txhash, raw_tx) |> case do {:ok, witness} -> {:cont, {:ok, [witness | witnesses]}} error -> {:halt, error} end end defp generic_decode(signed_tx_bytes) do {:ok, ExRLP.decode(signed_tx_bytes)} rescue _ -> {:error, :malformed_transaction_rlp} end def reconstruct([raw_witnesses | typed_tx_rlp_decoded_chunks]) do with true <- is_list(raw_witnesses) || {:error, :malformed_witnesses}, true <- Enum.all?(raw_witnesses, &Witness.valid?/1) || {:error, :malformed_witnesses}, {:ok, raw_tx} <- Transaction.dispatching_reconstruct(typed_tx_rlp_decoded_chunks), do: {:ok, %Transaction.Signed{raw_tx: raw_tx, sigs: raw_witnesses}} end def reconstruct(_), do: {:error, :malformed_transaction} end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/validator/fee_claim.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Validator.FeeClaim do @moduledoc """ Contains generic validation rules for `Transaction.Fee` transactions. Specific transaction type's validation is passed to `Transaction.Protocol.can_apply?` """ alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction @type fee_claim_error :: :surplus_in_token_not_collected | :claimed_and_collected_amounts_mismatch @spec can_claim_fees(Core.t(), Transaction.Recovered.t()) :: {:ok, %{}} | {{:error, fee_claim_error()}, Core.t()} def can_claim_fees( %Core{fee_claimer_address: owner, fees_paid: fees_paid} = state, %Transaction.Recovered{signed_tx: %{raw_tx: fee_tx}} ) do # NOTE: Fee claiming transaction does not transfer funds. It spends pseudo-output resultant of fees collection outputs = make_outputs(owner, fees_paid) case Transaction.Protocol.can_apply?(fee_tx, outputs) do {:ok, _} -> {:ok, %{}} {:error, _reason} = error -> {error, state} end end defp make_outputs(owner, fees_paid) do Enum.map(fees_paid, fn {currency, amount} -> Transaction.Fee.new_output(owner, currency, amount) end) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/validator/payment.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Validator.Payment do @moduledoc """ Provides functions for stateful transaction validation for transaction processing in OMG.Watcher.State.Core. Specific transaction type's validation is passed to `Transaction.Protocol.can_apply?` """ alias OMG.Output alias OMG.Watcher.Fees alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.State.UtxoSet alias OMG.Watcher.Utxo require Utxo @type can_apply_error :: :amounts_do_not_add_up | :fees_not_covered | :input_utxo_ahead_of_state | :unauthorized_spend | :utxo_not_found | :overpaying_fees | :multiple_potential_currency_fees @spec can_apply_tx(state :: Core.t(), tx :: Transaction.Recovered.t(), fees :: Fees.optional_fee_t()) :: {:ok, map()} | {{:error, can_apply_error()}, Core.t()} def can_apply_tx(state, tx, fees) do %Transaction.Recovered{signed_tx: %{raw_tx: raw_tx}, witnesses: witnesses} = tx inputs = Transaction.get_inputs(tx) with true <- not state.fee_claiming_started || {:error, :payments_rejected_during_fee_claiming}, :ok <- inputs_not_from_future_block?(state, inputs), {:ok, outputs_spent} <- UtxoSet.get_by_inputs(state.utxos, inputs), :ok <- authorized?(outputs_spent, witnesses), {:ok, implicit_paid_fee_by_currency} <- Transaction.Protocol.can_apply?(raw_tx, outputs_spent), :ok <- Fees.check_if_covered(implicit_paid_fee_by_currency, fees) do {:ok, implicit_paid_fee_by_currency} else {:error, _reason} = error -> {error, state} end end defp inputs_not_from_future_block?(%Core{height: blknum}, inputs) do no_utxo_from_future_block = Enum.all?(inputs, fn Utxo.position(input_blknum, _, _) -> blknum >= input_blknum end) if no_utxo_from_future_block, do: :ok, else: {:error, :input_utxo_ahead_of_state} end # Checks the outputs spent by this transaction have been authorized by correct witnesses @spec authorized?(list(Output.t()), list(Transaction.Witness.t())) :: :ok | {:error, :unauthorized_spend} defp authorized?(outputs_spent, witnesses) do outputs_spent |> Enum.with_index() |> Enum.map(fn {output_spent, index} -> can_spend?(output_spent, witnesses[index]) end) |> Enum.all?() |> if(do: :ok, else: {:error, :unauthorized_spend}) end defp can_spend?(%Output{owner: owner}, witness), do: owner == witness end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/validator.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Validator do @moduledoc """ Dispatches validation flow to payments application or fee claiming modules """ require OMG.Watcher.State.Transaction.Payment @maximum_block_size 65_536 # NOTE: Last processed transaction could potentially take his room but also generate `max_inputs` fee transactions @safety_margin 1 + OMG.Watcher.State.Transaction.Payment.max_inputs() @available_block_size @maximum_block_size - @safety_margin alias OMG.Watcher.Fees alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.State.Transaction.Validator @type can_process_tx_error :: :too_many_transactions_in_block | Validator.Payment.can_apply_error() | Validator.FeeClaim.fee_claim_error() @doc """ Checks, whether at a given state of the ledger, a particular transaction can be applied (!) to it, subject to particular fee requirements """ @spec can_process_tx(state :: Core.t(), tx :: Transaction.Recovered.t(), fees :: Fees.optional_fee_t()) :: {:ok, map()} | {:ok, map()} | {{:error, can_process_tx_error()}, Core.t()} def can_process_tx(%Core{} = state, %Transaction.Recovered{} = tx, fees) do with :ok <- validate_block_size(state), do: dispatch_validation(state, tx, fees) end defp validate_block_size(%Core{tx_index: number_of_transactions_in_block, fees_paid: fees_paid} = state) do fee_transactions_count = Enum.count(fees_paid) case number_of_transactions_in_block + fee_transactions_count > @available_block_size do true -> {{:error, :too_many_transactions_in_block}, state} false -> :ok end end defp dispatch_validation(state, %Transaction.Recovered{signed_tx: %{raw_tx: %Transaction.Payment{}}} = tx, fees) do Validator.Payment.can_apply_tx(state, tx, fees) end defp dispatch_validation( state, %Transaction.Recovered{signed_tx: %{raw_tx: %Transaction.Fee{}}} = tx, _fees ) do Validator.FeeClaim.can_claim_fees(state, tx) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction/witness.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.Witness do @moduledoc """ Code required to validate and recover raw witnesses (e.g. signatures) goes here. These should be called by the stateless validation, in order to put load off stateful validation (i.e. sig recovery) """ alias OMG.Watcher.Crypto @signature_length 65 @type t :: Crypto.address_t() @doc """ Pre-check done after decoding to quickly assert whether the witness has one of valid forms """ def valid?(witness) when is_binary(witness), do: signature_length?(witness) def valid?(_), do: false @doc """ Prepares the witness to be quickly used in stateful validation """ def recover(raw_witness, raw_txhash, _raw_tx) when is_binary(raw_witness), do: Crypto.recover_address(raw_txhash, raw_witness) defp signature_length?(sig) when byte_size(sig) == @signature_length, do: true defp signature_length?(_sig), do: false end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction do @moduledoc """ This module contains the public Transaction API to be prefered to access data of different transaction "flavors", like `Transaction.Signed` or `Transaction.Recovered` """ alias OMG.Watcher.Crypto alias OMG.Watcher.RawData alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @tx_types_modules OMG.Watcher.WireFormatTypes.tx_type_modules() @tx_types Map.keys(@tx_types_modules) @type any_flavor_t() :: __MODULE__.Signed.t() | __MODULE__.Recovered.t() | __MODULE__.Protocol.t() @type tx_bytes() :: binary() @type tx_hash() :: Crypto.hash_t() @type metadata() :: binary() | nil @type decode_error() :: :malformed_transaction_rlp | :malformed_inputs | :malformed_outputs | :malformed_address | :malformed_metadata | :unrecognized_transaction_type | :malformed_transaction defmacro is_metadata(metadata) do quote do is_binary(unquote(metadata)) and byte_size(unquote(metadata)) == 32 end end @type input_index_t() :: 0..3 def dispatching_reconstruct([raw_type | raw_tx_rlp_decoded_chunks]) when is_binary(raw_type) do case RawData.parse_uint256(raw_type) do {:ok, tx_type} when tx_type in @tx_types -> protocol_module = @tx_types_modules[tx_type] protocol_module.reconstruct([tx_type | raw_tx_rlp_decoded_chunks]) _ -> {:error, :unrecognized_transaction_type} end end def dispatching_reconstruct([_raw_type | _raw_tx_rlp_decoded_chunks]), do: {:error, :unrecognized_transaction_type} def dispatching_reconstruct(_), do: {:error, :malformed_transaction} @spec decode(tx_bytes()) :: {:ok, Transaction.Protocol.t()} | {:error, decode_error()} def decode(tx_bytes) do with {:ok, raw_tx_rlp_decoded_chunks} <- try_exrlp_decode(tx_bytes), do: dispatching_reconstruct(raw_tx_rlp_decoded_chunks) end def decode!(tx_bytes) do {:ok, tx} = decode(tx_bytes) tx end defp try_exrlp_decode(tx_bytes) do {:ok, ExRLP.decode(tx_bytes)} rescue _ -> {:error, :malformed_transaction_rlp} end defp encode(transaction) do Transaction.Protocol.get_data_for_rlp(transaction) |> ExRLP.encode() end defp hash(tx) do tx |> encode() |> Crypto.hash() end @doc """ Returns all inputs, never returns zero inputs """ @spec get_inputs(any_flavor_t()) :: [{:utxo_position, non_neg_integer(), non_neg_integer(), non_neg_integer()}] def get_inputs(%__MODULE__.Recovered{signed_tx: signed_tx}), do: get_inputs(signed_tx) def get_inputs(%__MODULE__.Signed{raw_tx: raw_tx}), do: get_inputs(raw_tx) def get_inputs(tx), do: Transaction.Protocol.get_inputs(tx) @doc """ Returns all outputs, never returns zero outputs """ @spec get_outputs(any_flavor_t()) :: list() def get_outputs(%__MODULE__.Recovered{signed_tx: signed_tx}), do: get_outputs(signed_tx) def get_outputs(%__MODULE__.Signed{raw_tx: raw_tx}), do: get_outputs(raw_tx) def get_outputs(tx), do: Transaction.Protocol.get_outputs(tx) @doc """ Returns the encoded bytes of the raw transaction involved, i.e. without the signatures """ @spec raw_txbytes(any_flavor_t()) :: binary def raw_txbytes(%__MODULE__.Recovered{signed_tx: signed_tx}), do: raw_txbytes(signed_tx) def raw_txbytes(%__MODULE__.Signed{raw_tx: raw_tx}), do: raw_txbytes(raw_tx) def raw_txbytes(raw_tx), do: encode(raw_tx) @doc """ Returns the hash of the raw transaction involved, i.e. without the signatures """ @spec raw_txhash(any_flavor_t()) :: tx_hash() def raw_txhash(%__MODULE__.Recovered{tx_hash: hash}), do: hash def raw_txhash(%__MODULE__.Signed{raw_tx: raw_tx}), do: raw_txhash(raw_tx) def raw_txhash(raw_tx), do: hash(raw_tx) end defprotocol OMG.Watcher.State.Transaction.Protocol do @moduledoc """ Should be implemented for any type of transaction processed in the system """ alias OMG.Output alias OMG.Watcher.State.Transaction @doc """ Transforms structured data into RLP-structured (encodable) list of fields """ @spec get_data_for_rlp(t()) :: list(any()) def get_data_for_rlp(tx) @doc """ List of input pointers (e.g. of which one implementation is `utxo_pos`) this transaction is intending to spend """ @spec get_inputs(t()) :: list(OMG.Watcher.Utxo.Position.t()) def get_inputs(tx) @doc """ List of outputs this transaction intends to create """ @spec get_outputs(t()) :: list(Output.t()) def get_outputs(tx) @doc """ Custom validation of the transaction with respect to its witnesses. Part of stateless validation routine """ @spec valid?(t(), Transaction.Signed.t()) :: true | {:error, atom} def valid?(tx, signed_tx) @doc """ Custom stateful validity, based on pre-fetched subset of input UTXOs. Check's if the `tx` can be applied given the `input_utxos` presented, presumably UTXOs in the current state of `OMG.Watcher.State` Should also return the fees that this transaction is paying, mapped by currency; for fee validation """ @spec can_apply?(t(), list(Output.t())) :: {:ok, map()} | {:error, atom} def can_apply?(tx, input_utxos) end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state/utxo_set.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.UtxoSet do @moduledoc """ Handles all the operations done on the UTXOs held in the ledger. Provides the requested UTXOs by a collection of input pointers. Trades in transaction effects (new utxos, utxos to delete). Translates the modifications to itself into DB updates, and is able to interpret the UTXO query result from DB. Intended to handle any kind UTXO _subsets_ of the entire UTXO set, relying on that the subset of UTXOs is selected correctly. """ alias OMG.Watcher.Crypto alias OMG.Output alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @type t() :: %{OMG.Watcher.Utxo.Position.t() => Utxo.t()} @type query_result_t() :: list({OMG.DB.utxo_pos_db_t(), OMG.Watcher.Utxo.t()}) @spec init(query_result_t()) :: t() def init(utxos_query_result) do utxos_query_result |> Enum.reject(&(&1 == :not_found)) |> Enum.into(%{}, fn {db_input_pointer, db_utxo} -> {OMG.Watcher.Utxo.Position.from_db_key(db_input_pointer), Utxo.from_db_value(db_utxo)} end) end @doc """ Provides the outputs that are pointed by `inputs` provided """ @spec get_by_inputs(t(), list(OMG.Watcher.Utxo.Position.t())) :: {:ok, list(Output.t())} | {:error, :utxo_not_found} def get_by_inputs(utxos, inputs) do with {:ok, utxos_for_inputs} <- get_utxos_by_inputs(utxos, inputs), do: {:ok, utxos_for_inputs |> Enum.reverse() |> Enum.map(fn %Utxo{output: output} -> output end)} end @doc """ Updates itself given a list of spent input pointers and a map of UTXOs created upon a transaction """ @spec apply_effects(t(), list(OMG.Watcher.Utxo.Position.t()), t()) :: t() def apply_effects(utxos, spent_input_pointers, new_utxos_map) do utxos |> Map.merge(new_utxos_map) |> Map.drop(spent_input_pointers) end @doc """ Returns the DB updates required given a list of spent input pointers and a map of UTXOs created upon a transaction """ @spec db_updates(list(OMG.Watcher.Utxo.Position.t()), t()) :: list({:put, :utxo, {Utxo.Position.db_t(), Utxo.t()}} | {:delete, :utxo, Utxo.Position.db_t()}) def db_updates(spent_input_pointers, new_utxos_map) do db_updates_new_utxos = new_utxos_map |> Enum.map(&utxo_to_db_put/1) db_updates_spent_utxos = spent_input_pointers |> Enum.map(&utxo_to_db_delete/1) Enum.concat(db_updates_new_utxos, db_updates_spent_utxos) end @spec exists?(t(), OMG.Watcher.Utxo.Position.t()) :: boolean() def exists?(utxos, input_pointer), do: Map.has_key?(utxos, input_pointer) @doc """ Searches the UTXO set for a particular UTXO created with a `txhash` on `oindex` position. Current implementation is **expensive** """ @spec find_matching_utxo(t(), Transaction.tx_hash(), non_neg_integer()) :: {OMG.Watcher.Utxo.Position.t(), Utxo.t()} def find_matching_utxo(utxos, requested_txhash, oindex) do utxos |> Stream.filter(&utxo_kv_created_by?(&1, requested_txhash)) |> Enum.find(&utxo_kv_has_oindex_equal?(&1, oindex)) end @doc """ Streams the UTXO key-value pairs found to be owner by a particular address """ @spec filter_owned_by(t(), Crypto.address_t()) :: Enumerable.t() def filter_owned_by(utxos, address) do Stream.filter(utxos, fn utxo_kv -> utxo_kv_get_owner(utxo_kv) == address end) end @doc """ Turns any enumerable of UTXOs (for example an instance of `OMG.Watcher.State.UtxoSet.t` here) and produces a new enumerable where the UTXO k-v pairs got zipped with UTXO positions coming from the data confined in the UTXO set """ @spec zip_with_positions(t() | Enumerable.t()) :: Enumerable.t() def zip_with_positions(utxos) do Stream.map(utxos, fn utxo_kv -> {utxo_kv, utxo_kv_get_position(utxo_kv)} end) end defp get_utxos_by_inputs(utxos, inputs) do Enum.reduce_while(inputs, {:ok, []}, fn input, acc -> get_utxo(utxos, input, acc) end) end defp get_utxo(utxos, position, {:ok, acc}) do case Map.get(utxos, position) do nil -> {:halt, {:error, :utxo_not_found}} found -> {:cont, {:ok, [found | acc]}} end end defp utxo_to_db_put({input_pointer, utxo}), do: {:put, :utxo, {Utxo.Position.to_input_db_key(input_pointer), Utxo.to_db_value(utxo)}} defp utxo_to_db_delete(input_pointer), do: {:delete, :utxo, Utxo.Position.to_input_db_key(input_pointer)} # based on some key-value pair representing {input_pointer, utxo}, get its position from somewhere defp utxo_kv_get_position(utxo_kv) defp utxo_kv_get_position({Utxo.position(_, _, _) = utxo_pos, _utxo}), do: utxo_pos defp utxo_kv_get_position({_non_utxo_pos_input_pointer, %{utxo_pos: Utxo.position(_, _, _) = utxo_pos}}), do: utxo_pos # based on some key-value pair representing {input_pointer, utxo}, get its owner defp utxo_kv_get_owner(utxo_kv) defp utxo_kv_get_owner({_input_pointer, %Utxo{output: %{owner: owner}}}), do: owner defp utxo_kv_get_owner({%{owner: owner}, _output_without_owner_specified}), do: owner defp utxo_kv_created_by?({_input_pointer, %Utxo{creating_txhash: requested_txhash}}, requested_txhash), do: true defp utxo_kv_created_by?({_input_pointer, %Utxo{}}, _), do: false defp utxo_kv_has_oindex_equal?(utxo_kv, oindex) do Utxo.position(_, _, utxo_kv_oindex) = utxo_kv_get_position(utxo_kv) utxo_kv_oindex == oindex end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/state.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State do @moduledoc """ A GenServer serving the ledger, for functional core and more info see `OMG.Watcher.State.Core`. Keeps the state of the ledger, mainly the spendable UTXO set that can be employed in both `OMG.Watcher.ChildChain` and `OMG.Watcher.Watcher`. Maintains the state of the UTXO set by: - recognizing deposits - executing child chain transactions - recognizing exits Assumes that all stateless transaction validation is done outside of `exec/2`, so it accepts `OMG.Watcher.State.Transaction.Recovered` """ alias OMG.DB alias OMG.Watcher.Fees alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.State.Transaction.Validator alias OMG.Watcher.State.UtxoSet alias OMG.Watcher.Utxo use GenServer require Logger require Utxo @type exec_error :: Validator.can_process_tx_error() @timeout 10_000 ### Client @doc """ Starts the `GenServer` maintaining the ledger """ def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc """ Executes a single, statelessly validated child chain transaction. May take information on the fees required, in case fees are charged. Checks statefull validity and executes a transaction on `OMG.Watcher.State` when successful. Otherwise, returns an error and has no effect on `OMG.Watcher.State` and the ledger """ @spec exec(tx :: Transaction.Recovered.t(), fees :: Fees.optional_fee_t()) :: {:ok, {Transaction.tx_hash(), pos_integer, non_neg_integer}} | {:error, exec_error()} def exec(tx, input_fees) do GenServer.call(__MODULE__, {:exec, tx, input_fees}, @timeout) end @doc """ Intended for the `OMG.Watcher.Watcher`. "Closes" a block, acknowledging that all transactions have been executed, and the next `exec/2` will belong to the next block. Depends on the caller to do persistence. Synchronous """ @spec close_block() :: {:ok, list(Core.db_update())} def close_block() do GenServer.call(__MODULE__, :close_block, @timeout) end @doc """ Recognizes a list of deposits based on Ethereum events. Depends on the caller to do persistence. """ @spec deposit(deposits :: [Core.deposit()]) :: {:ok, list(Core.db_update())} # empty list clause to not block state for a no-op def deposit([]), do: {:ok, []} def deposit(deposits) do GenServer.call(__MODULE__, {:deposit, deposits}, @timeout) end @doc """ Recognizes a list of exits based on various triggers. Returns exit validities which indicate which of the UTXO positions actually pointed to UTXOs in the UTXO set of the ledger. For a list of things that can be triggers see `OMG.Watcher.State.Core.extract_exiting_utxo_positions/2`. Depends on the caller to do persistence. """ @spec exit_utxos(exiting_utxo_triggers :: Core.exiting_utxo_triggers_t()) :: {:ok, list(Core.db_update()), Core.validities_t()} # empty list clause to not block state for a no-op def exit_utxos([]), do: {:ok, [], {[], []}} def exit_utxos(exiting_utxo_triggers) do GenServer.call(__MODULE__, {:exit_utxos, exiting_utxo_triggers}, @timeout) end @doc """ Provides a peek into the UTXO set to check if particular output exist (have not been spent) """ @spec utxo_exists?(Utxo.Position.t()) :: boolean() def utxo_exists?(utxo) do GenServer.call(__MODULE__, {:utxo_exists, utxo}, @timeout) end @doc """ Returns the current `blknum` and whether at the beginning of a block. The beginning of the block is `true/false` depending on whether there have been no transactions executed yet for the current child chain block """ @spec get_status() :: {non_neg_integer(), boolean()} def get_status() do GenServer.call(__MODULE__, :get_status, @timeout) end ### Server @doc """ Initializes the state. UTXO set is not loaded now. """ def init(opts) do {:ok, child_top_block_number} = DB.get_single_value(:child_top_block_number) child_block_interval = Keyword.fetch!(opts, :child_block_interval) fee_claimer_address = Keyword.fetch!(opts, :fee_claimer_address) metrics_collection_interval = Keyword.fetch!(opts, :metrics_collection_interval) {:ok, _data} = result = Core.extract_initial_state(child_top_block_number, child_block_interval, fee_claimer_address) _ = Logger.info("Started #{inspect(__MODULE__)}, height: #{child_top_block_number}}") {:ok, _} = :timer.send_interval(metrics_collection_interval, self(), :send_metrics) result end def handle_info(:send_metrics, state) do :ok = :telemetry.execute([:process, __MODULE__], %{}, state) {:noreply, state} end @doc """ see `exec/2` see `deposit/1` see `exit_utxos/1` Flow: - translates the triggers to UTXO positions digestible by the UTXO set - exits the UTXOs from the ledger if they exists, reports invalidity wherever they don't - returns the `db_updates` to be applied by the caller see `utxo_exists/1` see `get_status/0` see `close_block/0` Works exactly like `handle_cast(:form_block)` but: - is synchronous - relies on the caller to handle persistence, instead of handling itself Someday, one might want to skip some of computations done (like calculating the root hash, which is scrapped) """ def handle_call({:exec, tx, fees}, _from, state) do db_utxos = tx |> Transaction.get_inputs() |> fetch_utxos_from_db(state) state |> Core.with_utxos(db_utxos) |> Core.exec(tx, fees) |> case do {:ok, tx_result, new_state} -> {:reply, {:ok, tx_result}, new_state} {tx_result, new_state} -> {:reply, tx_result, new_state} end end def handle_call({:deposit, deposits}, _from, state) do if Code.ensure_loaded?(OMG.WatcherInfo.DB.EthEvent) do :ok = Kernel.apply(OMG.WatcherInfo.DB.EthEvent, :insert_deposits!, [deposits]) end {:ok, db_updates, new_state} = Core.deposit(deposits, state) {:reply, {:ok, db_updates}, new_state} end def handle_call({:exit_utxos, exiting_utxo_triggers}, _from, state) do exiting_utxos = Core.extract_exiting_utxo_positions(exiting_utxo_triggers, state) db_utxos = fetch_utxos_from_db(exiting_utxos, state) state = Core.with_utxos(state, db_utxos) {:ok, {db_updates, validities}, new_state} = Core.exit_utxos(exiting_utxos, state) {:reply, {:ok, db_updates, validities}, new_state} end def handle_call({:utxo_exists, utxo_pos}, _from, state) do db_utxos = fetch_utxos_from_db([utxo_pos], state) new_state = Core.with_utxos(state, db_utxos) {:reply, Core.utxo_exists?(utxo_pos, new_state), new_state} end def handle_call(:get_status, _from, state) do {:reply, Core.get_status(state), state} end def handle_call(:close_block, _from, state) do {:ok, {block, db_updates}, new_state} = Core.form_block(state) :ok = publish_block_to_event_bus(block) {:reply, {:ok, db_updates}, new_state} end defp publish_block_to_event_bus(block) do {:child_chain, "blocks"} |> OMG.Bus.Event.new(:enqueue_block, block) |> OMG.Bus.direct_local_broadcast() end @spec fetch_utxos_from_db(list(OMG.Watcher.Utxo.Position.t()), Core.t()) :: UtxoSet.t() defp fetch_utxos_from_db(utxo_pos_list, state) do utxo_pos_list |> Stream.reject(&Core.utxo_processed?(&1, state)) |> Enum.map(&utxo_from_db/1) |> UtxoSet.init() end defp utxo_from_db(input_pointer) do # `DB` query can return `:not_found` which is filtered out by following `is_input_pointer?` with {:ok, utxo_kv} <- DB.utxo(Utxo.Position.to_input_db_key(input_pointer)), do: utxo_kv end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/supervisor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Supervisor do @moduledoc """ Starts and supervises child processes for the security-critical watcher. It may start its own child processes or start other supervisors. """ use Supervisor require Logger alias OMG.Eth.RootChain alias OMG.Status.Alert.Alarm alias OMG.Watcher.DatadogEvent.ContractEventConsumer alias OMG.Watcher.Monitor alias OMG.Watcher.SyncSupervisor alias OMG.Watcher.Tracer def start_link() do Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) end def init(:ok) do {:ok, contract_deployment_height} = RootChain.get_root_deployment_height() children = [ {Monitor, [ Alarm, %{ id: SyncSupervisor, start: {SyncSupervisor, :start_link, [[contract_deployment_height: contract_deployment_height]]}, restart: :permanent, type: :supervisor } ]} ] is_datadog_disabled = is_disabled?() rest_children = if is_datadog_disabled do children else create_event_consumer_children() ++ children end opts = [strategy: :one_for_one] _ = Logger.info("Starting #{inspect(__MODULE__)}") Supervisor.init(rest_children, opts) end defp create_event_consumer_children() do topics = Enum.map( [ "blocks", "DepositCreated", "InFlightExitInputPiggybacked", "InFlightExitOutputPiggybacked", "BlockSubmitted", "ExitFinalized", "ExitChallenged", "InFlightExitChallenged", "InFlightExitChallengeResponded", "InFlightExitInputBlocked", "InFlightExitOutputBlocked", "InFlightExitInputWithdrawn", "InFlightExitOutputWithdrawn", "InFlightExitStarted", "ExitStarted" ], &{:root_chain, &1} ) Enum.map( topics, fn topic -> ContractEventConsumer.prepare_child( topic: topic, release: Application.get_env(:omg_watcher, :release), current_version: Application.get_env(:omg_watcher, :current_version), publisher: OMG.Status.Metric.Datadog ) end ) end @spec is_disabled?() :: boolean() defp is_disabled?(), do: Application.get_env(:omg_watcher, Tracer)[:disabled?] end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/sync_supervisor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.SyncSupervisor do @moduledoc """ Starts and supervises security-critical watcher's child processes and supervisors related to rootchain synchronisations. """ use Supervisor require Logger alias OMG.Watcher alias OMG.Watcher.API.StatusCache alias OMG.Watcher.API.StatusCache.Storage alias OMG.Watcher.ChildManager alias OMG.Watcher.Configuration alias OMG.Watcher.CoordinatorSetup alias OMG.Watcher.EthereumEventAggregator alias OMG.Watcher.EthereumEventListener alias OMG.Watcher.ExitProcessor alias OMG.Watcher.Monitor @events_bucket :events_bucket @status_cache :status_cache def status_cache() do @status_cache end def events_bucket() do @events_bucket end def start_link(args) do Supervisor.start_link(__MODULE__, args, name: __MODULE__) end def init(args) do # Assuming the values max_restarts and max_seconds, # then, if more than max_restarts restarts occur within max_seconds seconds, # the supervisor terminates all child processes and then itself. # The termination reason for the supervisor itself in that case will be shutdown. # max_restarts defaults to 3 and max_seconds defaults to 5. # We have 16 children, roughly 14 of them have a dependency to the internetz. # The internetz is flaky. We account for that and allow the flakyness to pass # by increasing the restart strategy. But not too much, because if the internetz is # really off, HeightMonitor should catch that in max 8 seconds and raise an alarm. max_restarts = 3 max_seconds = 5 opts = [strategy: :one_for_one, max_restarts: max_restarts * 10, max_seconds: max_seconds * 2] _ = Logger.info("Starting #{inspect(__MODULE__)}") :ok = Storage.ensure_ets_init(status_cache()) :ok = ensure_ets_init(events_bucket()) Supervisor.init(children(args), opts) end defp children(args) do contract_deployment_height = Keyword.fetch!(args, :contract_deployment_height) exit_processor_sla_margin = Configuration.exit_processor_sla_margin() exit_processor_sla_margin_forced = Configuration.exit_processor_sla_margin_forced() metrics_collection_interval = Configuration.metrics_collection_interval() finality_margin = Configuration.exit_finality_margin() deposit_finality_margin = Configuration.deposit_finality_margin() ethereum_events_check_interval_ms = Configuration.ethereum_events_check_interval_ms() coordinator_eth_height_check_interval_ms = Configuration.coordinator_eth_height_check_interval_ms() min_exit_period_seconds = OMG.Eth.Configuration.min_exit_period_seconds() ethereum_block_time_seconds = OMG.Eth.Configuration.ethereum_block_time_seconds() child_block_interval = OMG.Eth.Configuration.child_block_interval() contracts = OMG.Eth.Configuration.contracts() [ {OMG.Watcher.RootChainCoordinator, CoordinatorSetup.coordinator_setup( metrics_collection_interval, coordinator_eth_height_check_interval_ms, finality_margin, deposit_finality_margin )}, {ExitProcessor, [ exit_processor_sla_margin: exit_processor_sla_margin, exit_processor_sla_margin_forced: exit_processor_sla_margin_forced, metrics_collection_interval: metrics_collection_interval, min_exit_period_seconds: min_exit_period_seconds, ethereum_block_time_seconds: ethereum_block_time_seconds, child_block_interval: child_block_interval ]}, %{ id: OMG.Watcher.BlockGetter.Supervisor, start: {OMG.Watcher.BlockGetter.Supervisor, :start_link, [[contract_deployment_height: contract_deployment_height]]}, restart: :permanent, type: :supervisor }, {EthereumEventAggregator, contracts: contracts, ets_bucket: events_bucket(), events: [ [name: :deposit_created, enrich: false], [name: :exit_started, enrich: true], [name: :exit_finalized, enrich: false], [name: :exit_challenged, enrich: false], [name: :in_flight_exit_started, enrich: true], [name: :in_flight_exit_deleted, enrich: false], [name: :in_flight_exit_input_piggybacked, enrich: false], [name: :in_flight_exit_output_piggybacked, enrich: false], [name: :in_flight_exit_challenged, enrich: true], [name: :in_flight_exit_challenge_responded, enrich: false], [name: :in_flight_exit_input_blocked, enrich: false], [name: :in_flight_exit_output_blocked, enrich: false], [name: :in_flight_exit_input_withdrawn, enrich: false], [name: :in_flight_exit_output_withdrawn, enrich: false] ]}, EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :depositor, synced_height_update_key: :last_depositor_eth_height, get_events_callback: &EthereumEventAggregator.deposit_created/2, process_events_callback: &OMG.Watcher.State.deposit/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :exit_processor, synced_height_update_key: :last_exit_processor_eth_height, get_events_callback: &EthereumEventAggregator.exit_started/2, process_events_callback: &Watcher.ExitProcessor.new_exits/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :exit_finalizer, synced_height_update_key: :last_exit_finalizer_eth_height, get_events_callback: &EthereumEventAggregator.exit_finalized/2, process_events_callback: &Watcher.ExitProcessor.finalize_exits/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :exit_challenger, synced_height_update_key: :last_exit_challenger_eth_height, get_events_callback: &EthereumEventAggregator.exit_challenged/2, process_events_callback: &Watcher.ExitProcessor.challenge_exits/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :in_flight_exit_processor, synced_height_update_key: :last_in_flight_exit_processor_eth_height, get_events_callback: &EthereumEventAggregator.in_flight_exit_started/2, process_events_callback: &Watcher.ExitProcessor.new_in_flight_exits/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :piggyback_processor, synced_height_update_key: :last_piggyback_processor_eth_height, get_events_callback: &EthereumEventAggregator.in_flight_exit_piggybacked/2, process_events_callback: &Watcher.ExitProcessor.piggyback_exits/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :competitor_processor, synced_height_update_key: :last_competitor_processor_eth_height, get_events_callback: &EthereumEventAggregator.in_flight_exit_challenged/2, process_events_callback: &Watcher.ExitProcessor.new_ife_challenges/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :challenges_responds_processor, synced_height_update_key: :last_challenges_responds_processor_eth_height, get_events_callback: &EthereumEventAggregator.in_flight_exit_challenge_responded/2, process_events_callback: &Watcher.ExitProcessor.respond_to_in_flight_exits_challenges/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :piggyback_challenges_processor, synced_height_update_key: :last_piggyback_challenges_processor_eth_height, get_events_callback: &EthereumEventAggregator.in_flight_exit_blocked/2, process_events_callback: &Watcher.ExitProcessor.challenge_piggybacks/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :ife_exit_finalizer, synced_height_update_key: :last_ife_exit_finalizer_eth_height, get_events_callback: &EthereumEventAggregator.in_flight_exit_withdrawn/2, process_events_callback: &Watcher.ExitProcessor.finalize_in_flight_exits/1 ), EthereumEventListener.prepare_child( metrics_collection_interval: metrics_collection_interval, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, contract_deployment_height: contract_deployment_height, service_name: :in_flight_exit_deleted_processor, synced_height_update_key: :last_ife_exit_deleted_eth_height, get_events_callback: &EthereumEventAggregator.in_flight_exit_deleted/2, process_events_callback: &Watcher.ExitProcessor.delete_in_flight_exits/1 ), {StatusCache, [event_bus: OMG.Bus, ets: status_cache()]}, {ChildManager, [monitor: Monitor]} ] end defp ensure_ets_init(events_bucket) do case :ets.info(events_bucket) do :undefined -> ^events_bucket = :ets.new(events_bucket, [:bag, :public, :named_table]) :ok _ -> :ok end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/tracer.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Tracer do @moduledoc """ Trace Ecto requests and reports information to Datadog via Spandex """ use Spandex.Tracer, otp_app: :omg_watcher end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/typed_data_hash/config.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.TypedDataHash.Config do @moduledoc """ Separates computation of EIP-172 domain separator to allow precompute domain separator in TypedDataHash module's attribute, so it doesn't need to compute every time structure data is hashed. """ alias OMG.Eth.Encoding alias OMG.Watcher.TypedDataHash.Tools require Logger @doc """ Returns EIP-712 domain based on values from configuration in a format `signTypedData` expects. """ @spec domain_data_from_config() :: Tools.eip712_domain_t() def domain_data_from_config() do verifying_contract_addr = Application.get_env(:omg_eth, :contract_addr) |> Access.get(:plasma_framework) |> Encoding.from_hex() Application.fetch_env!(:omg_watcher, :eip_712_domain) |> Map.new() |> Map.put_new(:verifyingContract, verifying_contract_addr) |> Map.update!(:salt, &Encoding.from_hex/1) end @doc """ Computes default domain separator based on values from configuration. This value is taken to structured hash computation when no domain separator is passed. """ @spec domain_separator_from_config() :: OMG.Watcher.Crypto.hash_t() def domain_separator_from_config() do Tools.domain_separator(domain_data_from_config()) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/typed_data_hash/tools.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.TypedDataHash.Tools do @moduledoc """ Implements EIP-712 structural hashing primitives for Transaction type. See also: http://eips.ethereum.org/EIPS/eip-712 """ alias OMG.Output alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash.Types alias OMG.Watcher.Utxo require Utxo @type eip712_domain_t() :: %{ name: binary(), version: binary(), salt: OMG.Watcher.Crypto.hash_t(), verifyingContract: OMG.Watcher.Crypto.address_t() } @domain_encoded_type Types.encode_type(:EIP712Domain) @domain_type_hash Crypto.hash(@domain_encoded_type) @transaction_encoded_type Types.encode_type(:Transaction) @input_encoded_type Types.encode_type(:Input) @output_encoded_type Types.encode_type(:Output) @transaction_type_hash Crypto.hash(@transaction_encoded_type <> @input_encoded_type <> @output_encoded_type) @input_type_hash Crypto.hash(@input_encoded_type) @output_type_hash Crypto.hash(@output_encoded_type) @doc """ Computes Domain Separator `hashStruct(eip712Domain)`, @see: http://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator """ @spec domain_separator(eip712_domain_t(), Crypto.hash_t()) :: Crypto.hash_t() def domain_separator( %{ name: name, version: version, verifyingContract: verifying_contract, salt: salt }, domain_type_hash \\ @domain_type_hash ) do [ domain_type_hash, Crypto.hash(name), Crypto.hash(version), ABI.TypeEncoder.encode_raw([verifying_contract], [:address]), ABI.TypeEncoder.encode_raw([salt], [{:bytes, 32}]) ] |> Enum.join() |> Crypto.hash() end @spec hash_transaction( non_neg_integer(), list(Utxo.Position.t()), list(Output.t()), Transaction.metadata(), Crypto.hash_t(), Crypto.hash_t() ) :: Crypto.hash_t() def hash_transaction(plasma_framework_tx_type, inputs, outputs, metadata, empty_input_hash, empty_output_hash) do require Transaction.Payment raw_encoded_tx_type = ABI.TypeEncoder.encode_raw([plasma_framework_tx_type], [{:uint, 256}]) input_hashes = inputs |> Stream.map(&hash_input/1) |> Stream.concat(Stream.cycle([empty_input_hash])) |> Enum.take(Transaction.Payment.max_inputs()) output_hashes = outputs |> Stream.map(&hash_output/1) |> Stream.concat(Stream.cycle([empty_output_hash])) |> Enum.take(Transaction.Payment.max_outputs()) tx_data = ABI.TypeEncoder.encode_raw([0], [{:uint, 256}]) metadata = metadata || <<0::256>> [ @transaction_type_hash, raw_encoded_tx_type, input_hashes, output_hashes, tx_data, metadata ] |> List.flatten() |> Enum.join() |> Crypto.hash() end @spec hash_input(Utxo.Position.t()) :: Crypto.hash_t() def hash_input(Utxo.position(blknum, txindex, oindex)) do [ @input_type_hash, ABI.TypeEncoder.encode_raw([blknum], [{:uint, 256}]), ABI.TypeEncoder.encode_raw([txindex], [{:uint, 256}]), ABI.TypeEncoder.encode_raw([oindex], [{:uint, 256}]) ] |> Enum.join() |> Crypto.hash() end @spec hash_output(Output.t()) :: Crypto.hash_t() def hash_output(%Output{ owner: owner, currency: currency, amount: amount, output_type: output_type }) do [ @output_type_hash, ABI.TypeEncoder.encode_raw([output_type], [{:uint, 256}]), ABI.TypeEncoder.encode_raw([owner], [{:bytes, 20}]), ABI.TypeEncoder.encode_raw([currency], [:address]), ABI.TypeEncoder.encode_raw([amount], [{:uint, 256}]) ] |> Enum.join() |> Crypto.hash() end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/typed_data_hash/types.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.TypedDataHash.Types do @moduledoc """ Specifies all types needed to produce `eth_signTypedData` request. See: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification-of-the-eth_signtypeddata-json-rpc """ @type typedDataSignRequest_t() :: %{ types: map(), primaryType: binary(), domain: map(), message: map() } @make_spec &%{name: &1, type: &2} @eip_712_domain_spec [ @make_spec.("name", "string"), @make_spec.("version", "string"), @make_spec.("verifyingContract", "address"), @make_spec.("salt", "bytes32") ] @tx_spec Enum.concat([ [@make_spec.("txType", "uint256")], Enum.map(0..3, fn i -> @make_spec.("input" <> Integer.to_string(i), "Input") end), Enum.map(0..3, fn i -> @make_spec.("output" <> Integer.to_string(i), "Output") end), [@make_spec.("txData", "uint256")], [@make_spec.("metadata", "bytes32")] ]) @input_spec [ @make_spec.("blknum", "uint256"), @make_spec.("txindex", "uint256"), @make_spec.("oindex", "uint256") ] @output_spec [ @make_spec.("outputType", "uint256"), @make_spec.("outputGuard", "bytes20"), @make_spec.("currency", "address"), @make_spec.("amount", "uint256") ] @types %{ EIP712Domain: @eip_712_domain_spec, Transaction: @tx_spec, Input: @input_spec, Output: @output_spec } def eip712_types_specification(), do: %{ types: @types, primaryType: "Transaction" } def encode_type(type_name) when is_atom(type_name) do "#{type_name}(#{ @types[type_name] |> Enum.map(fn %{name: name, type: type} -> "#{type} #{name}" end) |> Enum.join(",") })" end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/typed_data_hash.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.TypedDataHash do @moduledoc """ Facilitates veryfing typed structured data (see: http://eips.ethereum.org/EIPS/eip-712) by producing a `hash_struct` for structured transaction data. These `struct_txhash`es are later used as digest to sign and recover signatures. """ alias OMG.Output alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @zero_address <<0::160>> # Precomputed hash of empty input for performance @empty_input_hash __MODULE__.Tools.hash_input(Utxo.position(0, 0, 0)) # Precomputed hash of empty output for performance @empty_output_hash __MODULE__.Tools.hash_output(%Output{ owner: @zero_address, currency: @zero_address, amount: 0, output_type: 0 }) # Prefix and version byte motivated by http://eips.ethereum.org/EIPS/eip-191 @eip_191_prefix <<0x19, 0x01>> @doc """ Computes a hash of encoded transaction as defined in EIP-712 """ @spec hash_struct(Transaction.Payment.t(), Crypto.domain_separator_t()) :: Crypto.hash_t() def hash_struct(%Transaction.Payment{} = raw_tx, domain_separator \\ nil) do domain_separator = domain_separator || __MODULE__.Config.domain_separator_from_config() Crypto.hash(@eip_191_prefix <> domain_separator <> hash_transaction(raw_tx)) end @spec hash_transaction(Transaction.Payment.t()) :: Crypto.hash_t() def hash_transaction(%Transaction.Payment{} = raw_tx) do __MODULE__.Tools.hash_transaction( raw_tx.tx_type, Transaction.get_inputs(raw_tx), Transaction.get_outputs(raw_tx), raw_tx.metadata, @empty_input_hash, @empty_output_hash ) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/utxo/position.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Utxo.Position do @moduledoc """ Representation of a UTXO position in the child chain, providing encoding/decoding to/from formats digestible in `Eth` and in the `OMG.DB` """ # these two offset constants are driven by the constants from the RootChain.sol contract @input_pointer_output_type 1 alias ExPlasma.Output, as: ExPlasmaOutput alias ExPlasma.Output.Position, as: ExPlasmaPosition alias OMG.Watcher.Utxo require Utxo @type t() :: { :utxo_position, # blknum non_neg_integer(), # txindex non_neg_integer(), # oindex non_neg_integer() } @type db_t() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()} @type input_db_key_t() :: {:input_pointer, pos_integer(), db_t()} defguardp is_position(blknum, txindex, oindex) when is_integer(blknum) and blknum >= 0 and is_integer(txindex) and txindex >= 0 and is_integer(oindex) and oindex >= 0 @doc """ Encode an input utxo position into an integer value. ## Examples iex> utxo_pos = {:utxo_position, 4, 5, 1} iex> OMG.Watcher.Utxo.Position.encode(utxo_pos) 4_000_050_001 """ @spec encode(t()) :: pos_integer() def encode(Utxo.position(blknum, txindex, oindex)) when is_position(blknum, txindex, oindex) do ExPlasmaPosition.pos(%{blknum: blknum, txindex: txindex, oindex: oindex}) end @doc """ Decode an integer or binary into a utxo position tuple. ## Examples # Decodes an integer encoded utxo position. iex> OMG.Watcher.Utxo.Position.decode!(4_000_050_001) {:utxo_position, 4, 5, 1} # Decode a binary encoded utxo position. iex> encoded_pos = <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 107, 235, 81>> iex> OMG.Watcher.Utxo.Position.decode!(encoded_pos) {:utxo_position, 4, 5, 1} """ @spec decode!(binary()) :: t() def decode!(encoded) do {:ok, decoded} = decode(encoded) decoded end @doc """ Decode an integer or binary into a utxo position tuple. ## Examples # Decode an integer encoded utxo position. iex> OMG.Watcher.Utxo.Position.decode(4_000_050_001) {:ok, {:utxo_position, 4, 5, 1}} # Returns an error if the value is too low. iex> OMG.Watcher.Utxo.Position.decode(0) {:error, :encoded_utxo_position_too_low} iex> OMG.Watcher.Utxo.Position.decode(-1) {:error, :encoded_utxo_position_too_low} # Decode a binary encoded utxo position. iex> encoded_pos = <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 107, 235, 81>> iex> OMG.Watcher.Utxo.Position.decode(encoded_pos) {:ok, {:utxo_position, 4, 5, 1}} """ @spec decode(binary()) :: {:ok, t()} | {:error, :encoded_utxo_position_too_low | {:blknum, :exceeds_maximum}} def decode(encoded) when is_number(encoded) and encoded <= 0, do: {:error, :encoded_utxo_position_too_low} def decode(encoded) when is_integer(encoded) and encoded > 0, do: do_decode(encoded) def decode(encoded) when is_binary(encoded) and byte_size(encoded) == 32, do: do_decode(encoded) # TODO(achiurizo) # Refactor to_input_db_key/1 and to_db_key/1. Doing this because # this was merged from a previous module where one code path still wants the 3 item tuple. @doc """ Convert a utxo position into the input db key tuple. ## Examples iex> utxo_pos = {:utxo_position, 1, 2, 3} iex> OMG.Watcher.Utxo.Position.to_input_db_key(utxo_pos) {:input_pointer, 1, {1, 2, 3}} """ @spec to_input_db_key(t()) :: {:input_pointer, unquote(@input_pointer_output_type), db_t()} def to_input_db_key(Utxo.position(blknum, txindex, oindex)) when is_position(blknum, txindex, oindex), do: {:input_pointer, @input_pointer_output_type, {blknum, txindex, oindex}} @doc """ Convert a utxo position into the db key tuple. (legacy?) ## Examples iex> utxo_pos = {:utxo_position, 1, 2, 3} iex> OMG.Watcher.Utxo.Position.to_db_key(utxo_pos) {1, 2, 3} """ @spec to_db_key(t()) :: db_t() def to_db_key(Utxo.position(blknum, txindex, oindex)), do: {blknum, txindex, oindex} # TODO(achiurizo) # Refactor so we only have one db key type. @doc """ Convert an input db key tuple into a utxo position. ## Examples # Convert an input db key tuple into a utxo position. iex> input_db_key = {:input_pointer, 1, {1, 2, 3}} iex> OMG.Watcher.Utxo.Position.from_db_key(input_db_key) {:utxo_position, 1, 2, 3} # Convert a 'legacy' db key tuple into a utxo position iex> legacy_input_db_key = {1, 2, 3} iex> OMG.Watcher.Utxo.Position.from_db_key(legacy_input_db_key) {:utxo_position, 1, 2, 3} """ @spec from_db_key(db_t() | input_db_key_t()) :: t() def from_db_key({:input_pointer, _output_type, db_value}), do: from_db_key(db_value) def from_db_key({blknum, txindex, oindex}) when is_position(blknum, txindex, oindex), do: Utxo.position(blknum, txindex, oindex) # TODO(achiurizo) # better name for this function, like to_rlp/1. @doc """ Returns the rlp-encodable data for the given utxo position. ## Examples iex> utxo_pos = {:utxo_position, 1, 2, 3} iex> OMG.Watcher.Utxo.Position.get_data_for_rlp(utxo_pos) <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 155, 24, 35>> """ @spec get_data_for_rlp(t()) :: binary() def get_data_for_rlp(Utxo.position(blknum, txindex, oindex)) do utxo = ExPlasmaPosition.new(blknum, txindex, oindex) ExPlasmaPosition.to_rlp(utxo) end defp do_decode(encoded) when is_binary(encoded) do {:ok, %ExPlasmaOutput{output_id: %{blknum: blknum, txindex: txindex, oindex: oindex}}} = ExPlasmaOutput.decode_id(encoded) {:ok, Utxo.position(blknum, txindex, oindex)} end defp do_decode(encoded) when is_integer(encoded) do {:ok, utxo} = ExPlasmaPosition.to_map(encoded) {:ok, Utxo.position(utxo.blknum, utxo.txindex, utxo.oindex)} end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/utxo.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Utxo do @moduledoc """ Manipulates a single unspent transaction output (UTXO) held be the child chain state. """ alias OMG.Output alias OMG.Watcher.State.Transaction defstruct [:output, :creating_txhash] @type t() :: %__MODULE__{ output: Output.t(), creating_txhash: Transaction.tx_hash() } @doc """ Inserts a representation of an UTXO position, usable in guards. See Utxo.Position for handling of these entities """ defmacro position(blknum, txindex, oindex) do quote do {:utxo_position, unquote(blknum), unquote(txindex), unquote(oindex)} end end defguardp is_nil_or_binary(creating_tx_hash) when is_nil(creating_tx_hash) or is_binary(creating_tx_hash) # NOTE: we have no migrations, so we handle data compatibility here (make_db_update/1 and from_db_kv/1), OMG-421 def to_db_value(%__MODULE__{output: output, creating_txhash: creating_txhash}) when is_nil_or_binary(creating_txhash) do %{creating_txhash: creating_txhash} |> Map.put(:output, Output.to_db_value(output)) end def from_db_value(%{output: output, creating_txhash: creating_txhash}) when is_nil_or_binary(creating_txhash) do value = %{ output: Output.from_db_value(output), creating_txhash: creating_txhash } struct!(__MODULE__, value) end # Reading from old db format, only `OMG.Output.FungibleMoreVPToken` def from_db_value(%{owner: owner, currency: currency, amount: amount, creating_txhash: creating_txhash}) when is_nil_or_binary(creating_txhash) do output = %{owner: owner, currency: currency, amount: amount} value = %{ output: Output.from_db_value(output), creating_txhash: creating_txhash } struct!(__MODULE__, value) end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/utxo_exit/core.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.UtxoExit.Core do @moduledoc """ Module provides API for compose exit """ alias OMG.Output alias OMG.Watcher.Block alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo alias OMG.Watcher.Utxo.Position require Utxo def compose_block_standard_exit(:not_found, _), do: {:error, :utxo_not_found} def compose_block_standard_exit(db_block, Utxo.position(blknum, txindex, _) = utxo_pos) do %Block{transactions: sorted_txs_bytes, number: ^blknum} = block = Block.from_db_value(db_block) with {:ok, signed_tx_bytes} <- get_tx_by_index(sorted_txs_bytes, txindex), signed_tx = Transaction.Signed.decode!(signed_tx_bytes), :ok <- get_output_by_index(signed_tx, utxo_pos) do {:ok, %{ utxo_pos: Position.encode(utxo_pos), txbytes: Transaction.raw_txbytes(signed_tx), proof: Block.inclusion_proof(block, txindex) }} end end @spec compose_deposit_standard_exit({:ok, {tuple, map}} | :not_found) :: {:error, :no_deposit_for_given_blknum} | {:ok, %{utxo_pos: non_neg_integer, txbytes: binary, proof: binary}} def compose_deposit_standard_exit({:ok, {db_utxo_pos, db_utxo_value}}) do utxo_pos = Position.from_db_key(db_utxo_pos) %Utxo{output: %Output{amount: amount, currency: currency, owner: owner}} = Utxo.from_db_value(db_utxo_value) tx = Transaction.Payment.new([], [{owner, currency, amount}]) txs = [Transaction.Signed.encode(%Transaction.Signed{raw_tx: tx, sigs: []})] {:ok, %{ utxo_pos: Position.encode(utxo_pos), txbytes: Transaction.raw_txbytes(tx), proof: Block.inclusion_proof(txs, 0) }} end def compose_deposit_standard_exit(:not_found), do: {:error, :no_deposit_for_given_blknum} defp get_tx_by_index(sorted_txs, txindex) do sorted_txs |> Enum.at(txindex) |> case do nil -> {:error, :utxo_not_found} found -> {:ok, found} end end defp get_output_by_index(tx, Utxo.position(_, _, oindex)) do tx |> Transaction.get_outputs() |> Enum.at(oindex) |> case do nil -> {:error, :utxo_not_found} _found -> :ok end end end ================================================ FILE: apps/omg_watcher/lib/omg_watcher/wire_format_types.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.WireFormatTypes do @moduledoc """ Provides wire format's tx/output type values and mapping to modules which decodes them. """ @type tx_type_to_module_map() :: %{non_neg_integer() => atom()} @tx_type_values %{ tx_payment_v1: 1, tx_fee_token_claim: 3 } @tx_type_modules %{ 1 => OMG.Watcher.State.Transaction.Payment, 3 => OMG.Watcher.State.Transaction.Fee } @module_tx_types %{ OMG.Watcher.State.Transaction.Payment => 1, OMG.Watcher.State.Transaction.Fee => 3 } @input_pointer_type_values %{ input_pointer_utxo_position: 1 } @output_type_values %{ output_payment_v1: 1, output_fee_token_claim: 2 } @output_type_modules %{ 1 => OMG.Output, 2 => OMG.Output } @known_tx_types Map.keys(@tx_type_values) @known_input_pointer_types Map.keys(@input_pointer_type_values) @known_output_types Map.keys(@output_type_values) @doc """ Returns wire format type value of known transaction type """ @spec tx_type_for(tx_type :: atom()) :: non_neg_integer() def tx_type_for(tx_type) when tx_type in @known_tx_types, do: @tx_type_values[tx_type] @doc """ Returns module atom that is able to decode transaction of given type """ @spec tx_type_modules() :: tx_type_to_module_map() def tx_type_modules(), do: @tx_type_modules @doc """ Returns the tx type that is associated with the given module """ @spec module_tx_types() :: %{atom() => non_neg_integer()} def module_tx_types(), do: @module_tx_types @doc """ Returns wire format type value of known input pointer type """ @spec input_pointer_type_for(input_pointer_type :: atom()) :: non_neg_integer() def input_pointer_type_for(input_pointer_type) when input_pointer_type in @known_input_pointer_types, do: @input_pointer_type_values[input_pointer_type] @doc """ Returns wire format type value of known output type """ @spec output_type_for(output_type :: atom()) :: non_neg_integer() def output_type_for(output_type) when output_type in @known_output_types, do: @output_type_values[output_type] @doc """ Returns module atom that is able to decode output of given type """ @spec output_type_modules() :: tx_type_to_module_map() def output_type_modules(), do: @output_type_modules end ================================================ FILE: apps/omg_watcher/lib/omg_watcher.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher do @moduledoc """ Watcher is responsible for syncing and validating the child chain, and providing a secure interface to it. For details see [here](README.md) and [here](docs/tesuji_blockchain_design.md) """ end ================================================ FILE: apps/omg_watcher/mix.exs ================================================ defmodule OMG.Watcher.MixProject do use Mix.Project def project() do [ app: :omg_watcher, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end # Run "mix help compile.app" to learn about applications. def application() do [ mod: {OMG.Watcher.Application, []}, start_phases: [{:attach_telemetry, []}], extra_applications: [:logger, :runtime_tools, :telemetry, :phoenix, :poison] ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end # Specifies which paths to compile per environment. defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp deps() do [ {:ex_plasma, "~> 0.2.0"}, {:ex_rlp, "~> 0.5.3"}, {:merkle_tree, "~> 2.0.0"}, {:telemetry, "~> 0.4.1"}, # there's no apparent reason why libsecp256k1, spandex need to be included as dependencies # to this umbrella application apart from mix ecto.gen.migration not working, so here they are, copied from # the parent (main) mix.exs {:spandex, "~> 3.0.2"}, # UMBRELLA {:omg_bus, in_umbrella: true}, {:omg_status, in_umbrella: true}, {:omg_db, in_umbrella: true}, {:omg_eth, in_umbrella: true}, {:omg_utils, in_umbrella: true}, # TEST ONLY # here only to leverage common test helpers and code {:fake_server, "~> 2.1", only: [:dev, :test], runtime: false}, {:briefly, "~> 0.3.0", only: [:dev, :test]} ] end end ================================================ FILE: apps/omg_watcher/test/fixtures.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Fixtures do use ExUnitFixtures.FixtureModule use OMG.DB.Fixtures use OMG.Eth.Fixtures alias OMG.Eth.Configuration alias OMG.Status.Alert.Alarm alias OMG.Watcher.State.Core import OMG.Watcher.TestHelper @eth <<0::160>> @fee_claimer_address "NO FEE CLAIMER ADDR!" deffixture(entities, do: entities()) deffixture(alice(entities), do: entities.alice) deffixture(bob(entities), do: entities.bob) deffixture(carol(entities), do: entities.carol) deffixture(stable_alice(entities), do: entities.stable_alice) deffixture(stable_bob(entities), do: entities.stable_bob) deffixture(stable_mallory(entities), do: entities.stable_mallory) deffixture state_empty() do child_block_interval = Configuration.child_block_interval() {:ok, state} = Core.extract_initial_state(0, child_block_interval, @fee_claimer_address) state end deffixture state_alice_deposit(state_empty, alice) do do_deposit(state_empty, alice, %{amount: 10, currency: @eth, blknum: 1}) end deffixture state_stable_alice_deposit(state_empty, stable_alice) do do_deposit(state_empty, stable_alice, %{amount: 10, currency: @eth, blknum: 1}) end deffixture in_beam_watcher(db_initialized, contract) do :ok = db_initialized _ = contract case System.get_env("DOCKER_GETH") do nil -> :ok _ -> # have to hack my way out of this so that we can migrate the watcher integration tests out Application.put_env(:omg_watcher, :exit_processor_sla_margin, 40) end {:ok, started_apps} = Application.ensure_all_started(:omg_db) {:ok, started_security_watcher} = Application.ensure_all_started(:omg_watcher) {:ok, started_watcher_api} = Application.ensure_all_started(:omg_watcher_rpc) wait_for_web() on_exit(fn -> Application.put_env(:omg_db, :path, nil) (started_apps ++ started_security_watcher ++ started_watcher_api) |> Enum.reverse() |> Enum.map(fn app -> :ok = Application.stop(app) end) Process.sleep(5_000) end) end deffixture test_server do server_id = :watcher_test_server {:ok, pid} = FakeServer.start(server_id) real_addr = Application.fetch_env!(:omg_watcher, :child_chain_url) old_client_env = Application.fetch_env!(:omg_watcher, :child_chain_url) {:ok, port} = FakeServer.port(server_id) fake_addr = "http://localhost:#{port}" on_exit(fn -> Application.put_env(:omg_watcher, :child_chain_url, old_client_env) FakeServer.stop(server_id) end) %{ real_addr: real_addr, fake_addr: fake_addr, server_id: server_id, server_pid: pid } end defp wait_for_web(), do: wait_for_web(100) defp wait_for_web(counter) do case Keyword.has_key?(Alarm.all(), elem(Alarm.main_supervisor_halted(__MODULE__), 0)) do true -> Process.sleep(100) wait_for_web(counter - 1) false -> :ok end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/api/account_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.AccountTest do use ExUnit.Case, async: false alias OMG.Watcher.API.Account alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo @eth <<0::160>> @payment_output_type OMG.Watcher.WireFormatTypes.output_type_for(:output_payment_v1) setup do db_path = Briefly.create!(directory: true) Application.put_env(:omg_db, :path, db_path, persistent: true) :ok = OMG.DB.init(db_path) {:ok, started_apps} = Application.ensure_all_started(:omg_db) on_exit(fn -> Application.put_env(:omg_db, :path, nil) started_apps |> Enum.reverse() |> Enum.map(fn app -> :ok = Application.stop(app) end) end) :ok end describe "get_exitable_utxos/1" do test "returns an empty list if the address does not have any utxo" do alice = TestHelper.generate_entity() assert Account.get_exitable_utxos(alice.addr) == [] end test "returns utxos for the given address" do alice = TestHelper.generate_entity() bob = TestHelper.generate_entity() blknum = 1927 txindex = 78 oindex = 1 _ = OMG.DB.multi_update([ {:put, :utxo, { {blknum, txindex, oindex}, %{ output: %{amount: 333, currency: @eth, owner: alice.addr, output_type: @payment_output_type}, creating_txhash: nil } }}, {:put, :utxo, { {blknum, txindex, oindex + 1}, %{ output: %{amount: 999, currency: @eth, owner: bob.addr, output_type: @payment_output_type}, creating_txhash: nil } }} ]) [utxo] = Account.get_exitable_utxos(alice.addr) assert %{blknum: ^blknum, txindex: ^txindex, oindex: ^oindex} = utxo end test "does not return exiting utxos" do alice = TestHelper.generate_entity() amount = 333 blknum = 1927 txindex = 78 oindex = 1 utxo_position = Utxo.position(blknum, txindex, oindex) _ = OMG.DB.multi_update([ {:put, :utxo, { {blknum, txindex, oindex}, %{ output: %{amount: amount, currency: @eth, owner: alice.addr, output_type: @payment_output_type}, creating_txhash: nil } }}, {:put, :exit_info, { Utxo.Position.to_db_key(utxo_position), %{ amount: amount, currency: @eth, owner: alice.addr, is_active: true, exit_id: 1, exiting_txbytes: <<0>>, eth_height: 1, root_chain_txhash: nil } }} ]) assert [] == Account.get_exitable_utxos(alice.addr) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/api/alarm_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.AlarmTest do use ExUnit.Case, async: false alias OMG.Watcher.API.Alarm setup %{} do {:ok, apps} = Application.ensure_all_started(:omg_status) system_alarm = {:system_memory_high_watermark, []} system_disk_alarm = {{:disk_almost_full, "/dev/null"}, []} app_alarm = {:ethereum_connection_error, %{node: Node.self(), reporter: Reporter}} on_exit(fn -> apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) %{system_alarm: system_alarm, system_disk_alarm: system_disk_alarm, app_alarm: app_alarm} end test "if alarms are returned when there are no alarms raised", _ do _ = OMG.Status.Alert.Alarm.clear_all() {:ok, []} = Alarm.get_alarms() end test "if alarms are returned when there are alarms raised", %{ system_alarm: system_alarm, system_disk_alarm: system_disk_alarm, app_alarm: app_alarm } do :ok = :alarm_handler.set_alarm(system_alarm) :ok = :alarm_handler.set_alarm(app_alarm) :ok = :alarm_handler.set_alarm(system_disk_alarm) find_alarms = [ {{:disk_almost_full, "/dev/null"}, []}, {:ethereum_connection_error, %{node: Node.self(), reporter: Reporter}}, {:system_memory_high_watermark, []} ] {:ok, alarms} = Alarm.get_alarms() ^find_alarms = Enum.filter( alarms, fn alarm -> Enum.member?(find_alarms, alarm) end ) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/api/status_cache_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.API.StatusCacheTest do use ExUnit.Case, async: true alias __MODULE__.BusMock alias __MODULE__.IntegrationModuleMock alias OMG.Watcher.API.StatusCache alias OMG.Watcher.API.StatusCache.Storage alias OMG.Watcher.SyncSupervisor setup do :ok = Storage.ensure_ets_init(SyncSupervisor.status_cache()) :ok end describe "get/0" do test "read from set ets" do :ets.insert(SyncSupervisor.status_cache(), {:status, :yolo}) :yolo = StatusCache.get() :ets.delete(SyncSupervisor.status_cache(), :status) end end describe "start_link/1" do test "process stands up and inserts data on block message" do {:ok, pid} = StatusCache.start_link( ets: SyncSupervisor.status_cache(), event_bus: BusMock, integration_module: IntegrationModuleMock ) assert StatusCache.get() == %{41 => 42} :erlang.trace(pid, true, [:receive]) Kernel.send(pid, {:internal_event_bus, :ethereum_new_height, 43}) assert_receive {:trace, _, :receive, {:internal_event_bus, :ethereum_new_height, 43}} :erlang.trace(pid, false, [:receive]) assert StatusCache.get() == %{43 => 42} end end defmodule IntegrationModuleMock do def get_status(eth_block_number) do {:ok, %{eth_block_number => 42}} end def get_ethereum_height() do {:ok, 42 - 1} end end defmodule BusMock do def subscribe(_, _) do :ok end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/block_getter/core_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockGetter.CoreTest do use ExUnitFixtures use ExUnit.Case, async: true use OMG.Watcher.Fixtures use Plug.Test alias OMG.Watcher.Block alias OMG.Watcher.BlockGetter.BlockApplication alias OMG.Watcher.BlockGetter.Core alias OMG.Watcher.Event @eth <<0::160>> def assert_check(result, status, value) do assert {^status, new_state, ^value} = result new_state end def assert_check(result, value) do assert {new_state, ^value} = result new_state end defp handle_downloaded_block(state, {:ok, block}, error, events) do assert {^error, %{events: ^events}} = new_state = Core.handle_downloaded_block(state, {:ok, block}) new_state end defp handle_downloaded_block(state, {:ok, block}) do assert {:ok, new_state} = Core.handle_downloaded_block(state, {:ok, block}) new_state end defp handle_downloaded_block(state, block) do assert {:ok, new_state} = Core.handle_downloaded_block(state, {:ok, block}) new_state end test "get numbers of blocks to download" do init_state(init_opts: [maximum_number_of_pending_blocks: 4]) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000, 2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 4_000}) |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([5_000, 6_000]) end test "first block to download number is not zero" do init_state(start_block_number: 7_000, interval: 100, init_opts: [maximum_number_of_pending_blocks: 4]) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([7_100, 7_200, 7_300, 7_400]) |> handle_downloaded_block(%BlockApplication{number: 7_200}) |> handle_downloaded_block({:ok, %BlockApplication{number: 7_100}}) end test "does not download same blocks twice and respects increasing next block number" do init_state(init_opts: [maximum_number_of_pending_blocks: 5]) |> Core.get_numbers_of_blocks_to_download(4_000) |> assert_check([1_000, 2_000, 3_000]) |> Core.get_numbers_of_blocks_to_download(2_000) |> assert_check([]) |> Core.get_numbers_of_blocks_to_download(8_000) |> assert_check([4_000, 5_000]) end test "downloaded duplicated and unexpected block" do state = init_state(init_opts: [maximum_number_of_pending_blocks: 5]) |> Core.get_numbers_of_blocks_to_download(3_000) |> assert_check([1_000, 2_000]) assert {{:error, :duplicate}, state} = state |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> Core.handle_downloaded_block({:ok, %BlockApplication{number: 2_000}}) assert {{:error, :unexpected_block}, _} = state |> Core.handle_downloaded_block({:ok, %BlockApplication{number: 3_000}}) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "decodes block and validates transaction execution", %{ alice: alice, bob: bob, state_alice_deposit: state_alice_deposit } do block = [OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}])] |> Block.hashed_txs_at(26_000) state = process_single_block(block) synced_height = 2 assert {[ %BlockApplication{ transactions: [tx], eth_height: ^synced_height, eth_height_done: true } ], _, _, _} = Core.get_blocks_to_apply(state, [%{blknum: block.number, eth_height: synced_height}], synced_height) # check feasibility of transactions from block to consume at the OMG.State assert {:ok, tx_result, _} = OMG.Watcher.State.Core.exec(state_alice_deposit, tx, :ignore_fees) assert {:ok, ^state} = Core.validate_executions([{:ok, tx_result}], block, state) assert {:ok, []} = Core.chain_ok(state) end @tag fixtures: [:alice, :bob] test "decodes and executes tx with different currencies, always with no fee required", %{alice: alice, bob: bob} do other_currency = <<1::160>> block = [ OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], other_currency, [{bob, 7}, {alice, 3}]), OMG.Watcher.TestHelper.create_recovered([{2, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]) ] |> Block.hashed_txs_at(26_000) state = process_single_block(block) synced_height = 2 assert {[%BlockApplication{transactions: [_tx1, _tx2]}], _, _, _} = Core.get_blocks_to_apply(state, [%{blknum: block.number, eth_height: synced_height}], synced_height) end defp process_single_block(%Block{hash: requested_hash} = block) do block_height = 25_000 interval = 1_000 {state, _} = init_state(start_block_number: block_height, interval: interval) |> Core.get_numbers_of_blocks_to_download(block_height + 2 * interval) assert {:ok, decoded_block} = Core.validate_download_response({:ok, block}, requested_hash, block_height + interval, 0, 0) handle_downloaded_block(state, decoded_block) end @tag fixtures: [:alice] test "does not validate block with invalid hash", %{alice: alice} do matching_bad_returned_hash = <<12::256>> state = init_state() block = %Block{ Block.hashed_txs_at([OMG.Watcher.TestHelper.create_recovered([{1_000, 20, 0, alice}], @eth, [{alice, 100}])], 1) | hash: matching_bad_returned_hash } assert {:error, {:incorrect_hash, matching_bad_returned_hash, 1}} == Core.validate_download_response({:ok, block}, matching_bad_returned_hash, 1, 0, 0) events = [%Event.InvalidBlock{error_type: :incorrect_hash, hash: matching_bad_returned_hash, blknum: 1}] assert {{:error, :incorrect_hash}, %{events: ^events}} = Core.handle_downloaded_block(state, {:error, {:incorrect_hash, matching_bad_returned_hash, 1}}) end @tag fixtures: [:alice] test "check error returned by decoding, one of Transaction.Recovered.recover_from checks", %{alice: alice} do # NOTE: this test only test if Transaction.Recovered.recover_from-specific checks are run and errors returned # the more extensive testing of such checks is done in API.CoreTest where it belongs %Block{hash: hash} = block = [OMG.Watcher.TestHelper.create_recovered([{1_000, 20, 0, alice}], @eth, [{alice, 100}])] |> Block.hashed_txs_at(1) block = %{block | transactions: block.transactions ++ [<<34>>]} # a particular Transaction.Recovered.recover_from error instance assert {:error, {:malformed_transaction, hash, 1}} == Core.validate_download_response({:ok, block}, hash, 1, 0, 0) end test "check error returned by decode_block, hash mismatch checks" do hash = <<12::256>> block = Block.hashed_txs_at([], 1) assert {:error, {:bad_returned_hash, hash, 1}} == Core.validate_download_response({:ok, block}, hash, 1, 0, 0) end test "the blknum is checked against the requested one" do %Block{hash: hash} = block = Block.hashed_txs_at([], 1) assert {:error, {:bad_returned_number, ^hash, 2}} = Core.validate_download_response({:ok, block}, hash, 2, 0, 0) end test "handle_downloaded_block function called once with PotentialWithholdingReport doesn't return BlockWithholding event, and get_numbers_of_blocks_to_download function returns this block" do {:ok, %Core.PotentialWithholdingReport{}} = potential_withholding = Core.validate_download_response({:error, :error_reason}, <<>>, 2_000, 0, 0) init_state() |> Core.get_numbers_of_blocks_to_download(3_000) |> assert_check([1_000, 2_000]) |> handle_downloaded_block(potential_withholding) |> Core.get_numbers_of_blocks_to_download(3_000) |> assert_check([2_000]) end test "handle_downloaded_block function called twice with PotentialWithholdingReport returns BlockWithholding event" do requested_hash = <<1>> init_state(init_opts: [maximum_number_of_pending_blocks: 5, maximum_block_withholding_time_ms: 0]) |> Core.get_numbers_of_blocks_to_download(3_000) |> assert_check([1_000, 2_000]) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, requested_hash, 2_000, 0, 0)) |> handle_downloaded_block( Core.validate_download_response({:error, :error_reason}, requested_hash, 2_000, 0, 1), {:error, :withholding}, [%Event.BlockWithholding{blknum: 2000, hash: requested_hash}] ) end test "get_numbers_of_blocks_to_download function returns number of potential withholding block which then is canceled" do init_state(init_opts: [maximum_number_of_pending_blocks: 4, maximum_block_withholding_time_ms: 0]) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000, 2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 1_000}) |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, <<>>, 3_000, 0, 0)) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([3_000]) |> handle_downloaded_block(%BlockApplication{number: 3_000}) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([5_000, 6_000, 7_000]) end test "get_numbers_of_blocks_to_download does not return blocks that are being downloaded" do init_state(init_opts: [maximum_number_of_pending_blocks: 4, maximum_block_withholding_time_ms: 0]) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000, 2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 1_000}) |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, <<>>, 3_000, 0, 0)) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([3_000, 5_000, 6_000]) |> handle_downloaded_block(%BlockApplication{number: 5_000}) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([7_000]) end test "get_numbers_of_blocks_to_download function doesn't return next blocks if state doesn't have empty slots left" do init_state(init_opts: [maximum_number_of_pending_blocks: 3]) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000, 2_000, 3_000]) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, <<>>, 1_000, 0, 0)) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, <<>>, 2_000, 0, 0)) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, <<>>, 3_000, 0, 0)) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000, 2_000, 3_000]) end test "handle_downloaded_block function after maximum_block_withholding_time_ms returns BlockWithholding event" do requested_hash = <<1>> init_state(init_opts: [maximum_number_of_pending_blocks: 4, maximum_block_withholding_time_ms: 1000]) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, requested_hash, 3_000, 0, 0)) |> handle_downloaded_block(Core.validate_download_response({:error, :error_reason}, requested_hash, 3_000, 0, 500)) |> handle_downloaded_block( Core.validate_download_response({:error, :error_reason}, requested_hash, 3_000, 0, 1000), {:error, :withholding}, [%Event.BlockWithholding{blknum: 3_000, hash: requested_hash}] ) end test "allows progressing when no unchallenged exits are detected" do assert {:ok, []} = init_state() |> Core.consider_exits({:ok, []}) |> Core.chain_ok() assert {:ok, []} = init_state() |> Core.consider_exits({:ok, [%Event.InvalidExit{}]}) |> Core.chain_ok() end @tag :capture_log test "prevents progressing when unchallenged_exit is detected" do assert {:error, []} = init_state() |> Core.consider_exits({{:error, :unchallenged_exit}, []}) |> Core.chain_ok() end @tag :capture_log test "prevents applying when started with an unchallenged_exit" do state = init_state(exit_processor_results: {{:error, :unchallenged_exit}, []}) assert {:error, []} = Core.chain_ok(state) end test "validate_executions function prevent getter from progressing when invalid block is detected" do state = init_state() block = %Block{number: 1, hash: <<>>} assert {{:error, {:tx_execution, :some_exec_error_reason}}, state} = Core.validate_executions([{:error, :some_exec_error_reason}], block, state) assert {:error, [%Event.InvalidBlock{error_type: :tx_execution, hash: "", blknum: 1}]} = Core.chain_ok(state) end test "after detecting twice same maximum possible potential withholdings get_numbers_of_blocks_to_download don't return this block" do potential_withholding_1_000 = Core.validate_download_response({:error, :error_reson}, <<>>, 1_000, 0, 0) potential_withholding_2_000 = Core.validate_download_response({:error, :error_reson}, <<>>, 2_000, 0, 0) init_state(init_opts: [maximum_number_of_pending_blocks: 2, maximum_block_withholding_time_ms: 10_000]) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000, 2_000]) |> handle_downloaded_block(potential_withholding_1_000) |> handle_downloaded_block(potential_withholding_2_000) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000, 2_000]) |> handle_downloaded_block(potential_withholding_2_000) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([2_000]) |> handle_downloaded_block(potential_withholding_1_000) |> Core.get_numbers_of_blocks_to_download(20_000) |> assert_check([1_000]) end test "applying block updates height" do state = init_state(synced_height: 0, init_opts: [maximum_number_of_pending_blocks: 5]) |> Core.get_numbers_of_blocks_to_download(4_000) |> assert_check([1_000, 2_000, 3_000]) |> handle_downloaded_block(%BlockApplication{number: 1_000}) |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> handle_downloaded_block(%BlockApplication{number: 3_000}) synced_height = 2 next_synced_height = synced_height + 1 assert {[application1, application2], 0, [], state} = Core.get_blocks_to_apply( state, [%{blknum: 1_000, eth_height: synced_height}, %{blknum: 2_000, eth_height: synced_height}], synced_height ) assert {state, 0, []} = Core.apply_block(state, application1) assert {state, ^synced_height, [{:put, :last_block_getter_eth_height, ^synced_height}]} = Core.apply_block(state, application2) assert {[application3], ^synced_height, [], state} = Core.get_blocks_to_apply(state, [%{blknum: 3_000, eth_height: next_synced_height}], next_synced_height) assert {state, ^next_synced_height, [{:put, :last_block_getter_eth_height, ^next_synced_height}]} = Core.apply_block(state, application3) # weird case when submissions for next_synced_height are now empty assert {[], ^next_synced_height, [], ^state} = Core.get_blocks_to_apply(state, [], next_synced_height) # moving forward next_synced_height2 = next_synced_height + 1 assert {[], ^next_synced_height2, [{:put, :last_block_getter_eth_height, ^next_synced_height2}], _} = Core.get_blocks_to_apply(state, [], next_synced_height2) end test "long running applying block scenario" do # this test replicates a long running scenario, with various inputs from the root chain coordinator # We're testing if we're applying blocks and height updates correctly # child block submissions on the root chain, by eth_height submissions = %{ 57 => [%{blknum: 0, eth_height: 57}], 58 => [%{blknum: 1000, eth_height: 58}], 59 => [%{blknum: 2000, eth_height: 59}], 60 => [%{blknum: 3000, eth_height: 60}], 61 => [%{blknum: 4000, eth_height: 61}, %{blknum: 5000, eth_height: 61}], 62 => [], 63 => [%{blknum: 6000, eth_height: 63}, %{blknum: 7000, eth_height: 63}], 64 => [] } # take a flattened list of submissions between two heights (inclusive, just like Eth events API works) take_submissions = fn {first, last} -> range = Enum.to_list(Range.new(first, last)) submissions |> Map.take(range) |> Enum.flat_map(fn {_k, v} -> v end) end state = init_state(synced_height: 58, start_block_number: 1_000, init_opts: [maximum_number_of_pending_blocks: 3]) |> Core.get_numbers_of_blocks_to_download(16_000_000) |> assert_check([2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> handle_downloaded_block(%BlockApplication{number: 3_000}) |> handle_downloaded_block(%BlockApplication{number: 4_000}) # coordinator dwells in the past assert {[], 58, [], _} = Core.get_blocks_to_apply(state, take_submissions.({58, 58}), 58) # coordinator allows into the future assert {[application0, application1000], 58, [], state_alt} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 60)), 60 ) assert {_, 59, [{:put, :last_block_getter_eth_height, 59}]} = Core.apply_block(state_alt, application0) assert {_, 60, [{:put, :last_block_getter_eth_height, 60}]} = Core.apply_block(state_alt, application1000) # coordinator on time assert {[^application0], 58, [], state} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 59)), 59 ) assert {state, 59, [{:put, :last_block_getter_eth_height, 59}]} = Core.apply_block(state, application0) state = state |> Core.get_numbers_of_blocks_to_download(16_000_000) |> assert_check([5_000, 6_000, 7_000]) |> handle_downloaded_block(%BlockApplication{number: 5_000}) |> handle_downloaded_block(%BlockApplication{number: 6_000}) # coordinator dwells in the past assert {[], 59, [], ^state} = Core.get_blocks_to_apply(state, take_submissions.({59, 59}), 59) # coordinator allows into the future assert {[^application1000, application4000, application5000], 59, [], state_alt} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 61)), 61 ) assert {state_alt, 60, [{:put, :last_block_getter_eth_height, 60}]} = Core.apply_block(state_alt, application1000) assert {state_alt, 60, []} = Core.apply_block(state_alt, application4000) assert {_, 61, [{:put, :last_block_getter_eth_height, 61}]} = Core.apply_block(state_alt, application5000) # coordinator on time assert {[^application1000], 59, [], state} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 60)), 60 ) assert {state, 60, [{:put, :last_block_getter_eth_height, 60}]} = Core.apply_block(state, application1000) # coordinator dwells in the past assert {[], 60, [], ^state} = Core.get_blocks_to_apply(state, take_submissions.({60, 60}), 60) # coordinator allows into the future assert {[^application4000, ^application5000], 60, [], state_alt} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 62)), 62 ) assert {state_alt, 60, []} = Core.apply_block(state_alt, application4000) assert {_, 61, [{:put, :last_block_getter_eth_height, 61}]} = Core.apply_block(state_alt, application5000) # coordinator on time assert {[^application4000, ^application5000], 60, [], state} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 61)), 61 ) assert {state, 60, []} = Core.apply_block(state, application4000) assert {state, 61, [{:put, :last_block_getter_eth_height, 61}]} = Core.apply_block(state, application5000) # coordinator dwells in the past assert {[], 61, [], ^state} = Core.get_blocks_to_apply(state, take_submissions.({61, 61}), 61) # coordinator allows into the future assert {[application6000], 61, [], state_alt} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 63)), 63 ) application7000 = %BlockApplication{number: 7_000, eth_height: 63, eth_height_done: true} assert {state_alt, 61, []} = Core.apply_block(state_alt, application6000) assert {_, 63, [{:put, :last_block_getter_eth_height, 63}]} = Core.apply_block(state_alt, application7000) # coordinator on time assert {[], 62, [{:put, :last_block_getter_eth_height, 62}], state} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 62)), 62 ) # coordinator dwells in the past assert {[], 62, [], ^state} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 62)), 62 ) # coordinator allows into the future assert {[^application6000], 62, [], state_alt} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 64)), 64 ) assert {_, 62, []} = Core.apply_block(state_alt, application6000) # coordinator on time assert {[^application6000], 62, [], state} = Core.get_blocks_to_apply( state, take_submissions.(Core.get_eth_range_for_block_submitted_events(state, 63)), 63 ) assert {state, 62, []} = Core.apply_block(state, application6000) assert {_, 63, [{:put, :last_block_getter_eth_height, 63}]} = Core.apply_block(state, application7000) end test "gets continous ranges of blocks to apply" do state = init_state(synced_height: 0, init_opts: [maximum_number_of_pending_blocks: 5]) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([1_000, 2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 1_000}) |> handle_downloaded_block(%BlockApplication{number: 3_000}) |> handle_downloaded_block(%BlockApplication{number: 4_000}) {[%BlockApplication{eth_height: 1, eth_height_done: true}], _, _, state} = Core.get_blocks_to_apply(state, [%{blknum: 1_000, eth_height: 1}, %{blknum: 2_000, eth_height: 2}], 2) state = state |> handle_downloaded_block(%BlockApplication{number: 2_000}) {[%BlockApplication{eth_height: 2, eth_height_done: true}], _, _, _} = Core.get_blocks_to_apply(state, [%{blknum: 1_000, eth_height: 1}, %{blknum: 2_000, eth_height: 2}], 2) end test "do not download blocks when there are too many downloaded blocks not yet applied" do state = init_state( synced_height: 0, init_opts: [maximum_number_of_pending_blocks: 5, maximum_number_of_unapplied_blocks: 3] ) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([1_000, 2_000, 3_000]) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([]) |> handle_downloaded_block(%BlockApplication{number: 1_000}) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([]) synced_height = 1 {_, _, _, state} = Core.get_blocks_to_apply(state, [%{blknum: 1_000, eth_height: synced_height}], synced_height) {_, [4_000]} = Core.get_numbers_of_blocks_to_download(state, 5_000) end test "when State is not at the beginning should not init state properly" do assert init_state(state_at_beginning: false) == {:error, :not_at_block_beginning} end test "maximum_number_of_pending_blocks can't be too low" do assert init_state(init_opts: [maximum_number_of_pending_blocks: 0]) == {:error, :maximum_number_of_pending_blocks_too_low} end test "BlockGetter omits submissions of already applied blocks" do state = init_state(synced_height: 1, start_block_number: 1000) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 2_000}) submissions = [%{blknum: 1_000, eth_height: 1}, %{blknum: 2_000, eth_height: 2}] {[application], 1, [], state} = Core.get_blocks_to_apply(state, submissions, 2) # apply that and see if we won't get the same thing again {state, 2, _} = Core.apply_block(state, application) {[], 2, _, _} = Core.get_blocks_to_apply(state, submissions, 2) end test "an unapplied block appears in an already synced eth block (due to reorg)" do state = init_state(synced_height: 2, start_block_number: 1000) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> handle_downloaded_block(%BlockApplication{number: 3_000}) {[ %BlockApplication{number: 2_000, eth_height: 1, eth_height_done: true}, %BlockApplication{number: 3_000, eth_height: 3, eth_height_done: true} ], 2, _, _} = Core.get_blocks_to_apply(state, [%{blknum: 2_000, eth_height: 1}, %{blknum: 3_000, eth_height: 3}], 3) end test "an already applied child chain block appears in a block above synced_height (due to a reorg)" do state = init_state(start_block_number: 1_000) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([2_000, 3_000, 4_000]) |> handle_downloaded_block(%BlockApplication{number: 2_000}) {[%BlockApplication{number: 2_000, eth_height: 3, eth_height_done: true}], 1, [], _} = Core.get_blocks_to_apply(state, [%{blknum: 1_000, eth_height: 3}, %{blknum: 2_000, eth_height: 3}], 3) end test "apply block with eth_height lower than synced_height" do state = init_state(synced_height: 2) |> Core.get_numbers_of_blocks_to_download(2_000) |> assert_check([1_000]) |> handle_downloaded_block(%BlockApplication{number: 1_000}) {[application], 2, [], state} = Core.get_blocks_to_apply(state, [%{blknum: 1_000, eth_height: 1}], 3) {_, 1, _} = Core.apply_block(state, application) end test "apply a block that moved forward" do state = init_state(synced_height: 1, start_block_number: 1000) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([2_000, 3_000, 4_000]) # block 2_000 first appears at height 3 {[], 1, [], state} = Core.get_blocks_to_apply(state, [%{blknum: 2_000, eth_height: 3}, %{blknum: 3_000, eth_height: 4}], 4) # download blocks state = state |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> handle_downloaded_block(%BlockApplication{number: 3_000}) # block then moves forward {[application1, application2], 1, [], state} = Core.get_blocks_to_apply(state, [%{blknum: 2_000, eth_height: 4}, %{blknum: 3_000, eth_height: 4}], 4) # the block is applied at height it was first seen {state, 1, _} = Core.apply_block(state, application1) {_, 4, _} = Core.apply_block(state, application2) end test "apply a block that moved backward" do state = init_state(synced_height: 1, start_block_number: 1000) |> Core.get_numbers_of_blocks_to_download(5_000) |> assert_check([2_000, 3_000, 4_000]) # block 2_000 first appears at height 3 {[], 1, [], state} = Core.get_blocks_to_apply(state, [%{blknum: 2_000, eth_height: 3}, %{blknum: 3_000, eth_height: 4}], 4) # download blocks state = state |> handle_downloaded_block(%BlockApplication{number: 2_000}) |> handle_downloaded_block(%BlockApplication{number: 3_000}) # block then moves backward {[application1, application2], 1, [], state} = Core.get_blocks_to_apply(state, [%{blknum: 2_000, eth_height: 2}, %{blknum: 3_000, eth_height: 4}], 4) # the block is applied at updated height {state, 2, _} = Core.apply_block(state, application1) {_, 4, _} = Core.apply_block(state, application2) end test "move forward even though an applied block appears in submissions" do state = init_state(start_block_number: 1_000, synced_height: 2) |> Core.get_numbers_of_blocks_to_download(3_000) |> assert_check([2_000]) {[], 3, [_], _} = Core.get_blocks_to_apply(state, [%{blknum: 1_000, eth_height: 1}], 3) end test "returns valid eth range" do # properly looks `block_getter_reorg_margin` number of blocks backward state = init_state(synced_height: 100, block_getter_reorg_margin: 10) assert {100 - 10, 101} == Core.get_eth_range_for_block_submitted_events(state, 101) # beginning of the range is no less than 0 state = init_state(synced_height: 0, block_getter_reorg_margin: 10) assert {0, 101} == Core.get_eth_range_for_block_submitted_events(state, 101) end defp init_state(opts \\ []) do init_params = [ start_block_number: 0, interval: 1_000, synced_height: 1, block_getter_reorg_margin: 5, state_at_beginning: true, exit_processor_results: {:ok, []}, init_opts: [] ] |> Keyword.merge(opts) |> Map.new() with {:ok, state} <- Core.init( init_params.start_block_number, init_params.interval, init_params.synced_height, init_params.block_getter_reorg_margin, init_params.state_at_beginning, init_params.exit_processor_results, init_params.init_opts ), do: state end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/block_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.BlockTest do @moduledoc """ Simple unit test of part of `OMG.Watcher.Block`. """ use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.Block alias OMG.Watcher.TestHelper defp eth(), do: <<0::160>> describe "hashed_txs_at/2" do @tag fixtures: [:stable_alice, :stable_bob] test "returns a block with the list of transactions and a computed merkle root hash", %{ stable_alice: alice, stable_bob: bob } do tx_1 = TestHelper.create_recovered([{1, 0, 0, alice}], eth(), [{bob, 100}]) tx_2 = TestHelper.create_recovered([{1, 1, 1, alice}], eth(), [{bob, 100}]) transactions = [tx_1, tx_2] assert Block.hashed_txs_at(transactions, 10) == %Block{ hash: <<189, 245, 69, 5, 94, 45, 148, 210, 5, 89, 98, 245, 201, 111, 222, 48, 61, 114, 145, 55, 122, 84, 196, 156, 254, 80, 85, 184, 3, 205, 163, 233>>, number: 10, transactions: [ tx_1.signed_tx_bytes, tx_2.signed_tx_bytes ] } end @tag timeout: 60_000 * 3 @tag fixtures: [:stable_alice, :stable_bob] test "correctly calculates hash for a lot of transactions", %{ stable_alice: alice, stable_bob: bob } do transactions = Enum.map(1..64_000, fn index -> TestHelper.create_recovered([{1, index, index, alice}], eth(), [{bob, 100}]) end) block = Block.hashed_txs_at(transactions, 10) assert block.hash == <<12, 40, 202, 7, 16, 175, 119, 138, 7, 95, 8, 3, 148, 93, 162, 168, 136, 226, 196, 236, 83, 62, 220, 75, 59, 52, 6, 18, 249, 52, 124, 228>> end test "handles an empty list of transactions" do assert Block.hashed_txs_at([], 10) == %Block{ hash: <<246, 9, 190, 253, 254, 144, 102, 254, 20, 231, 67, 179, 98, 62, 174, 135, 143, 188, 70, 128, 5, 96, 136, 22, 131, 44, 157, 70, 15, 42, 149, 210>>, number: 10, transactions: [] } end end describe "to_api_format/1" do test "formats to map for API" do block = %Block{ hash: "hash", number: 10, transactions: ["tx_1_bytes", "tx_2_bytes"] } assert Block.to_api_format(block) == %{ blknum: 10, hash: "hash", transactions: ["tx_1_bytes", "tx_2_bytes"] } end end describe "to_db_value/1" do test "formats to DB format with valid inputs" do block = %Block{ hash: "hash", number: 10, transactions: ["tx_1_bytes", "tx_2_bytes"] } assert Block.to_db_value(block) == %{ hash: "hash", number: 10, transactions: ["tx_1_bytes", "tx_2_bytes"] } end test "fails to format when the list of transactions is not a list" do # Not sure how we want to handle this, there is currently no # fallback function in the Block module assert_raise FunctionClauseError, fn -> Block.to_db_value(%Block{ hash: "hash", number: 10, transactions: %{} }) end end test "fails to format when the hash is not a binary" do assert_raise FunctionClauseError, fn -> Block.to_db_value(%Block{ hash: 1, number: 10, transactions: [] }) end end test "fails to format when the number is not an integer" do assert_raise FunctionClauseError, fn -> Block.to_db_value(%Block{ hash: 1, number: "10", transactions: [] }) end end end describe "from_db_value/1" do test "formats from DB format with valid inputs" do block = %{ hash: "hash", number: 10, transactions: ["tx_1_bytes", "tx_2_bytes"] } assert Block.from_db_value(block) == %Block{ hash: "hash", number: 10, transactions: ["tx_1_bytes", "tx_2_bytes"] } end test "fails to format when the list of transactions is not a list" do # Not sure how we want to handle this, there is currently no # fallback function in the Block module assert_raise FunctionClauseError, fn -> Block.from_db_value(%{ hash: "hash", number: 10, transactions: %{} }) end end test "fails to format when the hash is not a binary" do assert_raise FunctionClauseError, fn -> Block.from_db_value(%{ hash: 1, number: 10, transactions: [] }) end end test "fails to format when the number is not an integer" do assert_raise FunctionClauseError, fn -> Block.from_db_value(%{ hash: 1, number: "10", transactions: [] }) end end end describe "inclusion_proof/2" do # The tests below checks merkle proof normally tested via speaking to the contract # (integration tests) against a fixed binary. The motivation for having such # test is a quick test of whether the merkle proving didn't change. @tag fixtures: [:stable_alice] test "calculates the inclusion proof when a list of transactions is given", %{ stable_alice: alice } do # odd number of transactions, just in case tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}]) tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}]) tx_3 = TestHelper.create_encoded([{1, 0, 1, alice}], eth(), [{alice, 2}]) proof = Block.inclusion_proof([tx_1, tx_2, tx_3], 1) assert <<141, 42, 165, 123, 233, 242, 135, 178>> <> _ = proof assert is_binary(proof) assert byte_size(proof) == 32 * 16 end @tag fixtures: [:stable_alice] test "calculates the inclusion proof when a block is given", %{ stable_alice: alice } do tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}]) tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}]) proof = Block.inclusion_proof(%Block{transactions: [tx_1, tx_2]}, 1) assert is_binary(proof) assert byte_size(proof) == 32 * 16 end @tag fixtures: [:stable_alice] test "calculating a proof via a block or a list of transactions return the same result", %{ stable_alice: alice } do tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}]) tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}]) block_proof = Block.inclusion_proof(%Block{transactions: [tx_1, tx_2]}, 1) transactions_proof = Block.inclusion_proof([tx_1, tx_2], 1) assert block_proof == transactions_proof end test "calculates the inclusion proof when an empty list is given" do proof = Block.inclusion_proof([], 1) assert is_binary(proof) assert byte_size(proof) == 32 * 16 end test "raises an error when an invalid input is given (map)" do assert_raise FunctionClauseError, fn -> Block.inclusion_proof(%{}, 1) end end @tag fixtures: [:alice] test "calculates a block merkle proof for deposit transactions", %{alice: alice} do tx = TestHelper.create_encoded([], eth(), [{alice, 7}]) proof = Block.inclusion_proof(%Block{transactions: [tx]}, 0) assert is_binary(proof) assert byte_size(proof) == 32 * 16 end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/block_validator_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.BlockValidatorTest do use ExUnit.Case, async: true use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.BlockValidator alias OMG.Watcher.Merkle alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper @alice TestHelper.generate_entity() @bob TestHelper.generate_entity() @eth <<0::160>> @payment_tx_type OMG.Watcher.WireFormatTypes.tx_type_for(:tx_payment_v1) @fee_claimer <<27::160>> @transaction_upper_limit 2 |> :math.pow(16) |> Kernel.trunc() describe "stateless_validate/1" do test "returns an error if a transaction within the block is not correctly formed (e.g. duplicate inputs in this test)" do input_1 = {1, 0, 0, @alice} input_2 = {2, 0, 0, @alice} input_3 = {3, 0, 0, @alice} signed_valid_tx = TestHelper.create_signed([input_1, input_2], @eth, [{@bob, 10}]) signed_invalid_tx = TestHelper.create_signed([input_3, input_3], @eth, [{@bob, 10}]) %{sigs: sigs_valid} = signed_valid_tx %{sigs: sigs_invalid} = signed_invalid_tx txbytes_valid = Transaction.raw_txbytes(signed_valid_tx) txbytes_invalid = Transaction.raw_txbytes(signed_invalid_tx) [_, inputs_valid, outputs_valid, _, _] = ExRLP.decode(txbytes_valid) [_, inputs_invalid, outputs_invalid, _, _] = ExRLP.decode(txbytes_invalid) hash_valid = ExRLP.encode([sigs_valid, @payment_tx_type, inputs_valid, outputs_valid, 0, <<0::256>>]) hash_invalid = ExRLP.encode([ sigs_invalid, @payment_tx_type, inputs_invalid, outputs_invalid, 0, <<0::256>> ]) block = %{ hash: Merkle.hash([txbytes_valid, txbytes_invalid]), number: 1000, transactions: [hash_invalid, hash_valid] } assert {:error, :duplicate_inputs} == BlockValidator.stateless_validate(block) end test "accepts correctly formed transactions" do recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}, {2, 0, 0, @alice}], @eth, [{@bob, 10}]) recovered_tx_2 = TestHelper.create_recovered([{3, 0, 0, @alice}, {4, 0, 0, @alice}], @eth, [{@bob, 10}]) signed_txbytes_1 = recovered_tx_1.signed_tx_bytes signed_txbytes_2 = recovered_tx_2.signed_tx_bytes block = %{ hash: derive_merkle_root([recovered_tx_1, recovered_tx_2]), number: 1000, transactions: [signed_txbytes_1, signed_txbytes_2] } assert {:ok, true} == BlockValidator.stateless_validate(block) end test "returns an error if the given hash does not match the reconstructed Merkle root hash" do recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) signed_txbytes = Enum.map([recovered_tx_1, recovered_tx_2], fn tx -> tx.signed_tx_bytes end) block = %{ hash: "0x0", number: 1000, transactions: signed_txbytes } assert {:error, :invalid_merkle_root} == BlockValidator.stateless_validate(block) end test "accepts a matching Merkle root hash" do recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) signed_txbytes = Enum.map([recovered_tx_1, recovered_tx_2], fn tx -> tx.signed_tx_bytes end) valid_merkle_root = derive_merkle_root([recovered_tx_1, recovered_tx_2]) block = %{ hash: valid_merkle_root, number: 1000, transactions: signed_txbytes } assert {:ok, true} = BlockValidator.stateless_validate(block) end test "rejects a block with no transactions or more transactions than the defined limit" do oversize_block = %{ hash: "0x0", number: 1000, transactions: List.duplicate("0x0", @transaction_upper_limit + 1) } undersize_block = %{ hash: "0x0", number: 1000, transactions: [] } assert {:error, :transactions_exceed_block_limit} = BlockValidator.stateless_validate(oversize_block) assert {:error, :empty_block} = BlockValidator.stateless_validate(undersize_block) end test "rejects a block that uses the same input in different transactions" do duplicate_input = {1, 0, 0, @alice} recovered_tx_1 = TestHelper.create_recovered([duplicate_input], @eth, [{@bob, 10}]) recovered_tx_2 = TestHelper.create_recovered([duplicate_input], @eth, [{@bob, 10}]) signed_txbytes_1 = recovered_tx_1.signed_tx_bytes signed_txbytes_2 = recovered_tx_2.signed_tx_bytes block = %{ hash: derive_merkle_root([recovered_tx_1, recovered_tx_2]), number: 1000, transactions: [signed_txbytes_1, signed_txbytes_2] } assert {:error, :block_duplicate_inputs} == BlockValidator.stateless_validate(block) end end describe "stateless_validate/1 (fee validation)" do test "rejects a block if there are multiple fee transactions of the same currency" do input_1 = {1, 0, 0, @alice} input_2 = {2, 0, 0, @alice} payment_tx_1 = TestHelper.create_recovered([input_1], @eth, [{@bob, 10}]) payment_tx_2 = TestHelper.create_recovered([input_2], @eth, [{@bob, 10}]) fee_tx_1 = TestHelper.create_recovered_fee_tx(1, @fee_claimer, @eth, 1) fee_tx_2 = TestHelper.create_recovered_fee_tx(1, @fee_claimer, @eth, 1) signed_txbytes = Enum.map([payment_tx_1, payment_tx_2, fee_tx_1, fee_tx_2], fn tx -> tx.signed_tx_bytes end) block = %{ hash: derive_merkle_root([payment_tx_1, payment_tx_2, fee_tx_1, fee_tx_2]), number: 1000, transactions: signed_txbytes } assert {:error, :duplicate_fee_transaction_for_ccy} = BlockValidator.stateless_validate(block) end test "rejects a block if fee transactions are not at the tail of the transactions' list (one fee currency)" do input_1 = {1, 0, 0, @alice} input_2 = {2, 0, 0, @alice} payment_tx_1 = TestHelper.create_recovered([input_1], @eth, [{@bob, 10}]) payment_tx_2 = TestHelper.create_recovered([input_2], @eth, [{@bob, 10}]) fee_tx = TestHelper.create_recovered_fee_tx(1, @fee_claimer, @eth, 5) invalid_ordered_transactions = [payment_tx_1, fee_tx, payment_tx_2] signed_txbytes = Enum.map(invalid_ordered_transactions, fn tx -> tx.signed_tx_bytes end) block = %{ hash: derive_merkle_root(invalid_ordered_transactions), number: 1000, transactions: signed_txbytes } assert {:error, :unexpected_transaction_type_at_fee_index} = BlockValidator.stateless_validate(block) end test "rejects a block if fee transactions are not at the tail of the transactions' list (two fee currencies)" do ccy_1 = @eth ccy_2 = <<1::160>> ccy_1_fee = 1 ccy_2_fee = 1 input_1 = {1, 0, 0, @alice} input_2 = {2, 0, 0, @alice} payment_tx_1 = TestHelper.create_recovered([input_1], ccy_1, [{@bob, 10}]) payment_tx_2 = TestHelper.create_recovered([input_2], ccy_2, [{@bob, 10}]) fee_tx_1 = TestHelper.create_recovered_fee_tx(1, @fee_claimer, ccy_1, ccy_1_fee) fee_tx_2 = TestHelper.create_recovered_fee_tx(1, @fee_claimer, ccy_2, ccy_2_fee) invalid_ordered_transactions = [payment_tx_1, fee_tx_1, payment_tx_2, fee_tx_2] signed_txbytes = Enum.map(invalid_ordered_transactions, fn tx -> tx.signed_tx_bytes end) block = %{ hash: derive_merkle_root(invalid_ordered_transactions), number: 1000, transactions: signed_txbytes } assert {:error, :unexpected_transaction_type_at_fee_index} = BlockValidator.stateless_validate(block) end end @spec derive_merkle_root([Transaction.Recovered.t()]) :: binary() defp(derive_merkle_root(transactions)) do transactions |> Enum.map(&Transaction.raw_txbytes/1) |> Merkle.hash() end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/child_manager_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ChildManagerTest do use ExUnit.Case, async: true alias OMG.Watcher.ChildManager test "that the process starts, sends a checkin to us and shuts down" do # start a mock module so that the child manager has someone to report to {:ok, _} = __MODULE__.Monitor.start(self()) {:ok, pid} = ChildManager.start_link(monitor: __MODULE__.Monitor) # when does the mananger send a health check? %{timer: timer} = :sys.get_state(pid) # we wait for that long assert_receive(:got_health_checkin, Kernel.trunc(timer * 1.5)) # make sure child manager has shutdown _ = Process.sleep(100) assert Process.alive?(pid) == false end defmodule Monitor do use GenServer def start(parent) do GenServer.start(__MODULE__, [parent], name: __MODULE__) end def init([parent]) do {:ok, parent} end def health_checkin() do GenServer.cast(__MODULE__, :health_checkin) end def handle_cast(:health_checkin, state) do _ = send(state, :got_health_checkin) {:stop, :normal, state} end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/crypto_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.CryptoTest do use ExUnit.Case, async: true doctest OMG.Watcher.Crypto @moduledoc """ A sanity and compatibility check of the crypto implementation. """ alias OMG.Watcher.Crypto alias OMG.Watcher.DevCrypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash describe "recover_address/2" do # Tests that we can digest, sign, and recover. test "recovers address of the signer from a binary-encoded signature" do {:ok, priv} = DevCrypto.generate_private_key() {:ok, pub} = DevCrypto.generate_public_key(priv) {:ok, address} = Crypto.generate_address(pub) msg = :crypto.strong_rand_bytes(32) sig = DevCrypto.signature_digest(msg, priv) assert {:ok, ^address} = Crypto.recover_address(msg, sig) end # Test that we can sign and verify test "recovers address from an encoded transaction" do {:ok, priv} = DevCrypto.generate_private_key() {:ok, pub} = DevCrypto.generate_public_key(priv) {:ok, address} = Crypto.generate_address(pub) raw_tx = Transaction.Payment.new([{1000, 1, 0}], []) signature = DevCrypto.signature(raw_tx, priv) assert byte_size(signature) == 65 assert raw_tx |> TypedDataHash.hash_struct() |> Crypto.recover_address(signature) |> (&match?({:ok, ^address}, &1)).() refute Transaction.Payment.new([{1000, 0, 1}], []) |> TypedDataHash.hash_struct() |> Crypto.recover_address(signature) |> (&match?({:ok, ^address}, &1)).() end end describe "generate_address/1" do test "generates an address with SHA3" do # test vectors below were generated using pyethereum's sha3 and privtoaddr py_priv = "7880aec93413f117ef14bd4e6d130875ab2c7d7d55a064fac3c2f7bd51516380" py_pub = "c4d178249d840f548b09ad8269e8a3165ce2c170" priv = Crypto.hash(<<"11">>) {:ok, pub} = DevCrypto.generate_public_key(priv) {:ok, address} = Crypto.generate_address(pub) {:ok, decoded_private} = Base.decode16(py_priv, case: :lower) {:ok, decoded_address} = Base.decode16(py_pub, case: :lower) assert ^decoded_private = priv assert ^address = decoded_address end test "generates an address with a public signature" do # test vector was generated using plasma.utils.utils.sign/2 from plasma-mvp py_signature = "b8670d619701733e1b4d10149bc90eb4eb276760d2f77a08a5428d4cbf2eadbd656f374c187b1ac80ce31d8c62076af26150e52ef1f33bfc07c6d244da7ca38c1c" msg = Crypto.hash("1234") priv = Crypto.hash("11") {:ok, pub} = DevCrypto.generate_public_key(priv) {:ok, _} = Crypto.generate_address(pub) sig = DevCrypto.signature_digest(msg, priv) assert ^sig = Base.decode16!(py_signature, case: :lower) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/datadog_event/contract_event_consumer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.DatadogEvent.ContractEventConsumerTest do @moduledoc false use ExUnit.Case, async: true alias OMG.Watcher.DatadogEvent.ContractEventConsumer setup_all do Application.ensure_all_started(:omg_bus) on_exit(fn -> :ok = Application.stop(:omg_bus) end) end setup do pid = :erlang.pid_to_list(self()) start_supervised( ContractEventConsumer.prepare_child( topic: {:root_chain, "#{pid}"}, release: "child_chain", current_version: "test-123", publisher: __MODULE__.DatadogEventMock ) ) :ok end test "if an event put on omg bus is consumed by the event consumer and published on the publisher interface" do topic_name = self() |> :erlang.pid_to_list() |> to_string() sig = "#{topic_name}(bytes32)" data = [%{event_signature: sig}] {:root_chain, topic_name} |> OMG.Bus.Event.new(:data, data) |> OMG.Bus.direct_local_broadcast() assert_receive {:event, _, _} end test "if a list of events put on omg bus is consumed by the event consumer, broken down and published individually on the publisher interface" do topic_name = self() |> :erlang.pid_to_list() |> to_string() sig = "#{topic_name}(bytes32)" data = [%{event_signature: sig, pos: 1}, %{event_signature: sig, pos: 2}] {:root_chain, topic_name} |> OMG.Bus.Event.new(:data, data) |> OMG.Bus.direct_local_broadcast() Enum.each(data, fn ev -> %{pos: pos} = ev assert_receive {:event, message, _} [[_, pos_from_message]] = Regex.scan(~r{pos:.*?(\d).*?}, message) assert pos == String.to_integer(pos_from_message) end) end defmodule DatadogEventMock do def event(title, message, options) do pid = title |> String.to_charlist() |> :erlang.list_to_pid() Kernel.send(pid, {:event, message, options}) :ok end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/datadog_event/encode_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.DatadogEvent.EncodeTest do @moduledoc false use ExUnit.Case, async: true alias OMG.Watcher.DatadogEvent.Encode test "if deposit created event can be decoded from log" do deposit_created_event = %{ amount: 10, blknum: 1, currency: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, eth_height: 390, event_signature: "DepositCreated(address,uint256,address,uint256)", log_index: 0, owner: <<59, 159, 76, 29, 210, 110, 11, 229, 147, 55, 59, 29, 54, 206, 226, 0, 140, 190, 184, 55>>, root_chain_txhash: <<77, 114, 166, 63, 244, 47, 29, 181, 10, 242, 195, 110, 139, 49, 65, 1, 210, 254, 163, 224, 0, 53, 117, 243, 2, 152, 233, 21, 63, 227, 216, 238>> } assert Encode.make_it_readable!(deposit_created_event) == %{ amount: 10, blknum: 1, currency: "0x0000000000000000000000000000000000000000", eth_height: 390, event_signature: "DepositCreated(address,uint256,address,uint256)", log_index: 0, owner: "0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837", root_chain_txhash: "0x4d72a63ff42f1db50af2c36e8b314101d2fea3e0003575f30298e9153fe3d8ee" } end test "if input piggybacked event log can be decoded" do input_piggybacked_event = %{ eth_height: 410, event_signature: "InFlightExitInputPiggybacked(address,bytes32,uint16)", log_index: 0, output_index: 1, owner: <<21, 19, 171, 205, 53, 144, 162, 94, 11, 237, 132, 6, 82, 217, 87, 57, 29, 222, 153, 85>>, root_chain_txhash: <<12, 201, 229, 85, 107, 189, 110, 234, 244, 48, 47, 68, 173, 202, 33, 87, 134, 255, 8, 207, 164, 74, 52, 190, 23, 96, 236, 166, 15, 151, 54, 79>>, tx_hash: <<255, 144, 183, 115, 3, 229, 107, 210, 48, 169, 173, 244, 166, 85, 58, 149, 245, 255, 181, 99, 72, 98, 5, 214, 251, 162, 93, 62, 70, 89, 73, 64>>, omg_data: %{piggyback_type: :input} } assert Encode.make_it_readable!(input_piggybacked_event) == %{ output_index: 1, owner: "0x1513abcd3590a25e0bed840652d957391dde9955", tx_hash: "0xff90b77303e56bd230a9adf4a6553a95f5ffb563486205d6fba25d3e46594940", eth_height: 410, event_signature: "InFlightExitInputPiggybacked(address,bytes32,uint16)", log_index: 0, omg_data: %{piggyback_type: :input}, root_chain_txhash: "0x0cc9e5556bbd6eeaf4302f44adca215786ff08cfa44a34be1760eca60f97364f" } end test "if output piggybacked event log can be decoded" do output_piggybacked_event = %{ eth_height: 408, event_signature: "InFlightExitOutputPiggybacked(address,bytes32,uint16)", log_index: 1, output_index: 1, owner: <<21, 19, 171, 205, 53, 144, 162, 94, 11, 237, 132, 6, 82, 217, 87, 57, 29, 222, 153, 85>>, root_chain_txhash: <<124, 244, 58, 96, 128, 233, 150, 119, 222, 224, 178, 108, 35, 228, 105, 177, 223, 156, 251, 86, 165, 195, 242, 160, 18, 61, 246, 237, 174, 123, 91, 94>>, tx_hash: <<255, 144, 183, 115, 3, 229, 107, 210, 48, 169, 173, 244, 166, 85, 58, 149, 245, 255, 181, 99, 72, 98, 5, 214, 251, 162, 93, 62, 70, 89, 73, 64>>, omg_data: %{piggyback_type: :output} } assert Encode.make_it_readable!(output_piggybacked_event) == %{ log_index: 1, omg_data: %{piggyback_type: :output}, output_index: 1, eth_height: 408, event_signature: "InFlightExitOutputPiggybacked(address,bytes32,uint16)", root_chain_txhash: "0x7cf43a6080e99677dee0b26c23e469b1df9cfb56a5c3f2a0123df6edae7b5b5e", owner: "0x1513abcd3590a25e0bed840652d957391dde9955", tx_hash: "0xff90b77303e56bd230a9adf4a6553a95f5ffb563486205d6fba25d3e46594940" } end test "if block emitted event log can be decoded" do block_submitted_event = %{ blknum: 1000, eth_height: 398, event_signature: "BlockSubmitted(uint256)", log_index: 0, root_chain_txhash: <<41, 117, 89, 151, 155, 94, 250, 133, 74, 210, 158, 33, 108, 118, 166, 76, 63, 67, 98, 27, 191, 61, 193, 110, 75, 49, 251, 12, 182, 220, 235, 244>> } assert Encode.make_it_readable!(block_submitted_event) == %{ blknum: 1000, eth_height: 398, event_signature: "BlockSubmitted(uint256)", log_index: 0, root_chain_txhash: "0x297559979b5efa854ad29e216c76a64c3f43621bbf3dc16e4b31fb0cb6dcebf4" } end test "if exit finalized event log can be decoded" do exit_finalized_event = %{ eth_height: 330, event_signature: "ExitFinalized(uint160)", exit_id: 1_423_280_346_484_099_708_949_144_162_169_101_241_792_387_057, log_index: 2, root_chain_txhash: <<190, 49, 10, 222, 65, 39, 140, 86, 7, 98, 3, 17, 183, 147, 99, 170, 82, 10, 196, 108, 123, 167, 84, 191, 48, 39, 213, 1, 197, 169, 95, 64>> } assert Encode.make_it_readable!(exit_finalized_event) == %{ eth_height: 330, event_signature: "ExitFinalized(uint160)", exit_id: 1_423_280_346_484_099_708_949_144_162_169_101_241_792_387_057, log_index: 2, root_chain_txhash: "0xbe310ade41278c5607620311b79363aa520ac46c7ba754bf3027d501c5a95f40" } end test "if in flight exit challanged can be decoded" do in_flight_exit_challanged_event = %{ challenger: <<122, 232, 25, 13, 153, 104, 203, 179, 181, 46, 86, 165, 107, 44, 212, 205, 94, 21, 164, 79>>, competitor_position: 115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935, eth_height: 406, event_signature: "InFlightExitChallenged(address,bytes32,uint256)", log_index: 0, root_chain_txhash: <<217, 227, 179, 170, 255, 129, 86, 218, 184, 176, 4, 136, 45, 59, 206, 131, 75, 168, 66, 201, 93, 239, 247, 236, 151, 218, 143, 148, 47, 135, 10, 180>>, tx_hash: <<117, 50, 82, 142, 194, 36, 57, 169, 161, 237, 95, 79, 206, 108, 214, 109, 113, 98, 90, 221, 98, 2, 206, 251, 151, 12, 16, 208, 79, 45, 80, 145>> } assert Encode.make_it_readable!(in_flight_exit_challanged_event) == %{ challenger: "0x7ae8190d9968cbb3b52e56a56b2cd4cd5e15a44f", competitor_position: 115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935, eth_height: 406, event_signature: "InFlightExitChallenged(address,bytes32,uint256)", log_index: 0, root_chain_txhash: "0xd9e3b3aaff8156dab8b004882d3bce834ba842c95deff7ec97da8f942f870ab4", tx_hash: "0x7532528ec22439a9a1ed5f4fce6cd66d71625add6202cefb970c10d04f2d5091" } end test "if exit challenged can be decoded " do exit_challenged_event = %{ eth_height: 287, event_signature: "ExitChallenged(uint256)", log_index: 0, root_chain_txhash: <<66, 82, 85, 28, 152, 229, 144, 134, 61, 240, 143, 214, 56, 156, 97, 106, 171, 81, 16, 56, 48, 106, 184, 247, 130, 36, 168, 45, 21, 7, 3, 37>>, utxo_pos: 1_000_000_000_000 } assert Encode.make_it_readable!(exit_challenged_event) == %{ eth_height: 287, event_signature: "ExitChallenged(uint256)", log_index: 0, root_chain_txhash: "0x4252551c98e590863df08fd6389c616aab511038306ab8f78224a82d15070325", utxo_pos: 1_000_000_000_000 } end test "if in flight exit challenge responded can be decoded" do in_flight_exit_challenge_responded_event = %{ challenge_position: 1_000_000_000_000, challenger: <<24, 230, 136, 50, 159, 249, 214, 25, 113, 8, 166, 102, 25, 145, 44, 218, 93, 158, 161, 99>>, eth_height: 293, event_signature: "InFlightExitChallengeResponded(address,bytes32,uint256)", log_index: 0, root_chain_txhash: <<63, 182, 54, 98, 165, 47, 220, 5, 212, 113, 254, 217, 43, 101, 201, 197, 58, 155, 13, 153, 11, 123, 174, 252, 227, 24, 166, 228, 250, 108, 213, 23>>, tx_hash: <<230, 15, 66, 108, 188, 55, 20, 186, 114, 53, 223, 36, 2, 123, 242, 150, 212, 213, 42, 26, 12, 179, 109, 70, 214, 200, 138, 57, 64, 249, 141, 107>> } assert Encode.make_it_readable!(in_flight_exit_challenge_responded_event) == %{ challenge_position: 1_000_000_000_000, challenger: "0x18e688329ff9d6197108a66619912cda5d9ea163", eth_height: 293, event_signature: "InFlightExitChallengeResponded(address,bytes32,uint256)", log_index: 0, root_chain_txhash: "0x3fb63662a52fdc05d471fed92b65c9c53a9b0d990b7baefce318a6e4fa6cd517", tx_hash: "0xe60f426cbc3714ba7235df24027bf296d4d52a1a0cb36d46d6c88a3940f98d6b" } end test "if challenge in flight exit not cannonical can be decoded" do eth_tx_input_event = %{ competing_tx: <<248, 116, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 238, 237, 1, 235, 148, 130, 28, 224, 68, 235, 159, 239, 63, 140, 241, 0, 192, 44, 230, 131, 216, 224, 52, 2, 224, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, competing_tx_input_index: 0, competing_tx_pos: 0, competing_tx_sig: <<213, 14, 110, 137, 144, 125, 5, 4, 94, 64, 55, 85, 66, 96, 210, 166, 41, 110, 42, 187, 199, 54, 83, 228, 31, 85, 4, 44, 153, 33, 56, 182, 104, 35, 67, 129, 11, 98, 78, 229, 81, 4, 199, 65, 155, 47, 3, 187, 179, 69, 65, 239, 135, 219, 72, 233, 93, 232, 14, 157, 74, 187, 190, 63, 28>>, in_flight_input_index: 0, in_flight_tx: <<248, 163, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 248, 92, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, input_tx_bytes: <<248, 83, 1, 192, 238, 237, 1, 235, 148, 140, 7, 214, 39, 36, 232, 102, 145, 82, 184, 199, 23, 67, 29, 135, 188, 216, 208, 23, 89, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, input_utxo_pos: 1_000_000_000 } assert Encode.make_it_readable!(eth_tx_input_event) == %{ competing_tx: "0xf87401e1a0000000000000000000000000000000000000000000000000000000003b9aca00eeed01eb94821ce044eb9fef3f8cf100c02ce683d8e03402e09400000000000000000000000000000000000000000980a00000000000000000000000000000000000000000000000000000000000000000", competing_tx_input_index: 0, competing_tx_pos: 0, competing_tx_sig: "0xd50e6e89907d05045e4037554260d2a6296e2abbc73653e41f55042c992138b6682343810b624ee55104c7419b2f03bbb34541ef87db48e95de80e9d4abbbe3f1c", in_flight_input_index: 0, in_flight_tx: "0xf8a301e1a0000000000000000000000000000000000000000000000000000000003b9aca00f85ced01eb948c07d62724e8669152b8c717431d87bcd8d0175994000000000000000000000000000000000000000005ed01eb948c07d62724e8669152b8c717431d87bcd8d017599400000000000000000000000000000000000000000480a00000000000000000000000000000000000000000000000000000000000000000", input_tx_bytes: "0xf85301c0eeed01eb948c07d62724e8669152b8c717431d87bcd8d017599400000000000000000000000000000000000000000a80a00000000000000000000000000000000000000000000000000000000000000000", input_utxo_pos: 1_000_000_000 } end test "if in flight exit input/output blocked can be decoded " do in_flight_exit_output_blocked_event = %{ challenger: <<213, 8, 156, 250, 64, 58, 96, 49, 161, 243, 131, 189, 70, 126, 152, 14, 208, 189, 92, 186>>, eth_height: 438, event_signature: "InFlightExitOutputBlocked(address,bytes32,uint16)", log_index: 0, output_index: 1, root_chain_txhash: <<152, 71, 150, 186, 105, 123, 83, 43, 230, 36, 2, 153, 144, 252, 109, 79, 114, 228, 225, 67, 76, 246, 141, 207, 59, 5, 179, 75, 121, 135, 196, 104>>, tx_hash: <<42, 63, 46, 245, 8, 132, 225, 35, 163, 42, 44, 64, 216, 103, 88, 232, 254, 91, 130, 169, 162, 184, 46, 44, 8, 73, 190, 111, 19, 201, 87, 2>>, omg_data: %{piggyback_type: :output} } assert Encode.make_it_readable!(in_flight_exit_output_blocked_event) == %{ challenger: "0xd5089cfa403a6031a1f383bd467e980ed0bd5cba", eth_height: 438, event_signature: "InFlightExitOutputBlocked(address,bytes32,uint16)", log_index: 0, omg_data: %{piggyback_type: :output}, output_index: 1, root_chain_txhash: "0x984796ba697b532be624029990fc6d4f72e4e1434cf68dcf3b05b34b7987c468", tx_hash: "0x2a3f2ef50884e123a32a2c40d86758e8fe5b82a9a2b82e2c0849be6f13c95702" } end test "if in flight exit started can be decoded" do in_flight_exit_started_event = %{ eth_height: 726, event_signature: "InFlightExitStarted(address,bytes32)", initiator: <<44, 106, 159, 66, 49, 128, 37, 205, 102, 39, 186, 242, 28, 70, 130, 1, 98, 32, 32, 223>>, log_index: 0, root_chain_txhash: <<240, 228, 74, 240, 210, 100, 67, 185, 229, 19, 60, 100, 245, 167, 31, 6, 164, 212, 208, 212, 12, 94, 116, 18, 181, 234, 13, 252, 178, 241, 161, 51>>, tx_hash: <<79, 70, 5, 59, 93, 245, 133, 9, 76, 198, 82, 221, 216, 195, 101, 150, 42, 56, 137, 194, 5, 53, 146, 241, 131, 49, 185, 90, 125, 255, 98, 14>> } assert Encode.make_it_readable!(in_flight_exit_started_event) == %{ eth_height: 726, event_signature: "InFlightExitStarted(address,bytes32)", initiator: "0x2c6a9f42318025cd6627baf21c468201622020df", log_index: 0, root_chain_txhash: "0xf0e44af0d26443b9e5133c64f5a71f06a4d4d0d40c5e7412b5ea0dfcb2f1a133", tx_hash: "0x4f46053b5df585094cc652ddd8c365962a3889c2053592f18331b95a7dff620e" } end test "if start in flight exit can be decoded " do in_flight_exit_start_event = %{ in_flight_tx: <<248, 124, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 210, 32, 127, 180, 0, 246, 245, 1, 243, 148, 118, 78, 248, 3, 28, 17, 248, 220, 42, 92, 18, 141, 145, 248, 79, 186, 190, 47, 160, 172, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 69, 99, 145, 130, 68, 244, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, in_flight_tx_sigs: [ <<52, 191, 197, 222, 130, 0, 246, 100, 25, 133, 115, 123, 250, 19, 77, 122, 226, 50, 133, 34, 71, 195, 27, 188, 147, 104, 200, 235, 121, 231, 64, 251, 107, 58, 88, 55, 118, 117, 53, 9, 224, 81, 93, 0, 167, 62, 195, 202, 233, 207, 237, 254, 185, 95, 207, 246, 144, 69, 242, 160, 58, 161, 96, 70, 28>> ], input_inclusion_proofs: [ <<243, 154, 134, 159, 98, 231, 92, 245, 240, 191, 145, 70, 136, 166, 178, 137, 202, 242, 4, 148, 53, 216, 230, 140, 92, 94, 109, 5, 228, 73, 19, 243, 78, 213, 192, 45, 109, 72, 200, 147, 36, 134, 201, 157, 58, 217, 153, 229, 216, 148, 157, 195, 190, 59, 48, 88, 204, 41, 121, 105, 12, 62, 58, 98, 28, 121, 43, 20, 191, 102, 248, 42, 243, 111, 0, 245, 251, 167, 1, 79, 160, 193, 226, 255, 60, 124, 39, 59, 254, 82, 60, 26, 207, 103, 220, 63, 95, 160, 128, 166, 134, 165, 160, 208, 92, 61, 72, 34, 253, 84, 214, 50, 220, 156, 192, 75, 22, 22, 4, 110, 186, 44, 228, 153, 235, 154, 247, 159, 94, 185, 73, 105, 10, 4, 4, 171, 244, 206, 186, 252, 124, 255, 250, 56, 33, 145, 183, 221, 158, 125, 247, 120, 88, 30, 111, 183, 142, 250, 179, 95, 211, 100, 201, 213, 218, 218, 212, 86, 155, 109, 212, 127, 127, 234, 186, 250, 53, 113, 248, 66, 67, 68, 37, 84, 131, 53, 172, 110, 105, 13, 208, 113, 104, 216, 188, 91, 119, 151, 156, 26, 103, 2, 51, 79, 82, 159, 87, 131, 247, 158, 148, 47, 210, 205, 3, 246, 229, 90, 194, 207, 73, 110, 132, 159, 222, 156, 68, 111, 171, 70, 168, 210, 125, 177, 227, 16, 15, 39, 90, 119, 125, 56, 91, 68, 227, 203, 192, 69, 202, 186, 201, 218, 54, 202, 224, 64, 173, 81, 96, 130, 50, 76, 150, 18, 124, 242, 159, 69, 53, 235, 91, 126, 186, 207, 226, 161, 214, 211, 170, 184, 236, 4, 131, 211, 32, 121, 168, 89, 255, 112, 249, 33, 89, 112, 168, 190, 235, 177, 193, 100, 196, 116, 232, 36, 56, 23, 76, 142, 235, 111, 188, 140, 180, 89, 75, 136, 201, 68, 143, 29, 64, 176, 155, 234, 236, 172, 91, 69, 219, 110, 65, 67, 74, 18, 43, 105, 92, 90, 133, 134, 45, 142, 174, 64, 179, 38, 143, 111, 55, 228, 20, 51, 123, 227, 142, 186, 122, 181, 187, 243, 3, 208, 31, 75, 122, 224, 127, 215, 62, 220, 47, 59, 224, 94, 67, 148, 138, 52, 65, 138, 50, 114, 80, 156, 67, 194, 129, 26, 130, 30, 92, 152, 43, 165, 24, 116, 172, 125, 201, 221, 121, 168, 12, 194, 240, 95, 111, 102, 76, 157, 187, 46, 69, 68, 53, 19, 125, 160, 108, 228, 77, 228, 85, 50, 165, 106, 58, 112, 7, 162, 208, 198, 180, 53, 247, 38, 249, 81, 4, 191, 166, 231, 7, 4, 111, 193, 84, 186, 233, 24, 152, 208, 58, 26, 10, 198, 249, 180, 94, 71, 22, 70, 226, 85, 90, 199, 158, 63, 232, 126, 177, 120, 30, 38, 242, 5, 0, 36, 12, 55, 146, 116, 254, 145, 9, 110, 96, 209, 84, 90, 128, 69, 87, 31, 218, 185, 181, 48, 208, 214, 231, 232, 116, 110, 120, 191, 159, 32, 244, 232, 111, 6>> ], input_txs: [ <<248, 91, 1, 192, 246, 245, 1, 243, 148, 118, 78, 248, 3, 28, 17, 248, 220, 42, 92, 18, 141, 145, 248, 79, 186, 190, 47, 160, 172, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 138, 199, 35, 4, 137, 232, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> ], input_utxos_pos: [2_002_000_000_000] } assert Encode.make_it_readable!(in_flight_exit_start_event) == %{ in_flight_tx: "0xf87c01e1a0000000000000000000000000000000000000000000000000000001d2207fb400f6f501f394764ef8031c11f8dc2a5c128d91f84fbabe2fa0ac940000000000000000000000000000000000000000884563918244f4000080a00000000000000000000000000000000000000000000000000000000000000000", in_flight_tx_sigs: [ "0x34bfc5de8200f6641985737bfa134d7ae232852247c31bbc9368c8eb79e740fb6b3a583776753509e0515d00a73ec3cae9cfedfeb95fcff69045f2a03aa160461c" ], input_inclusion_proofs: [ "0xf39a869f62e75cf5f0bf914688a6b289caf2049435d8e68c5c5e6d05e44913f34ed5c02d6d48c8932486c99d3ad999e5d8949dc3be3b3058cc2979690c3e3a621c792b14bf66f82af36f00f5fba7014fa0c1e2ff3c7c273bfe523c1acf67dc3f5fa080a686a5a0d05c3d4822fd54d632dc9cc04b1616046eba2ce499eb9af79f5eb949690a0404abf4cebafc7cfffa382191b7dd9e7df778581e6fb78efab35fd364c9d5dadad4569b6dd47f7feabafa3571f842434425548335ac6e690dd07168d8bc5b77979c1a6702334f529f5783f79e942fd2cd03f6e55ac2cf496e849fde9c446fab46a8d27db1e3100f275a777d385b44e3cbc045cabac9da36cae040ad516082324c96127cf29f4535eb5b7ebacfe2a1d6d3aab8ec0483d32079a859ff70f9215970a8beebb1c164c474e82438174c8eeb6fbc8cb4594b88c9448f1d40b09beaecac5b45db6e41434a122b695c5a85862d8eae40b3268f6f37e414337be38eba7ab5bbf303d01f4b7ae07fd73edc2f3be05e43948a34418a3272509c43c2811a821e5c982ba51874ac7dc9dd79a80cc2f05f6f664c9dbb2e454435137da06ce44de45532a56a3a7007a2d0c6b435f726f95104bfa6e707046fc154bae91898d03a1a0ac6f9b45e471646e2555ac79e3fe87eb1781e26f20500240c379274fe91096e60d1545a8045571fdab9b530d0d6e7e8746e78bf9f20f4e86f06" ], input_txs: [ "0xf85b01c0f6f501f394764ef8031c11f8dc2a5c128d91f84fbabe2fa0ac940000000000000000000000000000000000000000888ac7230489e8000080a00000000000000000000000000000000000000000000000000000000000000000" ], input_utxos_pos: [2_002_000_000_000] } end test "if in flight exit finalized can be decoded" do in_flight_exit_finalized_event = %{ eth_height: 335, event_signature: "InFlightExitOutputWithdrawn(uint160,uint16)", in_flight_exit_id: 3_853_567_223_408_339_354_111_409_210_931_346_801_537_991_844, log_index: 1, output_index: 1, root_chain_txhash: <<80, 248, 10, 40, 199, 180, 94, 87, 0, 214, 231, 86, 164, 157, 76, 108, 238, 189, 92, 74, 82, 133, 178, 138, 190, 185, 112, 88, 201, 65, 185, 102>>, omg_data: %{piggyback_type: :output} } assert Encode.make_it_readable!(in_flight_exit_finalized_event) == %{ eth_height: 335, event_signature: "InFlightExitOutputWithdrawn(uint160,uint16)", in_flight_exit_id: 3_853_567_223_408_339_354_111_409_210_931_346_801_537_991_844, log_index: 1, omg_data: %{piggyback_type: :output}, output_index: 1, root_chain_txhash: "0x50f80a28c7b45e5700d6e756a49d4c6ceebd5c4a5285b28abeb97058c941b966" } end test "if exit started can be decoded" do exit_started_event = %{ eth_height: 759, event_signature: "ExitStarted(address,uint160)", exit_id: 961_120_214_746_159_734_848_620_722_848_998_552_444_082_017, log_index: 1, owner: <<8, 133, 129, 36, 179, 184, 128, 198, 139, 54, 15, 211, 25, 204, 97, 218, 39, 84, 94, 154>>, root_chain_txhash: <<74, 130, 72, 184, 138, 23, 178, 190, 76, 96, 134, 161, 152, 70, 34, 222, 26, 96, 221, 163, 201, 221, 158, 206, 30, 249, 126, 209, 142, 250, 2, 140>> } assert Encode.make_it_readable!(exit_started_event) == %{ eth_height: 759, event_signature: "ExitStarted(address,uint160)", exit_id: 961_120_214_746_159_734_848_620_722_848_998_552_444_082_017, log_index: 1, owner: "0x08858124b3b880c68b360fd319cc61da27545e9a", root_chain_txhash: "0x4a8248b88a17b2be4c6086a1984622de1a60dda3c9dd9ece1ef97ed18efa028c" } end test "if start standard exit can be decoded" do start_standard_exit_event = %{ output_tx: <<248, 91, 1, 192, 246, 245, 1, 243, 148, 8, 133, 129, 36, 179, 184, 128, 198, 139, 54, 15, 211, 25, 204, 97, 218, 39, 84, 94, 154, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 13, 224, 182, 179, 167, 100, 0, 0, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, utxo_pos: 2_001_000_000_000 } assert Encode.make_it_readable!(start_standard_exit_event) == %{ output_tx: "0xf85b01c0f6f501f39408858124b3b880c68b360fd319cc61da27545e9a940000000000000000000000000000000000000000880de0b6b3a764000080a00000000000000000000000000000000000000000000000000000000000000000", utxo_pos: 2_001_000_000_000 } end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/ethereum_event_aggregator_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.EthereumEventAggregatorTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.Eth.RootChain.Abi alias OMG.Watcher.EthereumEventAggregator setup do table = :ets.new(String.to_atom("test-#{:rand.uniform(1000)}"), [:bag, :public, :named_table]) event_fetcher_name = String.to_atom("test-#{:rand.uniform(1000)}") start_supervised( {EthereumEventAggregator, name: event_fetcher_name, contracts: %{}, ets_bucket: table, events: [ [name: :deposit_created, enrich: false], [name: :exit_started, enrich: true], [name: :in_flight_exit_input_piggybacked, enrich: false], [name: :in_flight_exit_output_piggybacked, enrich: false], [name: :in_flight_exit_started, enrich: true], [name: :in_flight_exit_deleted, enrich: false] ]} ) {:ok, %{event_fetcher_name: event_fetcher_name, table: table}} end @tag common: true test "(watcher) the performance of event retrieving", %{ table: table, event_fetcher_name: event_fetcher_name, test: test_name } do defmodule test_name do alias OMG.Watcher.EthereumEventAggregatorTest def get_ethereum_events(_from_block, to_block, _signatures, _contracts) do deposits = for n <- 1..10_000, do: EthereumEventAggregatorTest.deposit_created_log(n) {:ok, [EthereumEventAggregatorTest.in_flight_exit_input_piggybacked_log(to_block) | deposits]} end def get_call_data(_tx_hash) do {:ok, EthereumEventAggregatorTest.start_standard_exit_log()} end end from_block = 1 to_block = 80_000 :sys.replace_state(event_fetcher_name, fn state -> Map.put(state, :rpc, test_name) end) events = event_fetcher_name |> :sys.get_state() |> Map.get(:events) _ = EthereumEventAggregator.deposit_created(event_fetcher_name, from_block, to_block) assert Enum.count(:ets.tab2list(table)) == Enum.count(events) * 80_000 end describe "start_link/1 and init/1" do test "(watcher) that events are correctly initialized ", %{ event_fetcher_name: event_fetcher_name } do assert event_fetcher_name |> :sys.get_state() |> Map.get(:events) == [ [ signature: "InFlightExitDeleted(uint160)", name: :in_flight_exit_deleted, enrich: false ], [ signature: "InFlightExitStarted(address,bytes32)", name: :in_flight_exit_started, enrich: true ], [ signature: "InFlightExitOutputPiggybacked(address,bytes32,uint16)", name: :in_flight_exit_output_piggybacked, enrich: false ], [ signature: "InFlightExitInputPiggybacked(address,bytes32,uint16)", name: :in_flight_exit_input_piggybacked, enrich: false ], [ signature: "ExitStarted(address,uint160)", name: :exit_started, enrich: true ], [ signature: "DepositCreated(address,uint256,address,uint256)", name: :deposit_created, enrich: false ] ] end test "(watcher) that signatures are correctly initialized ", %{ event_fetcher_name: event_fetcher_name } do assert event_fetcher_name |> :sys.get_state() |> Map.get(:event_signatures) |> Enum.sort() == Enum.sort([ "InFlightExitStarted(address,bytes32)", "InFlightExitOutputPiggybacked(address,bytes32,uint16)", "InFlightExitInputPiggybacked(address,bytes32,uint16)", "ExitStarted(address,uint160)", "InFlightExitDeleted(uint160)", "DepositCreated(address,uint256,address,uint256)" ]) end end describe "delete_old_logs/2" do # we start the test with a completely empty ETS table, meaning to events were retrieved yet # so the first call from a ETH event listener would actually retrieve values from Infura test "(watcher) that :delete_events_threshold_ethereum_block_height is respected and that events get deleted from ETS", %{ event_fetcher_name: event_fetcher_name, table: table, test: test_name } do defmodule test_name do alias OMG.Watcher.EthereumEventAggregatorTest def get_ethereum_events(from_block, to_block, _signatures, _contracts) do {:ok, [ EthereumEventAggregatorTest.deposit_created_log(from_block), EthereumEventAggregatorTest.exit_started_log(to_block), EthereumEventAggregatorTest.in_flight_exit_output_piggybacked_log(from_block), EthereumEventAggregatorTest.in_flight_exit_input_piggybacked_log(to_block) ]} end def get_call_data(_tx_hash) do {:ok, EthereumEventAggregatorTest.start_standard_exit_log()} end end from_block = 1 to_block = 3 :sys.replace_state(event_fetcher_name, fn state -> Map.put(state, :rpc, test_name) end) :sys.replace_state(event_fetcher_name, fn state -> Map.put(state, :delete_events_threshold_ethereum_block_height, 1) end) events = event_fetcher_name |> :sys.get_state() |> Map.get(:events) # create data that we need deposit_created = from_block |> deposit_created_log() |> Abi.decode_log() deposit_created_2 = from_block |> Kernel.+(1) |> deposit_created_log() |> Abi.decode_log() exit_started_log = to_block |> exit_started_log() |> Abi.decode_log() |> Map.put(:call_data, start_standard_exit_log() |> from_hex |> Abi.decode_function()) in_flight_exit_output_piggybacked_log = from_block |> in_flight_exit_output_piggybacked_log() |> Abi.decode_log() in_flight_exit_input_piggybacked_log = to_block |> in_flight_exit_input_piggybacked_log() |> Abi.decode_log() data = [ {from_block, get_signature_from_event(events, :deposit_created), [deposit_created]}, {from_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), [in_flight_exit_output_piggybacked_log]}, {from_block, get_signature_from_event(events, :in_flight_exit_started), []}, {from_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), [in_flight_exit_input_piggybacked_log]}, {from_block, get_signature_from_event(events, :exit_started), [exit_started_log]}, # this deposit will get called out below {from_block + 1, get_signature_from_event(events, :deposit_created), [deposit_created_2]}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_started), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_input_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :exit_started), []}, {to_block, get_signature_from_event(events, :deposit_created), []}, {to_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {to_block, get_signature_from_event(events, :exit_started), [exit_started_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), [in_flight_exit_input_piggybacked_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_started), []} ] _ = :ets.insert(table, data) from_block_2 = 2 to_block_2 = 3 # this should induce a ETS delete call assert EthereumEventAggregator.deposit_created(event_fetcher_name, from_block_2, to_block_2) == {:ok, [deposit_created_2]} what_should_be_left_in_db = [ {from_block + 1, get_signature_from_event(events, :deposit_created), [deposit_created_2]}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_started), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_input_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :exit_started), []}, {to_block, get_signature_from_event(events, :deposit_created), []}, {to_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {to_block, get_signature_from_event(events, :exit_started), [exit_started_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), [in_flight_exit_input_piggybacked_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_started), []} ] # we're just making sure that handle continue gets called after handle_call :ok = Process.sleep(100) assert Enum.sort(:ets.tab2list(table)) == Enum.sort(what_should_be_left_in_db) end end describe "api calls/2 calls/3 and store_logs/4" do # We also assert if blocks that did NOT have any events get commited to ETS as empty. # This is important because we do not want to re-scan blocks for which we know contain nothing. test "(watcher) if we get response for range and all events are commited to ETS", %{ event_fetcher_name: event_fetcher_name, table: table, test: test_name } do defmodule test_name do alias OMG.Watcher.EthereumEventAggregatorTest def get_ethereum_events(from_block, to_block, _signatures, _contracts) do {:ok, [ EthereumEventAggregatorTest.deposit_created_log(from_block), EthereumEventAggregatorTest.deposit_created_log(from_block + 1), EthereumEventAggregatorTest.exit_started_log(to_block), EthereumEventAggregatorTest.in_flight_exit_output_piggybacked_log(from_block), EthereumEventAggregatorTest.in_flight_exit_input_piggybacked_log(to_block), EthereumEventAggregatorTest.in_flight_exit_deleted_log(from_block) ]} end def get_call_data(_tx_hash) do {:ok, EthereumEventAggregatorTest.start_standard_exit_log()} end end # we need to set the RPC module with our mocked implementation :sys.replace_state(event_fetcher_name, fn state -> Map.put(state, :rpc, test_name) end) # we read the events from the aggregators state so that we're able to build the # event data later events = event_fetcher_name |> :sys.get_state() |> Map.get(:events) from_block = 1 to_block = 3 # we need to create events that we later expect when we call the aggregator APIs # for example, deposit_created and deposit_created_2 are expected if the range is from 1 to 3 deposit_created = from_block |> deposit_created_log() |> Abi.decode_log() deposit_created_2 = from_block |> Kernel.+(1) |> deposit_created_log() |> Abi.decode_log() exit_started_log = to_block |> exit_started_log() |> Abi.decode_log() |> Map.put(:call_data, start_standard_exit_log() |> from_hex |> Abi.decode_function()) in_flight_exit_output_piggybacked_log = from_block |> in_flight_exit_output_piggybacked_log() |> Abi.decode_log() in_flight_exit_input_piggybacked_log = to_block |> in_flight_exit_input_piggybacked_log() |> Abi.decode_log() exit_deleted = from_block |> in_flight_exit_deleted_log() |> Abi.decode_log() # now we're asserting that the API returns the correct events based on the range assert EthereumEventAggregator.exit_started(event_fetcher_name, from_block, to_block) == {:ok, [exit_started_log]} assert EthereumEventAggregator.in_flight_exit_piggybacked(event_fetcher_name, from_block, to_block) == {:ok, [in_flight_exit_input_piggybacked_log, in_flight_exit_output_piggybacked_log]} assert EthereumEventAggregator.deposit_created(event_fetcher_name, from_block, to_block) == {:ok, [deposit_created, deposit_created_2]} assert EthereumEventAggregator.in_flight_exit_deleted(event_fetcher_name, from_block, to_block) == {:ok, [exit_deleted]} # and now we're asserting that the API calls actually stored the events above # also that the events were stored at the right blknum key assert Enum.sort(:ets.tab2list(table)) == Enum.sort([ {from_block, get_signature_from_event(events, :in_flight_exit_deleted), [exit_deleted]}, {from_block, get_signature_from_event(events, :deposit_created), [deposit_created]}, {from_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), [in_flight_exit_output_piggybacked_log]}, {from_block, get_signature_from_event(events, :in_flight_exit_started), []}, {from_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), []}, {from_block, get_signature_from_event(events, :exit_started), []}, {from_block + 1, get_signature_from_event(events, :deposit_created), [deposit_created_2]}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_started), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_input_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :exit_started), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_deleted), []}, {to_block, get_signature_from_event(events, :deposit_created), []}, {to_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {to_block, get_signature_from_event(events, :exit_started), [exit_started_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), [in_flight_exit_input_piggybacked_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_started), []}, {to_block, get_signature_from_event(events, :in_flight_exit_deleted), []} ]) end test "(watcher) if we get response for range where from equals to and that all events are commited to ETS", %{ event_fetcher_name: event_fetcher_name, table: table, test: test_name } do defmodule test_name do alias OMG.Watcher.EthereumEventAggregatorTest def get_ethereum_events(from_block, to_block, _signatures, _contracts) do {:ok, [ EthereumEventAggregatorTest.deposit_created_log(from_block), EthereumEventAggregatorTest.exit_started_log(to_block), EthereumEventAggregatorTest.in_flight_exit_output_piggybacked_log(from_block), EthereumEventAggregatorTest.in_flight_exit_input_piggybacked_log(to_block) ]} end def get_call_data(_tx_hash) do {:ok, EthereumEventAggregatorTest.start_standard_exit_log()} end end :sys.replace_state(event_fetcher_name, fn state -> Map.put(state, :rpc, test_name) end) from_block = 1 to_block = 1 deposit_created = from_block |> deposit_created_log() |> Abi.decode_log() assert EthereumEventAggregator.deposit_created(event_fetcher_name, from_block, to_block) == {:ok, [deposit_created]} exit_started_log = to_block |> exit_started_log() |> Abi.decode_log() |> Map.put(:call_data, start_standard_exit_log() |> from_hex |> Abi.decode_function()) assert EthereumEventAggregator.exit_started(event_fetcher_name, from_block, to_block) == {:ok, [exit_started_log]} in_flight_exit_output_piggybacked_log = from_block |> in_flight_exit_output_piggybacked_log() |> Abi.decode_log() in_flight_exit_input_piggybacked_log = to_block |> in_flight_exit_input_piggybacked_log() |> Abi.decode_log() assert EthereumEventAggregator.in_flight_exit_piggybacked(event_fetcher_name, from_block, to_block) == {:ok, [in_flight_exit_input_piggybacked_log, in_flight_exit_output_piggybacked_log]} events = event_fetcher_name |> :sys.get_state() |> Map.get(:events) assert Enum.sort(:ets.tab2list(table)) == Enum.sort([ {from_block, get_signature_from_event(events, :in_flight_exit_deleted), []}, {from_block, get_signature_from_event(events, :deposit_created), [deposit_created]}, {to_block, get_signature_from_event(events, :exit_started), [exit_started_log]}, {from_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), [in_flight_exit_output_piggybacked_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), [in_flight_exit_input_piggybacked_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_started), []} ]) end end describe "get_logs/3" do test "(watcher) that data and order (blknum) is preserved in returned data when we fetch deposits", %{ event_fetcher_name: event_fetcher_name, table: table, test: test_name } do defmodule test_name do alias OMG.Watcher.EthereumEventAggregatorTest def get_ethereum_events(_from_block, _to_block, _signatures, _contracts) do {:ok, [ EthereumEventAggregatorTest.deposit_created_log(1), EthereumEventAggregatorTest.deposit_created_log(2) ]} end def get_call_data(_tx_hash) do {:ok, EthereumEventAggregatorTest.start_standard_exit_log()} end end from_block = 1 to_block = 3 # we get these events so that we're able to extract signatures # where we construct custom data events = event_fetcher_name |> :sys.get_state() |> Map.get(:events) # create data that we need # two deposits, one exit started and one in flight exit output piggybacked # and one in flight exit input piggynacked deposit_created = from_block |> deposit_created_log() |> Abi.decode_log() deposit_created_2 = from_block |> Kernel.+(1) |> deposit_created_log() |> Abi.decode_log() exit_started_log = to_block |> exit_started_log() |> Abi.decode_log() |> Map.put(:call_data, start_standard_exit_log() |> from_hex |> Abi.decode_function()) in_flight_exit_output_piggybacked_log = from_block |> in_flight_exit_output_piggybacked_log() |> Abi.decode_log() in_flight_exit_input_piggybacked_log = to_block |> in_flight_exit_input_piggybacked_log() |> Abi.decode_log() # we put the events into a list of events below # some are empty, others get filled by the data we created above # we just need to make sure, that the block number (from_block, from_block + 1, to_block) # coincides with the event data data = [ {from_block, get_signature_from_event(events, :deposit_created), [deposit_created]}, {from_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), [in_flight_exit_output_piggybacked_log]}, {from_block, get_signature_from_event(events, :in_flight_exit_started), []}, {from_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), [in_flight_exit_input_piggybacked_log]}, {from_block, get_signature_from_event(events, :exit_started), [exit_started_log]}, # this deposit will get called out below {from_block + 1, get_signature_from_event(events, :deposit_created), [deposit_created_2]}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_started), []}, {from_block + 1, get_signature_from_event(events, :in_flight_exit_input_piggybacked), []}, {from_block + 1, get_signature_from_event(events, :exit_started), []}, {to_block, get_signature_from_event(events, :deposit_created), []}, {to_block, get_signature_from_event(events, :in_flight_exit_output_piggybacked), []}, {to_block, get_signature_from_event(events, :exit_started), [exit_started_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_input_piggybacked), [in_flight_exit_input_piggybacked_log]}, {to_block, get_signature_from_event(events, :in_flight_exit_started), []} ] # data gets inserted into the ETS table that the event aggregator us using true = :ets.insert(table, data) # we want the event aggregator to use our mocked RPC module :sys.replace_state(event_fetcher_name, fn state -> Map.put(state, :rpc, test_name) end) # we assert that if we pull deposits in the from_block and to_block range # the deposits that we created above are returned in the correct order # and that there's no more non *empty* deposits, only those that we defined {:ok, data} = EthereumEventAggregator.deposit_created(event_fetcher_name, from_block, to_block) assert Enum.at(data, 0) == deposit_created assert Enum.at(data, 1) == deposit_created_2 # we defined two, so there shouldn't be any more! assert Enum.at(data, 2) == nil end end describe "handle_call/3, forward_call/5" do test "that APIs dont allow weird range (where from_block is bigger then to_block)", %{ event_fetcher_name: event_fetcher_name } do from = 3 to = 1 assert capture_log(fn -> assert EthereumEventAggregator.deposit_created(event_fetcher_name, from, to) == {:error, :check_range} end) =~ "[error]" assert capture_log(fn -> assert EthereumEventAggregator.exit_started(event_fetcher_name, from, to) == {:error, :check_range} end) =~ "[error]" assert capture_log(fn -> assert EthereumEventAggregator.exit_finalized(event_fetcher_name, from, to) == {:error, :check_range} end) =~ "[error]" end end # data that we extracted into helper functions def deposit_created_log(block_number) do %{ :event_signature => "DepositCreated(address,uint256,address,uint256)", "address" => "0x4e3aeff70f022a6d4cc5947423887e7152826cf7", "blockHash" => "0xe5b0487de36b161f2d3e8c228ad4e1e84ab1ae25ca4d5ef53f9f03298ab3545f", "blockNumber" => "0x" <> Integer.to_string(block_number, 16), "data" => "0x000000000000000000000000000000000000000000000000000000000000000a", "logIndex" => "0x0", "removed" => false, "topics" => [ "0x18569122d84f30025bb8dffb33563f1bdbfb9637f21552b11b8305686e9cb307", "0x0000000000000000000000003b9f4c1dd26e0be593373b1d36cee2008cbeb837", "0x0000000000000000000000000000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000000000000000000000000000000" ], "transactionHash" => "0x4d72a63ff42f1db50af2c36e8b314101d2fea3e0003575f30298e9153fe3d8ee", "transactionIndex" => "0x0" } end def exit_started_log(block_number) do %{ :event_signature => "ExitStarted(address,uint160)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x1bee6f75c74ceeb4817dc160e2fb56dd1337a9fc2980a2b013252cf1e620f246", "blockNumber" => "0x" <> Integer.to_string(block_number, 16), "data" => "0x000000000000000000000000002b191e750d8d4d3dcad14a9c8e5a5cf0c81761", "logIndex" => "0x1", "removed" => false, "topics" => [ "0xdd6f755cba05d0a420007aef6afc05e4889ab424505e2e440ecd1c434ba7082e", "0x00000000000000000000000008858124b3b880c68b360fd319cc61da27545e9a" ], "transactionHash" => "0x4a8248b88a17b2be4c6086a1984622de1a60dda3c9dd9ece1ef97ed18efa028c", "transactionIndex" => "0x0" } end def in_flight_exit_output_piggybacked_log(block_number) do %{ :event_signature => "InFlightExitOutputPiggybacked(address,bytes32,uint16)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x3e34475a29dafb28cd6deb65bc1782ccf6d73d6673d462a6d404ac0993d1e7eb", "blockNumber" => "0x" <> Integer.to_string(block_number, 16), "data" => "0x0000000000000000000000000000000000000000000000000000000000000001", "logIndex" => "0x1", "removed" => false, "topics" => [ "0x6ecd8e79a5f67f6c12b54371ada2ffb41bc128c61d9ac1e969f0aa2aca46cd78", "0x0000000000000000000000001513abcd3590a25e0bed840652d957391dde9955", "0xff90b77303e56bd230a9adf4a6553a95f5ffb563486205d6fba25d3e46594940" ], "transactionHash" => "0x7cf43a6080e99677dee0b26c23e469b1df9cfb56a5c3f2a0123df6edae7b5b5e", "transactionIndex" => "0x0" } end def in_flight_exit_input_piggybacked_log(block_number) do %{ :event_signature => "InFlightExitInputPiggybacked(address,bytes32,uint16)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0x6d95b14290cc2ac112f1560f2cd7aa0d747b91ec9cb1d47e11c205270d83c88c", "blockNumber" => "0x" <> Integer.to_string(block_number, 16), "data" => "0x0000000000000000000000000000000000000000000000000000000000000001", "logIndex" => "0x0", "removed" => false, "topics" => [ "0xa93c0e9b202feaf554acf6ef1185b898c9f214da16e51740b06b5f7487b018e5", "0x0000000000000000000000001513abcd3590a25e0bed840652d957391dde9955", "0xff90b77303e56bd230a9adf4a6553a95f5ffb563486205d6fba25d3e46594940" ], "transactionHash" => "0x0cc9e5556bbd6eeaf4302f44adca215786ff08cfa44a34be1760eca60f97364f", "transactionIndex" => "0x0" } end def in_flight_exit_deleted_log(block_number) do %{ :event_signature => "InFlightExitDeleted(uint160)", "address" => "0x92ce4d7773c57d96210c46a07b89acf725057f21", "blockHash" => "0xcafbc4b710c5fab8f3d719f65053637407231ecde31a859f1709e3478a2eda54", "blockNumber" => "0x" <> Integer.to_string(block_number, 16), "data" => "0x", "logIndex" => "0x2", "removed" => false, "topics" => [ "0x1991c4c350498b0cc937c6a08bc5bdecf2e4fdd9d918052a880f102e43dbe45c", "0x000000000000000000000000003fd275046f2823936fd97c1e3c8b225464d7f1" ], "transactionHash" => "0xbe310ade41278c5607620311b79363aa520ac46c7ba754bf3027d501c5a95f40", "transactionIndex" => "0x0" } end def start_standard_exit_log() do "0x70e014620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000001d1e4e4ea00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000005df85b01c0f6f501f39408858124b3b880c68b360fd319cc61da27545e9a940000000000000000000000000000000000000000880de0b6b3a764000080a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200f39a869f62e75cf5f0bf914688a6b289caf2049435d8e68c5c5e6d05e44913f34ed5c02d6d48c8932486c99d3ad999e5d8949dc3be3b3058cc2979690c3e3a621c792b14bf66f82af36f00f5fba7014fa0c1e2ff3c7c273bfe523c1acf67dc3f5fa080a686a5a0d05c3d4822fd54d632dc9cc04b1616046eba2ce499eb9af79f5eb949690a0404abf4cebafc7cfffa382191b7dd9e7df778581e6fb78efab35fd364c9d5dadad4569b6dd47f7feabafa3571f842434425548335ac6e690dd07168d8bc5b77979c1a6702334f529f5783f79e942fd2cd03f6e55ac2cf496e849fde9c446fab46a8d27db1e3100f275a777d385b44e3cbc045cabac9da36cae040ad516082324c96127cf29f4535eb5b7ebacfe2a1d6d3aab8ec0483d32079a859ff70f9215970a8beebb1c164c474e82438174c8eeb6fbc8cb4594b88c9448f1d40b09beaecac5b45db6e41434a122b695c5a85862d8eae40b3268f6f37e414337be38eba7ab5bbf303d01f4b7ae07fd73edc2f3be05e43948a34418a3272509c43c2811a821e5c982ba51874ac7dc9dd79a80cc2f05f6f664c9dbb2e454435137da06ce44de45532a56a3a7007a2d0c6b435f726f95104bfa6e707046fc154bae91898d03a1a0ac6f9b45e471646e2555ac79e3fe87eb1781e26f20500240c379274fe91096e60d1545a8045571fdab9b530d0d6e7e8746e78bf9f20f4e86f06" end defp from_hex("0x" <> encoded), do: Base.decode16!(encoded, case: :lower) defp get_signature_from_event(events, name) do events |> Enum.find(fn event -> Keyword.get(event, :name) == name end) |> Keyword.get(:signature) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/ethereum_event_listener/core_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Watcher.EthereumEventListener.CoreTest do use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.Configuration alias OMG.Watcher.EthereumEventListener.Core alias OMG.Watcher.RootChainCoordinator.SyncGuide @db_key :db_key @service_name :name @request_max_size 5 test "respects request_max_size argument" do create_state(0, request_max_size: 10) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 20, root_chain_height: 10}) |> assert_range({1, 10}) create_state(0, request_max_size: 10) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 11, root_chain_height: 10}) |> assert_range({1, 10}) create_state(0, request_max_size: 10) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 10, root_chain_height: 10}) |> assert_range({1, 10}) end test "event range is capped at the SyncGuide sync_height" do # if request_max_size is taken into account it would # push the event range above the threshold and would remove the reorg protection create_state(0, request_max_size: 2) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 1, root_chain_height: 10}) |> assert_range({1, 1}) end test "get events range is capped at request_max_size and the events range returned is less then SyncGuide sync_height" do create_state(0, request_max_size: 2) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 4, root_chain_height: 10}) |> assert_range({1, 2}) end test "works well close to zero" do 0 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 1, root_chain_height: 10}) |> assert_range({1, 1}) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 8, root_chain_height: 10}) |> assert_range({2, 6}) 0 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 9, root_chain_height: 10}) |> assert_range({1, 5}) end test "always returns correct height to check in" do state = 0 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 1, root_chain_height: 10}) |> assert_range({1, 1}) assert state.synced_height == 1 end test "produces next ethereum height range to get events from" do 0 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 5, root_chain_height: 10}) |> assert_range({1, 5}) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 5, root_chain_height: 10}) |> assert_range(:dont_fetch_events) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 7, root_chain_height: 10}) |> assert_range({6, 7}) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 7, root_chain_height: 10}) |> assert_range(:dont_fetch_events) end test "if synced requested higher than root chain height" do # doesn't make too much sense, but still should work well 0 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 5, root_chain_height: 5}) |> assert_range({1, 5}) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 7, root_chain_height: 5}) |> assert_range({6, 7}) end test "will be eager to get more events, even if none are pulled at first. All will be returned" do 0 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 2, root_chain_height: 2}) |> assert_range({1, 2}) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 4, root_chain_height: 4}) |> assert_range({3, 4}) end test "restart allows to continue with proper bounds" do 1 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 4, root_chain_height: 10}) |> assert_range({2, 4}) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 4, root_chain_height: 10}) |> assert_range(:dont_fetch_events) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 5, root_chain_height: 10}) |> assert_range({5, 5}) 3 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 3, root_chain_height: 10}) |> assert_range(:dont_fetch_events) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 5, root_chain_height: 10}) |> assert_range({4, 5}) 3 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 7, root_chain_height: 10}) |> assert_range({4, 7}) end test "wont move over if not allowed by sync_height" do 5 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 6, root_chain_height: 10}) |> assert_range({6, 6}) end test "can get an empty events list when events too fresh" do 4 |> create_state() |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 6, root_chain_height: 10}) |> assert_range({5, 6}) end test "persists/checks in eth_height without margins substracted, and never goes negative" do create_state(0, request_max_size: 10) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 6, root_chain_height: 10}) |> assert_range({1, 6}) end test "tolerates being asked to sync on height already synced" do create_state(5) |> Core.calc_events_range_set_height(%SyncGuide{sync_height: 1, root_chain_height: 10}) |> assert_range(:dont_fetch_events) end defp create_state(height, opts \\ []) do request_max_size = Keyword.get(opts, :request_max_size, @request_max_size) # this assert is meaningful - currently we want to explicitly check_in the height read from DB assert {state, ^height} = Core.init( @db_key, @service_name, height, Configuration.ethereum_events_check_interval_ms(), request_max_size ) state end defp assert_range({range, state}, expect) do assert range == expect state end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/canonicity_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.CanonicityTest do @moduledoc """ Test of the logic of exit processor - detecting conditions related to canonicity game and challenging them: - competitors - invalid competitors """ use OMG.Watcher.ExitProcessor.Case, async: true alias OMG.Watcher.Block alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper @eth <<0::160>> @late_blknum 10_000 describe "sanity checks" do test "can process empty challenges and responses", %{processor_empty: empty, processor_filled: filled} do {^empty, []} = Core.new_ife_challenges(empty, []) {^filled, []} = Core.new_ife_challenges(filled, []) {^empty, []} = Core.respond_to_in_flight_exits_challenges(empty, []) {^filled, []} = Core.respond_to_in_flight_exits_challenges(filled, []) end end describe "finds competitors and allows canonicity challenges" do test "none if input never spent elsewhere", %{processor_filled: processor} do assert {:ok, []} = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5} |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) end test "none if different input spent in some tx from appendix", %{processor_filled: processor, transactions: [tx1 | _], unrelated_tx: comp} do txbytes = txbytes(tx1) processor = processor |> start_ife_from(comp) assert {:ok, []} = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5} |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) assert {:error, :competitor_not_found} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.get_competitor_for_ife(processor, txbytes) end test "none if different input spent in some tx from block", %{processor_filled: processor, transactions: [tx1 | _], unrelated_tx: comp} do txbytes = txbytes(tx1) exit_processor_request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp], 3000)] } assert {:ok, []} = exit_processor_request |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) assert {:error, :competitor_not_found} = exit_processor_request |> Core.get_competitor_for_ife(processor, txbytes) end test "none if input spent in _same_ tx in block", %{processor_filled: processor, transactions: [tx1 | _]} do txbytes = txbytes(tx1) exit_processor_request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([tx1], 3000)] } assert {:ok, []} = exit_processor_request |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) assert {:error, :competitor_not_found} = exit_processor_request |> Core.get_competitor_for_ife(processor, txbytes) end test "none if input spent in _same_ tx in tx appendix", %{processor_filled: processor, transactions: [tx | _]} do txbytes = txbytes(tx) processor = processor |> start_ife_from(tx) assert {:ok, []} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) assert {:error, :competitor_not_found} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.get_competitor_for_ife(processor, txbytes) end test "each other, if input spent in different ife", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp)} processor = processor |> start_ife_from(comp) assert {:ok, events} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert_events(events, [%Event.NonCanonicalIFE{txbytes: txbytes}, %Event.NonCanonicalIFE{txbytes: comp_txbytes}]) assert {:ok, %{ in_flight_txbytes: ^txbytes, in_flight_input_index: 0, competing_txbytes: ^comp_txbytes, competing_input_index: 1, competing_sig: ^comp_signature, competing_tx_pos: Utxo.position(0, 0, 0), competing_proof: "" }} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.get_competitor_for_ife(processor, txbytes) end test "a competitor that's submitted as challenge to other IFE", %{alice: alice, processor_filled: processor, transactions: [tx1, tx2 | _]} do # ifes in processor here aren't competitors to each other, but the challenge filed for tx2 is a competitor # for tx1, which is what we want to detect: comp = TestHelper.create_recovered([{1, 0, 0, alice}, {2, 1, 0, alice}], [{alice, @eth, 1}]) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp)} txbytes = Transaction.raw_txbytes(tx1) challenge_event = ife_challenge(tx2, comp) {processor, _} = Core.new_ife_challenges(processor, [challenge_event]) exit_processor_request = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} assert {:ok, [%Event.NonCanonicalIFE{txbytes: ^txbytes}]} = exit_processor_request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:ok, %{ in_flight_txbytes: ^txbytes, competing_txbytes: ^comp_txbytes, competing_input_index: 0, competing_sig: ^comp_signature }} = exit_processor_request |> Core.get_competitor_for_ife(processor, txbytes) end test "a single competitor included in a block, with proof", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp)} other_blknum = 3000 exit_processor_request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp], other_blknum)] } assert {:ok, [%Event.NonCanonicalIFE{txbytes: ^txbytes}]} = exit_processor_request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:ok, %{ in_flight_txbytes: ^txbytes, in_flight_input_index: 0, competing_txbytes: ^comp_txbytes, competing_input_index: 1, competing_sig: ^comp_signature, competing_tx_pos: Utxo.position(^other_blknum, 0, 0), competing_proof: proof_bytes }} = exit_processor_request |> Core.get_competitor_for_ife(processor, txbytes) assert_proof_sound(proof_bytes) end test "handle two competitors, when the younger one already challenged", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) comp_txbytes = txbytes(comp) other_blknum = 3000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp, comp], other_blknum)], ife_input_spending_blocks_result: [Block.hashed_txs_at([comp, comp], other_blknum)] } # the transaction is firstmost submitted as a competitor, plus we run the preliminary lookup processor = processor |> start_ife_from(comp) |> Core.find_ifes_in_blocks(request) # after the first, intermediate challenges, there should still be that event active assert_intermediate_result = fn processor -> assert {:ok, [%Event.NonCanonicalIFE{txbytes: ^txbytes}]} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) # this request always returns the oldest competitor, even if we later use a different one assert {:ok, %{competing_txbytes: ^comp_txbytes, competing_tx_pos: Utxo.position(^other_blknum, 0, 0)}} = request |> Core.get_competitor_for_ife(processor, txbytes) end # sanity check - no challenges yet assert_intermediate_result.(processor) # now `comp` is used to challenge with no inclusion proof: challenge with IFE (no position, incomplete) challenge = ife_challenge(tx1, comp) {processor, _} = Core.new_ife_challenges(processor, [challenge]) assert_intermediate_result.(processor) # challenge with the younger competitor (still incomplete challenge) young_challenge = ife_challenge(tx1, comp, competitor_position: Utxo.position(other_blknum, 1, 0)) {processor, _} = Core.new_ife_challenges(processor, [young_challenge]) assert_intermediate_result.(processor) # challenge with the older competitor (complete!) older_challenge = ife_challenge(tx1, comp, competitor_position: Utxo.position(other_blknum, 0, 0)) {processor, _} = Core.new_ife_challenges(processor, [older_challenge]) # the tx1 IFE got challenged by the oldest competitor now; finally, it's over: assert {:ok, []} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:error, :no_viable_competitor_found} = request |> Core.get_competitor_for_ife(processor, txbytes) end test "handle two competitors, when both are non canonical and used to challenge", %{alice: alice, processor_filled: processor, transactions: [tx1 | _]} do comp1 = TestHelper.create_recovered([{1, 0, 0, alice}], [{alice, @eth, 1}]) comp2 = TestHelper.create_recovered([{1, 0, 0, alice}], [{alice, @eth, 2}]) txbytes = txbytes(tx1) request = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} processor = processor |> start_ife_from(comp1) |> start_ife_from(comp2) # before any challenge assert {:ok, [_, _, _]} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:ok, %{competing_tx_pos: Utxo.position(0, 0, 0)}} = request |> Core.get_competitor_for_ife(processor, txbytes) # after challenge - one event less + no need to challenge more {processor, _} = Core.new_ife_challenges(processor, [ife_challenge(tx1, comp1)]) assert {:ok, [_, _]} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:error, :no_viable_competitor_found} = request |> Core.get_competitor_for_ife(processor, txbytes) end test "don't show competitors, if IFE tx is included", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) comp_txbytes = txbytes(comp) other_blknum = 3000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx1], other_blknum)] } processor = processor |> start_ife_from(comp) |> Core.find_ifes_in_blocks(request) # notice this is `comp` having a competitor reported, not `tx1` assert {:ok, [%Event.NonCanonicalIFE{txbytes: ^comp_txbytes}]} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:error, :no_viable_competitor_found} = request |> Core.get_competitor_for_ife(processor, txbytes) end test "don't show competitors, if IFE tx is included and is the oldest", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) other_blknum = 3000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([tx1, comp], other_blknum)], ife_input_spending_blocks_result: [Block.hashed_txs_at([tx1, comp], other_blknum)] } processor = processor |> Core.find_ifes_in_blocks(request) # notice this is `comp` having a competitor reported, not `tx1` assert {:ok, []} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:error, :no_viable_competitor_found} = request |> Core.get_competitor_for_ife(processor, txbytes) end test "show competitors, if IFE tx is included but not the oldest", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) other_blknum = 3000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp, tx1], other_blknum)], ife_input_spending_blocks_result: [Block.hashed_txs_at([comp, tx1], other_blknum)] } processor = processor |> Core.find_ifes_in_blocks(request) # notice this is `comp` having a competitor reported, not `tx1` assert {:ok, [%Event.NonCanonicalIFE{txbytes: ^txbytes}]} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:ok, %{}} = request |> Core.get_competitor_for_ife(processor, txbytes) end test "detects that non-canonical ife becomes unchallenged exit when sla period passes", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) other_blknum = 3000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5 + processor.sla_margin, blocks_result: [Block.hashed_txs_at([comp, tx1], other_blknum)], ife_input_spending_blocks_result: [Block.hashed_txs_at([comp, tx1], other_blknum)] } processor = processor |> Core.find_ifes_in_blocks(request) assert {{:error, :unchallenged_exit}, [%Event.UnchallengedNonCanonicalIFE{txbytes: ^txbytes}]} = request |> check_validity_filtered(processor, only: [Event.UnchallengedNonCanonicalIFE]) end test "show competitors, if IFE tx is included but not the oldest - distinct blocks", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) block1 = Block.hashed_txs_at([comp], 3000) block2 = Block.hashed_txs_at([tx1], 4000) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [block1, block2], # note the flipped order here, all still works as the blocks should be processed starting from oldest ife_input_spending_blocks_result: [block2, block1] } processor = processor |> Core.find_ifes_in_blocks(request) # notice this is `comp` having a competitor reported, not `tx1` assert {:ok, [%Event.NonCanonicalIFE{txbytes: ^txbytes}]} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:ok, %{}} = request |> Core.get_competitor_for_ife(processor, txbytes) end test "none if IFE is challenged enough already", %{processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do txbytes = txbytes(tx1) other_blknum = 3000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp], other_blknum)] } challenge = ife_challenge(tx1, comp, competitor_position: Utxo.position(other_blknum, 0, 0), competing_tx_input_index: 1) {processor, _} = Core.new_ife_challenges(processor, [challenge]) assert {:ok, []} = request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:error, :no_viable_competitor_found} = request |> Core.get_competitor_for_ife(processor, txbytes) end test "a competitor having the double-spend on various input indices", %{alice: alice, processor_empty: processor} do tx = TestHelper.create_recovered([{1, 0, 0, alice}, {1, 2, 1, alice}], [{alice, @eth, 1}]) txbytes = txbytes(tx) processor = processor |> start_ife_from(tx) input_spent_in_idx0 = {1, 0, 0} input_spent_in_idx1 = {1, 2, 1} other_input1 = {110, 2, 1} other_input2 = {111, 2, 1} other_input3 = {112, 2, 1} comps = [ Transaction.Payment.new([input_spent_in_idx0], [{alice.addr, @eth, 1}]), Transaction.Payment.new([other_input1, input_spent_in_idx0], [{alice.addr, @eth, 1}]), Transaction.Payment.new([other_input1, other_input2, input_spent_in_idx0], [{alice.addr, @eth, 1}]), Transaction.Payment.new([other_input1, other_input2, other_input3, input_spent_in_idx0], [{alice.addr, @eth, 1}]), Transaction.Payment.new([input_spent_in_idx1], [{alice.addr, @eth, 1}]), Transaction.Payment.new([other_input1, input_spent_in_idx1], [{alice.addr, @eth, 1}]), Transaction.Payment.new([other_input1, other_input2, input_spent_in_idx1], [{alice.addr, @eth, 1}]), Transaction.Payment.new([other_input1, other_input2, other_input3, input_spent_in_idx1], [{alice.addr, @eth, 1}]) ] expected_input_ids = [{0, 0}, {1, 0}, {2, 0}, {3, 0}, {0, 1}, {1, 1}, {2, 1}, {3, 1}] check = fn {comp, {competing_input_index, in_flight_input_index}} -> # unfortunately, transaction validity requires us to duplicate a signature for every non-zero input required_priv_key_list = comp |> Transaction.get_inputs() |> Enum.count() |> (&List.duplicate(alice.priv, &1)).() other_recovered = TestHelper.sign_recover!(comp, required_priv_key_list) exit_processor_request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([other_recovered], 3000)] } assert {:ok, [%Event.NonCanonicalIFE{txbytes: ^txbytes}]} = exit_processor_request |> check_validity_filtered(processor, only: [Event.NonCanonicalIFE]) assert {:ok, %{ in_flight_input_index: ^in_flight_input_index, competing_input_index: ^competing_input_index }} = exit_processor_request |> Core.get_competitor_for_ife(processor, txbytes) end comps |> Enum.zip(expected_input_ids) |> Enum.each(check) end test "a competitor being signed on various positions", %{processor_filled: processor, transactions: [tx1 | _], alice: alice, bob: bob} do comp = TestHelper.create_recovered([{10, 2, 1, bob}, {1, 0, 0, alice}], [{alice, @eth, 1}]) comp_signature = sig(comp, 1) exit_processor_request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp], 3000)] } assert {:ok, %{competing_sig: ^comp_signature}} = exit_processor_request |> Core.get_competitor_for_ife(processor, txbytes(tx1)) end test "a best competitor, included earliest in a block, regardless of conflicting utxo position", %{alice: alice, processor_filled: processor, transactions: [tx1 | _], competing_tx: comp} do # NOTE that the recent competitor spends an __older__ input. Also note the reversing of block results done below # Regardless of these, the best competitor (from blknum 2000) must always be returned # NOTE also that non-included competitors always are considered last, and hence worst and never are returned # first the included competitors recovered_recent = TestHelper.create_recovered([{1, 0, 0, alice}], [{alice, @eth, 1}]) recovered_oldest = TestHelper.create_recovered([{1, 0, 0, alice}, {2, 2, 1, alice}], [{alice, @eth, 1}]) # ife-related competitor processor = processor |> start_ife_from(comp) exit_processor_request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([recovered_oldest], 2000), Block.hashed_txs_at([recovered_recent], 3000)] } txbytes = txbytes(tx1) assert {:ok, %{competing_tx_pos: Utxo.position(2000, 0, 0)}} = exit_processor_request |> Core.get_competitor_for_ife(processor, txbytes) assert {:ok, %{competing_tx_pos: Utxo.position(2000, 0, 0)}} = exit_processor_request |> Map.update!(:blocks_result, &Enum.reverse/1) |> struct!() |> Core.get_competitor_for_ife(processor, txbytes) # check also that the rule applies to order of txs within a block assert {:ok, %{competing_tx_pos: Utxo.position(2000, 0, 0)}} = exit_processor_request |> Map.put(:blocks_result, [Block.hashed_txs_at([recovered_oldest, recovered_recent], 2000)]) |> struct!() |> Core.get_competitor_for_ife(processor, txbytes) end test "by asking for utxo existence concerning active ifes and standard exits", %{processor_empty: processor, alice: alice} do standard_exit_tx = TestHelper.create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 10}, {alice, 10}]) ife_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) standard_exiting_pos = Utxo.position(2_000, 0, 1) processor = processor |> start_se_from(standard_exit_tx, standard_exiting_pos) |> start_ife_from(ife_tx) assert %{utxos_to_check: [Utxo.position(1, 0, 0), _standard_exiting_pos]} = %ExitProcessor.Request{blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) end test "by asking for utxo spends concerning active ifes", %{processor_filled: processor} do assert %{spends_to_get: [Utxo.position(1, 2, 1)]} = %ExitProcessor.Request{ utxos_to_check: [Utxo.position(1, 2, 1), Utxo.position(112, 2, 1)], utxo_exists_result: [false, false] } |> Core.determine_spends_to_get(processor) end test "by not asking for utxo spends concerning non-active ifes", %{processor_empty: processor, transactions: [tx | _]} do processor = processor |> start_ife_from(tx, status: :inactive) assert %{utxos_to_check: []} = %ExitProcessor.Request{blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) end test "by not asking for utxo existence concerning finalized ifes", %{processor_empty: processor, transactions: [tx | _]} do tx_hash = Transaction.raw_txhash(tx) ife_id = 123 processor = processor |> start_ife_from(tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 1, :input) |> piggyback_ife_from(tx_hash, 2, :input) finalizations = [ %{in_flight_exit_id: ife_id, output_index: 1, omg_data: %{piggyback_type: :input}}, %{in_flight_exit_id: ife_id, output_index: 2, omg_data: %{piggyback_type: :input}} ] {:ok, processor, _} = Core.finalize_in_flight_exits(processor, finalizations, %{}) assert %{utxos_to_check: []} = %ExitProcessor.Request{blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) end test "returns input txs and input utxo positions for canonicity challenges", %{processor_filled: processor, transactions: [tx | _], competing_tx: comp} do txbytes = txbytes(tx) processor = processor |> start_ife_from(comp) request = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5} assert {:ok, %{input_tx: "input_tx", input_utxo_pos: Utxo.position(1, 0, 0)}} = Core.get_competitor_for_ife(request, processor, txbytes) end test "by not asking for spends on no ifes", %{processor_empty: processor} do assert %{spends_to_get: []} = %ExitProcessor.Request{utxos_to_check: [Utxo.position(1, 0, 0)], utxo_exists_result: [false]} |> Core.determine_spends_to_get(processor) end test "none if input not yet created during sync", %{processor_filled: processor} do assert %{utxos_to_check: to_check} = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 13} |> Core.determine_utxo_existence_to_get(processor) assert Utxo.position(9000, 0, 1) not in to_check end test "for nonexistent tx doesn't crash", %{transactions: [tx | _], processor_empty: processor} do txbytes = Transaction.raw_txbytes(tx) assert {:error, :ife_not_known_for_tx} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.get_competitor_for_ife(processor, txbytes) end test "for malformed input txbytes doesn't crash", %{processor_empty: processor} do assert {:error, :malformed_transaction} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.get_competitor_for_ife(processor, <<0>>) end end describe "detects the need and allows to respond to canonicity challenges" do test "against a competitor", %{processor_filled: processor, transactions: [tx1 | _] = txs, competing_tx: comp} do {challenged_processor, _} = Core.new_ife_challenges(processor, [ife_challenge(tx1, comp)]) txbytes = Transaction.raw_txbytes(tx1) other_blknum = 3000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at(txs, other_blknum)] } challenged_processor = challenged_processor |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidIFEChallenge{txbytes: ^txbytes}]} = request |> check_validity_filtered(challenged_processor, only: [Event.InvalidIFEChallenge]) assert {:ok, %{ in_flight_txbytes: ^txbytes, in_flight_tx_pos: Utxo.position(^other_blknum, 0, 0), in_flight_proof: proof_bytes }} = Core.prove_canonical_for_ife(challenged_processor, txbytes) assert_proof_sound(proof_bytes) end test "proving canonical for nonexistent tx doesn't crash", %{processor_empty: processor, transactions: [tx | _]} do txbytes = Transaction.raw_txbytes(tx) request = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} processor = processor |> Core.find_ifes_in_blocks(request) assert {:error, :ife_not_known_for_tx} = Core.prove_canonical_for_ife(processor, txbytes) end test "for malformed input txbytes doesn't crash", %{processor_empty: processor} do request = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} processor = processor |> Core.find_ifes_in_blocks(request) assert {:error, :malformed_transaction} = Core.prove_canonical_for_ife(processor, <<0>>) end test "none if ifes are fresh and canonical by default", %{processor_filled: processor} do assert {:ok, []} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) end test "none if challenge gets responded and ife canonical", %{processor_filled: processor, transactions: [tx | _] = txs, competing_tx: comp} do txbytes = Transaction.raw_txbytes(tx) other_blknum = 3000 {processor, _} = Core.new_ife_challenges(processor, [ife_challenge(tx, comp)]) {processor, _} = processor |> Core.respond_to_in_flight_exits_challenges([ife_response(tx, Utxo.position(other_blknum, 0, 0))]) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at(txs, other_blknum)] } processor = processor |> Core.find_ifes_in_blocks(request) assert {:ok, []} = request |> check_validity_filtered(processor, only: [Event.InvalidIFEChallenge]) assert {:error, :no_viable_canonical_proof_found} = Core.prove_canonical_for_ife(processor, txbytes) end test "when there are two transaction inclusions to respond with", %{processor_filled: processor, transactions: [tx | _], competing_tx: comp} do txbytes = Transaction.raw_txbytes(tx) other_blknum = 3000 {processor, _} = Core.new_ife_challenges(processor, [ife_challenge(tx, comp)]) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, # NOTE: `tx` is included twice ife_input_spending_blocks_result: [Block.hashed_txs_at([tx, tx], other_blknum)] } processor = processor |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidIFEChallenge{txbytes: ^txbytes}]} = request |> check_validity_filtered(processor, only: [Event.InvalidIFEChallenge]) # older is returned but we'll respond with the younger first and then older assert {:ok, %{in_flight_tx_pos: Utxo.position(^other_blknum, 0, 0)}} = Core.prove_canonical_for_ife(processor, txbytes) {processor, _} = processor |> Core.respond_to_in_flight_exits_challenges([ife_response(tx, Utxo.position(other_blknum, 1, 0))]) assert {:ok, []} = request |> check_validity_filtered(processor, only: [Event.InvalidIFEChallenge]) assert {:error, :no_viable_canonical_proof_found} = Core.prove_canonical_for_ife(processor, txbytes) {processor, _} = processor |> Core.respond_to_in_flight_exits_challenges([ife_response(tx, Utxo.position(other_blknum, 0, 0))]) assert {:ok, []} = request |> check_validity_filtered(processor, only: [Event.InvalidIFEChallenge]) assert {:error, :no_viable_canonical_proof_found} = Core.prove_canonical_for_ife(processor, txbytes) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/core/state_interaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Core.StateInteractionTest do @moduledoc """ Test talking to OMG.State.Core """ use ExUnit.Case, async: false alias OMG.Eth.Configuration alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper, only: [start_se_from: 3, start_se_from: 4, start_ife_from: 2, start_ife_from: 3, piggyback_ife_from: 4] @default_min_exit_period_seconds 120 @default_child_block_interval 1000 @eth <<0::160>> @fee_claimer_address "NO FEE CLAIMER ADDR!" @early_blknum 1_000 @late_blknum 10_000 @utxo_pos1 Utxo.position(2, 0, 0) @utxo_pos2 Utxo.position(@late_blknum - 1_000, 0, 1) @fee %{@eth => [1]} # needs to match up with the default from `ExitProcessor.Case` :( @exit_id 9876 setup do db_path = Briefly.create!(directory: true) Application.put_env(:omg_db, :path, db_path, persistent: true) :ok = OMG.DB.init() {:ok, started_apps} = Application.ensure_all_started(:omg_db) {:ok, processor_empty} = Core.init([], [], [], @default_min_exit_period_seconds, @default_child_block_interval) child_block_interval = Configuration.child_block_interval() {:ok, state_empty} = State.Core.extract_initial_state(0, child_block_interval, @fee_claimer_address) on_exit(fn -> Application.put_env(:omg_db, :path, nil) Enum.map(started_apps, fn app -> :ok = Application.stop(app) end) end) {:ok, %{alice: TestHelper.generate_entity(), processor_empty: processor_empty, state_empty: state_empty}} end test "can work with State to determine and notify invalid exits", %{processor_empty: processor, state_empty: state, alice: alice} do exiting_position = Utxo.Position.encode(@utxo_pos1) standard_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) processor = start_se_from(processor, standard_exit_tx, @utxo_pos1) assert {:ok, [%Event.InvalidExit{utxo_pos: ^exiting_position}]} = %ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) |> mock_utxo_exists(state) |> Core.check_validity(processor) end test "exits of utxos that couldn't have been seen created yet never excite events", %{processor_empty: processor, state_empty: state, alice: alice} do standard_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) processor = start_se_from(processor, standard_exit_tx, Utxo.position(@late_blknum, 0, 0)) assert {:ok, []} = %ExitProcessor.Request{eth_height_now: 13, blknum_now: @early_blknum} |> Core.determine_utxo_existence_to_get(processor) |> mock_utxo_exists(state) |> Core.check_validity(processor) end test "handles invalid exit finalization - doesn't forget and causes a byzantine chain report", %{processor_empty: processor, state_empty: state, alice: alice} do standard_exit_tx1 = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) standard_exit_tx2 = TestHelper.create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 10}, {alice, 10}]) processor = processor |> start_se_from(standard_exit_tx1, @utxo_pos1, exit_id: 1) |> start_se_from(standard_exit_tx2, @utxo_pos2, eth_height: 4, exit_id: 2) # exits invalidly finalize and continue/start emitting events and complain {:ok, {_, two_spend}, state_after_spend} = [1, 2] |> Enum.map(&Core.exit_key_by_exit_id(processor, &1)) |> State.Core.exit_utxos(state) # finalizing here - note that without `finalize_exits`, we would just get a single invalid exit event # with - we get 3, because we include the invalidly finalized on which will hurt forever # (see persistence tests for the "forever" part) assert {processor, _} = Core.finalize_exits(processor, two_spend) assert {{:error, :unchallenged_exit}, [_event1, _event2, _event3]} = %ExitProcessor.Request{eth_height_now: 12, blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) |> mock_utxo_exists(state_after_spend) |> Core.check_validity(processor) end test "can work with State to determine valid exits and finalize them", %{processor_empty: processor, state_empty: state_empty, alice: alice} do state = state_empty |> TestHelper.do_deposit(alice, %{amount: 10, currency: @eth, blknum: 2}) standard_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) processor = start_se_from(processor, standard_exit_tx, @utxo_pos1) assert {:ok, []} = %ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) |> mock_utxo_exists(state) |> Core.check_validity(processor) # go into the future - old exits work the same assert {:ok, []} = %ExitProcessor.Request{eth_height_now: 105, blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) |> mock_utxo_exists(state) |> Core.check_validity(processor) # exit validly finalizes and continues to not emit any events {:ok, {_, spends}, _} = [@exit_id] |> Enum.map(&Core.exit_key_by_exit_id(processor, &1)) |> State.Core.exit_utxos(state) assert {processor, [{:put, :exit_info, {{2, 0, 0}, _}}]} = Core.finalize_exits(processor, spends) assert %ExitProcessor.Request{utxos_to_check: []} = Core.determine_utxo_existence_to_get(%ExitProcessor.Request{blknum_now: @late_blknum}, processor) end test "only asking for spends concerning ifes", %{alice: alice, processor_empty: processor, state_empty: state_empty} do processor = start_ife_from(processor, TestHelper.create_recovered([{1, 0, 0, alice}], [{alice, @eth, 1}])) state = state_empty |> TestHelper.do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) comp = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]) # first sanity-check as if the utxo was not spent yet assert %{utxos_to_check: utxos_to_check, utxo_exists_result: utxo_exists_result, spends_to_get: spends_to_get} = %ExitProcessor.Request{blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) |> mock_utxo_exists(state) |> Core.determine_spends_to_get(processor) assert {Utxo.position(1, 0, 0), false} not in Enum.zip(utxos_to_check, utxo_exists_result) assert Utxo.position(1, 0, 0) not in spends_to_get # spend and see that Core now requests the relevant utxo checks and spends to get {:ok, _, state} = OMG.Watcher.State.Core.exec(state, comp, @fee) {:ok, {block, _}, state} = OMG.Watcher.State.Core.form_block(state) assert %{utxos_to_check: utxos_to_check, utxo_exists_result: utxo_exists_result, spends_to_get: spends_to_get} = %ExitProcessor.Request{blknum_now: @late_blknum, blocks_result: [block]} |> Core.determine_utxo_existence_to_get(processor) |> mock_utxo_exists(state) |> Core.determine_spends_to_get(processor) assert {Utxo.position(1, 0, 0), false} in Enum.zip(utxos_to_check, utxo_exists_result) assert Utxo.position(1, 0, 0) in spends_to_get end test "can work with State to exit utxos from in-flight transactions", %{processor_empty: processor, state_empty: state, alice: alice} do # canonical & included ife_exit_tx1 = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]) ife_id1 = 1 # non-canonical ife_exit_tx2 = TestHelper.create_recovered([{2, 0, 0, alice}], @eth, [{alice, 19}]) tx_hash2 = State.Transaction.raw_txhash(ife_exit_tx2) ife_id2 = 2 {:ok, {tx_hash1, _, _}, state} = state |> TestHelper.do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> TestHelper.do_deposit(alice, %{amount: 10, currency: @eth, blknum: 2}) |> OMG.Watcher.State.Core.exec(ife_exit_tx1, @fee) {:ok, {block, _}, state} = State.Core.form_block(state) request = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [block]} processor = processor |> start_ife_from(ife_exit_tx1, exit_id: ife_id1) |> start_ife_from(ife_exit_tx2, exit_id: ife_id2) |> piggyback_ife_from(tx_hash1, 0, :output) |> piggyback_ife_from(tx_hash2, 0, :input) |> Core.find_ifes_in_blocks(request) finalizations = [ %{in_flight_exit_id: ife_id1, output_index: 0, omg_data: %{piggyback_type: :output}}, %{in_flight_exit_id: ife_id2, output_index: 0, omg_data: %{piggyback_type: :input}} ] ife_id1 = <> ife_id2 = <> ife_tx1_output_pos = Utxo.position(1000, 0, 0) ife_tx2_input_pos = Utxo.position(2, 0, 0) {:ok, %{^ife_id1 => exiting_positions1, ^ife_id2 => exiting_positions2}, [ {%{in_flight_exit_id: ^ife_id1}, [^ife_tx1_output_pos]}, {%{in_flight_exit_id: ^ife_id2}, [^ife_tx2_input_pos]} ]} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, finalizations) assert {:ok, {[{:delete, :utxo, _}, {:delete, :utxo, _}], {[Utxo.position(1000, 0, 0), Utxo.position(2, 0, 0)], []}}, _} = State.Core.exit_utxos(exiting_positions1 ++ exiting_positions2, state) end test "tolerates piggybacked outputs exiting if they're concerning non-included IFE txs", %{processor_empty: processor, state_empty: state, alice: alice} do ife_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) tx_hash = State.Transaction.raw_txhash(ife_exit_tx) ife_id = 1 # ife tx cannot be found in blocks, hence `ife_input_spending_blocks_result: []` request = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: []} processor = processor |> start_ife_from(ife_exit_tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 0, :output) |> Core.find_ifes_in_blocks(request) finalizations = [%{in_flight_exit_id: ife_id, output_index: 0, omg_data: %{piggyback_type: :output}}] ife_id = <> {:ok, %{^ife_id => exiting_positions}, []} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, finalizations) {:ok, {_, {[], [] = invalidities}}, _} = State.Core.exit_utxos(exiting_positions, state) assert {:ok, processor, [_]} = Core.finalize_in_flight_exits(processor, finalizations, %{ife_id => invalidities}) assert [] = Core.get_active_in_flight_exits(processor) end test "acts on invalidities reported when exiting utxos in State", %{processor_empty: processor, state_empty: state, alice: alice} do # canonical & included ife_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]) # double-spending the piggybacked ouptut spending_tx = TestHelper.create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 8}]) ife_id = 1 state = TestHelper.do_deposit(state, alice, %{amount: 10, currency: @eth, blknum: 1}) {:ok, {tx_hash, _, _}, state} = OMG.Watcher.State.Core.exec(state, ife_exit_tx, @fee) {:ok, {_, _, _}, state} = OMG.Watcher.State.Core.exec(state, spending_tx, @fee) {:ok, {block, _}, state} = OMG.Watcher.State.Core.form_block(state) request = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [block]} processor = processor |> start_ife_from(ife_exit_tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 0, :output) |> Core.find_ifes_in_blocks(request) finalizations = [%{in_flight_exit_id: ife_id, output_index: 0, omg_data: %{piggyback_type: :output}}] ife_id = <> [exiting_utxo] = Transaction.get_inputs(spending_tx) {:ok, %{^ife_id => exiting_positions}, [{%{in_flight_exit_id: ^ife_id}, [^exiting_utxo]}]} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, finalizations) {:ok, {_, {[], [_] = invalidities}}, _} = State.Core.exit_utxos(exiting_positions, state) assert {:ok, processor, [_]} = Core.finalize_in_flight_exits(processor, finalizations, %{ife_id => invalidities}) assert [_] = Core.get_active_in_flight_exits(processor) end test "deleting in-flight exits works with State", %{processor_empty: processor, state_empty: state, alice: alice} do ife_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]) ife_id = 1 state = TestHelper.do_deposit(state, alice, %{amount: 10, currency: @eth, blknum: 1}) {:ok, _, state} = OMG.Watcher.State.Core.form_block(state) {_processor, deleted_utxos, _db_updates} = processor |> start_ife_from(ife_exit_tx, exit_id: ife_id) |> Core.delete_in_flight_exits([%{exit_id: ife_id}]) assert {:ok, {[{:delete, :utxo, _}], {[{:utxo_position, 1, 0, 0}], []}}, _} = State.Core.exit_utxos(deleted_utxos, state) end defp mock_utxo_exists(%ExitProcessor.Request{utxos_to_check: positions} = request, state) do %{request | utxo_exists_result: positions |> Enum.map(&OMG.Watcher.State.Core.utxo_exists?(&1, state))} end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/core_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.CoreTest do @moduledoc """ Test of the logic of exit processor - various generic tests: starting events, some sanity checks, ife listing """ use OMG.Watcher.ExitProcessor.Case, async: true alias OMG.Watcher.Block alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.ExitProcessor.InFlightExitInfo alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper import ExUnit.CaptureLog, only: [capture_log: 1] @eth <<0::160>> @late_blknum 10_000 @utxo_pos1 Utxo.position(2, 0, 0) @utxo_pos2 Utxo.position(@late_blknum - 1_000, 0, 1) describe "generic sanity checks" do test "can start new standard exits one by one or batched", %{processor_empty: empty, alice: alice, bob: bob} do standard_exit_tx1 = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) standard_exit_tx2 = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 10}, {bob, 10}]) {event1, status1} = se_event_status(standard_exit_tx1, @utxo_pos1) {event2, status2} = se_event_status(standard_exit_tx2, @utxo_pos2) events = [event1, event2] statuses = [status1, status2] {state2, _} = Core.new_exits(empty, Enum.slice(events, 0, 1), Enum.slice(statuses, 0, 1)) {final_state, _} = Core.new_exits(empty, events, statuses) assert {^final_state, _} = Core.new_exits(state2, Enum.slice(events, 1, 1), Enum.slice(statuses, 1, 1)) end test "new_exits sanity checks", %{processor_empty: processor} do {:error, :unexpected_events} = processor |> Core.new_exits([:anything], []) {:error, :unexpected_events} = processor |> Core.new_exits([], [:anything]) end test "can process empty new exits, empty in flight exits", %{processor_empty: empty, processor_filled: filled} do assert {^empty, []} = Core.new_exits(empty, [], []) assert {^empty, []} = Core.new_in_flight_exits(empty, [], []) assert {^filled, []} = Core.new_exits(filled, [], []) assert {^filled, []} = Core.new_in_flight_exits(filled, [], []) end test "empty processor returns no exiting utxo positions", %{processor_empty: empty} do assert %ExitProcessor.Request{utxos_to_check: []} = Core.determine_utxo_existence_to_get(%ExitProcessor.Request{blknum_now: @late_blknum}, empty) end test "in flight exits sanity checks", %{processor_empty: state, in_flight_exit_events: events} do assert {state, []} == Core.new_in_flight_exits(state, [], []) assert {:error, :unexpected_events} == Core.new_in_flight_exits(state, Enum.slice(events, 0, 1), []) assert {:error, :unexpected_events} == Core.new_in_flight_exits(state, [], [{:anything, 1}]) end test "knows exits by exit_id the moment they start", %{processor_empty: processor, alice: alice} do standard_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) assert nil == Core.exit_key_by_exit_id(processor, 314) processor = processor |> start_se_from(standard_exit_tx, @utxo_pos1, exit_id: 314) assert @utxo_pos1 == Core.exit_key_by_exit_id(processor, 314) assert nil == Core.exit_key_by_exit_id(processor, 315) end test "knows exits by exit_id after challenging", %{processor_empty: processor, alice: alice} do standard_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) {processor, _} = processor |> start_se_from(standard_exit_tx, @utxo_pos1, exit_id: 314) |> Core.challenge_exits([%{utxo_pos: Utxo.Position.encode(@utxo_pos1)}]) assert @utxo_pos1 == Core.exit_key_by_exit_id(processor, 314) end test "doesn't know ife by exit_id because NOT IMPLEMENTED, remove when it's implemented", %{processor_empty: processor, alice: alice} do standard_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) {processor, _} = processor |> start_ife_from(standard_exit_tx, exit_id: 314) |> Core.challenge_exits([%{utxo_pos: Utxo.Position.encode(@utxo_pos1)}]) # because not implemented yet # TODO fix when implemented assert nil == Core.exit_key_by_exit_id(processor, 314) end end describe "check_sla_margin/4" do test "allows only safe margins if not forcing" do assert {:error, :sla_margin_too_big} = Core.check_sla_margin(10, false, 100, 15) assert :ok = Core.check_sla_margin(10, false, 300, 15) end test "allows anything if forcing" do capture_log(fn -> assert :ok = Core.check_sla_margin(10, true, 100, 15) end) assert :ok = Core.check_sla_margin(10, true, 300, 15) end end describe "active SE/IFE listing (only IFEs for now)" do test "properly processes new in flight exits, returns all of them on request", %{processor_empty: processor, in_flight_exit_events: events} do assert [] == Core.get_active_in_flight_exits(processor) # some statuses as received from the contract statuses = [{active_ife_status(), 1}, {active_ife_status(), 2}] {processor, _} = Core.new_in_flight_exits(processor, events, statuses) ifes_response = Core.get_active_in_flight_exits(processor) assert ifes_response |> Enum.count() == 2 end test "correct format of getting all ifes", %{processor_filled: processor, transactions: [tx1, tx2 | _]} do assert [ %{ txbytes: Transaction.raw_txbytes(tx1), txhash: Transaction.raw_txhash(tx1), eth_height: 1, piggybacked_inputs: [], piggybacked_outputs: [] }, %{ txbytes: Transaction.raw_txbytes(tx2), txhash: Transaction.raw_txhash(tx2), eth_height: 4, piggybacked_inputs: [], piggybacked_outputs: [] } ] == Core.get_active_in_flight_exits(processor) |> Enum.sort_by(& &1.eth_height) end test "reports piggybacked inputs/outputs when getting ifes", %{processor_empty: processor, transactions: [tx | _]} do txhash = Transaction.raw_txhash(tx) processor = start_ife_from(processor, tx) assert [%{piggybacked_inputs: [], piggybacked_outputs: []}] = Core.get_active_in_flight_exits(processor) processor = piggyback_ife_from(processor, txhash, 0, :input) assert [%{piggybacked_inputs: [0], piggybacked_outputs: []}] = Core.get_active_in_flight_exits(processor) processor = processor |> piggyback_ife_from(txhash, 0, :output) |> piggyback_ife_from(txhash, 1, :output) assert [%{piggybacked_inputs: [0], piggybacked_outputs: [0, 1]}] = Core.get_active_in_flight_exits(processor) end test "challenges don't affect the list of IFEs returned", %{processor_filled: processor, transactions: [tx | _], competing_tx: comp} do assert Core.get_active_in_flight_exits(processor) |> Enum.count() == 2 {processor2, _} = Core.new_ife_challenges(processor, [ife_challenge(tx, comp)]) assert Core.get_active_in_flight_exits(processor2) |> Enum.count() == 2 # sanity assert processor2 != processor end end describe "handling of spent blknums result" do test "asks for the right blocks when all are spent correctly" do assert [1000] = Core.handle_spent_blknum_result([{:ok, 1000}], [@utxo_pos1]) assert [] = Core.handle_spent_blknum_result([], []) assert [2000, 1000] = Core.handle_spent_blknum_result([{:ok, 2000}, {:ok, 1000}], [@utxo_pos2, @utxo_pos1]) end test "asks for blocks just once" do assert [1000] = Core.handle_spent_blknum_result([{:ok, 1000}, {:ok, 1000}], [@utxo_pos2, @utxo_pos1]) end @tag :capture_log test "asks for the right blocks if some spends are missing" do assert [1000] = Core.handle_spent_blknum_result([:not_found, {:ok, 1000}], [@utxo_pos2, @utxo_pos1]) end end describe "finding IFE txs in blocks" do test "handles well situation when syncing is in progress", %{processor_filled: state} do assert %ExitProcessor.Request{utxos_to_check: [], ife_input_utxos_to_check: []} = %ExitProcessor.Request{eth_height_now: 13, blknum_now: 0} |> Core.determine_ife_input_utxos_existence_to_get(state) |> Core.determine_utxo_existence_to_get(state) end test "seeks all IFE txs' inputs spends in blocks", %{processor_filled: processor, transactions: txs} do request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5 } # for one piggybacked output, we're asking for its inputs positions to check utxo existence request = Core.determine_ife_input_utxos_existence_to_get(request, processor) expected_inputs = txs |> Enum.flat_map(&Transaction.get_inputs/1) assert Enum.sort(expected_inputs) == Enum.sort(request.ife_input_utxos_to_check) # if it turns out to not exists, we're fetching the spending block request = request |> struct!(%{ife_input_utxo_exists_result: [false, true, true, true]}) |> Core.determine_ife_spends_to_get(processor) assert length(request.ife_input_spends_to_get) == 1 assert hd(request.ife_input_spends_to_get) in expected_inputs end test "seeks IFE txs in blocks, correctly if IFE inputs duplicate", %{processor_filled: processor, alice: alice, transactions: txs} do other_tx = TestHelper.create_recovered([{1, 0, 0, alice}], [{alice, @eth, 1}]) processor = start_ife_from(processor, other_tx) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5 } # for one piggybacked output, we're asking for its inputs positions to check utxo existence request = Core.determine_ife_input_utxos_existence_to_get(request, processor) expected_inputs = txs |> Enum.flat_map(&Transaction.get_inputs/1) assert Enum.sort(expected_inputs) == Enum.sort(request.ife_input_utxos_to_check) end test "seeks IFE txs in blocks only if not already found", %{processor_filled: processor, transactions: [tx1, tx2]} do request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx1], 3000)] } processor = processor |> Core.find_ifes_in_blocks(request) # for one piggybacked output, we're asking for its inputs positions to check utxo existence request = Core.determine_ife_input_utxos_existence_to_get(request, processor) expected_inputs = Transaction.get_inputs(tx2) assert Enum.sort(expected_inputs) == Enum.sort(request.ife_input_utxos_to_check) end end describe "active_standard_exiting_utxos" do test "returns a set of exiting utxo positions" do utxo_pos_active = {active_blknum, active_txindex, active_txoutput} = {1000, 0, 0} active_exit = %{ amount: 1, block_timestamp: 1, currency: <<1::160>>, eth_height: 1, exit_id: 1, exiting_txbytes: "txbytes", is_active: true, owner: <<1::160>>, root_chain_txhash: <<1::256>>, scheduled_finalization_time: 2, spending_txhash: nil } utxo_pos_inactive = {1000, 0, 1} inactive_exit = Map.replace!(active_exit, :is_active, false) db_exits = [{utxo_pos_active, active_exit}, {utxo_pos_inactive, inactive_exit}] expected = MapSet.new([Utxo.position(active_blknum, active_txindex, active_txoutput)]) assert expected == Core.active_standard_exiting_utxos(db_exits) end end describe "active_in_flight_exiting_inputs" do test "returns a set of exiting utxo positions" do expected_utxos = [Utxo.position(2001, 0, 0), Utxo.position(2002, 0, 0)] db_exits = [ {<<1>>, prepare_fake_ife_db_kv(false, [Utxo.position(1001, 0, 0)])}, {<<2>>, prepare_fake_ife_db_kv(true, expected_utxos)} ] assert MapSet.new(expected_utxos) == Core.active_in_flight_exiting_inputs(db_exits) end defp prepare_fake_ife_db_kv(is_active, utxos_pos) do raw_tx_map = %{tx_type: 1, inputs: [], outputs: [], metadata: <<0::256>>} signed_tx_map = %{raw_tx: raw_tx_map, sigs: []} utxo_pos = Utxo.position(0, 0, 0) db_value_map = %{ tx: signed_tx_map, exit_map: %{}, tx_pos: utxo_pos, oldest_competitor: utxo_pos, contract_id: <<1>>, timestamp: 0, eth_height: 100, relevant_from_blknum: 0, input_txs: [], input_utxos_pos: [], is_canonical: true, is_active: true } |> Map.update!(:is_active, fn _ -> is_active end) |> Map.update!(:input_utxos_pos, fn _ -> utxos_pos end) # sanity check - we need above date to be parsed correctly assert {<<1>>, %InFlightExitInfo{}} = InFlightExitInfo.from_db_kv({<<1>>, db_value_map}) db_value_map end end describe "delete_in_flight_exits/2" do test "returns deleted utxos and database updates", %{processor_empty: processor, alice: alice} do ife_exit_tx1 = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]) ife_id1 = 1 tx_hash1 = Transaction.raw_txhash(ife_exit_tx1) ife_exit_tx2 = TestHelper.create_recovered([{2, 0, 1, alice}, {2, 0, 2, alice}], @eth, [{alice, 9}]) ife_id2 = 2 ife_exit_tx3 = TestHelper.create_recovered([{3, 0, 1, alice}, {3, 0, 2, alice}], @eth, [{alice, 9}]) ife_id3 = 3 tx_hash3 = Transaction.raw_txhash(ife_exit_tx3) {_processor, deleted_utxos, db_updates} = processor |> start_ife_from(ife_exit_tx1, exit_id: ife_id1) |> start_ife_from(ife_exit_tx2, exit_id: ife_id2) |> start_ife_from(ife_exit_tx3, exit_id: ife_id3) |> Core.delete_in_flight_exits([%{exit_id: ife_id1}, %{exit_id: ife_id3}]) assert Enum.sort(deleted_utxos) == Enum.sort([{:utxo_position, 3, 0, 1}, {:utxo_position, 3, 0, 2}, {:utxo_position, 1, 0, 0}]) assert Enum.sort(db_updates) == Enum.sort([{:delete, :in_flight_exit_info, tx_hash1}, {:delete, :in_flight_exit_info, tx_hash3}]) end test "deletes in-flight exits from processor", %{processor_empty: processor, alice: alice} do ife_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]) ife_id = 1 {processor, _deleted_utxos, _db_updates} = processor |> start_ife_from(ife_exit_tx, exit_id: ife_id) |> Core.delete_in_flight_exits([%{exit_id: ife_id}]) assert Enum.empty?(processor.in_flight_exits) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/exit_info_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.ExitInfoTest do @moduledoc """ Test of the logic of the exit_info module """ use OMG.Watcher.ExitProcessor.Case, async: true alias OMG.Watcher.ExitProcessor.ExitInfo @recently_added_keys [:root_chain_txhash, :scheduled_finalization_time, :block_timestamp] @utxo_pos_1 {1000, 0, 0} @exit_1 [ amount: 1, block_timestamp: 1, currency: <<1::160>>, eth_height: 1, exit_id: 1, exiting_txbytes: "txbytes", is_active: false, owner: <<1::160>>, root_chain_txhash: <<1::256>>, scheduled_finalization_time: 2, spending_txhash: nil ] @exit_info_1 struct!(ExitInfo, @exit_1) @min_exit_period 20 @child_block_interval 1000 describe "from_db_kv/1" do test "default recently added keys to nil for existing entries without said key" do exit_info = Map.drop(@exit_info_1, @recently_added_keys) {_, exit_info_struct} = ExitInfo.from_db_kv({@utxo_pos_1, exit_info}) Enum.each(@recently_added_keys, fn recently_added_key -> value = Map.get(exit_info_struct, recently_added_key) assert value == nil end) end test "accepts an exit argument with recently added keys and includes them in the struct" do {_, exit_info_struct} = ExitInfo.from_db_kv({@utxo_pos_1, @exit_info_1}) Enum.each(@recently_added_keys, fn recently_added_key -> assert Map.get(exit_info_struct, recently_added_key) == Map.get(@exit_info_1, recently_added_key) end) end end describe "calculate_sft/4" do test "calculates scheduled finalisation time correctly if UTXO was created by a deposit" do deposit_blknum = 2001 utxo_creation_ts = 50 # By setting the exit timestamp at within @min_exit_period from the creation of the UTXO, # the fact that the UTXO was a deposit changes the resulting scheduled finalisation time, # thereby testing as intended. exit_ts = utxo_creation_ts + @min_exit_period - 10 expected_sft = max(exit_ts + @min_exit_period, utxo_creation_ts + @min_exit_period) assert {:ok, expected_sft} == ExitInfo.calculate_sft( deposit_blknum, exit_ts, utxo_creation_ts, @min_exit_period, @child_block_interval ) end test "calculates scheduled finalisation time correctly if UTXO was created by a child chain transaction" do blknum = 2000 utxo_creation_ts = 50 # By setting the exit timestamp at within @min_exit_period from the creation of the UTXO, # the fact that the UTXO was not created by a deposit changes the resulting scheduled finalisation time, # thereby testing as intended. exit_ts = utxo_creation_ts + @min_exit_period - 10 expected_sft = max(exit_ts + @min_exit_period, utxo_creation_ts + 2 * @min_exit_period) assert {:ok, expected_sft} == ExitInfo.calculate_sft( blknum, exit_ts, utxo_creation_ts, @min_exit_period, @child_block_interval ) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/finalizations_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.FinalizationsTest do @moduledoc """ Test of the logic of exit processor - finalizing various flavors of exits and handling finalization validity """ use OMG.Watcher.ExitProcessor.Case, async: true alias OMG.Watcher.Block alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper @exit_id 1 describe "sanity checks" do test "can process empty finalizations", %{processor_empty: empty, processor_filled: filled} do assert {^empty, []} = Core.finalize_exits(empty, {[], []}) assert {^filled, []} = Core.finalize_exits(filled, {[], []}) assert {:ok, %{}, []} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(empty, []) assert {:ok, %{}, []} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(filled, []) assert {:ok, ^empty, []} = Core.finalize_in_flight_exits(empty, [], %{}) assert {:ok, ^filled, []} = Core.finalize_in_flight_exits(filled, [], %{}) end end describe "determining utxos that are exited by finalization" do test "signals all included txs' outputs as exiting when piggybacked output exits", %{processor_empty: processor, transactions: [tx1 | _]} do ife_id1 = 1 tx_hash1 = Transaction.raw_txhash(tx1) tx1_blknum = 3000 # both IFE txs are inlcuded in one of the blocks and picked up by the `ExitProcessor` request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx1], tx1_blknum)] } processor = processor |> start_ife_from(tx1, exit_id: ife_id1) |> piggyback_ife_from(tx_hash1, 0, :input) |> piggyback_ife_from(tx_hash1, 1, :input) |> piggyback_ife_from(tx_hash1, 0, :output) |> piggyback_ife_from(tx_hash1, 1, :output) |> Core.find_ifes_in_blocks(request) finalizations = [ %{in_flight_exit_id: ife_id1, output_index: 0, omg_data: %{piggyback_type: :output}}, %{in_flight_exit_id: ife_id1, output_index: 1, omg_data: %{piggyback_type: :output}} ] ife_id1 = <> tx1_first_output = Utxo.position(tx1_blknum, 0, 0) tx1_second_output = Utxo.position(tx1_blknum, 0, 1) assert { :ok, %{^ife_id1 => [^tx1_first_output, ^tx1_second_output]}, [{%{output_index: 0}, [^tx1_first_output]}, {%{output_index: 1}, [^tx1_second_output]}] } = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, finalizations) end test "doesn't signal non-included txs' outputs as exiting when piggybacked output exits", %{processor_empty: processor, transactions: [tx1 | _]} do ife_id1 = 2 tx_hash1 = Transaction.raw_txhash(tx1) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [] } processor = processor |> start_ife_from(tx1, exit_id: ife_id1) |> piggyback_ife_from(tx_hash1, 0, :output) |> piggyback_ife_from(tx_hash1, 1, :output) |> Core.find_ifes_in_blocks(request) finalizations = [ %{in_flight_exit_id: ife_id1, output_index: 0, omg_data: %{piggyback_type: :output}}, %{in_flight_exit_id: ife_id1, output_index: 1, omg_data: %{piggyback_type: :output}} ] ife_id1 = <> assert {:ok, %{^ife_id1 => []}, []} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, finalizations) end test "returns utxos that should be spent when exit finalizes, two ifes combined", %{processor_empty: processor, transactions: [tx1, tx2 | _]} do ife_id1 = 1 ife_id2 = 2 tx_hash1 = Transaction.raw_txhash(tx1) tx_hash2 = Transaction.raw_txhash(tx2) tx2_blknum = 3000 # both IFE txs are inlcuded in one of the blocks and picked up by the `ExitProcessor` request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx1, tx2], tx2_blknum)] } processor = processor |> start_ife_from(tx1, exit_id: ife_id1) |> start_ife_from(tx2, exit_id: ife_id2) |> Core.find_ifes_in_blocks(request) |> piggyback_ife_from(tx_hash1, 0, :input) |> piggyback_ife_from(tx_hash1, 1, :input) |> piggyback_ife_from(tx_hash2, 0, :output) |> piggyback_ife_from(tx_hash2, 1, :output) finalizations = [ %{in_flight_exit_id: ife_id1, output_index: 0, omg_data: %{piggyback_type: :input}}, %{in_flight_exit_id: ife_id2, output_index: 0, omg_data: %{piggyback_type: :output}} ] assert {:ok, %{}, []} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, []) ife_id1 = <> ife_id2 = <> tx1_first_input = tx1 |> Transaction.get_inputs() |> hd() tx2_first_output = Utxo.position(tx2_blknum, 1, 0) assert { :ok, %{^ife_id1 => [^tx1_first_input], ^ife_id2 => [^tx2_first_output]}, [ {%{in_flight_exit_id: ^ife_id1}, [^tx1_first_input]}, {%{in_flight_exit_id: ^ife_id2}, [^tx2_first_output]} ] } = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, finalizations) end test "fails when unknown in-flight exit is being finalized", %{processor_empty: processor} do finalization = %{in_flight_exit_id: @exit_id, output_index: 1, omg_data: %{piggyback_type: :input}} {:unknown_in_flight_exit, unknown_exits} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, [finalization]) assert unknown_exits == MapSet.new([<<@exit_id::192>>]) end test "fails when exiting an output that is not piggybacked", %{processor_empty: processor, transactions: [tx | _]} do tx_hash = Transaction.raw_txhash(tx) ife_id = 123 processor = processor |> start_ife_from(tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 1, :input) finalization1 = %{in_flight_exit_id: ife_id, output_index: 1, omg_data: %{piggyback_type: :input}} finalization2 = %{in_flight_exit_id: ife_id, output_index: 2, omg_data: %{piggyback_type: :input}} expected_unknown_piggybacks = [ %{in_flight_exit_id: <>, output_index: 2, omg_data: %{piggyback_type: :input}} ] {:inactive_piggybacks_finalizing, ^expected_unknown_piggybacks} = Core.prepare_utxo_exits_for_in_flight_exit_finalizations(processor, [finalization1, finalization2]) end end describe "in-flight exit finalization" do test "exits piggybacked transaction inputs", %{processor_empty: processor, transactions: [tx | _]} do ife_id = 123 tx_hash = Transaction.raw_txhash(tx) processor = processor |> start_ife_from(tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 0, :input) |> piggyback_ife_from(tx_hash, 1, :input) assert {:ok, processor, [{:put, :in_flight_exit_info, _}]} = Core.finalize_in_flight_exits( processor, [%{in_flight_exit_id: ife_id, output_index: 0, omg_data: %{piggyback_type: :input}}], %{} ) assert {:ok, _, [{:put, :in_flight_exit_info, _}]} = Core.finalize_in_flight_exits( processor, [%{in_flight_exit_id: ife_id, output_index: 1, omg_data: %{piggyback_type: :input}}], %{} ) end test "exits piggybacked transaction outputs", %{processor_empty: processor, transactions: [tx | _]} do ife_id = 123 tx_hash = Transaction.raw_txhash(tx) processor = processor |> start_ife_from(tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 0, :output) |> piggyback_ife_from(tx_hash, 1, :output) assert {:ok, _, [{:put, :in_flight_exit_info, _}]} = Core.finalize_in_flight_exits( processor, [ %{in_flight_exit_id: ife_id, output_index: 1, omg_data: %{piggyback_type: :output}}, %{in_flight_exit_id: ife_id, output_index: 0, omg_data: %{piggyback_type: :output}} ], %{} ) end test "deactivates in-flight exit after all piggybacked outputs are finalized", %{processor_empty: processor, transactions: [tx | _]} do ife_id = 123 tx_hash = Transaction.raw_txhash(tx) processor = processor |> start_ife_from(tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 1, :input) |> piggyback_ife_from(tx_hash, 2, :input) {:ok, processor, _} = Core.finalize_in_flight_exits( processor, [%{in_flight_exit_id: ife_id, output_index: 1, omg_data: %{piggyback_type: :input}}], %{} ) [_] = Core.get_active_in_flight_exits(processor) {:ok, processor, _} = Core.finalize_in_flight_exits( processor, [%{in_flight_exit_id: ife_id, output_index: 2, omg_data: %{piggyback_type: :input}}], %{} ) assert [] == Core.get_active_in_flight_exits(processor) end test "finalizing multiple times returns an error since it is not possible", %{processor_empty: processor, transactions: [tx | _]} do ife_id = 123 tx_hash = Transaction.raw_txhash(tx) processor = processor |> start_ife_from(tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 1, :input) finalization = %{in_flight_exit_id: ife_id, output_index: 1, omg_data: %{piggyback_type: :input}} {:ok, processor, _} = Core.finalize_in_flight_exits(processor, [finalization], %{}) {:inactive_piggybacks_finalizing, _} = Core.finalize_in_flight_exits(processor, [finalization], %{}) end test "finalizing perserve in flights exits that are not being finalized", %{processor_empty: processor, transactions: [tx1, tx2]} do ife_id1 = 123 tx_hash1 = Transaction.raw_txhash(tx1) ife_id2 = 124 tx_hash2 = Transaction.raw_txhash(tx2) processor = processor |> start_ife_from(tx1, exit_id: ife_id1) |> start_ife_from(tx2, exit_id: ife_id2) |> piggyback_ife_from(tx_hash1, 1, :input) finalization = %{in_flight_exit_id: ife_id1, output_index: 1, omg_data: %{piggyback_type: :input}} {:ok, processor, _} = Core.finalize_in_flight_exits(processor, [finalization], %{}) [%{txhash: ^tx_hash2}] = Core.get_active_in_flight_exits(processor) end test "fails when unknown in-flight exit is being finalized", %{processor_empty: processor} do finalization = %{in_flight_exit_id: @exit_id, output_index: 1, omg_data: %{piggyback_type: :input}} {:unknown_in_flight_exit, unknown_exits} = Core.finalize_in_flight_exits(processor, [finalization], %{}) assert unknown_exits == MapSet.new([<<@exit_id::192>>]) end test "fails when exiting an output that is not piggybacked", %{processor_empty: processor, transactions: [tx | _]} do tx_hash = Transaction.raw_txhash(tx) ife_id = 123 processor = processor |> start_ife_from(tx, exit_id: ife_id) |> piggyback_ife_from(tx_hash, 1, :input) finalization1 = %{in_flight_exit_id: ife_id, output_index: 1, omg_data: %{piggyback_type: :input}} finalization2 = %{in_flight_exit_id: ife_id, output_index: 2, omg_data: %{piggyback_type: :input}} expected_unknown_piggybacks = [ %{in_flight_exit_id: <>, output_index: 2, omg_data: %{piggyback_type: :input}} ] {:inactive_piggybacks_finalizing, ^expected_unknown_piggybacks} = Core.finalize_in_flight_exits(processor, [finalization1, finalization2], %{}) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/in_flight_exit_info_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.InFlightExitInfoTest do @moduledoc false use OMG.Watcher.ExitProcessor.Case, async: true alias OMG.Watcher.ExitProcessor.InFlightExitInfo alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo.Position @eth <<0::160>> describe "get_input_utxos/1" do test "returns a list of input utxos" do inputs1 = [ Position.encode({:utxo_position, 1, 0, 0}), Position.encode({:utxo_position, 1, 0, 1}) ] inputs2 = [Position.encode({:utxo_position, 1, 0, 2})] ife_infos = [ ife_info_with_inputs(inputs1), ife_info_with_inputs(inputs2) ] expected = inputs1 ++ inputs2 assert InFlightExitInfo.get_input_utxos(ife_infos) == expected end end defp ife_info_with_inputs(inputs) do tx = Transaction.Payment.new( [{1, 0, 0}], [{"alice", @eth, 1}, {"alice", @eth, 2}], <<0::256>> ) %InFlightExitInfo{ tx: %Transaction.Signed{raw_tx: tx, sigs: <<1::520>>}, timestamp: 1, contract_id: <<1::160>>, eth_height: 1, is_active: true, input_utxos_pos: inputs } end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/persistence_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.PersistenceTest do @moduledoc """ Test focused on the persistence bits of `OMG.Watcher.ExitProcessor.Core`. The aim of this test is to ensure, that whatever state the processor ends up being in will be revived from the DB """ use ExUnitFixtures use OMG.DB.RocksDBCase, async: true alias OMG.DB.Models.PaymentExitInfo alias OMG.Watcher.DevCrypto alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper @default_min_exit_period_seconds 120 @default_child_block_interval 1000 @eth <<0::160>> @utxo_pos1 Utxo.position(1, 0, 0) @utxo_pos2 Utxo.position(1_000, 0, 1) @zero_exit_id 0 @non_zero_exit_id 1 @zero_sig <<0::520>> setup %{db_pid: db_pid} do :ok = OMG.DB.initiation_multiupdate(db_pid) alice = OMG.Watcher.TestHelper.generate_entity() carol = OMG.Watcher.TestHelper.generate_entity() {:ok, processor_empty} = Core.init([], [], [], @default_min_exit_period_seconds, @default_child_block_interval) transactions = [ Transaction.Payment.new([{1, 0, 0}, {1, 2, 1}], [{alice.addr, @eth, 1}, {carol.addr, @eth, 2}]), Transaction.Payment.new([{2, 1, 0}, {2, 2, 1}], [{alice.addr, @eth, 1}, {carol.addr, @eth, 2}]) ] [txbytes1, txbytes2] = transactions |> Enum.map(&Transaction.raw_txbytes/1) exits = {[ %{ owner: alice.addr, eth_height: 2, exit_id: 1, call_data: %{utxo_pos: Utxo.Position.encode(@utxo_pos1), output_tx: txbytes1}, root_chain_txhash: <<1::256>>, block_timestamp: 1, scheduled_finalization_time: 2 }, %{ owner: alice.addr, eth_height: 4, exit_id: 2, call_data: %{utxo_pos: Utxo.Position.encode(@utxo_pos2), output_tx: txbytes2}, root_chain_txhash: <<2::256>>, block_timestamp: 3, scheduled_finalization_time: 4 } ], [ {true, Utxo.Position.encode(@utxo_pos1), Utxo.Position.encode(@utxo_pos1), alice.addr, 10, 0}, {false, Utxo.Position.encode(@utxo_pos2), Utxo.Position.encode(@utxo_pos2), alice.addr, 10, 0} ]} {:ok, %{alice: alice, carol: carol, processor_empty: processor_empty, transactions: transactions, exits: exits}} end test "persist finalizations with mixed validities", %{processor_empty: processor, db_pid: db_pid, exits: {exit_events, statuses}} do processor |> persist_new_exits(exit_events, statuses, db_pid) |> persist_finalize_exits({[@utxo_pos1], [@utxo_pos2]}, db_pid) end test "persist finalizations with all valid", %{processor_empty: processor, db_pid: db_pid, exits: {exit_events, statuses}} do processor |> persist_new_exits(exit_events, statuses, db_pid) |> persist_finalize_exits({[@utxo_pos1, @utxo_pos2], []}, db_pid) end test "persist finalizations with all invalid", %{processor_empty: processor, db_pid: db_pid, exits: {exit_events, statuses}} do processor |> persist_new_exits(exit_events, statuses, db_pid) |> persist_finalize_exits({[], [@utxo_pos1, @utxo_pos2]}, db_pid) end test "persist challenges", %{processor_empty: processor, db_pid: db_pid, exits: {exit_events, statuses}} do processor |> persist_new_exits(exit_events, statuses, db_pid) |> persist_challenge_exits([@utxo_pos1], db_pid) end test "persist multiple challenges", %{processor_empty: processor, db_pid: db_pid, exits: {exit_events, statuses}} do processor |> persist_new_exits(exit_events, statuses, db_pid) |> persist_challenge_exits([@utxo_pos2, @utxo_pos1], db_pid) end test "persist started ifes regardless of status", %{processor_empty: processor, alice: alice, carol: carol, db_pid: db_pid} do txs = [ Transaction.Payment.new([{1, 0, 0}, {1, 2, 1}], [{alice.addr, @eth, 1}]), Transaction.Payment.new([{2, 1, 0}, {2, 2, 1}], [{alice.addr, @eth, 1}, {carol.addr, @eth, 2}]) ] contract_statuses = [{active_ife_status(), @non_zero_exit_id}, {inactive_ife_status(), @zero_exit_id}] processor |> persist_new_ifes(txs, [[alice.priv], [alice.priv, carol.priv]], contract_statuses, db_pid) end test "persist new challenges, responses and piggybacks", %{processor_empty: processor, alice: alice, db_pid: db_pid} do tx = Transaction.Payment.new([{2, 1, 0}], [{alice.addr, @eth, 1}, {alice.addr, @eth, 2}]) hash = Transaction.raw_txhash(tx) competing_tx = Transaction.Payment.new([{2, 1, 0}, {1, 0, 0}], [{alice.addr, @eth, 2}, {alice.addr, @eth, 1}]) challenge = %{ tx_hash: hash, competitor_position: Utxo.Position.encode(@utxo_pos2), call_data: %{ competing_tx: Transaction.raw_txbytes(competing_tx), competing_tx_input_index: 0, competing_tx_sig: @zero_sig } } piggybacks1 = [ %{tx_hash: hash, output_index: 0, omg_data: %{piggyback_type: :input}}, %{tx_hash: hash, output_index: 0, omg_data: %{piggyback_type: :output}} ] piggybacks2 = [%{tx_hash: hash, output_index: 1, omg_data: %{piggyback_type: :output}}] processor |> persist_new_ifes([tx], [[alice.priv]], db_pid) |> persist_new_piggybacks(piggybacks1, db_pid) |> persist_new_piggybacks(piggybacks2, db_pid) |> persist_new_ife_challenges([challenge], db_pid) |> persist_challenge_piggybacks(piggybacks2, db_pid) |> persist_challenge_piggybacks(piggybacks1, db_pid) |> persist_respond_to_in_flight_exits_challenges([ife_response(tx, @utxo_pos1)], db_pid) end test "persist ife finalizations", %{processor_empty: processor, alice: alice, db_pid: db_pid} do tx = Transaction.Payment.new([{2, 1, 0}], [{alice.addr, @eth, 1}, {alice.addr, @eth, 2}]) hash = Transaction.raw_txhash(tx) piggybacks1 = [ %{tx_hash: hash, output_index: 0, omg_data: %{piggyback_type: :input}}, %{tx_hash: hash, output_index: 0, omg_data: %{piggyback_type: :output}} ] piggybacks2 = [%{tx_hash: hash, output_index: 1, omg_data: %{piggyback_type: :output}}] processor |> persist_new_ifes([tx], [[alice.priv]], db_pid) |> persist_new_piggybacks(piggybacks1, db_pid) |> persist_new_piggybacks(piggybacks2, db_pid) |> persist_finalize_ifes( [%{in_flight_exit_id: @non_zero_exit_id, output_index: 0, omg_data: %{piggyback_type: :input}}], db_pid ) |> persist_finalize_ifes( [ %{in_flight_exit_id: @non_zero_exit_id, output_index: 0, omg_data: %{piggyback_type: :output}}, %{in_flight_exit_id: @non_zero_exit_id, output_index: 1, omg_data: %{piggyback_type: :output}} ], db_pid ) end # mimics `&OMG.Watcher.ExitProcessor.init/1` defp state_from(db_pid) do {:ok, db_exits} = PaymentExitInfo.all_exit_infos(db_pid) {:ok, db_ifes} = PaymentExitInfo.all_in_flight_exits_infos(db_pid) {:ok, db_competitors} = OMG.DB.competitors_info(db_pid) {:ok, state} = Core.init(db_exits, db_ifes, db_competitors, @default_min_exit_period_seconds, @default_child_block_interval) state end defp persist_common(processor, db_updates, db_pid) do assert :ok = OMG.DB.multi_update(db_updates, db_pid) assert processor == state_from(db_pid) processor end defp persist_new_exits(processor, exit_events, contract_statuses, db_pid) do {processor, db_updates} = Core.new_exits(processor, exit_events, contract_statuses) persist_common(processor, db_updates, db_pid) end defp persist_finalize_exits(processor, validities, db_pid) do {processor, db_updates} = Core.finalize_exits(processor, validities) persist_common(processor, db_updates, db_pid) end defp persist_challenge_exits(processor, utxo_positions, db_pid) do {processor, db_updates} = Core.challenge_exits(processor, utxo_positions |> Enum.map(&%{utxo_pos: Utxo.Position.encode(&1)})) persist_common(processor, db_updates, db_pid) end defp persist_new_ifes(processor, txs, priv_keys, statuses \\ nil, db_pid) do in_flight_exit_events = txs |> Enum.zip(priv_keys) |> Enum.map(fn {tx, keys} -> {tx, DevCrypto.sign(tx, keys)} end) |> Enum.map(fn {tx, signed_tx} -> ife_event(tx, sigs: signed_tx.sigs) end) statuses = statuses || List.duplicate({active_ife_status(), @non_zero_exit_id}, length(in_flight_exit_events)) {processor, db_updates} = Core.new_in_flight_exits(processor, in_flight_exit_events, statuses) persist_common(processor, db_updates, db_pid) end defp persist_new_piggybacks(processor, piggybacks, db_pid) do {processor, db_updates} = Core.new_piggybacks(processor, piggybacks) persist_common(processor, db_updates, db_pid) end defp persist_new_ife_challenges(processor, challenges, db_pid) do {processor, db_updates} = Core.new_ife_challenges(processor, challenges) persist_common(processor, db_updates, db_pid) end defp persist_respond_to_in_flight_exits_challenges(processor, challenges, db_pid) do {processor, db_updates} = Core.respond_to_in_flight_exits_challenges(processor, challenges) persist_common(processor, db_updates, db_pid) end defp persist_challenge_piggybacks(processor, piggybacks, db_pid) do {processor, db_updates} = Core.challenge_piggybacks(processor, piggybacks) persist_common(processor, db_updates, db_pid) end defp persist_finalize_ifes(processor, finalizations, db_pid) do {:ok, processor, db_updates} = Core.finalize_in_flight_exits(processor, finalizations, %{}) persist_common(processor, db_updates, db_pid) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/piggyback_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.PiggybackTest do @moduledoc """ Test of the logic of exit processor - detecting conditions related to piggybacks """ # this is where the setup comes from!!! use OMG.Watcher.ExitProcessor.Case, async: true alias OMG.Watcher.Block alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper # needs to match up with the default from `ExitProcessor.Case` :( @exit_id 9876 @eth <<0::160>> describe "sanity checks" do test "throwing when unknown piggyback events arrive", %{processor_filled: processor, ife_tx_hashes: [ife_id | _]} do non_existent_exit_id = <<0>> index_beyond_bound = 4 catch_error(piggyback_ife_from(processor, non_existent_exit_id, 0, :input)) catch_error(piggyback_ife_from(processor, ife_id, index_beyond_bound, :output)) # cannot piggyback twice the same output updated_processor = piggyback_ife_from(processor, ife_id, 0, :input) catch_error(piggyback_ife_from(updated_processor, ife_id, 0, :input)) end test "can process empty piggybacks and challenges", %{processor_empty: empty, processor_filled: filled} do {^empty, []} = Core.new_piggybacks(empty, []) {^filled, []} = Core.new_piggybacks(filled, []) {^empty, []} = Core.challenge_piggybacks(empty, []) {^filled, []} = Core.challenge_piggybacks(filled, []) end test "can process new piggybacks in batch", %{processor_filled: processor, ife_tx_hashes: [tx_hash1, tx_hash2]} do updated_processor = processor |> piggyback_ife_from(tx_hash1, 0, :input) |> piggyback_ife_from(tx_hash2, 0, :input) assert {^updated_processor, _} = Core.new_piggybacks(processor, [ %{tx_hash: tx_hash1, output_index: 0, omg_data: %{piggyback_type: :input}}, %{tx_hash: tx_hash2, output_index: 0, omg_data: %{piggyback_type: :input}} ]) end end test "forgets challenged piggybacks", %{processor_filled: processor, ife_tx_hashes: [tx_hash1, tx_hash2]} do processor = processor |> piggyback_ife_from(tx_hash1, 0, :input) |> piggyback_ife_from(tx_hash2, 0, :input) # sanity: there are some piggybacks after piggybacking, to be removed later assert [%{piggybacked_inputs: [_]}, %{piggybacked_inputs: [_]}] = Core.get_active_in_flight_exits(processor) {processor, _} = Core.challenge_piggybacks(processor, [%{tx_hash: tx_hash1, output_index: 0, omg_data: %{piggyback_type: :input}}]) assert [%{txhash: ^tx_hash1, piggybacked_inputs: []}, %{piggybacked_inputs: [0]}] = Core.get_active_in_flight_exits(processor) |> Enum.sort_by(&length(&1.piggybacked_inputs)) end test "can open and challenge two piggybacks at one call", %{processor_filled: processor, ife_tx_hashes: [tx_hash1, tx_hash2]} do events = [ %{tx_hash: tx_hash1, output_index: 0, omg_data: %{piggyback_type: :input}}, %{tx_hash: tx_hash2, output_index: 0, omg_data: %{piggyback_type: :input}} ] {processor, _} = Core.new_piggybacks(processor, events) # sanity: there are some piggybacks after piggybacking, to be removed later assert [%{piggybacked_inputs: [_]}, %{piggybacked_inputs: [_]}] = Core.get_active_in_flight_exits(processor) {processor, _} = Core.challenge_piggybacks(processor, events) assert [%{piggybacked_inputs: []}, %{piggybacked_inputs: []}] = Core.get_active_in_flight_exits(processor) end describe "available piggybacks" do test "detects multiple available piggybacks, with all the fields", %{processor_filled: processor, transactions: [tx1, tx2], alice: alice, carol: carol} do txbytes_1 = Transaction.raw_txbytes(tx1) txbytes_2 = Transaction.raw_txbytes(tx2) assert {:ok, events} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.check_validity(processor) assert_events(events, [ %Event.PiggybackAvailable{ available_inputs: [%{address: alice.addr, index: 0}, %{address: carol.addr, index: 1}], available_outputs: [%{address: alice.addr, index: 0}, %{address: carol.addr, index: 1}], txbytes: txbytes_1 }, %Event.PiggybackAvailable{ available_inputs: [%{address: alice.addr, index: 0}, %{address: carol.addr, index: 1}], available_outputs: [%{address: alice.addr, index: 0}, %{address: carol.addr, index: 1}], txbytes: txbytes_2 } ]) end test "detects available piggyback because tx not seen in valid block, regardless of competitors", %{processor_empty: processor, alice: alice} do # testing this because everywhere else, the test fixtures always imply competitors tx = TestHelper.create_recovered([{1, 0, 0, alice}], [{alice, @eth, 1}]) txbytes = txbytes(tx) processor = processor |> start_ife_from(tx) assert {:ok, [%Event.PiggybackAvailable{txbytes: ^txbytes}]} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.check_validity(processor) end test "detects available piggyback correctly, even if signed multiple times", %{processor_empty: processor, alice: alice} do # there is leeway in the contract, that allows IFE transactions to hold non-zero signatures for zero-inputs # we want to be sure that this doesn't crash the `ExitProcessor` tx = Transaction.Payment.new([{1, 0, 0}], [{alice.addr, @eth, 1}]) txbytes = txbytes(tx) # superfluous signatures %{sigs: sigs} = signed_tx = OMG.Watcher.DevCrypto.sign(tx, [alice.priv]) processor = processor |> start_ife_from(signed_tx, sigs: sigs) assert {:ok, [%Event.PiggybackAvailable{txbytes: ^txbytes}]} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> Core.check_validity(processor) end test "doesn't detect available piggybacks because txs seen in valid block", %{processor_filled: processor, transactions: [tx1, tx2]} do txbytes2 = txbytes(tx2) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx1], 3000)] } processor = processor |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.PiggybackAvailable{txbytes: ^txbytes2}]} = request |> Core.check_validity(processor) end test "transaction with different input/output owners", %{alice: alice, bob: bob, carol: carol, processor_empty: processor} do tx = TestHelper.create_recovered([{1, 0, 0, alice}, {1, 2, 1, bob}], [{carol, @eth, 1}]) alice_addr = alice.addr bob_addr = bob.addr carol_addr = carol.addr txbytes = txbytes(tx) processor = processor |> start_ife_from(tx) assert {:ok, [ %Event.PiggybackAvailable{ available_inputs: [%{address: ^alice_addr, index: 0}, %{address: ^bob_addr, index: 1}], available_outputs: [%{address: ^carol_addr, index: 0}], txbytes: ^txbytes } ]} = check_validity_filtered(%ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5}, processor, only: [Event.PiggybackAvailable] ) end test "when input is already piggybacked, it is not reported in piggyback available event", %{alice: alice, processor_empty: processor} do tx = TestHelper.create_recovered([{1, 0, 0, alice}, {1, 2, 1, alice}], [{alice, @eth, 1}]) tx_hash = Transaction.raw_txhash(tx) processor = processor |> start_ife_from(tx) |> piggyback_ife_from(tx_hash, 0, :input) assert {:ok, [ %Event.PiggybackAvailable{ available_inputs: [%{index: 1}], available_outputs: [%{index: 0}] } ]} = check_validity_filtered(%ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5}, processor, only: [Event.PiggybackAvailable] ) end test "when output is already piggybacked, it is not reported in piggyback available event", %{alice: alice, processor_empty: processor} do tx = TestHelper.create_recovered([{1, 0, 0, alice}, {1, 2, 1, alice}], [{alice, @eth, 1}]) tx_hash = Transaction.raw_txhash(tx) processor = processor |> start_ife_from(tx) |> piggyback_ife_from(tx_hash, 0, :output) assert {:ok, [ %Event.PiggybackAvailable{ available_inputs: [%{index: 0}, %{index: 1}], available_outputs: [] } ]} = check_validity_filtered(%ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5}, processor, only: [Event.PiggybackAvailable] ) end test "when output is already piggybacked, it is not reported in piggyback available event, even if challenged", %{alice: alice, processor_empty: processor} do tx = TestHelper.create_recovered([{1, 0, 0, alice}, {1, 2, 1, alice}], [{alice, @eth, 1}]) tx_hash = Transaction.raw_txhash(tx) {processor, _} = processor |> start_ife_from(tx) |> piggyback_ife_from(tx_hash, 0, :output) |> Core.challenge_piggybacks([%{tx_hash: tx_hash, output_index: 0, omg_data: %{piggyback_type: :output}}]) assert {:ok, [ %Event.PiggybackAvailable{ available_inputs: [%{index: 0}, %{index: 1}], available_outputs: [] } ]} = check_validity_filtered(%ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5}, processor, only: [Event.PiggybackAvailable] ) end test "when ife is finalized, it's outputs are not reported as available for piggyback", %{alice: alice, processor_empty: processor} do tx = TestHelper.create_recovered([{1, 0, 0, alice}, {1, 2, 1, alice}], [{alice, @eth, 1}]) tx_hash = Transaction.raw_txhash(tx) processor = processor |> start_ife_from(tx) |> piggyback_ife_from(tx_hash, 0, :input) finalization = %{in_flight_exit_id: @exit_id, output_index: 0, omg_data: %{piggyback_type: :input}} {:ok, processor, _} = Core.finalize_in_flight_exits(processor, [finalization], %{}) assert {:ok, []} = check_validity_filtered(%ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5}, processor, only: [Event.PiggybackAvailable] ) end test "challenged IFEs emit the same piggybacks as canonical ones", %{processor_filled: processor, transactions: [tx | _], competing_tx: comp} do assert {:ok, events_canonical} = Core.check_validity(%ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5}, processor) {challenged_processor, _} = Core.new_ife_challenges(processor, [ife_challenge(tx, comp)]) assert {:ok, events_challenged} = Core.check_validity(%ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5}, challenged_processor) assert_events(events_canonical, events_challenged) end end describe "evaluates correctness of new piggybacks" do test "no event if input double-spent but not piggybacked", %{processor_filled: processor, competing_tx: comp} do processor = processor |> start_ife_from(comp) assert {:ok, []} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> check_validity_filtered(processor, only: [Event.InvalidPiggyback]) end test "no event if output spent but not piggybacked", %{alice: alice, processor_filled: processor, transactions: [tx | _]} do tx_blknum = 3000 # 2. transaction which spends that piggybacked output comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}], [{alice, @eth, 1}]) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)] } # 3. stuff happens in the contract, but NO PIGGYBACK! processor = processor |> start_ife_from(comp) |> Core.find_ifes_in_blocks(request) assert {:ok, []} = %ExitProcessor.Request{blknum_now: 5000, eth_height_now: 5} |> check_validity_filtered(processor, only: [Event.InvalidPiggyback]) end test "detects double-spend of an input, found in IFE", %{processor_filled: state, transactions: [tx | _], competing_tx: comp, ife_tx_hashes: [ife_id | _]} do txbytes = txbytes(tx) {comp_txbytes, other_sig} = {txbytes(comp), sig(comp, 1)} state = state |> start_ife_from(comp) |> piggyback_ife_from(ife_id, 0, :input) request = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5} assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [0], outputs: []}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_input_index: 0, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 1, spending_sig: ^other_sig }} = Core.get_input_challenge_data(request, state, txbytes, 0) end test "detects double-spend of an input, found in IFE, even if finalized", %{processor_filled: state, transactions: [tx | _], competing_tx: comp, ife_tx_hashes: [tx_hash | _]} do txbytes = txbytes(tx) # this comes from `ExitProcessor.Case` and could use some improvement to not be so dispersed exit_id = 1 {:ok, state, _} = state |> start_ife_from(comp) |> piggyback_ife_from(tx_hash, 0, :input) |> Core.finalize_in_flight_exits( [%{in_flight_exit_id: exit_id, output_index: 0, omg_data: %{piggyback_type: :input}}], %{} ) request = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5} assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [0], outputs: []}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) end test "doesn't detect double-spend of an input, found in IFE, if challenged", %{processor_filled: state, transactions: [tx | _], competing_tx: comp, ife_tx_hashes: [tx_hash | _]} do txbytes = txbytes(tx) {state, _} = state |> start_ife_from(comp) |> piggyback_ife_from(tx_hash, 0, :input) |> Core.challenge_piggybacks([%{tx_hash: tx_hash, output_index: 0, omg_data: %{piggyback_type: :input}}]) request = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5} assert {:ok, []} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:error, :no_double_spend_on_particular_piggyback} = Core.get_input_challenge_data(request, state, txbytes, 0) end test "detects double-spend of an input, found in a block", %{processor_filled: state, transactions: [tx | _], competing_tx: comp, ife_tx_hashes: [ife_id | _]} do txbytes = txbytes(tx) {comp_txbytes, comp_sig} = {txbytes(comp), sig(comp, 1)} comp_blknum = 4000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp], comp_blknum)] } state = state |> piggyback_ife_from(ife_id, 0, :input) |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [0], outputs: []}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_input_index: 0, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 1, spending_sig: ^comp_sig }} = Core.get_input_challenge_data(request, state, txbytes, 0) end test "detects double-spend of an output, found in a IFE", %{alice: alice, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do # 1. transaction which is, ife'd, output piggybacked, and included in a block txbytes = txbytes(tx) tx_blknum = 3000 # 2. transaction which spends that piggybacked output comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}], [{alice, @eth, 1}]) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp)} request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)] } # 3. stuff happens in the contract state = state |> start_ife_from(comp) |> piggyback_ife_from(ife_id, 0, :output) |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [], outputs: [0]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_output_pos: Utxo.position(^tx_blknum, 0, 0), in_flight_proof: proof_bytes, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 0, spending_sig: ^comp_signature }} = Core.get_output_challenge_data(request, state, txbytes, 0) assert_proof_sound(proof_bytes) end test "detects that invalid piggyback becomes unchalleneged exit when sla period passes", %{alice: alice, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do # 1. transaction which is, ife'd, output piggybacked, and included in a block txbytes = txbytes(tx) tx_blknum = 3000 # 2. transaction which spends that piggybacked output comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}], [{alice, @eth, 1}]) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5 + state.sla_margin, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)] } # 3. stuff happens in the contract state = state |> start_ife_from(comp) |> piggyback_ife_from(ife_id, 0, :output) |> Core.find_ifes_in_blocks(request) assert {{:error, :unchallenged_exit}, [ %Event.UnchallengedPiggyback{txbytes: ^txbytes, inputs: [], outputs: [0]} ]} = check_validity_filtered(request, state, only: [Event.UnchallengedPiggyback]) end test "detects double-spend of an output, found in a IFE, even if finalized", %{alice: alice, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [tx_hash | _]} do txbytes = txbytes(tx) tx_blknum = 3000 # this comes from `ExitProcessor.Case` and could use some improvement to not be so dispersed exit_id = 1 comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}], [{alice, @eth, 1}]) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)] } {:ok, state, _} = state |> start_ife_from(comp) |> piggyback_ife_from(tx_hash, 0, :output) |> Core.find_ifes_in_blocks(request) |> Core.finalize_in_flight_exits( [%{in_flight_exit_id: exit_id, output_index: 0, omg_data: %{piggyback_type: :output}}], %{} ) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [], outputs: [0]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) end test "doesn't detect double-spend of an output, found in a IFE, if challenged", %{alice: alice, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [tx_hash | _]} do txbytes = txbytes(tx) tx_blknum = 3000 comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}], [{alice, @eth, 1}]) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)] } {state, _} = state |> start_ife_from(comp) |> piggyback_ife_from(tx_hash, 0, :output) |> Core.find_ifes_in_blocks(request) |> Core.challenge_piggybacks([%{tx_hash: tx_hash, output_index: 0, omg_data: %{piggyback_type: :output}}]) assert {:ok, []} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:error, :no_double_spend_on_particular_piggyback} = Core.get_output_challenge_data(request, state, txbytes, 0) end test "detects and proves double-spend of an output, found in a block", %{alice: alice, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do # this time, the piggybacked-output-spending tx is going to be included in a block, which requires more back&forth # 1. transaction which is, ife'd, output piggybacked, and included in a block txbytes = txbytes(tx) tx_blknum = 3000 # 2. transaction which spends that piggybacked output comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}], [{alice, @eth, 1}]) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp)} comp_blknum = 4000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)], blocks_result: [Block.hashed_txs_at([comp], comp_blknum)] } # 3. stuff happens in the contract state = state |> piggyback_ife_from(ife_id, 0, :output) |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [], outputs: [0]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_output_pos: Utxo.position(^tx_blknum, 0, 0), in_flight_proof: proof_bytes, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 0, spending_sig: ^comp_signature }} = Core.get_output_challenge_data(request, state, txbytes, 0) assert_proof_sound(proof_bytes) end test "detects and proves double-spend of an output, found in a block, various output indices", %{carol: carol, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do txbytes = txbytes(tx) tx_blknum = 3000 comp = TestHelper.create_recovered([{tx_blknum, 0, 1, carol}], [{carol, @eth, 1}]) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp)} comp_blknum = 4000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)], blocks_result: [Block.hashed_txs_at([comp], comp_blknum)] } state = state |> piggyback_ife_from(ife_id, 1, :output) |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [], outputs: [1]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_output_pos: Utxo.position(^tx_blknum, 0, 1), in_flight_proof: proof_bytes, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 0, spending_sig: ^comp_signature }} = Core.get_output_challenge_data(request, state, txbytes, 1) assert_proof_sound(proof_bytes) end test "detects and proves double-spend of an output, found in a block, various spending input indices", %{alice: alice, carol: carol, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do txbytes = txbytes(tx) tx_blknum = 3000 comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}, {tx_blknum, 0, 1, carol}], [{alice, @eth, 1}]) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp, 1)} comp_blknum = 4000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)], blocks_result: [Block.hashed_txs_at([comp], comp_blknum)] } state = state |> piggyback_ife_from(ife_id, 1, :output) |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [], outputs: [1]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_output_pos: Utxo.position(^tx_blknum, 0, 1), in_flight_proof: proof_bytes, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 1, spending_sig: ^comp_signature }} = Core.get_output_challenge_data(request, state, txbytes, 1) assert_proof_sound(proof_bytes) end test "proves and proves double-spend of an output, found in a block, for various inclusion positions", %{alice: alice, bob: bob, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do other_tx = TestHelper.create_recovered([{10_000, 0, 0, bob}], [{alice, @eth, 1}]) txbytes = txbytes(tx) tx_blknum = 3000 comp = TestHelper.create_recovered([{tx_blknum, 1, 0, alice}], [{alice, @eth, 1}]) {comp_txbytes, comp_signature} = {txbytes(comp), sig(comp)} comp_blknum = 4000 request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([other_tx, tx], tx_blknum)], blocks_result: [Block.hashed_txs_at([comp], comp_blknum)] } state = state |> piggyback_ife_from(ife_id, 0, :output) |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [], outputs: [0]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_output_pos: Utxo.position(^tx_blknum, 1, 0), in_flight_proof: proof_bytes, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 0, spending_sig: ^comp_signature }} = Core.get_output_challenge_data(request, state, txbytes, 0) assert_proof_sound(proof_bytes) end test "detects no double-spend of an input, if a different input is being spent in block", %{processor_filled: state, competing_tx: comp, ife_tx_hashes: [ife_id | _]} do # NOTE: the piggybacked index is the second one, compared to the invalid piggyback situation request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [Block.hashed_txs_at([comp], 4000)] } state = state |> piggyback_ife_from(ife_id, 1, :input) |> Core.find_ifes_in_blocks(request) assert {:ok, []} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) end test "detects no double-spend of an output, if a different output is being spent in block", %{alice: alice, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do # NOTE: the piggybacked index is the second one, compared to the invalid piggyback situation tx_blknum = 3000 # 2. transaction which _doesn't_ spend that piggybacked output comp = TestHelper.create_recovered([{tx_blknum, 0, 0, alice}], [{alice, @eth, 1}]) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)], blocks_result: [Block.hashed_txs_at([comp], 4000)] } state = state |> piggyback_ife_from(ife_id, 1, :output) |> Core.find_ifes_in_blocks(request) assert {:ok, []} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) end test "does not look into ife_input_spending_blocks_result when it should not", %{processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], 3000)] } state = state |> piggyback_ife_from(ife_id, 0, :output) |> Core.find_ifes_in_blocks(request) # now zero out the prior result to make a sanity check of well-behaving wrt. to the database results request = %{request | blocks_result: [], ife_input_spending_blocks_result: nil} assert {:ok, []} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:error, _} = Core.get_output_challenge_data(request, state, txbytes(tx), 0) end test "detects multiple double-spends in single IFE, correctly as more piggybacks appear", %{alice: alice, processor_filled: state, transactions: [tx | _], ife_tx_hashes: [ife_id | _]} do tx_blknum = 3000 txbytes = txbytes(tx) comp = TestHelper.create_recovered( [{1, 0, 0, alice}, {1, 2, 1, alice}, {tx_blknum, 0, 0, alice}, {tx_blknum, 0, 1, alice}], [{alice, @eth, 1}] ) {comp_txbytes, alice_sig} = {txbytes(comp), sig(comp)} request = %ExitProcessor.Request{ blknum_now: 4000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], tx_blknum)] } state = state |> start_ife_from(comp) |> piggyback_ife_from(ife_id, 0, :input) |> Core.find_ifes_in_blocks(request) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [0], outputs: []}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) state = state |> piggyback_ife_from(ife_id, 1, :input) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [0, 1], outputs: []}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) state = state |> piggyback_ife_from(ife_id, 0, :output) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [0, 1], outputs: [0]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) state = state |> piggyback_ife_from(ife_id, 1, :output) assert {:ok, [%Event.InvalidPiggyback{txbytes: ^txbytes, inputs: [0, 1], outputs: [0, 1]}]} = check_validity_filtered(request, state, only: [Event.InvalidPiggyback]) assert {:ok, %{ in_flight_input_index: 1, in_flight_txbytes: ^txbytes, spending_txbytes: ^comp_txbytes, spending_input_index: 1, spending_sig: ^alice_sig }} = Core.get_input_challenge_data(request, state, txbytes, 1) assert {:ok, %{ in_flight_txbytes: ^txbytes, in_flight_output_pos: Utxo.position(^tx_blknum, 0, 0), in_flight_proof: inclusion_proof, spending_txbytes: ^comp_txbytes, spending_input_index: 2, spending_sig: ^alice_sig }} = Core.get_output_challenge_data(request, state, txbytes, 0) assert_proof_sound(inclusion_proof) end test "returns input txs and input utxo positions for invalid input piggyback challenges", %{processor_filled: state, transactions: [tx | _], competing_tx: comp, ife_tx_hashes: [ife_id | _]} do txbytes = txbytes(tx) state = state |> start_ife_from(comp) |> piggyback_ife_from(ife_id, 0, :input) request = %ExitProcessor.Request{blknum_now: 1000, eth_height_now: 5} assert {:ok, %{input_tx: "input_tx", input_utxo_pos: Utxo.position(1, 0, 0)}} = Core.get_input_challenge_data(request, state, txbytes, 0) end end describe "produces challenges for bad piggybacks" do test "produces single challenge proof on double-spent piggyback input", %{ invalid_piggyback_on_input: %{ state: state, request: request, ife_input_index: ife_input_index, ife_txbytes: ife_txbytes, spending_txbytes: spending_txbytes, spending_input_index: spending_input_index, spending_sig: spending_sig } } do assert {:ok, %{ in_flight_input_index: ^ife_input_index, in_flight_txbytes: ^ife_txbytes, spending_txbytes: ^spending_txbytes, spending_input_index: ^spending_input_index, spending_sig: ^spending_sig }} = Core.get_input_challenge_data(request, state, ife_txbytes, ife_input_index) end test "fail when asked to produce proof for wrong oindex", %{ invalid_piggyback_on_input: %{ state: state, request: request, ife_input_index: bad_pb_output, ife_txbytes: txbytes } } do assert bad_pb_output != 1 assert {:error, :no_double_spend_on_particular_piggyback} = Core.get_input_challenge_data(request, state, txbytes, 1) end test "fail when asked to produce proof for wrong txhash", %{invalid_piggyback_on_input: %{state: state, request: request}, unrelated_tx: comp} do comp_txbytes = Transaction.raw_txbytes(comp) assert {:error, :ife_not_known_for_tx} = Core.get_input_challenge_data(request, state, comp_txbytes, 0) assert {:error, :ife_not_known_for_tx} = Core.get_output_challenge_data(request, state, comp_txbytes, 0) end test "fail when asked to produce proof for wrong badly encoded tx", %{invalid_piggyback_on_input: %{state: state, request: request}} do assert {:error, :malformed_transaction} = Core.get_input_challenge_data(request, state, <<0>>, 0) assert {:error, :malformed_transaction} = Core.get_output_challenge_data(request, state, <<0>>, 0) end test "fail when asked to produce proof for illegal oindex", %{invalid_piggyback_on_input: %{state: state, request: request, ife_txbytes: txbytes}} do assert {:error, :piggybacked_index_out_of_range} = Core.get_input_challenge_data(request, state, txbytes, -1) assert {:error, :piggybacked_index_out_of_range} = Core.get_output_challenge_data(request, state, txbytes, -1) end test "will fail if asked to produce proof for wrong output", %{ invalid_piggyback_on_output: %{ state: state, request: request, ife_input_index: bad_pb_output, ife_txbytes: txbytes } } do assert 2 != bad_pb_output - 4 assert {:error, :no_double_spend_on_particular_piggyback} = Core.get_output_challenge_data(request, state, txbytes, 2) end test "will fail if asked to produce proof for correct piggyback on output", %{ invalid_piggyback_on_output: %{ state: state, request: request, ife_good_pb_index: good_pb_output, ife_txbytes: txbytes } } do assert {:error, :no_double_spend_on_particular_piggyback} = Core.get_output_challenge_data(request, state, txbytes, good_pb_output - 4) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/standard_exit_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.StandardExitTest do @moduledoc """ Test of the logic of exit processor, in the area of standard exits """ use OMG.Watcher.ExitProcessor.Case, async: false alias OMG.Watcher.Block alias OMG.Watcher.Event alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper, only: [start_ife_from: 2, start_se_from: 3, start_se_from: 4, check_validity_filtered: 3] @eth <<0::160>> @deposit_blknum 1 @deposit_blknum2 2 @early_blknum 1_000 @blknum @early_blknum @late_blknum 10_000 @blknum2 @late_blknum - 1_000 @utxo_pos_tx Utxo.position(@blknum, 0, 0) @utxo_pos_tx2 Utxo.position(@blknum2, 0, 1) @utxo_pos_deposit Utxo.position(@deposit_blknum, 0, 0) @utxo_pos_deposit2 Utxo.position(@deposit_blknum2, 0, 0) @deposit_input2 {@deposit_blknum2, 0, 0} # needs to match up with the default from `ExitProcessor.Case` :( @exit_id 9876 @default_min_exit_period_seconds 120 @default_child_block_interval 1000 setup do {:ok, empty} = Core.init([], [], [], @default_min_exit_period_seconds, @default_child_block_interval) db_path = Briefly.create!(directory: true) Application.put_env(:omg_db, :path, db_path, persistent: true) :ok = OMG.DB.init() {:ok, started_apps} = Application.ensure_all_started(:omg_db) on_exit(fn -> Application.put_env(:omg_db, :path, nil) Enum.map(started_apps, fn app -> :ok = Application.stop(app) end) end) %{processor_empty: empty, alice: TestHelper.generate_entity(), bob: TestHelper.generate_entity()} end describe "Core.determine_standard_challenge_queries" do test "doesn't ask for anything and stops if deposit utxo not spent at all", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_deposit(@utxo_pos_deposit, alice) assert {:error, :utxo_not_spent} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_deposit} |> Core.determine_standard_challenge_queries(processor, true) end test "doesn't ask for anything and stops if tx utxo not spent at all", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) assert {:error, :utxo_not_spent} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx} |> Core.determine_standard_challenge_queries(processor, true) end test "asks for correct data: deposit utxo double spent in IFE", %{alice: alice, processor_empty: processor} do ife_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 1}]) processor = processor |> start_se_from_deposit(@utxo_pos_deposit, alice) |> start_ife_from(ife_tx) assert {:ok, %ExitProcessor.Request{se_spending_blocks_to_get: []}} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_deposit} |> Core.determine_standard_challenge_queries(processor, true) end test "asks for correct data: deposit utxo double spent outside an IFE", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_deposit(@utxo_pos_deposit, alice) assert {:ok, %ExitProcessor.Request{se_spending_blocks_to_get: [@utxo_pos_deposit]}} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_deposit} |> Core.determine_standard_challenge_queries(processor, false) end test "asks for correct data: tx utxo double spent in an IFE", %{alice: alice, processor_empty: processor} do ife_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 1}]) processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) |> start_ife_from(ife_tx) assert {:ok, %ExitProcessor.Request{se_spending_blocks_to_get: []}} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx} |> Core.determine_standard_challenge_queries(processor, true) end test "asks for correct data: tx utxo double spent outside an IFE", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) assert {:ok, %ExitProcessor.Request{se_spending_blocks_to_get: [@utxo_pos_tx]}} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx} |> Core.determine_standard_challenge_queries(processor, false) end test "stops immediately, if exit not found, utxo exists", %{processor_empty: processor} do assert {:error, :exit_not_found} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx} |> Core.determine_standard_challenge_queries(processor, true) end test "stops immediately, if exit not found, utxo doesn't exist", %{processor_empty: processor} do assert {:error, :exit_not_found} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx} |> Core.determine_standard_challenge_queries(processor, false) end end describe "Core.create_challenge" do test "returns a deposit exiting_tx as part of the challenge response", %{alice: alice, processor_empty: processor} do exiting_tx = TestHelper.create_recovered([], [{alice, @eth, 10}]) processor = processor |> start_se_from(exiting_tx, @utxo_pos_deposit) recovered_spend = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}]) {txbytes, _alice_sig} = get_bytes_sig(recovered_spend) {exiting_txbytes, _} = get_bytes_sig(exiting_tx) assert {:ok, %{exiting_tx: ^exiting_txbytes, txbytes: ^txbytes}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_deposit, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend], @blknum)] } |> Core.create_challenge(processor) end test "returns a block exiting_tx as part of the challenge response", %{alice: alice, processor_empty: processor} do exiting_tx = TestHelper.create_recovered([Tuple.append(@deposit_input2, alice)], [{alice, @eth, 10}]) processor = processor |> start_se_from(exiting_tx, @utxo_pos_tx) recovered_spend = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) {txbytes, _alice_sig} = get_bytes_sig(recovered_spend) {exiting_txbytes, _} = get_bytes_sig(exiting_tx) assert {:ok, %{exiting_tx: ^exiting_txbytes, txbytes: ^txbytes}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend], @late_blknum)] } |> Core.create_challenge(processor) end test "creates challenge: deposit utxo double spent in IFE", %{alice: alice, processor_empty: processor} do ife_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 1}]) {txbytes, alice_sig} = get_bytes_sig(ife_tx) processor = processor |> start_se_from_deposit(@utxo_pos_deposit, alice) |> start_ife_from(ife_tx) assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_deposit} |> Core.create_challenge(processor) end test "creates challenge: deposit utxo double spent outside an IFE", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_deposit(@utxo_pos_deposit, alice) recovered_spend = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) {txbytes, alice_sig} = get_bytes_sig(recovered_spend) assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_deposit, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend], @blknum)] } |> Core.create_challenge(processor) end test "creates challenge: tx utxo double spent in an IFE", %{alice: alice, processor_empty: processor} do # quite similar to the deposit utxo case, but leaving the test in for completeness ife_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 1}]) {txbytes, alice_sig} = get_bytes_sig(ife_tx) processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) |> start_ife_from(ife_tx) assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx} |> Core.create_challenge(processor) end test "creates challenge: tx utxo double spent outside an IFE", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) recovered_spend = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) {txbytes, alice_sig} = get_bytes_sig(recovered_spend) assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend], @blknum)] } |> Core.create_challenge(processor) end test "creates challenge: tx utxo double spent outside an IFE, but there is an unrelated IFE open", %{alice: alice, processor_empty: processor} do unrelated = TestHelper.create_recovered([{@blknum, 10, 0, alice}], @eth, [{alice, 1}]) processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) |> start_ife_from(unrelated) recovered_spend = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) {txbytes, alice_sig} = get_bytes_sig(recovered_spend) assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend], @blknum)] } |> Core.create_challenge(processor) end test "creates challenge: tx utxo double spent on input various positions", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) input = {@blknum, 0, 0, alice} recovered_spends = [ TestHelper.create_recovered([input], @eth, [{alice, 10}]), TestHelper.create_recovered([{1, 0, 0, alice}, input], @eth, [{alice, 10}]), TestHelper.create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}, input], @eth, [{alice, 10}]), TestHelper.create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}, {3, 0, 0, alice}, input], @eth, [{alice, 10}]) ] recovered_spends |> Enum.with_index() |> Enum.map(fn {recovered_spend, expected_index} -> {txbytes, alice_sig} = get_bytes_sig(recovered_spend) assert {:ok, %{exit_id: @exit_id, input_index: ^expected_index, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend], @blknum)] } |> Core.create_challenge(processor) end) end test "creates challenge: tx utxo double spent signed_by different signers", %{alice: alice, bob: bob, processor_empty: processor} do tx1 = Transaction.Payment.new([@deposit_input2], [{alice.addr, @eth, 10}]) tx2 = Transaction.Payment.new([@deposit_input2], [{bob.addr, @eth, 10}]) processor1 = processor |> start_se_from(tx1, @utxo_pos_tx) processor2 = processor |> start_se_from(tx2, @utxo_pos_tx) recovered_spends = [ TestHelper.create_recovered([{1, 0, 0, bob}, {@blknum, 0, 0, alice}], @eth, [{alice, 10}]), TestHelper.create_recovered([{1, 0, 0, alice}, {@blknum, 0, 0, bob}], @eth, [{alice, 10}]) ] recovered_spends |> Enum.zip([processor1, processor2]) |> Enum.map(fn {recovered_spend, processor} -> {txbytes, second_sig} = get_bytes_sig(recovered_spend, 1) assert {:ok, %{exit_id: @exit_id, input_index: 1, txbytes: ^txbytes, sig: ^second_sig}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend], @blknum)] } |> Core.create_challenge(processor) end) end test "creates challenge: both utxos spent don't interfere", %{alice: alice, processor_empty: processor} do tx = Transaction.Payment.new([@deposit_input2], [{alice.addr, @eth, 10}, {alice.addr, @eth, 10}]) processor = processor |> start_se_from(tx, @utxo_pos_tx) recovered_spend = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) recovered_spend2 = TestHelper.create_recovered([{@blknum, 0, 1, alice}], @eth, [{alice, 10}]) {txbytes, alice_sig} = get_bytes_sig(recovered_spend) assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend, recovered_spend2], @blknum)] } |> Core.create_challenge(processor) end test "creates challenge: tx utxo double spent in both block and IFE don't interfere", %{alice: alice, processor_empty: processor} do ife_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 1}]) {txbytes, alice_sig} = get_bytes_sig(ife_tx) processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) |> start_ife_from(ife_tx) # same tx spends in both assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^txbytes, sig: ^alice_sig}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([ife_tx], @blknum)] } |> Core.create_challenge(processor) # different txs spend, block tx takes preference recovered_spend2 = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) {block_txbytes, alice_sig2} = get_bytes_sig(recovered_spend2) assert {:ok, %{exit_id: @exit_id, input_index: 0, txbytes: ^block_txbytes, sig: ^alice_sig2}} = %ExitProcessor.Request{ se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [Block.hashed_txs_at([recovered_spend2], @blknum)] } |> Core.create_challenge(processor) end test "doesn't create challenge: tx utxo not double spent", %{alice: alice, processor_empty: processor} do processor = processor |> start_se_from_block_tx(@utxo_pos_tx, alice) assert {:error, :utxo_not_spent} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: []} |> Core.create_challenge(processor) assert {:error, :utxo_not_spent} = %ExitProcessor.Request{se_exiting_pos: @utxo_pos_tx, se_spending_blocks_result: [:not_found]} |> Core.create_challenge(processor) end end describe "Core.check_validity" do test "detect invalid standard exit based on utxo missing in main ledger", %{processor_empty: processor, alice: alice} do exiting_pos = @utxo_pos_tx exiting_pos_enc = Utxo.Position.encode(exiting_pos) # standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}]) %{signed_tx_bytes: signed_tx_bytes, tx_hash: tx_hash} = standard_exit_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) request = %ExitProcessor.Request{ eth_height_now: 5, blknum_now: @late_blknum, utxos_to_check: [exiting_pos], utxo_exists_result: [false] } # before the exit starts assert {:ok, []} = Core.check_validity(request, processor) # after processor = start_se_from(processor, standard_exit_tx, exiting_pos) block_updates = [{:put, :block, %{number: @blknum, hash: <<0::160>>, transactions: [signed_tx_bytes]}}] spent_blknum_updates = [{:put, :spend, {Utxo.Position.to_input_db_key(@utxo_pos_tx), @blknum}}] :ok = OMG.DB.multi_update(block_updates ++ spent_blknum_updates) assert {:ok, [%Event.InvalidExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash}]} = Core.check_validity(request, processor) end test "detect old invalid standard exit", %{processor_empty: processor, alice: alice} do exiting_pos = @utxo_pos_tx exiting_pos_enc = Utxo.Position.encode(exiting_pos) %{signed_tx_bytes: signed_tx_bytes, tx_hash: tx_hash} = standard_exit_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) request = %ExitProcessor.Request{ eth_height_now: 50, blknum_now: @late_blknum, utxos_to_check: [exiting_pos], utxo_exists_result: [false] } processor = start_se_from(processor, standard_exit_tx, exiting_pos) block_updates = [{:put, :block, %{number: @blknum, hash: <<0::160>>, transactions: [signed_tx_bytes]}}] spent_blknum_updates = [{:put, :spend, {Utxo.Position.to_input_db_key(@utxo_pos_tx), @blknum}}] :ok = OMG.DB.multi_update(block_updates ++ spent_blknum_updates) assert {{:error, :unchallenged_exit}, [ %Event.UnchallengedExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash}, %Event.InvalidExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash} ]} = Core.check_validity(request, processor) end test "invalid exits that have been witnessed already inactive don't excite events", %{processor_empty: processor, alice: alice} do exiting_pos = @utxo_pos_tx standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}]) request = %ExitProcessor.Request{ eth_height_now: 13, blknum_now: @late_blknum, utxos_to_check: [exiting_pos], utxo_exists_result: [false] } processor = processor |> start_se_from(standard_exit_tx, exiting_pos, inactive: true) assert {:ok, []} = request |> Core.check_validity(processor) end test "exits of utxos that couldn't have been seen created yet never excite querying the ledger", %{processor_empty: processor, alice: alice} do exiting_pos = @utxo_pos_tx2 standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 1}, {alice, 1}]) processor = processor |> start_se_from(standard_exit_tx, exiting_pos) assert %ExitProcessor.Request{utxos_to_check: []} = %ExitProcessor.Request{eth_height_now: 13, blknum_now: @early_blknum} |> Core.determine_utxo_existence_to_get(processor) end test "detect invalid standard exit based on ife tx which spends same input", %{processor_empty: processor, alice: alice} do standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}]) %{tx_hash: tx_hash} = tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], [{alice, @eth, 1}]) exiting_pos = @utxo_pos_tx exiting_pos_enc = Utxo.Position.encode(exiting_pos) processor = processor |> start_se_from(standard_exit_tx, exiting_pos) |> start_ife_from(tx) assert {:ok, [%Event.InvalidExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash}]} = check_validity_filtered(%ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum}, processor, only: [Event.InvalidExit] ) end test "ifes and standard exits don't interfere", %{alice: alice, processor_empty: processor, transactions: [tx | _]} do %{signed_tx_bytes: signed_tx_bytes, tx_hash: tx_hash} = standard_exit_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}]) processor = processor |> start_se_from(standard_exit_tx, @utxo_pos_tx) |> start_ife_from(tx) assert %{utxos_to_check: [_, Utxo.position(1, 2, 1), @utxo_pos_tx]} = exit_processor_request = %ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) block_updates = [{:put, :block, %{number: @blknum, hash: <<0::160>>, transactions: [signed_tx_bytes]}}] spent_blknum_updates = [{:put, :spend, {Utxo.Position.to_input_db_key(@utxo_pos_tx), @blknum}}] :ok = OMG.DB.multi_update(block_updates ++ spent_blknum_updates) # here it's crucial that the missing utxo related to the ife isn't interpeted as a standard invalid exit # that missing utxo isn't enough for any IFE-related event too assert {:ok, [%Event.InvalidExit{spending_txhash: ^tx_hash}]} = exit_processor_request |> struct!(utxo_exists_result: [false, false, false]) |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) end test "ifes and standard exits don't interfere, when standard exit is challenged", %{alice: alice, processor_empty: processor, transactions: [tx | _]} do standard_exit_tx = TestHelper.create_recovered([], @eth, [{alice, 10}]) {processor, _} = processor |> start_se_from(standard_exit_tx, @utxo_pos_deposit) |> start_ife_from(tx) |> Core.challenge_exits([%{utxo_pos: Utxo.Position.encode(@utxo_pos_deposit)}]) # doesn't check the challenged SE utxo assert %{utxos_to_check: [_, Utxo.position(1, 2, 1)]} = exit_processor_request = %ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) # doesn't alert on the challenged SE, despite it being a double-spend wrt the IFE assert {:ok, []} = exit_processor_request |> struct!(utxo_exists_result: [false, false]) |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) end test "ifes and standard exits don't interfere if all valid", %{alice: alice, processor_empty: processor, transactions: [tx | _]} do standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}]) processor = processor |> start_se_from(standard_exit_tx, @utxo_pos_tx) |> start_ife_from(tx) assert %{utxos_to_check: [_, Utxo.position(1, 2, 1), @utxo_pos_tx]} = exit_processor_request = %ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum} |> Core.determine_utxo_existence_to_get(processor) assert {:ok, []} = exit_processor_request |> struct!(utxo_exists_result: [true, true, true]) |> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable]) end end describe "challenge events" do test "can challenge exits, which are then forgotten completely", %{processor_empty: processor, alice: alice} do standard_exit_tx1 = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) standard_exit_tx2 = TestHelper.create_recovered([{@blknum2, 0, 1, alice}], @eth, [{alice, 10}, {alice, 10}]) processor = processor |> start_se_from(standard_exit_tx1, @utxo_pos_deposit2) |> start_se_from(standard_exit_tx2, @utxo_pos_tx2) # sanity assert %ExitProcessor.Request{utxos_to_check: [_, _]} = Core.determine_utxo_existence_to_get(%ExitProcessor.Request{blknum_now: @late_blknum}, processor) {processor, _} = processor |> Core.challenge_exits([@utxo_pos_deposit2, @utxo_pos_tx2] |> Enum.map(&%{utxo_pos: Utxo.Position.encode(&1)})) assert %ExitProcessor.Request{utxos_to_check: []} = Core.determine_utxo_existence_to_get(%ExitProcessor.Request{blknum_now: @late_blknum}, processor) end test "can process challenged exits", %{processor_empty: processor, alice: alice} do # see the contract and `Eth.RootChain.get_standard_exit_structs/1` for some explanation why like this # this is what an exit looks like after a challenge zero_status = {false, 0, 0, 0, 0, 0} standard_exit_tx = TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) processor = processor |> start_se_from(standard_exit_tx, @utxo_pos_deposit2, status: zero_status) # sanity assert %ExitProcessor.Request{utxos_to_check: []} = Core.determine_utxo_existence_to_get(%ExitProcessor.Request{blknum_now: @late_blknum}, processor) # pinning because challenge shouldn't change the already challenged exit in the processor {^processor, _} = Core.challenge_exits(processor, [%{utxo_pos: Utxo.Position.encode(@utxo_pos_deposit2)}]) end end defp start_se_from_deposit(processor, exiting_pos, alice) do tx = TestHelper.create_recovered([], [{alice, @eth, 10}]) start_se_from(processor, tx, exiting_pos) end defp start_se_from_block_tx(processor, exiting_pos, alice) do tx = TestHelper.create_recovered([Tuple.append(@deposit_input2, alice)], [{alice, @eth, 10}]) start_se_from(processor, tx, exiting_pos) end defp get_bytes_sig(tx, sig_idx \\ 0), do: {Transaction.raw_txbytes(tx), Enum.at(tx.signed_tx.sigs, sig_idx)} end ================================================ FILE: apps/omg_watcher/test/omg_watcher/exit_processor/tools_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.ToolsTest do @moduledoc """ Test of the logic of exit processor - various generic tests: starting events, some sanity checks, ife listing """ use OMG.Watcher.ExitProcessor.Case, async: true alias OMG.Watcher.ExitProcessor.Tools alias OMG.Watcher.Utxo require Utxo describe "to_bus_events/1" do setup _ do {:ok, %{ finalizations: [ %{in_flight_exit_id: <<1::192>>, log_index: 1, root_chain_txhash: <<1::256>>, eth_height: 1}, %{in_flight_exit_id: <<2::192>>, log_index: 2, root_chain_txhash: <<2::256>>, eth_height: 2} ], start_ife_events: [ %{log_index: 1, root_chain_txhash: <<11::256>>, tx_hash: <<255::256>>, eth_height: 110}, %{log_index: 2, root_chain_txhash: <<12::256>>, tx_hash: <<255::256>>, eth_height: 111} ], utxos: [ Utxo.position(1, 0, 0), Utxo.position(1000, 0, 0), Utxo.position(2000, 0, 0) ] }} end test "mapping single finalization", %{finalizations: [f1 | _], utxos: [utxo_1 | _]} do utxo_pos = Utxo.Position.encode(utxo_1) assert [ %{log_index: 1, root_chain_txhash: <<1::256>>, call_data: %{utxo_pos: ^utxo_pos}} ] = Tools.to_bus_events_data([{f1, [utxo_1]}]) end test "mapping multiple finalizations", %{finalizations: [f1, f2 | _], utxos: utxos} do [utxo_1, utxo_2, utxo_3 | _] = utxos [utxo_pos_1, utxo_pos_2, utxo_pos_3 | _] = Enum.map(utxos, &Utxo.Position.encode/1) assert [ %{log_index: 2, root_chain_txhash: <<2::256>>, eth_height: 2, call_data: %{utxo_pos: ^utxo_pos_2}}, %{log_index: 2, root_chain_txhash: <<2::256>>, eth_height: 2, call_data: %{utxo_pos: ^utxo_pos_3}}, %{log_index: 1, root_chain_txhash: <<1::256>>, eth_height: 1, call_data: %{utxo_pos: ^utxo_pos_1}} ] = Tools.to_bus_events_data([{f1, [utxo_1]}, {f2, [utxo_2, utxo_3]}]) end test "finalization without exiting utxos does not produce events", %{finalizations: [f1, f2 | _], utxos: [utxo_1 | _]} do utxo_pos = Utxo.Position.encode(utxo_1) assert [ %{log_index: 2, root_chain_txhash: <<2::256>>, call_data: %{utxo_pos: ^utxo_pos}} ] = Tools.to_bus_events_data([{f1, []}, {f2, [utxo_1]}]) end test "empty finalization list does not produce events" do assert [] = Tools.to_bus_events_data([]) end test "mapping new_in_flight_exits events", %{start_ife_events: [s1, s2 | _], utxos: utxos} do [utxo_pos_1, utxo_pos_2, utxo_pos_3] = encoded_utxos = utxos |> Enum.map(&Utxo.Position.encode/1) |> Enum.take(3) events_with_utxos = [ {s1, Enum.take(encoded_utxos, 2)}, {s2, Enum.drop(encoded_utxos, 2)} ] assert [ %{log_index: 2, root_chain_txhash: <<12::256>>, eth_height: 111, call_data: %{utxo_pos: ^utxo_pos_3}}, %{log_index: 1, root_chain_txhash: <<11::256>>, eth_height: 110, call_data: %{utxo_pos: ^utxo_pos_1}}, %{log_index: 1, root_chain_txhash: <<11::256>>, eth_height: 110, call_data: %{utxo_pos: ^utxo_pos_2}} ] = Tools.to_bus_events_data(events_with_utxos) end test "mapping piggyback_exits events" do txhash = <<255::256>> piggyback_events = [ %{ log_index: 1, root_chain_txhash: <<11::256>>, tx_hash: txhash, eth_height: 210, output_index: 1, omg_data: %{piggyback_type: :output} }, %{ log_index: 2, root_chain_txhash: <<12::256>>, tx_hash: txhash, eth_height: 210, output_index: 0, omg_data: %{piggyback_type: :input} }, %{ log_index: 3, root_chain_txhash: <<13::256>>, tx_hash: txhash, eth_height: 210, output_index: 3, omg_data: %{piggyback_type: :output} } ] # Note: Piggyback to input in log_index: 2 is ignored assert [ %{log_index: 3, root_chain_txhash: <<13::256>>, call_data: %{txhash: ^txhash, oindex: 3}}, %{log_index: 1, root_chain_txhash: <<11::256>>, call_data: %{txhash: ^txhash, oindex: 1}} ] = Tools.to_bus_events_data(piggyback_events) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/fees/fee_filter_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Fees.FeeFilterTest do @moduledoc false use ExUnit.Case, async: true alias OMG.Watcher.Fees.FeeFilter doctest OMG.Watcher.Fees.FeeFilter @eth <<0::160>> @not_eth_1 <<1::size(160)>> @not_eth_2 <<2::size(160)>> @payment_tx_type OMG.Watcher.WireFormatTypes.tx_type_for(:tx_payment_v1) @payment_fees %{ @eth => %{ amount: 1, subunit_to_unit: 1_000_000_000_000_000_000, pegged_amount: 4, pegged_currency: "USD", pegged_subunit_to_unit: 100, updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") }, @not_eth_1 => %{ amount: 3, subunit_to_unit: 1000, pegged_amount: 4, pegged_currency: "USD", pegged_subunit_to_unit: 100, updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") } } @fees %{ @payment_tx_type => @payment_fees, 2 => @payment_fees, 3 => %{ @not_eth_2 => %{ amount: 3, subunit_to_unit: 1000, pegged_amount: 4, pegged_currency: "USD", pegged_subunit_to_unit: 100, updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") } } } describe "filter/2" do test "does not filter tx_type when given an empty list" do assert FeeFilter.filter(@fees, [], []) == {:ok, @fees} end test "does not filter tx_type when given a nil value" do assert FeeFilter.filter(@fees, nil, []) == {:ok, @fees} end test "does not filter currencies when given an empty list" do assert FeeFilter.filter(@fees, [], []) == {:ok, @fees} end test "does not filter currencies when given a nil value" do assert FeeFilter.filter(@fees, [], nil) == {:ok, @fees} end test "filter fees by currency given a list of currencies" do assert FeeFilter.filter(@fees, [], [@eth]) == {:ok, %{ @payment_tx_type => Map.take(@payment_fees, [@eth]), 2 => Map.take(@payment_fees, [@eth]), 3 => %{} }} assert FeeFilter.filter(@fees, [], [@not_eth_2]) == {:ok, %{@payment_tx_type => %{}, 2 => %{}, 3 => @fees[3]}} end test "filter fees by tx_type when given a list of tx_types" do assert FeeFilter.filter(@fees, [1, 2], []) == {:ok, Map.drop(@fees, [3])} end test "filter fees by both tx_type and currencies" do assert FeeFilter.filter(@fees, [1, 2], [@eth]) == {:ok, %{ @payment_tx_type => Map.take(@payment_fees, [@eth]), 2 => Map.take(@payment_fees, [@eth]) }} end test "returns an error when given an unsupported currency" do other_token = <<9::160>> assert FeeFilter.filter(@fees, [], [other_token]) == {:error, :currency_fee_not_supported} end test "returns an error when given an unsupported tx_type" do tx_type = 99_999 assert FeeFilter.filter(@fees, [tx_type], []) == {:error, :tx_type_not_supported} end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/fees_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.FeesTest do @moduledoc false use ExUnitFixtures use OMG.Watcher.Fixtures use ExUnit.Case, async: true import OMG.Watcher.TestHelper alias __MODULE__.DummyTransaction alias OMG.Watcher.Fees doctest OMG.Watcher.Fees @eth <<0::160>> @not_eth_1 <<1::size(160)>> @payment_tx_type OMG.Watcher.WireFormatTypes.tx_type_for(:tx_payment_v1) @payment_fees %{ @eth => [1], @not_eth_1 => [3] } @fees %{ @payment_tx_type => @payment_fees } describe "check_if_covered/2" do test "returns :ok when given fees are 0 and :ignore_fees is passed" do assert Fees.check_if_covered(%{@eth => 0}, :ignore_fees) == :ok end test "returns :ok when given positive fees and :ignore_fees is passed" do assert Fees.check_if_covered(%{@eth => 1, @not_eth_1 => 2}, :ignore_fees) == :ok end test "returns :overpaying_fees when given positive fees and :no_fees_required is passed" do assert Fees.check_if_covered(%{@not_eth_1 => 0, @eth => 1}, :no_fees_required) == {:error, :overpaying_fees} end test "returns :ok when given fees are 0 and :no_fees_required is passed" do assert Fees.check_if_covered(%{@eth => 0}, :no_fees_required) == :ok end test "returns :ok when fees are exactly covered by one currency" do assert Fees.check_if_covered(%{@not_eth_1 => 3, @eth => 0}, @payment_fees) == :ok end test "returns :ok when fees are exactly covered by one currency with previous fees" do fees = @payment_fees |> Map.put(@eth, [1, 2]) |> Map.put(@not_eth_1, [1, 3]) assert Fees.check_if_covered(%{@not_eth_1 => 3, @eth => 0}, fees) == :ok end test "returns :multiple_potential_currency_fees when multiple implicit fees are given" do assert Fees.check_if_covered(%{@eth => 2, @not_eth_1 => 2}, @payment_fees) == {:error, :multiple_potential_currency_fees} end test "returns :fees_not_covered when no positive implicit fees given" do other_currency = <<2::160>> assert Fees.check_if_covered(%{other_currency => 0}, @payment_fees) == {:error, :fees_not_covered} end test "returns :fees_not_covered when the implicit fees currency does not match any of the supported fee currencies" do other_currency = <<2::160>> assert Fees.check_if_covered(%{other_currency => 100}, @payment_fees) == {:error, :fees_not_covered} end test "returns :fees_not_covered when fees do not cover the fee price" do assert Fees.check_if_covered(%{@not_eth_1 => 1}, @payment_fees) == {:error, :fees_not_covered} end test "returns :fees_not_covered when fees do not cover the fee price with previous fees" do fees = @payment_fees |> Map.put(@eth, [1, 2]) |> Map.put(@not_eth_1, [3, 1]) assert Fees.check_if_covered(%{@not_eth_1 => 2}, fees) == {:error, :fees_not_covered} end test "returns :overpaying_fees when fees cover more than the fee price" do assert Fees.check_if_covered(%{@not_eth_1 => 4}, @payment_fees) == {:error, :overpaying_fees} end test "returns :overpaying_fees when fees cover more than the fee price with previous fees" do fees = @payment_fees |> Map.put(@eth, [1, 2]) |> Map.put(@not_eth_1, [1, 3]) assert Fees.check_if_covered(%{@not_eth_1 => 2}, fees) == {:error, :overpaying_fees} end @tag fixtures: [:alice, :bob] test "returns :ok when one input is dedicated for fee payment, and outputs are other tokens", %{alice: alice, bob: bob} do # a token that we don't allow to pay the fees in other_token = <<2::160>> # it is presumed that one input is `other_token` (to cover outputs) and the other input is `@not_eth_1` to cover # the fee only. Note that `@not_eth_1` doesn't appear in the outputs transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, other_token, 5}, {alice, other_token, 5}]) fees = Fees.for_transaction(transaction, @fees) # here we tell `Fees` that 3 `@not_eth_1` was sent to cover the fee assert Fees.check_if_covered(%{@not_eth_1 => 3, other_token => 0}, fees) == :ok end end describe "for_transaction/2" do @tag fixtures: [:alice, :bob] test "returns the fee map when not a merge transaction", %{alice: alice, bob: bob} do transaction = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}]) assert Fees.for_transaction(transaction, @fees) == @payment_fees end @tag fixtures: [:alice] test "returns :no_fees_required for merge transactions", %{alice: alice} do transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 10}]) assert Fees.for_transaction(transaction, @fees) == :no_fees_required end @tag fixtures: [:alice] test "returns :no_fees_required for valid merge transactions with multiple inputs/ouputs", %{alice: alice} do transaction = create_recovered( [{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}, {2, 1, 0, alice}], [{alice, @eth, 10}, {alice, @eth, 10}] ) assert Fees.for_transaction(transaction, @fees) == :no_fees_required end test "returns an empty hash when given an unsuported tx type" do transaction = %OMG.Watcher.State.Transaction.Recovered{ signed_tx: %OMG.Watcher.State.Transaction.Signed{raw_tx: DummyTransaction.new(), sigs: []}, tx_hash: "", witnesses: [], signed_tx_bytes: "" } assert Fees.for_transaction(transaction, @fees) == %{} end @tag fixtures: [:alice, :bob] test "returns an empty hash when given invalid tx type", %{alice: alice, bob: bob} do fees = %{ 999 => %{ @eth => %{ amount: 1, subunit_to_unit: 1_000_000_000_000_000_000, pegged_amount: 4, pegged_currency: "USD", pegged_subunit_to_unit: 100, updated_at: DateTime.from_iso8601("2019-01-01T10:10:00+00:00") } } } transaction = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}]) assert Fees.for_transaction(transaction, fees) == %{} end end defmodule DummyTransaction do defstruct [] def new(), do: %__MODULE__{} end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/http_rpc/adapter_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.HttpRPC.AdapterTest do use ExUnit.Case, async: true import FakeServer alias OMG.Utils.AppVersion alias OMG.Watcher.HttpRPC.Adapter describe "rpc_post/3" do test_with_server "includes X-Watcher-Version header" do route("/path", FakeServer.Response.ok()) _ = Adapter.rpc_post(%{}, "path", FakeServer.address()) expected_watcher_version = AppVersion.version(:omg_watcher_info) assert request_received( "/path", method: "POST", headers: %{"content-type" => "application/json", "x-watcher-version" => expected_watcher_version} ) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/block_getter_1_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.BlockGetter1Test do @moduledoc """ This test is intended to be the major smoke/integration test of the Watcher It tests whether valid/invalid blocks, deposits and exits are tracked correctly within the Watcher """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.Watcher.Integration.Fixtures use Plug.Test require OMG.Watcher.Utxo alias OMG.Eth alias OMG.Watcher.BlockGetter alias OMG.Watcher.Event alias OMG.Watcher.Integration.BadChildChainServer alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.TestHelper alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper @timeout 40_000 @eth <<0::160>> @moduletag :mix_based_child_chain @moduletag timeout: 150_000 @tag fixtures: [:in_beam_watcher, :stable_alice, :token, :stable_alice_deposits, :test_server] test "transaction which is spending an exiting output before the `sla_margin` causes an invalid_exit event only", %{stable_alice: alice, stable_alice_deposits: {deposit_blknum, _}, test_server: context} do Process.sleep(12_000) tx = TestHelper.create_encoded([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 9}]) %{"blknum" => exit_blknum} = WatcherHelper.submit(tx) # Here we're preparing invalid block bad_block_number = 2_000 bad_tx = TestHelper.create_recovered([{exit_blknum, 0, 0, alice}], @eth, [{alice, 9}]) %{hash: bad_block_hash, number: _, transactions: _} = bad_block = OMG.Watcher.Block.hashed_txs_at([bad_tx], bad_block_number) # from now on the child chain server is broken until end of test route = BadChildChainServer.prepare_route_to_inject_bad_block(context, bad_block) :sys.replace_state(BlockGetter, fn state -> config = state.config new_config = %{config | child_chain_url: "http://localhost:#{route.port}"} %{state | config: new_config} end) IntegrationTest.wait_for_block_fetch(exit_blknum, @timeout) Process.sleep(12_000) %{ "txbytes" => txbytes, "proof" => proof, "utxo_pos" => utxo_pos } = WatcherHelper.get_exit_data(exit_blknum, 0, 0) {:ok, %{"status" => "0x1", "blockNumber" => _eth_height}} = utxo_pos |> RootChainHelper.start_exit( txbytes, proof, alice.addr ) |> DevHelper.transact_sync!() # THIS IS CHILDCHAIN CODE # Here we're manually submitting invalid block to the root chain # NOTE: this **must** come after `start_exit` is mined (see just above) but still not later than # `sla_margin` after exit start, hence the `config/test.exs` entry for the margin is rather high {:ok, _} = Eth.submit_block(bad_block_hash, 2, 1) IntegrationTest.wait_for_byzantine_events([%Event.InvalidExit{}.name], @timeout) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/block_getter_2_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.BlockGetter2Test do @moduledoc """ This test is intended to be the major smoke/integration test of the Watcher It tests whether valid/invalid blocks, deposits and exits are tracked correctly within the Watcher """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.Watcher.Integration.Fixtures use Plug.Test require OMG.Watcher.Utxo alias OMG.Eth alias OMG.Watcher.BlockGetter alias OMG.Watcher.Event alias OMG.Watcher.Integration.BadChildChainServer alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.TestHelper alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper @timeout 60_000 @eth <<0::160>> @moduletag :mix_based_child_chain @moduletag timeout: 180_000 @tag fixtures: [:in_beam_watcher, :stable_alice, :token, :stable_alice_deposits, :test_server] test "transaction which is spending an exiting output after the `sla_margin` causes an unchallenged_exit event", %{stable_alice: alice, stable_alice_deposits: {deposit_blknum, _}, test_server: context} do Process.sleep(12_000) tx = TestHelper.create_encoded([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 9}]) %{"blknum" => exit_blknum} = WatcherHelper.submit(tx) # Here we're preparing invalid block bad_tx = OMG.Watcher.TestHelper.create_recovered([{exit_blknum, 0, 0, alice}], @eth, [{alice, 9}]) bad_block_number = 2_000 %{hash: bad_block_hash, number: _, transactions: _} = bad_block = OMG.Watcher.Block.hashed_txs_at([bad_tx], bad_block_number) # from now on the child chain server is broken until end of test route = BadChildChainServer.prepare_route_to_inject_bad_block(context, bad_block) :sys.replace_state(BlockGetter, fn state -> config = state.config new_config = %{config | child_chain_url: "http://localhost:#{route.port}"} %{state | config: new_config} end) IntegrationTest.wait_for_block_fetch(exit_blknum, @timeout) Process.sleep(10_000) %{ "txbytes" => txbytes, "proof" => proof, "utxo_pos" => utxo_pos } = WatcherHelper.get_exit_data(exit_blknum, 0, 0) {:ok, %{"status" => "0x1", "blockNumber" => eth_height}} = utxo_pos |> RootChainHelper.start_exit( txbytes, proof, alice.addr ) |> DevHelper.transact_sync!() exit_processor_sla_margin = Application.fetch_env!(:omg_watcher, :exit_processor_sla_margin) DevHelper.wait_for_root_chain_block(eth_height + exit_processor_sla_margin, @timeout) # checking if both machines and humans learn about the byzantine condition assert WatcherHelper.capture_log(fn -> # Here we're manually submitting invalid block to the root chain # THIS IS CHILDCHAIN CODE {:ok, _} = Eth.submit_block(bad_block_hash, 2, 1) IntegrationTest.wait_for_byzantine_events( [%Event.InvalidExit{}.name, %Event.UnchallengedExit{}.name], @timeout ) end) =~ inspect(:unchallenged_exit) # we should still be able to challenge this "unchallenged exit" - just smoke testing the endpoint, details elsewhere WatcherHelper.get_exit_challenge(exit_blknum, 0, 0) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/block_getter_3_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.BlockGetter3Test do @moduledoc """ This test is intended to be the major smoke/integration test of the Watcher It tests whether valid/invalid blocks, deposits and exits are tracked correctly within the Watcher """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.Watcher.Integration.Fixtures use Plug.Test require OMG.Watcher.Utxo import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.Eth alias OMG.Watcher.Event alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.TestHelper alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper @timeout 40_000 @eth <<0::160>> @moduletag :mix_based_child_chain @moduletag timeout: 100_000 @tag fixtures: [:in_beam_watcher, :stable_alice, :token, :stable_alice_deposits] test "block getting halted by block withholding doesn't halt detection of new invalid exits", %{ stable_alice: alice, stable_alice_deposits: {deposit_blknum, _} } do Process.sleep(11_000) tx = TestHelper.create_encoded([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 9}]) %{"blknum" => deposit_blknum} = WatcherHelper.submit(tx) tx = TestHelper.create_encoded([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 8}]) %{"blknum" => tx_blknum, "txhash" => _tx_hash} = WatcherHelper.submit(tx) IntegrationTest.wait_for_block_fetch(tx_blknum, @timeout) {_, nonce} = get_next_blknum_nonce(tx_blknum) {:ok, _txhash} = Eth.submit_block(<<0::256>>, nonce, 20_000_000_000) # checking if both machines and humans learn about the byzantine condition assert capture_log(fn -> IntegrationTest.wait_for_byzantine_events([%Event.BlockWithholding{}.name], @timeout) end) =~ inspect(:withholding) %{ "txbytes" => txbytes, "proof" => proof, "utxo_pos" => utxo_pos } = WatcherHelper.get_exit_data(deposit_blknum, 0, 0) {:ok, %{"status" => "0x1", "blockNumber" => _eth_height}} = utxo_pos |> RootChainHelper.start_exit( txbytes, proof, alice.addr ) |> DevHelper.transact_sync!() IntegrationTest.wait_for_byzantine_events([%Event.BlockWithholding{}.name, %Event.InvalidExit{}.name], @timeout) end defp get_next_blknum_nonce(blknum) do child_block_interval = Application.fetch_env!(:omg_eth, :child_block_interval) next_blknum = blknum + child_block_interval {next_blknum, trunc(next_blknum / child_block_interval)} end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/block_getter_4_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.BlockGetter4Test do @moduledoc """ This test is intended to be the major smoke/integration test of the Watcher It tests whether valid/invalid blocks, deposits and exits are tracked correctly within the Watcher """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.Watcher.Integration.Fixtures use Plug.Test require OMG.Watcher.Utxo alias OMG.Eth alias OMG.Watcher.BlockGetter alias OMG.Watcher.Configuration alias OMG.Watcher.Event alias OMG.Watcher.Integration.BadChildChainServer alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.TestHelper alias Support.WatcherHelper @timeout 40_000 @eth <<0::160>> @moduletag :mix_based_child_chain @moduletag timeout: 100_000 @tag fixtures: [:in_beam_watcher, :test_server, :stable_alice, :stable_alice_deposits] test "operator claimed fees incorrectly (too much | little amount, not collected token)", %{ stable_alice: alice, test_server: context, stable_alice_deposits: {deposit_blknum, _} } do Process.sleep(11_000) fee_claimer = Configuration.fee_claimer_address() # preparing transactions for a fake block that overclaim fees txs = [ TestHelper.create_recovered([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 9}]), TestHelper.create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 8}]), TestHelper.create_recovered_fee_tx(1000, fee_claimer, @eth, 3) ] block_overclaiming_fees = OMG.Watcher.Block.hashed_txs_at(txs, 1000) # from now on the child chain server is broken until end of test route = BadChildChainServer.prepare_route_to_inject_bad_block( context, block_overclaiming_fees ) :sys.replace_state(BlockGetter, fn state -> config = state.config new_config = %{config | child_chain_url: "http://localhost:#{route.port}"} %{state | config: new_config} end) # checking if both machines and humans learn about the byzantine condition assert WatcherHelper.capture_log(fn -> {:ok, _txhash} = Eth.submit_block(block_overclaiming_fees.hash, 1, 20_000_000_000) IntegrationTest.wait_for_byzantine_events([%Event.InvalidBlock{}.name], @timeout) end) =~ inspect({:tx_execution, :claimed_and_collected_amounts_mismatch}) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/block_getter_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.BlockGetterTest do @moduledoc """ This test is intended to be the major smoke/integration test of the Watcher It tests whether valid/invalid blocks, deposits and exits are tracked correctly within the Watcher """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.Watcher.Integration.Fixtures use Plug.Test require OMG.Watcher.Utxo alias OMG.Eth alias OMG.Watcher.BlockGetter alias OMG.Watcher.Event alias OMG.Watcher.Integration.BadChildChainServer alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias Support.WatcherHelper @timeout 40_000 @eth <<0::160>> @moduletag :integration @moduletag :watcher @moduletag timeout: 100_000 @tag fixtures: [:in_beam_watcher, :test_server] test "hash of returned block does not match hash submitted to the root chain", %{test_server: context} do different_hash = <<0::256>> block_with_incorrect_hash = %{OMG.Watcher.Block.hashed_txs_at([], 1000) | hash: different_hash} # from now on the child chain server is broken until end of test route = BadChildChainServer.prepare_route_to_inject_bad_block( context, block_with_incorrect_hash, different_hash ) :sys.replace_state(BlockGetter, fn state -> config = state.config new_config = %{config | child_chain_url: "http://localhost:#{route.port}"} %{state | config: new_config} end) # checking if both machines and humans learn about the byzantine condition assert WatcherHelper.capture_log(fn -> {:ok, _txhash} = Eth.submit_block(different_hash, 1, 20_000_000_000) IntegrationTest.wait_for_byzantine_events([%Event.InvalidBlock{}.name], @timeout) end) =~ inspect({:error, :incorrect_hash}) end @tag fixtures: [:in_beam_watcher, :alice, :test_server] test "bad transaction with not existing utxo, detected by interactions with State", %{ alice: alice, test_server: context } do # preparing block with invalid transaction recovered = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) block_with_incorrect_transaction = OMG.Watcher.Block.hashed_txs_at([recovered], 1000) # from now on the child chain server is broken until end of test route = BadChildChainServer.prepare_route_to_inject_bad_block( context, block_with_incorrect_transaction ) :sys.replace_state(BlockGetter, fn state -> config = state.config new_config = %{config | child_chain_url: "http://localhost:#{route.port}"} %{state | config: new_config} end) invalid_block_hash = block_with_incorrect_transaction.hash # checking if both machines and humans learn about the byzantine condition assert WatcherHelper.capture_log(fn -> {:ok, _txhash} = Eth.submit_block(invalid_block_hash, 1, 20_000_000_000) IntegrationTest.wait_for_byzantine_events([%Event.InvalidBlock{}.name], @timeout) end) =~ inspect(:tx_execution) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/in_flight_exit_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.InFlightExitTest do @moduledoc """ This needs to go away real soon. """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test use OMG.Watcher.Integration.Fixtures alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper require Utxo @timeout 40_000 @eth <<0::160>> @hex_eth "0x0000000000000000000000000000000000000000" @moduletag :mix_based_child_chain # bumping the timeout to three minutes for the tests here, as they do a lot of transactions to Ethereum to test @moduletag timeout: 240_000 @tag fixtures: [:in_beam_watcher, :alice, :bob, :token, :alice_deposits] test "finalization of utxo double-spent in state leaves in-flight exit active and invalid; warns", %{alice: alice, bob: bob, alice_deposits: {deposit_blknum, _}} do Process.sleep(12_000) DevHelper.import_unlock_fund(bob) tx = TestHelper.create_signed([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 5}, {bob, 4}]) ife1 = tx |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() %{"blknum" => blknum} = tx |> Transaction.Signed.encode() |> WatcherHelper.submit() invalidating_tx = TestHelper.create_encoded([{blknum, 0, 0, alice}], @eth, [{alice, 4}]) %{"blknum" => invalidating_blknum} = WatcherHelper.submit(invalidating_tx) IntegrationTest.wait_for_block_fetch(invalidating_blknum, @timeout) _ = exit_in_flight_and_wait_for_ife(ife1, alice) # checking if both machines and humans learn about the byzantine condition assert WatcherHelper.capture_log(fn -> # :output type _ = piggyback_and_process_exits(tx, 0, alice) end) =~ "Invalid in-flight exit finalization" assert %{"in_flight_exits" => [_], "byzantine_events" => byzantine_events} = WatcherHelper.success?("/status.get") # invalid piggyback is past sla margin, unchallenged_piggyback event is emitted assert [%{"event" => "unchallenged_piggyback"}, %{"event" => "invalid_piggyback"}] = Enum.filter(byzantine_events, &(&1["event"] != "piggyback_available")) end defp exit_in_flight(%Transaction.Signed{} = tx, exiting_user) do get_in_flight_exit_response = tx |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() exit_in_flight(get_in_flight_exit_response, exiting_user) end defp exit_in_flight(get_in_flight_exit_response, exiting_user) do RootChainHelper.in_flight_exit( get_in_flight_exit_response["in_flight_tx"], get_in_flight_exit_response["input_txs"], get_in_flight_exit_response["input_utxos_pos"], get_in_flight_exit_response["input_txs_inclusion_proofs"], get_in_flight_exit_response["in_flight_tx_sigs"], exiting_user.addr ) |> DevHelper.transact_sync!() end defp exit_in_flight_and_wait_for_ife(tx, exiting_user) do {:ok, %{"status" => "0x1", "blockNumber" => eth_height}} = exit_in_flight(tx, exiting_user) exit_finality_margin = Application.fetch_env!(:omg_watcher, :exit_finality_margin) DevHelper.wait_for_root_chain_block(eth_height + exit_finality_margin + 1) end defp piggyback_and_process_exits(%Transaction.Signed{raw_tx: raw_tx}, index, output_owner) do raw_tx_bytes = Transaction.raw_txbytes(raw_tx) {:ok, %{"status" => "0x1"}} = raw_tx_bytes |> RootChainHelper.piggyback_in_flight_exit_on_output(index, output_owner.addr) |> DevHelper.transact_sync!() :ok = IntegrationTest.process_exits(1, @hex_eth, output_owner) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/in_flight_exit_test_1_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.InFlightExit1Test do @moduledoc """ This needs to go away real soon. """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test use OMG.Watcher.Integration.Fixtures alias OMG.Watcher.EthereumEventAggregator alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper require Utxo @timeout 40_000 @eth <<0::160>> @moduletag :mix_based_child_chain # bumping the timeout to three minutes for the tests here, as they do a lot of transactions to Ethereum to test @moduletag timeout: 180_000 @tag fixtures: [:in_beam_watcher, :alice, :bob, :token, :alice_deposits] test "invalid in-flight exit challenge is detected by watcher, because it contains no position", %{alice: alice, bob: bob, alice_deposits: {deposit_blknum, _}} do # we need to recognized the deposit on the childchain first Process.sleep(12_000) # tx1 is submitted then in-flight-exited # tx2 is in-flight-exited, it will be _invalidly_ used to challenge tx1! tx1 = TestHelper.create_signed([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 5}, {alice, 4}]) tx2 = TestHelper.create_signed([{deposit_blknum, 0, 0, alice}], @eth, [{bob, 9}]) ife1 = tx1 |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() ife2 = tx2 |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() assert %{ "blknum" => blknum, "txindex" => 0, "txhash" => <<_::256>> } = tx1 |> Transaction.Signed.encode() |> WatcherHelper.submit() IntegrationTest.wait_for_block_fetch(blknum, @timeout) raw_tx1_bytes = Transaction.raw_txbytes(tx1) raw_tx2_bytes = Transaction.raw_txbytes(tx2) {:ok, %{"status" => "0x1", "blockNumber" => _}} = exit_in_flight(ife1, alice) {:ok, %{"status" => "0x1", "blockNumber" => ife_eth_height}} = exit_in_flight(ife2, alice) # sanity check in-flight exit has started on root chain, wait for finality assert {:ok, [_, _]} = EthereumEventAggregator.in_flight_exit_started(0, ife_eth_height) exit_finality_margin = Application.fetch_env!(:omg_watcher, :exit_finality_margin) DevHelper.wait_for_root_chain_block(ife_eth_height + exit_finality_margin) ### # CANONICITY GAME ### # we're unable to get the invalid challenge using `in_flight_exit.get_competitor`! # ...so we need to stich it together from some pieces we have: %{sigs: [competing_sig | _]} = tx2 competing_tx_input_txbytes = [] |> Transaction.Payment.new([{alice.addr, @eth, 10}]) |> Transaction.raw_txbytes() competing_tx_input_utxo_pos = Utxo.Position.encode(Utxo.position(deposit_blknum, 0, 0)) {:ok, %{"status" => "0x1", "blockNumber" => _challenge_eth_height}} = competing_tx_input_txbytes |> RootChainHelper.challenge_in_flight_exit_not_canonical( competing_tx_input_utxo_pos, raw_tx1_bytes, 0, raw_tx2_bytes, 0, 0, "", competing_sig, alice.addr ) |> DevHelper.transact_sync!() # existence of `invalid_ife_challenge` event # vanishing of `non_canonical_ife` event expected_events = [ # this is the tx2's non-canonical-ife which we leave as is %{"event" => "non_canonical_ife"}, %{"event" => "invalid_ife_challenge"}, %{"event" => "piggyback_available"} ] :ok = wait_for(expected_events) # now included IFE transaction tx1 is challenged and non-canonical, let's respond get_prove_canonical_response = WatcherHelper.get_prove_canonical(raw_tx1_bytes) {:ok, %{"status" => "0x1", "blockNumber" => _response_eth_height}} = get_prove_canonical_response["in_flight_txbytes"] |> RootChainHelper.respond_to_non_canonical_challenge( get_prove_canonical_response["in_flight_tx_pos"], get_prove_canonical_response["in_flight_proof"], alice.addr ) |> DevHelper.transact_sync!() expected_events = [ # this is the tx2's non-canonical-ife which we leave as is %{"event" => "non_canonical_ife"}, %{"event" => "piggyback_available"} ] :ok = wait_for(expected_events) end defp exit_in_flight(%Transaction.Signed{} = tx, exiting_user) do get_in_flight_exit_response = tx |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() exit_in_flight(get_in_flight_exit_response, exiting_user) end defp exit_in_flight(get_in_flight_exit_response, exiting_user) do get_in_flight_exit_response["in_flight_tx"] |> RootChainHelper.in_flight_exit( get_in_flight_exit_response["input_txs"], get_in_flight_exit_response["input_utxos_pos"], get_in_flight_exit_response["input_txs_inclusion_proofs"], get_in_flight_exit_response["in_flight_tx_sigs"], exiting_user.addr ) |> DevHelper.transact_sync!() end defp wait_for(expected_events) do Enum.reduce_while(1..1000, 0, fn x, acc -> events = "/status.get" |> WatcherHelper.success?() |> Map.get("byzantine_events") |> Enum.map(&Map.take(&1, ["event"])) case events do ^expected_events -> {:halt, :ok} _ -> Process.sleep(10) {:cont, acc + x} end end) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/in_flight_exit_test_2_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.InFlightExit2Test do @moduledoc """ This needs to go away real soon. """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test use OMG.Watcher.Integration.Fixtures alias OMG.Watcher.EthereumEventAggregator alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper require Utxo @timeout 40_000 @eth <<0::160>> @moduletag :mix_based_child_chain # bumping the timeout to three minutes for the tests here, as they do a lot of transactions to Ethereum to test @moduletag timeout: 180_000 @tag fixtures: [:in_beam_watcher, :alice, :bob, :token, :alice_deposits] test "in-flight exit competitor is detected by watcher and proven with position immediately", %{alice: alice, bob: bob, alice_deposits: {deposit_blknum, _}} do # we need to recognized the deposit on the childchain first Process.sleep(12_000) # tx1 is submitted then in-flight-exited # tx2 is in-flight-exited tx1 = TestHelper.create_signed([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 5}, {alice, 4}]) tx2 = TestHelper.create_signed([{deposit_blknum, 0, 0, alice}], @eth, [{bob, 9}]) ife1 = tx1 |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() ife2 = tx2 |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() assert %{"blknum" => blknum} = tx1 |> Transaction.Signed.encode() |> WatcherHelper.submit() IntegrationTest.wait_for_block_fetch(blknum, @timeout) raw_tx2_bytes = Transaction.raw_txbytes(tx2) {:ok, %{"status" => "0x1", "blockNumber" => _}} = exit_in_flight(ife1, alice) {:ok, %{"status" => "0x1", "blockNumber" => ife_eth_height}} = exit_in_flight(ife2, alice) # sanity check in-flight exit has started on root chain, wait for finality assert {:ok, [_, _]} = EthereumEventAggregator.in_flight_exit_started(0, ife_eth_height) ### # EVENTS DETECTION ### # existence of competitors detected by checking if `non_canonical_ife` events exists # Also, there should be piggybacks on input/output available expected_events = [ # only a single non_canonical event, since on of the IFE tx is included! %{"event" => "non_canonical_ife"}, %{"event" => "piggyback_available"} ] :ok = wait_for(expected_events) # Check if IFE is recognized as IFE by watcher (kept separate from the above for readability) assert %{"in_flight_exits" => [%{}, %{}]} = WatcherHelper.success?("/status.get") ### # CANONICITY GAME ### assert %{"competing_tx_pos" => id, "competing_proof" => proof} = get_competitor_response = WatcherHelper.get_in_flight_exit_competitors(raw_tx2_bytes) assert id > 0 assert proof != "" {:ok, %{"status" => "0x1", "blockNumber" => _challenge_eth_height}} = RootChainHelper.challenge_in_flight_exit_not_canonical( get_competitor_response["input_tx"], get_competitor_response["input_utxo_pos"], get_competitor_response["in_flight_txbytes"], get_competitor_response["in_flight_input_index"], get_competitor_response["competing_txbytes"], get_competitor_response["competing_input_index"], get_competitor_response["competing_tx_pos"], get_competitor_response["competing_proof"], get_competitor_response["competing_sig"], alice.addr ) |> DevHelper.transact_sync!() # vanishing of `non_canonical_ife` event expected_events = [%{"event" => "piggyback_available"}] :ok = wait_for(expected_events) end defp exit_in_flight(%Transaction.Signed{} = tx, exiting_user) do get_in_flight_exit_response = tx |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() exit_in_flight(get_in_flight_exit_response, exiting_user) end defp exit_in_flight(get_in_flight_exit_response, exiting_user) do RootChainHelper.in_flight_exit( get_in_flight_exit_response["in_flight_tx"], get_in_flight_exit_response["input_txs"], get_in_flight_exit_response["input_utxos_pos"], get_in_flight_exit_response["input_txs_inclusion_proofs"], get_in_flight_exit_response["in_flight_tx_sigs"], exiting_user.addr ) |> DevHelper.transact_sync!() end defp wait_for(expected_events) do Enum.reduce_while(1..1000, 0, fn x, acc -> events = "/status.get" |> WatcherHelper.success?() |> Map.get("byzantine_events") |> Enum.map(&Map.take(&1, ["event"])) case events do ^expected_events -> {:halt, :ok} _ -> Process.sleep(10) {:cont, acc + x} end end) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/in_flight_exit_test_3_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.InFlightExit3Test do @moduledoc """ This needs to go away real soon. """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test use OMG.Watcher.Integration.Fixtures alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper require Utxo @timeout 40_000 @eth <<0::160>> @hex_eth "0x0000000000000000000000000000000000000000" @moduletag :mix_based_child_chain # bumping the timeout to three minutes for the tests here, as they do a lot of transactions to Ethereum to test @moduletag timeout: 180_000 @tag fixtures: [:in_beam_watcher, :alice, :bob, :token, :alice_deposits] test "honest and cooperating users exit in-flight transaction", %{alice: alice, bob: bob, alice_deposits: {deposit_blknum, _}} do # we need to recognized the deposit on the childchain first Process.sleep(12_000) DevHelper.import_unlock_fund(bob) tx = TestHelper.create_signed([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 4}, {bob, 5}]) ife1 = tx |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() %{"blknum" => blknum} = tx |> Transaction.Signed.encode() |> WatcherHelper.submit() IntegrationTest.wait_for_block_fetch(blknum, @timeout) {:ok, %{"status" => "0x1", "blockNumber" => _eth_height}} = exit_in_flight(ife1, alice) [ife] = wait_for_not_empty_in_flight_exits() assert is_map(ife) _ = piggyback_and_process_exits(tx, 1, :output, bob) expected_events = [] :ok = wait_for(expected_events) :ok = wait_for_empty_in_flight_exits() end defp piggyback_and_process_exits(%Transaction.Signed{raw_tx: raw_tx}, index, piggyback_type, output_owner) do raw_tx_bytes = Transaction.raw_txbytes(raw_tx) {:ok, %{"status" => "0x1"}} = case piggyback_type do :input -> RootChainHelper.piggyback_in_flight_exit_on_input(raw_tx_bytes, index, output_owner.addr) :output -> RootChainHelper.piggyback_in_flight_exit_on_output(raw_tx_bytes, index, output_owner.addr) end |> DevHelper.transact_sync!() :ok = IntegrationTest.process_exits(1, @hex_eth, output_owner) end defp exit_in_flight(%Transaction.Signed{} = tx, exiting_user) do get_in_flight_exit_response = tx |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() exit_in_flight(get_in_flight_exit_response, exiting_user) end defp exit_in_flight(get_in_flight_exit_response, exiting_user) do get_in_flight_exit_response["in_flight_tx"] |> RootChainHelper.in_flight_exit( get_in_flight_exit_response["input_txs"], get_in_flight_exit_response["input_utxos_pos"], get_in_flight_exit_response["input_txs_inclusion_proofs"], get_in_flight_exit_response["in_flight_tx_sigs"], exiting_user.addr ) |> DevHelper.transact_sync!() end defp wait_for(expected_events) do Enum.reduce_while(1..1000, 0, fn x, acc -> events = "/status.get" |> WatcherHelper.success?() |> Map.get("byzantine_events") |> Enum.map(&Map.take(&1, ["event"])) case events do ^expected_events -> {:halt, :ok} _ -> Process.sleep(10) {:cont, acc + x} end end) end defp wait_for_not_empty_in_flight_exits() do Enum.reduce_while(1..1000, 0, fn x, acc -> ife = "/status.get" |> WatcherHelper.success?() |> Map.get("in_flight_exits") case ife do [] -> Process.sleep(10) {:cont, acc + x} ife -> {:halt, ife} end end) end defp wait_for_empty_in_flight_exits() do Enum.reduce_while(1..1000, 0, fn x, acc -> ife = "/status.get" |> WatcherHelper.success?() |> Map.get("in_flight_exits") case ife do [] -> {:halt, :ok} _ -> Process.sleep(10) {:cont, acc + x} end end) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/in_flight_exit_test_4_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.InFlightExit4Test do @moduledoc """ This needs to go away real soon. """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test use OMG.Watcher.Integration.Fixtures alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper require Utxo @eth <<0::160>> @hex_eth "0x0000000000000000000000000000000000000000" @moduletag :mix_based_child_chain # bumping the timeout to three minutes for the tests here, as they do a lot of transactions to Ethereum to test @moduletag timeout: 180_000 # NOTE: if https://github.com/omgnetwork/elixir-omg/issues/994 is taken care of, this behavior will change, see comments # therein. @tag fixtures: [:in_beam_watcher, :alice, :bob, :token, :alice_deposits] test "finalization of output from non-included IFE tx - all is good", %{alice: alice, bob: bob, alice_deposits: {deposit_blknum, _}} do Process.sleep(12_000) DevHelper.import_unlock_fund(bob) tx = TestHelper.create_signed([{deposit_blknum, 0, 0, alice}], @eth, [{alice, 5}, {bob, 5}]) _ = exit_in_flight_and_wait_for_ife(tx, alice) piggyback_and_process_exits(tx, 1, :output, bob) expected_events = [] :ok = wait_for(expected_events) :ok = wait_for_empty_in_flight_exits() end defp exit_in_flight(%Transaction.Signed{} = tx, exiting_user) do get_in_flight_exit_response = tx |> Transaction.Signed.encode() |> WatcherHelper.get_in_flight_exit() exit_in_flight(get_in_flight_exit_response, exiting_user) end defp exit_in_flight(get_in_flight_exit_response, exiting_user) do RootChainHelper.in_flight_exit( get_in_flight_exit_response["in_flight_tx"], get_in_flight_exit_response["input_txs"], get_in_flight_exit_response["input_utxos_pos"], get_in_flight_exit_response["input_txs_inclusion_proofs"], get_in_flight_exit_response["in_flight_tx_sigs"], exiting_user.addr ) |> DevHelper.transact_sync!() end defp exit_in_flight_and_wait_for_ife(tx, exiting_user) do {:ok, %{"status" => "0x1", "blockNumber" => eth_height}} = exit_in_flight(tx, exiting_user) exit_finality_margin = Application.fetch_env!(:omg_watcher, :exit_finality_margin) DevHelper.wait_for_root_chain_block(eth_height + exit_finality_margin + 1) end defp piggyback_and_process_exits(%Transaction.Signed{raw_tx: raw_tx}, index, piggyback_type, output_owner) do raw_tx_bytes = Transaction.raw_txbytes(raw_tx) {:ok, %{"status" => "0x1"}} = case piggyback_type do :input -> RootChainHelper.piggyback_in_flight_exit_on_input(raw_tx_bytes, index, output_owner.addr) :output -> RootChainHelper.piggyback_in_flight_exit_on_output(raw_tx_bytes, index, output_owner.addr) end |> DevHelper.transact_sync!() :ok = IntegrationTest.process_exits(1, @hex_eth, output_owner) end defp wait_for(expected_events) do Enum.reduce_while(1..1000, 0, fn x, acc -> events = "/status.get" |> WatcherHelper.success?() |> Map.get("byzantine_events") |> Enum.map(&Map.take(&1, ["event"])) case events do ^expected_events -> {:halt, :ok} _ -> Process.sleep(10) {:cont, acc + x} end end) end defp wait_for_empty_in_flight_exits() do Enum.reduce_while(1..1000, 0, fn x, acc -> ife = "/status.get" |> WatcherHelper.success?() |> Map.get("in_flight_exits") case ife do [] -> {:halt, :ok} _ -> Process.sleep(10) {:cont, acc + x} end end) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/invalid_exit_1_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.InvalidExit1Test do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test use OMG.Watcher.Integration.Fixtures alias OMG.Watcher.Event alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper require Utxo @moduletag :mix_based_child_chain @moduletag timeout: 180_000 @timeout 40_000 @eth <<0::160>> @tag fixtures: [:in_beam_watcher, :stable_alice, :token, :stable_alice_deposits] test "invalid_exit without a challenge raises unchallenged_exit after sla_margin had passed, can be challenged", %{stable_alice: alice, stable_alice_deposits: {deposit_blknum, _}} do Process.sleep(11_000) %{"blknum" => first_tx_blknum} = [{deposit_blknum, 0, 0, alice}] |> TestHelper.create_encoded(@eth, [{alice, 9}]) |> WatcherHelper.submit() %{"blknum" => second_tx_blknum} = [{first_tx_blknum, 0, 0, alice}] |> TestHelper.create_encoded(@eth, [{alice, 8}]) |> WatcherHelper.submit() Process.sleep(11_000) IntegrationTest.wait_for_block_fetch(second_tx_blknum, @timeout) %{"txbytes" => txbytes, "proof" => proof, "utxo_pos" => tx_utxo_pos} = WatcherHelper.get_exit_data(first_tx_blknum, 0, 0) {:ok, %{"status" => "0x1", "blockNumber" => eth_height}} = tx_utxo_pos |> RootChainHelper.start_exit(txbytes, proof, alice.addr) |> DevHelper.transact_sync!() IntegrationTest.wait_for_byzantine_events([%Event.InvalidExit{}.name], @timeout) exit_processor_sla_margin = Application.fetch_env!(:omg_watcher, :exit_processor_sla_margin) DevHelper.wait_for_root_chain_block(eth_height + exit_processor_sla_margin, @timeout) IntegrationTest.wait_for_byzantine_events([%Event.InvalidExit{}.name, %Event.UnchallengedExit{}.name], @timeout) # after the notification has been received, a challenged is composed and sent, regardless of it being late challenge = WatcherHelper.get_exit_challenge(first_tx_blknum, 0, 0) assert {:ok, %{"status" => "0x1"}} = RootChainHelper.challenge_exit( challenge["exit_id"], challenge["exiting_tx"], challenge["txbytes"], challenge["input_index"], challenge["sig"], alice.addr ) |> DevHelper.transact_sync!() IntegrationTest.wait_for_byzantine_events([], @timeout) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/invalid_exit_2_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.InvalidExit2Test do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test use OMG.Watcher.Integration.Fixtures alias OMG.Watcher.Event alias OMG.Watcher.Integration.TestHelper, as: IntegrationTest alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.DevHelper alias Support.RootChainHelper alias Support.WatcherHelper require Utxo @moduletag :mix_based_child_chain @moduletag timeout: 240_000 @timeout 40_000 @eth <<0::160>> @tag fixtures: [:in_beam_watcher, :stable_alice, :token, :stable_alice_deposits] test "exit which is using already spent utxo from transaction and deposit causes to emit invalid_exit event", %{ stable_alice: alice, stable_alice_deposits: {deposit_blknum, _} } do Process.sleep(12_000) %{"txbytes" => deposit_txbytes, "proof" => deposit_proof, "utxo_pos" => deposit_utxo_pos} = WatcherHelper.get_exit_data(deposit_blknum, 0, 0) %{"blknum" => first_tx_blknum} = [{deposit_blknum, 0, 0, alice}] |> TestHelper.create_encoded(@eth, [{alice, 9}]) |> WatcherHelper.submit() Process.sleep(30_000) %{"blknum" => second_tx_blknum} = [{first_tx_blknum, 0, 0, alice}] |> TestHelper.create_encoded(@eth, [{alice, 8}]) |> WatcherHelper.submit() IntegrationTest.wait_for_block_fetch(second_tx_blknum, @timeout) Process.sleep(30_000) exit_data = WatcherHelper.get_exit_data(first_tx_blknum, 0, 0) %{"txbytes" => txbytes, "proof" => proof, "utxo_pos" => tx_utxo_pos} = exit_data {:ok, %{"status" => "0x1"}} = tx_utxo_pos |> RootChainHelper.start_exit(txbytes, proof, alice.addr) |> DevHelper.transact_sync!() {:ok, %{"status" => "0x1"}} = deposit_utxo_pos |> RootChainHelper.start_exit(deposit_txbytes, deposit_proof, alice.addr) |> DevHelper.transact_sync!() IntegrationTest.wait_for_byzantine_events([%Event.InvalidExit{}.name, %Event.InvalidExit{}.name], @timeout) # after the notification has been received, a challenged is composed and sent challenge = WatcherHelper.get_exit_challenge(first_tx_blknum, 0, 0) assert {:ok, %{"status" => "0x1"}} = challenge["exit_id"] |> RootChainHelper.challenge_exit( challenge["exiting_tx"], challenge["txbytes"], challenge["input_index"], challenge["sig"], alice.addr ) |> DevHelper.transact_sync!() # challenge standard exits from deposits challenge_exit_deposit = WatcherHelper.get_exit_challenge(deposit_blknum, 0, 0) assert {:ok, %{"status" => "0x1"}} = challenge_exit_deposit["exit_id"] |> RootChainHelper.challenge_exit( challenge_exit_deposit["exiting_tx"], challenge_exit_deposit["txbytes"], challenge_exit_deposit["input_index"], challenge_exit_deposit["sig"], alice.addr ) |> DevHelper.transact_sync!() IntegrationTest.wait_for_byzantine_events([], @timeout) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/monitor_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.MonitorTest do @moduledoc false import ExUnit.CaptureLog, only: [capture_log: 1] alias __MODULE__.ChildProcess alias OMG.Status.Alert.Alarm alias OMG.Watcher.Monitor use ExUnit.Case, async: true setup_all do {:ok, apps} = Application.ensure_all_started(:omg_status) on_exit(fn -> apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) :ok end setup do on_exit(fn -> case Process.whereis(Monitor) do nil -> :ok pid -> Process.exit(pid, :kill) end end) :ok end test "that a child process gets restarted after alarm is cleared" do child = ChildProcess.prepare_child() {:ok, monitor_pid} = start_and_attach_a_child([Alarm, child]) app_alarm = Alarm.ethereum_connection_error(__MODULE__) # the monitor is now started, we raise an alarm and kill it's child :ok = :alarm_handler.set_alarm(app_alarm) _ = Process.unlink(monitor_pid) {:links, [child_pid]} = Process.info(monitor_pid, :links) :erlang.trace(monitor_pid, true, [:receive]) # the child is now killed capture_log(fn -> true = Process.exit(Process.whereis(ChildProcess), :kill) end) # we prove that we're linked to the child process and that when it gets killed # we get the trap exit message assert_receive {:trace, ^monitor_pid, :receive, {:EXIT, ^child_pid, :killed}}, 5_000 {:links, links} = Process.info(monitor_pid, :links) assert Enum.empty?(links) == true # now we can clear the alarm and let the monitor restart the child process # and trace that the child process gets started capture_log(fn -> :ok = :alarm_handler.clear_alarm(app_alarm) end) assert_receive {:trace, ^monitor_pid, :receive, {:"$gen_cast", :start_child}} :erlang.trace(monitor_pid, false, [:receive]) # we now assert that our child was re-attached to the monitor Process.sleep(200) {:links, children} = Process.info(monitor_pid, :links) assert Enum.count(children) == 1 end test "that a child process does not get restarted if an alarm is cleared but it was not down" do child = ChildProcess.prepare_child() {:ok, monitor_pid} = start_and_attach_a_child([Alarm, child]) app_alarm = Alarm.ethereum_connection_error(__MODULE__) :ok = :alarm_handler.set_alarm(app_alarm) :erlang.trace(monitor_pid, true, [:receive]) {:links, links} = Process.info(monitor_pid, :links) # now we clear the alarm and let the monitor restart the child processes # in our case the child is alive so init should NOT be called capture_log(fn -> :ok = :alarm_handler.clear_alarm(app_alarm) end) assert_receive {:trace, ^monitor_pid, :receive, {:"$gen_cast", :start_child}}, 1500 # at this point we're just verifying that we didn't restart or start # another child assert Process.info(monitor_pid, :links) == {:links, links} end defp start_and_attach_a_child(opts) do case Monitor.start_link(opts) do {:ok, monitor_pid} -> {:ok, monitor_pid} {:error, {{:badmatch, {:error, {:already_started, _}}}, _}} -> Process.sleep(500) start_and_attach_a_child(opts) end end defmodule ChildProcess do @moduledoc """ Mocking a child process to Monitor """ use GenServer @spec prepare_child() :: %{id: atom(), start: tuple()} def prepare_child() do %{id: __MODULE__, start: {__MODULE__, :start_link, [[]]}} end def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) def init(_), do: {:ok, %{}} def terminate(_reason, _) do :ok end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/root_chain_coordinator_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RootChainCoordinatorTest do @moduledoc """ Smoke tests the imperative shells of `OMG.Watcher.EthereumEventListener` and `OMG.Watcher.RootChainCoordinator` working together """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.DB.Fixtures use OMG.Eth.Fixtures @moduletag :integration @moduletag :common setup do {:ok, bus_apps} = Application.ensure_all_started(:omg_bus) db_path = Briefly.create!(directory: true) :ok = Application.put_env(:omg_db, :path, db_path, persistent: true) :ok = OMG.DB.init() {:ok, eth_apps} = Application.ensure_all_started(:omg_eth) {:ok, status_apps} = Application.ensure_all_started(:omg_status) apps = bus_apps ++ eth_apps ++ status_apps on_exit(fn -> apps |> Enum.reverse() |> Enum.each(&Application.stop/1) end) :ok end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/integration/test_server_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.TestServerTest do use ExUnitFixtures use ExUnit.Case, async: false alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.HttpRPC.Client alias OMG.Watcher.Integration.TestServer @expected_block_hash <<0::256>> describe "/block.get -" do @response TestServer.make_response(%{ blknum: 123_000, hash: Encoding.to_hex(@expected_block_hash), transactions: [] }) @tag fixtures: [:test_server] test "successful response is parsed to expected map", %{test_server: context} do TestServer.with_route(context, "/block.get", @response) assert {:ok, %{ transactions: [], number: 123_000, hash: @expected_block_hash }} == Client.get_block(@expected_block_hash, context.fake_addr) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/merge_transaction_validator_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.MergeTransactionValidatorTest do @moduledoc false use ExUnitFixtures use ExUnit.Case, async: true import OMG.Watcher.TestHelper alias OMG.Watcher.MergeTransactionValidator alias OMG.Watcher.State.Transaction @eth <<0::160>> @not_eth <<1::size(160)>> describe "is_merge_transaction?/1" do @tag fixtures: [:alice] test "returns true when the transaction is a payment, has less outputs than inputs, has single currency, and has same account", %{alice: alice} do transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 10}]) assert MergeTransactionValidator.is_merge_transaction?(transaction) end test "returns false when transaction is not of payment type" do refute MergeTransactionValidator.is_merge_transaction?(%Transaction.Recovered{signed_tx: %{raw_tx: "fake"}}) end test "returns false when transaction doesn't consist of fungible-tokens only" do refute MergeTransactionValidator.is_merge_transaction?(%Transaction.Recovered{ signed_tx: %Transaction.Signed{raw_tx: %Transaction.Payment{inputs: [1, 2], outputs: [%{}]}} }) end @tag fixtures: [:alice] test "returns false when transaction has as many outputs than inputs", %{alice: alice} do transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 5}, {alice, 5}]) refute MergeTransactionValidator.is_merge_transaction?(transaction) end @tag fixtures: [:alice] test "returns false when transaction has more outputs than inputs", %{alice: alice} do transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 3}, {alice, 3}, {alice, 4}]) refute MergeTransactionValidator.is_merge_transaction?(transaction) end @tag fixtures: [:alice] test "returns false when not a single currency", %{alice: alice} do transaction = create_recovered( [{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}, {2, 1, 0, alice}], [{alice, @eth, 10}, {alice, @not_eth, 10}] ) refute MergeTransactionValidator.is_merge_transaction?(transaction) end @tag fixtures: [:alice, :bob] test "returns false when two different accounts in outputs", %{alice: alice, bob: bob} do transaction = create_recovered([{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}], @eth, [{bob, 10}, {alice, 10}]) refute MergeTransactionValidator.is_merge_transaction?(transaction) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/merkle_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.MerkleTest do @moduledoc false use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.Merkle describe "create_tx_proof/2" do test "creates merkle proofs based on list of values and index" do # We don't want to be testing the underlying library here, # we just want to ensure that our code calling it always # returns the same result values = ["abc", "def", "ghi"] proof_1 = Merkle.create_tx_proof(values, 1) proof_2 = Merkle.create_tx_proof(values, 2) assert proof_1 != proof_2 assert "c168262281c10d4285a4aecb" <> _ = Base.encode16(proof_1, case: :lower) end end describe "hash/1" do test "returns the merkle tree root for a list of transaction" do values = ["abc", "def", "ghi"] proof = values |> Merkle.hash() |> Base.encode16(case: :lower) assert proof == "2060aa204dd8b8cc723a8abf2ce20e982d0acbd4f95bdbfaca435495b5ad5dc6" end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/output_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.OutputTest do @moduledoc false use ExUnit.Case, async: true alias OMG.Output doctest OMG.Output describe "reconstruct/1" do test "returns an error if the output guard is invalid" do rlp_data = [ <<1>>, [ <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, <<1>> ] ] assert {:error, :output_guard_cant_be_zero} = Output.reconstruct(rlp_data) end test "returns an error if the output is malformed" do rlp_data = [] assert {:error, :malformed_outputs} = Output.reconstruct(rlp_data) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/raw_data_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RawDataTest do use ExUnit.Case, async: true # doctest OMG.Watcher.RawData alias OMG.Watcher.RawData describe "parse_amount/1" do test "rejects zero passed as amount" do [zero] = [0] |> ExRLP.encode() |> ExRLP.decode() assert {:error, :amount_cant_be_zero} == RawData.parse_amount(zero) end test "rejects integer greater than 32-bytes" do large = 2.0 |> :math.pow(8 * 32) |> Kernel.trunc() [too_large] = [large] |> ExRLP.encode() |> ExRLP.decode() assert {:error, :encoded_uint_too_big} == RawData.parse_amount(too_large) end test "rejects leading zeros encoded numbers" do [one] = [1] |> ExRLP.encode() |> ExRLP.decode() assert {:error, :leading_zeros_in_encoded_uint} == RawData.parse_amount(<<0>> <> one) end test "accepts 32-bytes positive integers" do large = 2.0 |> :math.pow(8 * 32) |> Kernel.trunc() big_just_enough = large - 1 [one, big] = [1, big_just_enough] |> ExRLP.encode() |> ExRLP.decode() assert {:ok, 1} == RawData.parse_amount(one) assert {:ok, big_just_enough} == RawData.parse_amount(big) end end describe "parse_address/1" do test "accepts 20-bytes binaries" do zero_addr = <<0::160>> non_zero_addr = <<2::160>> [zero, addr] = [zero_addr, non_zero_addr] |> ExRLP.encode() |> ExRLP.decode() assert {:ok, zero_addr} == RawData.parse_address(zero) assert {:ok, non_zero_addr} == RawData.parse_address(addr) end test "rejects binaries shorter or longer than address length" do too_short_addr = <<0::152>> too_long_addr = <<0::168>> [short, long] = [too_short_addr, too_long_addr] |> ExRLP.encode() |> ExRLP.decode() assert {:error, :malformed_address} == RawData.parse_address(short) assert {:error, :malformed_address} == RawData.parse_address(long) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/release_tasks/set_ethereum_events_check_interval_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ReleaseTasks.SetEthereumEventsCheckIntervalTest do use ExUnit.Case, async: true alias OMG.Watcher.ReleaseTasks.SetEthereumEventsCheckInterval @app :omg_watcher @env_key "ETHEREUM_EVENTS_CHECK_INTERVAL_MS" @config_key :ethereum_events_check_interval_ms test "that interval is set when the env var is present" do :ok = System.put_env(@env_key, "1234") config = SetEthereumEventsCheckInterval.load([], []) ethereum_events_check_interval_ms = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_events_check_interval_ms == 1234 :ok = System.delete_env(@env_key) end test "that the default config is used when the env var is not set" do old_config = Application.get_env(@app, @config_key) :ok = System.delete_env(@env_key) config = SetEthereumEventsCheckInterval.load([], []) ethereum_events_check_interval_ms = config |> Keyword.fetch!(@app) |> Keyword.fetch!(@config_key) assert ethereum_events_check_interval_ms == old_config end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/release_tasks/set_exit_processor_sla_margin_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ReleaseTasks.SetExitProcessorSLAMarginTest do use ExUnit.Case, async: true alias OMG.Watcher.ReleaseTasks.SetExitProcessorSLAMargin @app :omg_watcher test "if environment variables get applied in the configuration" do :ok = System.put_env("EXIT_PROCESSOR_SLA_MARGIN", "15") :ok = System.put_env("EXIT_PROCESSOR_SLA_MARGIN_FORCED", "TRUE") config = SetExitProcessorSLAMargin.load([], []) exit_processor_sla_margin = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:exit_processor_sla_margin) exit_processor_sla_margin_forced = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:exit_processor_sla_margin_forced) assert exit_processor_sla_margin == 15 assert exit_processor_sla_margin_forced == true end test "if default configuration is used when there's no environment variables" do :ok = System.delete_env("EXIT_PROCESSOR_SLA_MARGIN") :ok = System.delete_env("EXIT_PROCESSOR_SLA_MARGIN_FORCED") config = SetExitProcessorSLAMargin.load([], []) exit_processor_sla_margin = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:exit_processor_sla_margin) exit_processor_sla_margin_forced = config |> Keyword.fetch!(@app) |> Keyword.fetch!(:exit_processor_sla_margin_forced) exit_processor_sla_margin_updated = Application.get_env(@app, :exit_processor_sla_margin) exit_processor_sla_margin_forced_updated = Application.get_env(@app, :exit_processor_sla_margin_forced) assert exit_processor_sla_margin == exit_processor_sla_margin_updated assert exit_processor_sla_margin_forced == exit_processor_sla_margin_forced_updated end test "if exit is thrown when faulty margin configuration is used" do :ok = System.put_env("EXIT_PROCESSOR_SLA_MARGIN", "15a") catch_exit(SetExitProcessorSLAMargin.load([], [])) :ok = System.delete_env("EXIT_PROCESSOR_SLA_MARGIN") end test "if exit is thrown when faulty margin force configuration is used" do :ok = System.put_env("EXIT_PROCESSOR_SLA_MARGIN_FORCED", "15") catch_exit(SetExitProcessorSLAMargin.load([], [])) :ok = System.delete_env("EXIT_PROCESSOR_SLA_MARGIN_FORCED") end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/release_tasks/set_tracer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ReleaseTasks.SetTracerTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.Watcher.ReleaseTasks.SetTracer alias OMG.Watcher.Tracer @app :omg_watcher setup do {:ok, pid} = __MODULE__.System.start_link([]) nil = Process.put(__MODULE__.System, pid) :ok end test "if environment variables get applied in the configuration" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUE") :ok = __MODULE__.System.put_env("APP_ENV", "YOLO") :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 3") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) disabled = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:disabled?) env = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:env) assert disabled == true # if it's disabled, env doesn't matter, so we set it to an empty string assert env == "" end) end test "if default configuration is used when there's no environment variables" do :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 3") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) configuration = @app |> Application.get_env(Tracer) |> Keyword.put(:env, "") |> Enum.sort() tracer_config = config |> Keyword.get(@app) |> Keyword.get(Tracer) |> Enum.sort() assert configuration == tracer_config end) end test "if exit is thrown when faulty configuration is used" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUEeee") catch_exit(SetTracer.load([], system_adapter: __MODULE__.System)) end defmodule System do def start_link(args), do: GenServer.start_link(__MODULE__, args, []) def get_env(key), do: __MODULE__ |> Process.get() |> GenServer.call({:get_env, key}) def put_env(key, value), do: __MODULE__ |> Process.get() |> GenServer.call({:put_env, key, value}) def init(_), do: {:ok, %{}} def handle_call({:get_env, key}, _, state) do {:reply, state[key], state} end def handle_call({:put_env, key, value}, _, state) do {:reply, :ok, Map.put(state, key, value)} end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/root_chain_coordinator/core_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.RootChainCoordinator.CoreTest do use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.RootChainCoordinator.Core deffixture initial_state() do Core.init(%{:depositor => [], :exiter => [waits_for: :depositor]}, 10) end @tag fixtures: [:initial_state] test "does not synchronize service that is not allowed", %{initial_state: state} do {:error, :service_not_allowed} = Core.check_in(state, :c.pid(0, 1, 0), 10, :unallowed_service) end @tag fixtures: [:initial_state] test "synchronizes services", %{initial_state: state} do depositor_pid = :c.pid(0, 1, 0) exiter_pid = :c.pid(0, 2, 0) assert {:ok, state} = Core.check_in(state, exiter_pid, 1, :exiter) assert :nosync = Core.get_synced_info(state, depositor_pid) assert :nosync = Core.get_synced_info(state, exiter_pid) assert {:ok, state} = Core.check_in(state, depositor_pid, 2, :depositor) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 2} = Core.get_synced_info(state, exiter_pid) assert {:ok, state} = Core.check_in(state, exiter_pid, 2, :exiter) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 2} = Core.get_synced_info(state, exiter_pid) assert {:ok, state} = Core.check_in(state, depositor_pid, 3, :depositor) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 3} = Core.get_synced_info(state, exiter_pid) end @tag fixtures: [:initial_state] test "deregisters and registers a service", %{initial_state: state} do depositor_pid = :c.pid(0, 1, 0) exiter_pid = :c.pid(0, 2, 0) assert {:ok, state} = Core.check_in(state, exiter_pid, 1, :exiter) assert {:ok, state} = Core.check_in(state, depositor_pid, 1, :depositor) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 1} = Core.get_synced_info(state, exiter_pid) depositor_pid2 = :c.pid(0, 3, 0) assert {:ok, state} = Core.check_in(state, depositor_pid2, 2, :depositor) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid2) assert %{sync_height: 2} = Core.get_synced_info(state, exiter_pid) end @tag fixtures: [:initial_state] test "updates root chain height", %{initial_state: state} do depositor_pid = :c.pid(0, 1, 0) exiter_pid = :c.pid(0, 2, 0) assert {:ok, state} = Core.check_in(state, exiter_pid, 10, :exiter) assert {:ok, state} = Core.check_in(state, depositor_pid, 10, :depositor) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 10} = Core.get_synced_info(state, exiter_pid) assert {:ok, state} = Core.update_root_chain_height(state, 11) assert %{sync_height: 11} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 10} = Core.get_synced_info(state, exiter_pid) assert {:ok, state} = Core.update_root_chain_height(state, 14) assert %{sync_height: 14} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 10} = Core.get_synced_info(state, exiter_pid) end @tag fixtures: [:initial_state] test "reports synced heights", %{initial_state: state} do exiter_pid = :c.pid(0, 2, 0) assert %{root_chain_height: 10} == Core.get_ethereum_heights(state) assert {:ok, state} = Core.check_in(state, exiter_pid, 10, :exiter) assert %{root_chain_height: 10, exiter: 10} == Core.get_ethereum_heights(state) assert {:ok, state} = Core.update_root_chain_height(state, 11) assert %{root_chain_height: 11, exiter: 10} == Core.get_ethereum_heights(state) end @tag fixtures: [:initial_state] test "prevents huge queries to Ethereum client", %{initial_state: state} do depositor_pid = :c.pid(0, 1, 0) exiter_pid = :c.pid(0, 2, 0) assert {:ok, state} = Core.check_in(state, exiter_pid, 10, :exiter) assert {:ok, state} = Core.check_in(state, depositor_pid, 10, :depositor) assert {:ok, state} = Core.update_root_chain_height(state, 11_000_000) assert %{sync_height: new_sync_height} = Core.get_synced_info(state, depositor_pid) assert new_sync_height < 100_000 end @tag fixtures: [:initial_state] test "root chain back off is ignored", %{initial_state: state} do depositor_pid = :c.pid(0, 1, 0) exiter_pid = :c.pid(0, 2, 0) assert {:ok, state} = Core.check_in(state, exiter_pid, 10, :exiter) assert {:ok, state} = Core.check_in(state, depositor_pid, 10, :depositor) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 10} = Core.get_synced_info(state, exiter_pid) assert {:ok, state} = Core.update_root_chain_height(state, 9) assert %{sync_height: 10} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 10} = Core.get_synced_info(state, exiter_pid) assert {:ok, state} = Core.update_root_chain_height(state, 11) assert %{sync_height: 11} = Core.get_synced_info(state, depositor_pid) assert %{sync_height: 10} = Core.get_synced_info(state, exiter_pid) end @pid %{ depositor: :c.pid(0, 1, 0), exiter: :c.pid(0, 2, 0), depositor_finality: :c.pid(0, 3, 0), exiter_finality: :c.pid(0, 4, 0), getter: :c.pid(0, 5, 0), finalizer: :c.pid(0, 6, 0) } deffixture bigger_state() do state = Core.init( %{ :depositor => [], :exiter => [waits_for: :depositor], :depositor_finality => [finality_margin: 2], :exiter_finality => [waits_for: :depositor, finality_margin: 2], :getter => [waits_for: [depositor_finality: :no_margin]], :finalizer => [waits_for: [:getter, :depositor]] }, 10 ) {:ok, state} = Core.check_in(state, @pid[:depositor], 1, :depositor) {:ok, state} = Core.check_in(state, @pid[:exiter], 1, :exiter) {:ok, state} = Core.check_in(state, @pid[:depositor_finality], 1, :depositor_finality) {:ok, state} = Core.check_in(state, @pid[:exiter_finality], 1, :exiter_finality) {:ok, state} = Core.check_in(state, @pid[:getter], 1, :getter) {:ok, state} = Core.check_in(state, @pid[:finalizer], 1, :finalizer) state end @tag fixtures: [:bigger_state] test "waiting service will wait and progress accordingly", %{bigger_state: state} do assert %{sync_height: 1} = Core.get_synced_info(state, @pid[:exiter]) {:ok, state} = Core.check_in(state, @pid[:depositor], 2, :depositor) assert %{sync_height: 2} = Core.get_synced_info(state, @pid[:exiter]) {:ok, state} = Core.check_in(state, @pid[:depositor], 5, :depositor) assert %{sync_height: 5} = Core.get_synced_info(state, @pid[:exiter]) end @tag fixtures: [:bigger_state] test "waiting for multiple", %{bigger_state: state} do assert %{sync_height: 1} = Core.get_synced_info(state, @pid[:finalizer]) {:ok, state} = Core.check_in(state, @pid[:depositor], 2, :depositor) assert %{sync_height: 1} = Core.get_synced_info(state, @pid[:finalizer]) {:ok, state} = Core.check_in(state, @pid[:getter], 2, :getter) assert %{sync_height: 2} = Core.get_synced_info(state, @pid[:finalizer]) {:ok, state} = Core.check_in(state, @pid[:depositor], 5, :depositor) {:ok, state} = Core.check_in(state, @pid[:getter], 5, :getter) assert %{sync_height: 5} = Core.get_synced_info(state, @pid[:finalizer]) end @tag fixtures: [:bigger_state] test "waiting when margin of the awaited process should be skipped ahead", %{bigger_state: state} do assert %{sync_height: 3} = Core.get_synced_info(state, @pid[:getter]) {:ok, state} = Core.check_in(state, @pid[:depositor_finality], 5, :depositor_finality) assert %{sync_height: 7} = Core.get_synced_info(state, @pid[:getter]) {:ok, state} = Core.check_in(state, @pid[:depositor_finality], 8, :depositor_finality) assert %{sync_height: 10} = Core.get_synced_info(state, @pid[:getter]) assert {:ok, state} = Core.update_root_chain_height(state, 11) assert %{sync_height: 10} = Core.get_synced_info(state, @pid[:getter]) {:ok, state} = Core.check_in(state, @pid[:depositor_finality], 9, :depositor_finality) assert %{sync_height: 11} = Core.get_synced_info(state, @pid[:getter]) # sanity check - will not accidently spill over root chain height (but depositor wouldn't likely check in at 11) {:ok, state} = Core.check_in(state, @pid[:depositor_finality], 11, :depositor_finality) assert %{sync_height: 11} = Core.get_synced_info(state, @pid[:getter]) end @tag fixtures: [:bigger_state] test "waiting only for the finality margin", %{bigger_state: state} do assert %{sync_height: 8} = Core.get_synced_info(state, @pid[:depositor_finality]) {:ok, state} = Core.check_in(state, @pid[:depositor_finality], 5, :depositor_finality) assert %{sync_height: 8} = Core.get_synced_info(state, @pid[:depositor_finality]) assert {:ok, state} = Core.update_root_chain_height(state, 11) assert %{sync_height: 9} = Core.get_synced_info(state, @pid[:depositor_finality]) end @tag fixtures: [:bigger_state] test "waiting only for the finality margin and some service", %{bigger_state: state} do assert %{sync_height: 1} = Core.get_synced_info(state, @pid[:exiter_finality]) {:ok, state} = Core.check_in(state, @pid[:depositor], 5, :depositor) assert %{sync_height: 5} = Core.get_synced_info(state, @pid[:exiter_finality]) assert {:ok, state} = Core.update_root_chain_height(state, 11) assert %{sync_height: 5} = Core.get_synced_info(state, @pid[:exiter_finality]) {:ok, state} = Core.check_in(state, @pid[:depositor], 9, :depositor) assert %{sync_height: 9} = Core.get_synced_info(state, @pid[:exiter_finality]) # is reorg safe - root chain height going backwards is ignored assert {:ok, state} = Core.update_root_chain_height(state, 10) assert %{sync_height: 9} = Core.get_synced_info(state, @pid[:exiter_finality]) end test "behaves well close to zero", %{} do state = Core.init(%{:depositor => [finality_margin: 2], :exiter => [waits_for: :depositor, finality_margin: 2]}, 0) {:ok, state} = Core.check_in(state, @pid[:depositor], 0, :depositor) {:ok, state} = Core.check_in(state, @pid[:exiter], 0, :exiter) assert %{sync_height: 0} = Core.get_synced_info(state, @pid[:depositor]) assert %{sync_height: 0} = Core.get_synced_info(state, @pid[:exiter]) assert {:ok, state} = Core.update_root_chain_height(state, 1) assert %{sync_height: 0} = Core.get_synced_info(state, @pid[:depositor]) assert %{sync_height: 0} = Core.get_synced_info(state, @pid[:exiter]) assert {:ok, state} = Core.update_root_chain_height(state, 3) assert %{sync_height: 1} = Core.get_synced_info(state, @pid[:depositor]) assert %{sync_height: 0} = Core.get_synced_info(state, @pid[:exiter]) {:ok, state} = Core.check_in(state, @pid[:depositor], 1, :depositor) assert %{sync_height: 1} = Core.get_synced_info(state, @pid[:exiter]) end @tag fixtures: [:bigger_state] test "root chain heights reported observe the finality margin, if present", %{bigger_state: state} do assert %{root_chain_height: 10} = Core.get_synced_info(state, @pid[:depositor]) assert %{root_chain_height: 8} = Core.get_synced_info(state, @pid[:depositor_finality]) assert %{root_chain_height: 10} = Core.get_synced_info(state, @pid[:exiter]) assert %{root_chain_height: 8} = Core.get_synced_info(state, @pid[:exiter_finality]) assert %{root_chain_height: 10} = Core.get_synced_info(state, @pid[:getter]) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/signature_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.SignatureTest do use ExUnit.Case, async: true doctest OMG.Watcher.Signature alias OMG.Watcher.Crypto alias OMG.Watcher.Signature describe "recover_public/3" do test "returns an error from an invalid hash" do {:error, :invalid_recovery_id} = Signature.recover_public( <<2::256>>, 55, 38_938_543_279_057_362_855_969_661_240_129_897_219_713_373_336_787_331_739_561_340_553_100_525_404_231, 23_772_455_091_703_794_797_226_342_343_520_955_590_158_385_983_376_086_035_257_995_824_653_222_457_926 ) end test "recovers from generating a signed hash 1" do data = Base.decode16!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080", case: :lower ) hash = Crypto.hash(data) v = 27 r = 18_515_461_264_373_351_373_200_002_665_853_028_612_451_056_578_545_711_640_558_177_340_181_847_433_846 s = 46_948_507_304_638_947_509_940_763_649_030_358_759_909_902_576_025_900_602_547_168_820_602_576_006_531 {:ok, public_key} = Signature.recover_public(hash, v, r, s) assert public_key == <<75, 194, 163, 18, 101, 21, 63, 7, 231, 14, 11, 171, 8, 114, 78, 107, 133, 226, 23, 248, 205, 98, 140, 235, 98, 151, 66, 71, 187, 73, 51, 130, 206, 40, 202, 183, 154, 215, 17, 158, 225, 173, 62, 188, 219, 152, 161, 104, 5, 33, 21, 48, 236, 198, 207, 239, 161, 184, 142, 109, 255, 153, 35, 42>> end test "recovers from generating a signed hash 2" do {v, r, s} = {37, 18_515_461_264_373_351_373_200_002_665_853_028_612_451_056_578_545_711_640_558_177_340_181_847_433_846, 46_948_507_304_638_947_509_940_763_649_030_358_759_909_902_576_025_900_602_547_168_820_602_576_006_531} data = Base.decode16!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080", case: :lower ) hash = Crypto.hash(data) {:ok, public_key} = Signature.recover_public(hash, v, r, s, 1) assert public_key == <<75, 194, 163, 18, 101, 21, 63, 7, 231, 14, 11, 171, 8, 114, 78, 107, 133, 226, 23, 248, 205, 98, 140, 235, 98, 151, 66, 71, 187, 73, 51, 130, 206, 40, 202, 183, 154, 215, 17, 158, 225, 173, 62, 188, 219, 152, 161, 104, 5, 33, 21, 48, 236, 198, 207, 239, 161, 184, 142, 109, 255, 153, 35, 42>> end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/core_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.CoreTest do @moduledoc """ Tests functional behaviors of our high-throughput ledger being `OMG.Watcher.State.Core`. For test related to state persistence of this see `OMG.Watcher.State.PersistenceTest` """ use ExUnitFixtures use ExUnit.Case, async: true import OMG.Watcher.TestHelper require Logger require OMG.Watcher.Utxo alias OMG.Eth.Configuration alias OMG.Output alias OMG.Watcher.Block alias OMG.Watcher.Fees alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo @eth <<0::160>> @not_eth <<1::size(160)>> @interval Configuration.child_block_interval() @blknum1 @interval @blknum2 @interval * 2 @empty_block_hash <<246, 9, 190, 253, 254, 144, 102, 254, 20, 231, 67, 179, 98, 62, 174, 135, 143, 188, 70, 128, 5, 96, 136, 22, 131, 44, 157, 70, 15, 42, 149, 210>> @fee %{@eth => [1], @not_eth => [1]} @tag fixtures: [:alice, :bob, :state_empty] test "can spend deposits", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}]), @fee) |> success? |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{bob, 2}]), @fee) |> success? end describe "Lazy loaded utxo set" do @tag fixtures: [:alice, :bob, :state_alice_deposit] test "applies utxos with recent spends to check whether utxo should be fetched from db", %{alice: alice, bob: bob, state_alice_deposit: state} do # make some utxos state = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 5}, {bob, 2}, {alice, 2}]), @fee) |> success?() |> Core.exec(create_recovered([{1000, 0, 0, alice}], @eth, [{bob, 3}, {alice, 1}]), @fee) |> success?() deposit_pos = Utxo.position(1, 0, 0) assert Core.utxo_processed?(deposit_pos, state) == true spend_pos = Utxo.position(1000, 0, 0) assert Core.utxo_processed?(spend_pos, state) == true known_pos = [Utxo.position(1000, 0, 2), Utxo.position(1000, 1, 1)] assert Enum.map(known_pos, &Core.utxo_processed?(&1, state)) == [true, true] unknown_pos = [Utxo.position(1000, 2, 0), Utxo.position(1000, 1, 2)] assert Enum.map(unknown_pos, &Core.utxo_processed?(&1, state)) == [false, false] end @tag fixtures: [:alice, :state_empty] test "transaction input is missing in state", %{alice: alice, state_empty: state} do tx = create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]) state |> Core.with_utxos(%{}) |> Core.exec(tx, @fee) |> fail?(:utxo_not_found) end @tag fixtures: [:alice, :bob, :state_empty] test "all transaction inputs are merged from db", %{alice: alice, bob: bob, state_empty: state} do tx = create_recovered([{1000, 0, 0, alice}, {1000, 1, 0, alice}], @eth, [{bob, 7}, {alice, 2}]) db_utxos = make_utxos([{1000, 0, 0, alice, @eth, 5}, {1000, 1, 0, alice, @eth, 5}]) state |> Core.with_utxos(db_utxos) |> Core.exec(tx, @fee) |> success?() end @tag fixtures: [:alice, :bob, :state_empty] test "transaction utxos are mixed in memory and db", %{alice: alice, bob: bob, state_empty: state} do tx = create_recovered([{1000, 0, 0, alice}, {1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]) db_utxos = make_utxos([{1000, 0, 0, alice, @eth, 8}]) state |> do_deposit(alice, %{amount: 2, currency: @eth, blknum: 1}) |> Core.with_utxos(db_utxos) |> Core.exec(tx, @fee) |> success?() end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "spending utxo that resides in memory - double spend impossible", %{alice: alice, bob: bob, state_alice_deposit: state} do tx = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]) state |> Core.exec(tx, @fee) |> success?() |> Core.exec(tx, @fee) |> fail?(:utxo_not_found) end @tag fixtures: [:alice, :bob, :state_empty] test "extending state with same utxos does not change it", %{alice: alice, bob: bob, state_empty: state} do db_utxos = make_utxos([{1000, 0, 0, alice, @eth, 8}, {1000, 0, 1, bob, @eth, 2}]) state = Core.with_utxos(state, db_utxos) state |> Core.with_utxos(db_utxos) |> same?(state) end @tag fixtures: [:alice, :bob, :state_empty] test "extending state partially", %{alice: alice, bob: bob, state_empty: state} do db_utxos1 = make_utxos([{1000, 0, 0, alice, @eth, 6}]) db_utxos2 = make_utxos([{1000, 5, 0, alice, @eth, 6}]) tx = create_recovered([{1000, 0, 0, alice}, {1000, 5, 0, alice}], @eth, [{bob, 11}]) state |> Core.with_utxos(db_utxos1) |> Core.exec(tx, @fee) |> fail?(:utxo_not_found) |> Core.with_utxos(db_utxos2) |> Core.exec(tx, @fee) |> success?() end end describe "Transaction amounts and fees" do @tag fixtures: [:alice, :bob, :state_empty] test "fees are not needed when given :ignore_fees", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 5}, {alice, 5}]), :ignore_fees) |> success? end @tag fixtures: [:alice, :bob, :state_empty] test "fees can be overpaid when given :ignore_fees", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 1}, {alice, 1}]), :ignore_fees) |> success? end @tag fixtures: [:alice, :bob, :state_empty] test ":ignore_fees does not allow output amounts > input amounts", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 10}, {alice, 1}]), :ignore_fees) |> fail?(:amounts_do_not_add_up) end @tag fixtures: [:alice, :state_empty] test "output currencies must be included in input currencies", %{alice: alice, state_empty: state} do state1 = state |> do_deposit(alice, %{amount: 10, currency: @not_eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @not_eth, [{alice, 7}, {alice, 2}]), @fee) |> success? state1 |> Core.exec(create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 8}]), @fee) |> fail?(:amounts_do_not_add_up) state1 |> Core.exec( create_recovered([{1000, 0, 0, alice}], [{alice, @eth, 9}, {alice, @not_eth, 3}]), @fee ) |> fail?(:amounts_do_not_add_up) state1 |> Core.exec(create_recovered([{1000, 0, 0, alice}], [{alice, @not_eth, 6}]), @fee) |> success? end @tag fixtures: [:alice, :bob, :state_empty] test "amounts from multiple inputs must add up", %{alice: alice, bob: bob, state_empty: state} do state = do_deposit(state, alice, %{amount: 10, currency: @eth, blknum: 1}) # outputs exceed inputs state = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 7}, {bob, 4}]), @fee) |> fail?(:amounts_do_not_add_up) |> same?(state) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 2}, {alice, 7}]), @fee) |> success? state |> Core.exec( create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, [{alice, 7}, {bob, 2}]), @fee ) |> fail?(:fees_not_covered) |> same?(state) |> Core.exec( create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, [{alice, 9}, {bob, 2}]), @fee ) |> fail?(:amounts_do_not_add_up) |> same?(state) |> Core.exec( create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, [{alice, 6}, {bob, 2}]), @fee ) |> success?() end @tag fixtures: [:alice, :bob, :state_empty] test "Inputs exceeds outputs plus fee", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 4}, {alice, 3}]), @fee) |> fail?(:overpaying_fees) end @tag fixtures: [:alice, :bob, :state_empty] test "Inputs sums up exactly to outputs plus fee", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 5}, {alice, 4}]), @fee) |> success? end @tag fixtures: [:alice, :bob, :state_empty] test "Inputs are not sufficient for outputs plus fee", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 4}]), @fee) |> fail?(:fees_not_covered) end @tag fixtures: [:alice, :bob, :state_empty] test "Zero fee is not allowed, transaction is not processed", %{alice: alice, bob: bob, state_empty: state} do fee = %{@eth => %{amount: 0}} state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 3}, {alice, 7}]), fee) |> fail?(:fees_not_covered) end @tag fixtures: [:alice, :state_empty] test "Merge transaction is fee free", %{alice: alice, state_empty: state} do fees = %{@eth => %{amount: 2}} tx = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 15}]) fee = Fees.for_transaction(tx, fees) state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> do_deposit(alice, %{amount: 5, currency: @eth, blknum: 2}) |> Core.exec(tx, fee) |> success? end @tag fixtures: [:alice, :state_empty] test "Merge transaction is rejected when overpaying", %{alice: alice, state_empty: state} do fees = %{@eth => %{amount: 2}} tx = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 9}]) fee = Fees.for_transaction(tx, fees) state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> do_deposit(alice, %{amount: 5, currency: @eth, blknum: 2}) |> Core.exec(tx, fee) |> fail?(:overpaying_fees) end @tag fixtures: [:alice, :bob, :state_empty] test "respects fees for transactions with mixed currencies", %{ alice: alice, bob: bob, state_empty: state } do fees = %{@eth => [1], @not_eth => [1]} not_fee_token = <<2::160>> assert not_fee_token not in Map.keys(fees) state = state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> do_deposit(alice, %{amount: 2, currency: @not_eth, blknum: 2}) |> do_deposit(alice, %{amount: 1, currency: @not_eth, blknum: 3}) |> do_deposit(alice, %{amount: 10, currency: not_fee_token, blknum: 4}) # fee is paid in the same currency as an output state |> Core.exec(create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, @eth, 10}, {bob, @not_eth, 1}]), fees) |> success? # fee is paid in different currency than outputs state |> Core.exec(create_recovered([{1, 0, 0, alice}, {3, 0, 0, alice}], [{bob, @eth, 9}, {bob, @eth, 1}]), fees) |> success? # fee is paid from input not transferred by transaction state |> Core.exec( create_recovered([{1, 0, 0, alice}, {4, 0, 0, alice}], [{bob, not_fee_token, 9}, {bob, not_fee_token, 1}]), %{@eth => [10]} ) |> success? # fee is respected but amounts don't add up state |> Core.exec(create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, @eth, 10}, {bob, @eth, 1}]), fees) |> fail?(:amounts_do_not_add_up) # fee is not respected |> Core.exec(create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, @eth, 10}, {bob, @not_eth, 2}]), fees) |> fail?(:fees_not_covered) # transaction transferring only not fee currency still is obliged to fee |> Core.exec(create_recovered([{4, 0, 0, alice}], not_fee_token, [{bob, 3}, {alice, 7}]), fees) |> fail?(:fees_not_covered) end @tag fixtures: [:alice, :bob, :state_empty] test "can spend deposits with mixed currencies", %{ alice: alice, bob: bob, state_empty: state } do state |> do_deposit(alice, %{amount: 1, currency: @eth, blknum: 1}) |> do_deposit(alice, %{amount: 2, currency: @not_eth, blknum: 2}) |> Core.exec(create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, @eth, 1}, {bob, @not_eth, 1}]), @fee) |> success? end end @tag fixtures: [:alice, :bob, :state_empty] test "can spend a batch of deposits", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> do_deposit(bob, %{amount: 20, currency: @eth, blknum: 2}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 9}]), @fee) |> success? |> Core.exec(create_recovered([{2, 0, 0, bob}], @eth, [{alice, 19}]), @fee) |> success? end @tag fixtures: [:alice, :bob, :state_empty] test "can't spend when signature order does not match input order (restrictive spender checks)", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> do_deposit(bob, %{amount: 20, currency: @eth, blknum: 2}) |> Core.exec(create_recovered([{1, 0, 0, bob}, {2, 0, 0, alice}], @eth, [{bob, 10}]), @fee) |> fail?(:unauthorized_spend) end @tag fixtures: [:alice, :bob, :state_empty] test "deposits can arrive in any order; `OMG.Watcher.State.Core` doesn't care about this", %{alice: alice, bob: bob, state_empty: state} do state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 2}) |> do_deposit(bob, %{amount: 20, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{2, 0, 0, alice}], @eth, [{bob, 9}]), @fee) |> success? |> Core.exec(create_recovered([{1, 0, 0, bob}], @eth, [{alice, 19}]), @fee) |> success? end test "extract_initial_state function returns error when passed top block number as :not_found" do assert {:error, :top_block_number_not_found} = Core.extract_initial_state(:not_found, @interval, "NO FEE CLAIMER ADDR!") end @tag fixtures: [:alice, :bob, :state_empty] test "can't spend nonexistent", %{alice: alice, bob: bob, state_empty: state} do state_deposit = state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) state_deposit |> Core.exec(create_recovered([{1, 1, 0, alice}, {1, 0, 0, alice}], @eth, [{bob, 7}]), @fee) |> fail?(:utxo_not_found) |> same?(state_deposit) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "can't spend other people's funds", %{alice: alice, bob: bob, state_alice_deposit: state} do state |> Core.exec(create_recovered([{1, 0, 0, bob}], @eth, [{bob, 8}, {alice, 3}]), @fee) |> fail?(:unauthorized_spend) |> same?(state) |> Core.exec(create_recovered([{1, 0, 0, bob}], @eth, [{alice, 10}]), @fee) |> fail?(:unauthorized_spend) |> same?(state) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "all inputs must be authorized to be spent", %{alice: alice, bob: bob, state_alice_deposit: state} do state = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}]), @fee) |> success?() state |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, bob}], @eth, [{alice, 1}]), @fee) |> fail?(:unauthorized_spend) |> same?(state) |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}, {@blknum1, 0, 1, alice}], @eth, [{alice, 1}]), @fee) |> fail?(:unauthorized_spend) |> same?(state) state |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, [{alice, 8}]), @fee) |> success?() end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "can't spend spent", %{alice: alice, bob: bob, state_alice_deposit: state} do transactions = [ create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]), create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}]) ] for first <- transactions, second <- transactions do state |> Core.exec(first, @fee) |> success? |> Core.exec(second, @fee) |> fail?(:utxo_not_found) end end @tag fixtures: [:alice, :bob, :carol, :state_alice_deposit] test "can spend change and merge coins", %{ alice: alice, bob: bob, carol: carol, state_alice_deposit: state } do state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}]), @fee) |> success? |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}], @eth, [{carol, 5}]), @fee) |> success? |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{carol, 2}]), @fee) |> success? |> Core.exec(create_recovered([{@blknum1, 1, 0, carol}, {@blknum1, 2, 0, carol}], @eth, [{alice, 6}]), @fee) |> success? end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "can spend after block is formed", %{alice: alice, bob: bob, state_alice_deposit: state} do next_block_height = @blknum2 {:ok, {_, _}, state} = form_block_check(state) state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]), @fee) |> success? |> Core.exec(create_recovered([{next_block_height, 0, 0, bob}], @eth, [{bob, 6}]), @fee) |> success? end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "forming block doesn't unspend", %{alice: alice, bob: bob, state_alice_deposit: state} do recovered = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]) {:ok, {_, _}, state} = state |> Core.exec(recovered, @fee) |> success? |> form_block_check() Core.exec(state, recovered, @fee) |> fail?(:utxo_not_found) |> same?(state) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "can't double spend chained txs", %{alice: alice, bob: bob, state_alice_deposit: state} do recovered = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]) recovered2 = create_recovered([{1000, 0, 0, bob}], @eth, [{bob, 6}]) state |> Core.exec(recovered, @fee) |> success? |> Core.exec(recovered2, @fee) |> success? |> Core.exec(recovered2, @fee) |> fail?(:utxo_not_found) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "can't spend own output", %{bob: bob, state_alice_deposit: state} do # The transaction here is designed so that it would spend its own output. Sanity checking first {1000, true} = Core.get_status(state) recovered2 = create_recovered([{1000, 0, 0, bob}], @eth, [{bob, 6}]) state |> Core.exec(recovered2, @fee) |> fail?(:utxo_not_found) end @tag fixtures: [:stable_alice, :stable_bob, :state_stable_alice_deposit] test "forming block puts all transactions in a block", %{ stable_alice: alice, stable_bob: bob, state_stable_alice_deposit: state } do # odd number of transactions, just in case recovered_tx_1 = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}]) recovered_tx_2 = create_recovered([{@blknum1, 0, 0, bob}], @eth, [{alice, 3}, {bob, 2}]) recovered_tx_3 = create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 1}, {bob, 1}]) state = state |> Core.exec(recovered_tx_1, @fee) |> success? |> Core.exec(recovered_tx_2, @fee) |> success? |> Core.exec(recovered_tx_3, @fee) |> success? assert {:ok, {%Block{ transactions: [block_tx1, block_tx2, _third_tx], hash: block_hash, number: @blknum1 }, _}, _} = form_block_check(state) # precomputed fixed hash to check compliance with hashing algo assert <<240, 92, 32, 48, 163, 193, 58, 124, 248, 71>> <> _ = block_hash # Check that contents of the block can be recovered again to original txs assert {:ok, ^recovered_tx_1} = Transaction.Recovered.recover_from(block_tx1) assert {:ok, ^recovered_tx_2} = Transaction.Recovered.recover_from(block_tx2) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "forming block empty block after a non-empty block", %{ alice: alice, bob: bob, state_alice_deposit: state } do state = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]), @fee) |> success? {:ok, {_, _}, state} = form_block_check(state) expected_block = empty_block(@blknum2) assert {:ok, {^expected_block, _}, _} = form_block_check(state) end @tag fixtures: [:state_empty] test "no pending transactions at start (empty block, no db updates)", %{state_empty: state} do expected_block = empty_block() assert {:ok, {^expected_block, [{:put, :block, _}, {:put, :child_top_block_number, @blknum1}]}, _state} = form_block_check(state) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "spending produces db updates, that don't leak to next block", %{ alice: alice, bob: bob, state_alice_deposit: state } do # persistence tested in-depth elsewhere {:ok, {_, [_ | _]}, state} = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 2}]), @fee) |> success? |> form_block_check() assert {:ok, {_, [{:put, :block, _}, {:put, :child_top_block_number, @blknum2}]}, _} = form_block_check(state) end @tag fixtures: [:alice, :state_empty] test "depositing produces db updates, that don't leak to next block", %{ alice: alice, state_empty: state } do # persistence tested in-depth elsewhere assert {:ok, [_ | _], state} = Core.deposit([%{owner: alice.addr, currency: @eth, amount: 10, blknum: 1}], state) assert {:ok, {_, [{:put, :block, _}, {:put, :child_top_block_number, @blknum1}]}, _} = form_block_check(state) end @tag fixtures: [:alice, :state_alice_deposit, :state_empty] test "given exit infos in various forms translates to utxo positions", %{alice: alice, state_alice_deposit: state, state_empty: state_empty} do # this test checks whether all ways of calling `get_exiting_utxo_positions/2` translates # to given exiting utxo positions utxo_pos_exits = [Utxo.position(@blknum1, 0, 0), Utxo.position(@blknum1, 0, 1)] assert utxo_pos_exits == utxo_pos_exits |> Enum.map(&%{call_data: %{utxo_pos: Utxo.Position.encode(&1)}}) |> Core.extract_exiting_utxo_positions(state_empty) assert utxo_pos_exits == utxo_pos_exits |> Enum.map(&%{utxo_pos: Utxo.Position.encode(&1)}) |> Core.extract_exiting_utxo_positions(state_empty) assert utxo_pos_exits == utxo_pos_exits |> Enum.map(&Utxo.Position.encode/1) |> Core.extract_exiting_utxo_positions(state_empty) %Transaction.Recovered{tx_hash: tx_hash} = tx = create_recovered([{1, 0, 0, alice}], @eth, [{alice, 7}, {alice, 2}]) piggybacks = [ %{tx_hash: tx_hash, output_index: 0, omg_data: %{piggyback_type: :output}}, %{tx_hash: tx_hash, output_index: 1, omg_data: %{piggyback_type: :output}} ] state = state |> Core.exec(tx, @fee) |> success? assert utxo_pos_exits == Core.extract_exiting_utxo_positions(piggybacks, state) end @tag fixtures: [:alice, :state_alice_deposit] test "spends utxo validly when exiting", %{alice: alice, state_alice_deposit: state} do # persistence tested in-depth elsewhere state = state |> Core.exec( create_recovered([{1, 0, 0, alice}], @eth, [{alice, 6}, {alice, 3}]), @fee ) |> success? utxo_pos_exit_1 = Utxo.position(@blknum1, 0, 0) utxo_pos_exit_2 = Utxo.position(@blknum1, 0, 1) utxo_pos_exits = [utxo_pos_exit_1, utxo_pos_exit_2] assert {:ok, {[_ | _], {[^utxo_pos_exit_1, ^utxo_pos_exit_2], []}}, state_after_exit} = Core.exit_utxos(utxo_pos_exits, state) state_after_exit |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 6}]), @fee) |> fail?(:utxo_not_found) |> same?(state_after_exit) |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 2}]), @fee) |> fail?(:utxo_not_found) end @tag fixtures: [:alice, :state_empty] test "spends utxo from db when exiting", %{alice: alice, state_empty: state} do db_utxos = make_utxos([{@blknum1, 0, 0, alice, @eth, 6}, {@blknum1, 0, 1, alice, @eth, 3}]) extended_state = Core.with_utxos(state, db_utxos) utxo_pos_exit_1 = Utxo.position(@blknum1, 0, 0) utxo_pos_exit_2 = Utxo.position(@blknum1, 0, 1) utxo_pos_exits = [utxo_pos_exit_1, utxo_pos_exit_2] assert {:ok, {[_ | _], {[^utxo_pos_exit_1, ^utxo_pos_exit_2], []}}, state_after_exit} = Core.exit_utxos(utxo_pos_exits, extended_state) state_after_exit |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 6}]), @fee) |> fail?(:utxo_not_found) |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 2}]), @fee) |> fail?(:utxo_not_found) end @tag fixtures: [:alice, :state_alice_deposit] test "removed utxo after piggyback from available utxo", %{alice: alice, state_alice_deposit: state} do # persistence tested in-depth elsewhere tx = create_recovered([{1, 0, 0, alice}], @eth, [{alice, 7}, {alice, 2}]) state = state |> Core.exec(tx, @fee) |> success? utxo_pos_exits_in_flight = [%{call_data: %{in_flight_tx: Transaction.raw_txbytes(tx)}}] utxo_pos_exits_piggyback = [ %{tx_hash: Transaction.raw_txhash(tx), output_index: 0, omg_data: %{piggyback_type: :output}} ] expected_position = Utxo.position(@blknum1, 0, 0) assert {:ok, {[], {[], _}}, ^state} = utxo_pos_exits_in_flight |> Core.extract_exiting_utxo_positions(state) |> Core.exit_utxos(state) assert {:ok, {[_ | _], {[^expected_position], []}}, state_after_exit} = utxo_pos_exits_piggyback |> Core.extract_exiting_utxo_positions(state) |> Core.exit_utxos(state) state_after_exit |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 6}]), @fee) |> fail?(:utxo_not_found) |> same?(state_after_exit) |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 1}]), @fee) |> success? end @tag fixtures: [:alice, :state_alice_deposit] test "removed in-flight inputs from available utxo", %{alice: alice, state_alice_deposit: state} do # persistence tested in-depth elsewhere state = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 6}, {alice, 3}]), @fee) |> success? tx = create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 2}, {alice, 3}]) utxo_pos_exits_in_flight = [%{call_data: %{in_flight_tx: Transaction.raw_txbytes(tx)}}] expected_position = Utxo.position(@blknum1, 0, 0) exiting_utxos = Core.extract_exiting_utxo_positions(utxo_pos_exits_in_flight, state) assert {:ok, {[_ | _], {[^expected_position], _}}, state_after_exit} = Core.exit_utxos(exiting_utxos, state) state_after_exit |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 5}]), @fee) |> fail?(:utxo_not_found) |> same?(state_after_exit) |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 2}]), @fee) |> success? end @tag fixtures: [:state_empty] test "notifies about invalid utxo exiting", %{state_empty: state} do utxo_pos_exit_1 = Utxo.position(@blknum1, 0, 0) assert {:ok, {[], {[], [^utxo_pos_exit_1]}}, ^state} = Core.exit_utxos([utxo_pos_exit_1], state) end @tag fixtures: [:state_alice_deposit] test "ignores a piggyback of a non-included tx's outout", %{state_alice_deposit: state} do piggyback_event = %{tx_hash: 1, output_index: 0, omg_data: %{piggyback_type: :output}} assert {:ok, {[], {[], []}}, ^state} = [piggyback_event] |> Core.extract_exiting_utxo_positions(state) |> Core.exit_utxos(state) end @tag fixtures: [:state_alice_deposit] test "ignores on exiting, when input piggybacks are detected", %{state_alice_deposit: state} do piggyback_event = %{tx_hash: 1, output_index: 0, omg_data: %{piggyback_type: :input}} assert {:ok, {[], {[], []}}, ^state} = [piggyback_event] |> Core.extract_exiting_utxo_positions(state) |> Core.exit_utxos(state) end @tag fixtures: [:alice, :state_empty] test "tells if utxo exists", %{alice: alice, state_empty: state} do assert not Core.utxo_exists?(Utxo.position(1, 0, 0), state) state = state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) assert Core.utxo_exists?(Utxo.position(1, 0, 0), state) state = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]), @fee) |> success? assert not Core.utxo_exists?(Utxo.position(1, 0, 0), state) end @tag fixtures: [:alice, :state_empty] test "tells if utxo exists in db-extended state", %{alice: alice, state_empty: state} do state = Core.with_utxos(state, make_utxos([{1, 0, 0, alice, @eth, 10}])) assert Core.utxo_exists?(Utxo.position(1, 0, 0), state) state = state |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]), @fee) |> success? assert not Core.utxo_exists?(Utxo.position(1, 0, 0), state) end @tag fixtures: [:state_empty] test "Getting current block height on empty state", %{state_empty: state} do assert {@blknum1, _} = Core.get_status(state) end @tag fixtures: [:state_empty] test "Getting current block height with one formed block", %{state_empty: state} do {:ok, {_, _}, new_state} = form_block_check(state) assert {@blknum2, true} = Core.get_status(new_state) end @tag fixtures: [:alice, :state_empty] test "beginning of block changes when transactions executed and block formed", %{alice: alice, state_empty: state} do # at empty state it is at the beginning of the next block assert {@blknum1, true} = Core.get_status(state) # when we execute a tx it isn't at the beginning {:ok, _, state} = state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]), @fee) assert {@blknum1, false} = Core.get_status(state) # when a block has been newly formed it is at the beginning {:ok, _, state} = form_block_check(state) assert {@blknum2, true} = Core.get_status(state) end @tag fixtures: [:alice, :bob, :state_alice_deposit] test "Does not allow executing transactions with input utxos from the future", %{ alice: alice, bob: bob, state_alice_deposit: state } do future_deposit_blknum = @blknum1 + 1 state = do_deposit(state, alice, %{amount: 10, currency: @eth, blknum: future_deposit_blknum}) # input utxo blknum is greater than state's blknum state |> Core.exec(create_recovered([{future_deposit_blknum, 0, 0, alice}], @eth, [{bob, 5}, {alice, 4}]), @fee) |> fail?(:input_utxo_ahead_of_state) state |> Core.exec( create_recovered([{1, 0, 0, alice}, {future_deposit_blknum, 0, 0, alice}], @eth, [{bob, 5}, {alice, 4}]), @fee ) |> fail?(:input_utxo_ahead_of_state) # when non-existent input comes with a blknum of the current block fail with :utxo_not_found state |> Core.exec(create_recovered([{@blknum1, 1, 0, alice}], @eth, [{bob, 5}, {alice, 4}]), @fee) |> fail?(:utxo_not_found) end @tag fixtures: [:alice] test "no utxos that belong to address within the empty query result", %{alice: %{addr: alice}} do assert [] == Core.standard_exitable_utxos([], alice) end @tag fixtures: [:alice, :bob, :carol] test "getting user utxos from utxos_query_result", %{alice: alice, bob: bob, carol: carol} do output_type = OMG.Watcher.WireFormatTypes.output_type_for(:output_payment_v1) utxos_query_result = [ {{1000, 0, 0}, %{output: %{amount: 1, currency: @eth, owner: alice.addr, output_type: output_type}, creating_txhash: "nil"}}, {{2000, 1, 1}, %{output: %{amount: 2, currency: @eth, owner: bob.addr, output_type: output_type}, creating_txhash: "nil"}}, {{1000, 2, 0}, %{ output: %{amount: 3, currency: @not_eth, owner: alice.addr, output_type: output_type}, creating_txhash: "nil" }}, {{1000, 3, 1}, %{output: %{amount: 4, currency: @eth, owner: alice.addr, output_type: output_type}, creating_txhash: "nil"}}, {{1000, 4, 0}, %{output: %{amount: 5, currency: @eth, owner: bob.addr, output_type: output_type}, creating_txhash: "nil"}} ] assert [] == Core.standard_exitable_utxos(utxos_query_result, carol.addr) assert MapSet.equal?( MapSet.new([ %{blknum: 1000, txindex: 0, oindex: 0, otype: output_type, owner: alice.addr, currency: @eth, amount: 1}, %{ blknum: 1000, txindex: 2, oindex: 0, otype: output_type, owner: alice.addr, currency: @not_eth, amount: 3 }, %{blknum: 1000, txindex: 3, oindex: 1, otype: output_type, owner: alice.addr, currency: @eth, amount: 4} ]), MapSet.new(Core.standard_exitable_utxos(utxos_query_result, alice.addr)) ) assert Map.equal?( MapSet.new([ %{blknum: 1000, txindex: 4, oindex: 0, otype: output_type, owner: bob.addr, currency: @eth, amount: 5}, %{blknum: 2000, txindex: 1, oindex: 1, otype: output_type, owner: bob.addr, currency: @eth, amount: 2} ]), MapSet.new(Core.standard_exitable_utxos(utxos_query_result, bob.addr)) ) end describe "Automatic fees claiming" do setup do fee_claimer = OMG.Watcher.TestHelper.generate_entity() child_block_interval = Configuration.child_block_interval() {:ok, state_empty} = Core.extract_initial_state(0, child_block_interval, fee_claimer.addr) alice = OMG.Watcher.TestHelper.generate_entity() fees = %{@eth => [2]} state = state_empty |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 8}]), fees) |> success?() {:ok, [state: state, alice: alice, fees: fees, fee_claimer: fee_claimer, state_empty: state_empty]} end test "fee txs cannot be intermixed with payments", %{alice: alice, state: state, fees: fees} do fee_tx = create_recovered_fee_tx(1000, state.fee_claimer_address, @eth, 2) state # fees from 1st tx are available to claim |> Core.exec(fee_tx, fees) |> success?() # at this point no other payment can be processed |> Core.exec(create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 5}]), fees) |> fail?(:payments_rejected_during_fee_claiming) end test "cannot claim the same token twice", %{state: state, fees: fees} do fee_tx = create_recovered_fee_tx(1000, state.fee_claimer_address, @eth, 2) state # fees from 1st tx are available to claim |> Core.exec(fee_tx, fees) |> success?() # at this point no other payment can be processed |> Core.exec(fee_tx, fees) |> fail?(:surplus_in_token_not_collected) end test "cannot claim more than collected", %{state: state, fees: fees} do paid_fee = 2 fee_tx = create_recovered_fee_tx(1000, state.fee_claimer_address, @eth, paid_fee + 1) state |> Core.exec(fee_tx, fees) |> fail?(:claimed_and_collected_amounts_mismatch) end test "cannot claim less than collected", %{state: state, fees: fees} do paid_fee = 2 fee_tx = create_recovered_fee_tx(1000, state.fee_claimer_address, @eth, paid_fee - 1) state |> Core.exec(fee_tx, fees) |> fail?(:claimed_and_collected_amounts_mismatch) end test "no fees can be claimed after block is formed", %{state: state, fees: fees} do fee_tx = create_recovered_fee_tx(1000, state.fee_claimer_address, @eth, 2) # now it's possible to claim Eth fee (note: no state modification) state |> Core.exec(fee_tx, fees) |> success?() # block is formed without claiming fees {:ok, {_block, _dbupdates}, new_state} = form_block_check(state) # it's no longer possible to claim fees new_state |> Core.exec(fee_tx, fees) |> fail?(:surplus_in_token_not_collected) end test "fee is paid in one token only, many surpluses prohibited", %{alice: alice, state: state, fees: fees} do state |> do_deposit(alice, %{amount: 100, currency: @not_eth, blknum: 2}) |> Core.exec( create_recovered([{1000, 0, 0, alice}, {2, 0, 0, alice}], [{alice, @eth, 5}, {alice, @not_eth, 90}]), fees ) |> fail?(:multiple_potential_currency_fees) end test "zero surplus is not collectable", %{alice: alice, state: state, fees: fees} do state = state |> do_deposit(alice, %{amount: 100, currency: @not_eth, blknum: 2}) |> Core.exec( create_recovered([{1000, 0, 0, alice}, {2, 0, 0, alice}], [{alice, @eth, 6}, {alice, @not_eth, 100}]), fees ) |> success?() # not_eth currency is transferred in full - no surplus exists state |> Core.exec(create_recovered_fee_tx(1000, state.fee_claimer_address, @not_eth, 1), fees) |> fail?(:surplus_in_token_not_collected) end test "surpluses adding up for same-token-fees paid in a block", %{alice: alice, state: state, fees: fees} do state = state |> Core.exec(create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 6}]), fees) |> success?() # we can claim sum of the surpluses from 2 txs (one in setup & one above) collected = 2 + 2 state |> Core.exec(create_recovered_fee_tx(1000, state.fee_claimer_address, @eth, collected), fees) |> success?() end # this test takes ~26 seconds on my machine @tag slow: true @tag timeout: 60_000 * 3 test "long running full block test", %{alice: alice, state_empty: state, fees: fees} do Logger.warn("slow test is running, use --exclude slow to skip") maximum_block_size = 65_536 maximum_inputs_size = 4 eth_fee_rate = Enum.at(fees[@eth], 0) amount_for_fees = (1 + eth_fee_rate) * maximum_block_size available_after_1st_tx = amount_for_fees - eth_fee_rate # First tx is applied just to make below transactions generation easier state = state |> do_deposit(alice, %{amount: amount_for_fees, currency: @eth, blknum: 1}) |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, available_after_1st_tx}]), fees) |> success?() # we just send 1 payment and this reserves 1 spot for fee already_reserved = 2 # available space is block_size ntx_to_apply = maximum_block_size - (1 + maximum_inputs_size + already_reserved) {state, _} = Enum.reduce(0..ntx_to_apply, {state, available_after_1st_tx}, fn index, {curr_state, amount} -> new_amount = amount - eth_fee_rate new_state = curr_state |> Core.exec(create_recovered([{1000, index, 0, alice}], @eth, [{alice, new_amount}]), fees) |> success?() {new_state, new_amount} end) state # NOTE: I don't care about existing utxo actual position or available amount because block size is checked first |> Core.exec(create_recovered([{2, 0, 0, alice}], @eth, [{alice, 1_000_000}]), fees) |> fail?(:too_many_transactions_in_block) end end defp success?(result) do assert {:ok, _, state} = result state end defp fail?(result, expected_error) do assert {{:error, ^expected_error}, state} = result state end defp same?({{:error, _someerror}, state}, expected_state) do assert expected_state == state state end defp same?(state, expected_state) do assert expected_state == state state end defp empty_block(number \\ @blknum1) do %Block{transactions: [], hash: @empty_block_hash, number: number} end # used to check the invariants in form_block # use this throughout this test module instead of Core.form_block defp form_block_check(state) do {_, {block, db_updates}, _} = result = Core.form_block(state) # check if block returned and sent to db_updates is the same assert Enum.member?(db_updates, {:put, :block, Block.to_db_value(block)}) # check if that's the only db_update for block is_block_put? = fn {operation, type, _} -> operation == :put && type == :block end assert Enum.count(db_updates, is_block_put?) == 1 result end defp make_utxos(utxos) when is_list(utxos), do: Enum.into(utxos, %{}, &to_utxo_kv/1) defp to_utxo_kv({blknum, txindex, oindex, owner, currency, amount}), do: { Utxo.position(blknum, txindex, oindex), %Utxo{output: %Output{amount: amount, currency: currency, owner: owner.addr}} } end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/measurement_calculation_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.MeasurementCalculationTest do @moduledoc """ Testing functional behaviors. """ use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Eth.Encoding alias OMG.Output alias OMG.Watcher.State.Core alias OMG.Watcher.Utxo require Utxo @eth <<0::160>> @not_eth <<1::size(160)>> @tag fixtures: [:alice, :bob, :carol] test "calculate metrics from state", %{alice: alice, bob: bob, carol: carol} do utxos = %{ Utxo.position(2_000, 4076, 3) => %OMG.Watcher.Utxo{ output: %Output{amount: 700_000_000, currency: @eth, owner: alice} }, Utxo.position(1_000, 2559, 0) => %OMG.Watcher.Utxo{ output: %Output{amount: 111_111_111, currency: @not_eth, owner: alice} }, Utxo.position(8_000, 4854, 2) => %OMG.Watcher.Utxo{ output: %Output{amount: 77_000_000, currency: @eth, owner: bob} }, Utxo.position(7_000, 4057, 3) => %OMG.Watcher.Utxo{ output: %Output{amount: 222_222_222, currency: @not_eth, owner: carol} }, Utxo.position(7_000, 4057, 4) => %OMG.Watcher.Utxo{output: %{}} } assert MapSet.new(OMG.Watcher.State.MeasurementCalculation.calculate(%Core{utxos: utxos})) == MapSet.new([ {:unique_users, 3}, {:balance, 777_000_000, "currency:#{Encoding.to_hex(@eth)}"}, {:balance, 333_333_333, "currency:#{Encoding.to_hex(@not_eth)}"} ]) end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/persistence_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.PersistenceTest do @moduledoc """ Test focused on the persistence bits of `OMG.Watcher.State.Core` """ use ExUnitFixtures use ExUnit.Case, async: false import OMG.Watcher.TestHelper require OMG.Watcher.Utxo require Logger alias Ecto.Adapters.SQL.Sandbox alias OMG.Eth.Configuration alias OMG.Watcher.Block alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo alias Support.WaitFor @fee_claimer_address Base.decode16!("DEAD000000000000000000000000000000000000") @eth <<0::160>> @interval Configuration.child_block_interval() @blknum1 @interval setup do db_path = Briefly.create!(directory: true) Application.put_env(:omg_db, :path, db_path, persistent: true) :ok = OMG.DB.init() {:ok, started_apps} = Application.ensure_all_started(:omg_db) {:ok, bus_apps} = Application.ensure_all_started(:omg_bus) metrics_collection_interval = 60_000 {:ok, _} = Supervisor.start_link( [ {OMG.Watcher.State, [ fee_claimer_address: @fee_claimer_address, child_block_interval: @interval, metrics_collection_interval: metrics_collection_interval ]} ], strategy: :one_for_one ) Application.ensure_all_started(:postgrex) Application.ensure_all_started(:spandex_ecto) Application.ensure_all_started(:ecto) {:ok, _} = Supervisor.start_link( [ %{ id: OMG.WatcherInfo.DB.Repo, start: {OMG.WatcherInfo.DB.Repo, :start_link, []}, type: :supervisor } ], strategy: :one_for_one, name: WatcherInfo.Supervisor ) :ok = Sandbox.checkout(OMG.WatcherInfo.DB.Repo) Sandbox.mode(OMG.WatcherInfo.DB.Repo, {:shared, self()}) on_exit(fn -> Application.put_env(:omg_db, :path, nil) (started_apps ++ bus_apps) |> Enum.reverse() |> Enum.map(fn app -> :ok = Application.stop(app) end) end) {:ok, %{}} end @tag fixtures: [:alice, :bob] test "persists deposits and utxo is available after restart", %{alice: alice, bob: bob} do [ %{owner: bob, currency: @eth, amount: 10, blknum: 1}, %{owner: alice, currency: @eth, amount: 20, blknum: 2} ] |> persist_deposit() assert OMG.Watcher.State.utxo_exists?(Utxo.position(2, 0, 0)) :ok = restart_state() assert OMG.Watcher.State.utxo_exists?(Utxo.position(1, 0, 0)) assert OMG.Watcher.State.utxo_exists?(Utxo.position(2, 0, 0)) end @tag fixtures: [:alice] test "utxos are persisted", %{alice: alice} do [%{owner: alice, currency: @eth, amount: 20, blknum: 1}] |> persist_deposit() |> exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 19}])) |> persist_form() assert not OMG.Watcher.State.utxo_exists?(Utxo.position(1, 0, 0)) assert OMG.Watcher.State.utxo_exists?(Utxo.position(@blknum1, 0, 0)) end @tag fixtures: [:alice, :bob] test "utxos are available after restart", %{alice: alice, bob: bob} do [%{owner: alice, currency: @eth, amount: 20, blknum: 1}] |> persist_deposit() |> exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 17}, {alice, 2}])) |> exec(create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, [{bob, 18}])) |> persist_form() :ok = restart_state() assert not OMG.Watcher.State.utxo_exists?(Utxo.position(@blknum1, 0, 0)) assert not OMG.Watcher.State.utxo_exists?(Utxo.position(@blknum1, 0, 1)) assert OMG.Watcher.State.utxo_exists?(Utxo.position(@blknum1, 1, 0)) end @tag fixtures: [:alice, :bob] test "cannot double spend from the transactions within the same block", %{alice: alice, bob: bob} do :ok = persist_deposit([%{owner: alice, currency: @eth, amount: 10, blknum: 1}]) # after the restart newly up state won't have deposit's utxo in memory :ok = restart_state() assert :ok == exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}])) assert :utxo_not_found == exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}])) end @tag fixtures: [:alice] test "blocks and spends are persisted", %{alice: alice} do tx = create_recovered([{1, 0, 0, alice}], @eth, [{alice, 19}]) [%{owner: alice, currency: @eth, amount: 20, blknum: 1}] |> persist_deposit() |> exec(tx) |> persist_form() assert {:ok, [hash]} = OMG.DB.block_hashes([@blknum1]) :ok = restart_state() assert {:ok, [db_block]} = OMG.DB.blocks([hash]) %Block{number: @blknum1, transactions: [payment_tx], hash: ^hash} = Block.from_db_value(db_block) assert {:ok, tx} == Transaction.Recovered.recover_from(payment_tx) assert {:ok, 1000} == tx |> Transaction.get_inputs() |> hd() |> Utxo.Position.to_input_db_key() |> OMG.DB.spent_blknum() end @tag fixtures: [:alice] test "exiting utxo is deleted from state", %{alice: alice} do utxo_positions = [ Utxo.position(@blknum1, 0, 0), Utxo.position(@blknum1, 0, 1) ] [%{owner: alice, currency: @eth, amount: 20, blknum: 1}] |> persist_deposit() |> exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 19}])) |> persist_form() |> persist_exit_utxos(utxo_positions) :ok = restart_state() assert not OMG.Watcher.State.utxo_exists?(Utxo.position(@blknum1, 0, 0)) assert not OMG.Watcher.State.utxo_exists?(Utxo.position(@blknum1, 0, 1)) end @tag fixtures: [:alice] test "cannot spend just exited utxo", %{alice: alice} do :ok = persist_deposit([%{owner: alice, currency: @eth, amount: 20, blknum: 1}]) {:ok, _, _} = OMG.Watcher.State.exit_utxos([Utxo.position(1, 0, 0)]) # exit db_updates won't get persisted yet, but alice tries to spent it immediately assert exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 20}])) == :utxo_not_found # retry above with empty in-memory utxoset :ok = restart_state() {:ok, _, _} = OMG.Watcher.State.exit_utxos([Utxo.position(1, 0, 0)]) assert exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 20}])) == :utxo_not_found end defp persist_deposit(deposits) do {:ok, db_updates} = deposits |> make_deposits() |> OMG.Watcher.State.deposit() :ok = OMG.DB.multi_update(db_updates) end defp persist_form(:ok), do: persist_form() defp persist_form() do state = :sys.get_state(OMG.Watcher.State) {:ok, {_block, db_updates}, new_state} = OMG.Watcher.State.Core.form_block(state) :ok = OMG.DB.multi_update(db_updates) :sys.replace_state(OMG.Watcher.State, fn _ -> new_state end) :ok end defp exec(:ok, tx), do: exec(tx) defp exec(tx) do fee = %{@eth => [1]} case OMG.Watcher.State.exec(tx, fee) do {:ok, _} -> :ok {:error, reason} -> reason end end defp persist_exit_utxos(:ok, exit_infos), do: persist_exit_utxos(exit_infos) defp persist_exit_utxos(exit_infos) do {:ok, db_updates, _} = OMG.Watcher.State.exit_utxos(exit_infos) :ok = OMG.DB.multi_update(db_updates) end defp make_deposits(list) do Enum.map(list, fn %{owner: owner, currency: currency, amount: amount, blknum: blknum} -> %{ root_chain_txhash: <>, log_index: 0, owner: owner.addr, currency: currency, amount: amount, blknum: blknum, eth_height: 1 } end) end defp restart_state() do GenServer.stop(OMG.Watcher.State) WaitFor.ok(fn -> if(GenServer.whereis(OMG.Watcher.State), do: :ok) end) _ = Logger.info("OMG.Watcher.State restarted") :ok end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/transaction/fee_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.FeeTest do @moduledoc false use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.State.Transaction @eth <<0::160>> @other_token <<127::160>> setup do {:ok, [alice: OMG.Watcher.TestHelper.generate_entity()]} end describe "new/2" do test "can be encoded to binary form and back", %{alice: owner} do fee_tx = Transaction.Fee.new(1000, {owner.addr, @eth, 1551}) rlp_form = Transaction.raw_txbytes(fee_tx) assert fee_tx == Transaction.decode!(rlp_form) end test "hash can be computed with protocol implementation", %{alice: owner} do fee_tx = Transaction.Fee.new(1000, {owner.addr, @eth, 1551}) fee_txhash = Transaction.raw_txhash(fee_tx) assert <<_::256>> = fee_txhash assert Transaction.raw_txhash(Transaction.Fee.new(1000, {owner.addr, @eth, 1551})) == fee_txhash assert Transaction.raw_txhash(Transaction.Fee.new(1001, {owner.addr, @eth, 1551})) != fee_txhash assert Transaction.raw_txhash(Transaction.Fee.new(1000, {owner.addr, @other_token, 1551})) != fee_txhash end test "fee-tx should be recoverable from binary form", %{alice: owner} do fee_tx = Transaction.Fee.new(1000, {owner.addr, @eth, 1551}) tx_rlp = Transaction.Signed.encode(%Transaction.Signed{raw_tx: fee_tx, sigs: []}) assert {:ok, %Transaction.Recovered{ signed_tx: %Transaction.Signed{raw_tx: ^fee_tx} }} = Transaction.Recovered.recover_from(tx_rlp) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/transaction/recovered_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.RecoveredTest do @moduledoc """ This test the public-most APIs regarging the transaction, being mainly centered around: - recovery and stateless validation done in `Transaction.Recovered` - usability of recovered transactions in `OMG.Watcher.State` - detecting and reporting invalidly encoded, malformed, illegal transactions """ use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.DevCrypto alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias OMG.Watcher.WireFormatTypes require Utxo @payment_tx_type WireFormatTypes.tx_type_for(:tx_payment_v1) @payment_output_type WireFormatTypes.output_type_for(:output_payment_v1) @zero_address <<0::160>> @eth <<0::160>> @empty_signature <<0::size(520)>> describe "APIs used by the `OMG.Watcher.State.exec/1`" do @tag fixtures: [:alice, :state_alice_deposit, :bob] test "using created transaction in child chain", %{alice: alice, bob: bob, state_alice_deposit: state} do state = TestHelper.do_deposit(state, alice, %{amount: 10, currency: @eth, blknum: 2}) payment = Transaction.Payment.new([{1, 0, 0}, {2, 0, 0}], [{bob.addr, @eth, 19}]) payment |> DevCrypto.sign([alice.priv, alice.priv]) |> assert_tx_usable(state) end @tag fixtures: [:alice, :state_alice_deposit, :bob] test "using created transaction with one input in child chain", %{ alice: alice, bob: bob, state_alice_deposit: state } do payment = Transaction.Payment.new([{1, 0, 0}], [{bob.addr, @eth, 9}]) payment |> DevCrypto.sign([alice.priv]) |> assert_tx_usable(state) end @tag fixtures: [:alice, :bob] test "recovering spenders: different signers, one output", %{alice: alice, bob: bob} do {:ok, recovered} = [{3000, 0, 0}, {3000, 0, 1}] |> Transaction.Payment.new([{alice.addr, @eth, 10}]) |> DevCrypto.sign([bob.priv, alice.priv]) |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from() assert recovered.witnesses == %{0 => bob.addr, 1 => alice.addr} end @tag fixtures: [:alice, :bob] test "signed transaction is valid in various empty input/output combinations", %{ alice: alice, bob: bob } do transaction_list = [ {[], [{alice, @eth, 7}]}, {[{1, 2, 3, alice}], [{alice, @eth, 7}]}, {[{1, 2, 3, alice}], [{alice, @eth, 7}, {bob, @eth, 3}]}, {[{1, 2, 3, alice}, {2, 3, 4, bob}], [{alice, @eth, 7}, {bob, @eth, 3}]}, {[{1, 2, 3, alice}, {2, 3, 4, bob}, {2, 3, 5, bob}], [{alice, @eth, 7}, {bob, @eth, 3}]}, {[{1, 2, 3, alice}, {2, 3, 4, bob}, {2, 3, 5, bob}], [{alice, @eth, 7}, {bob, @eth, 3}, {bob, @eth, 3}]}, {[{1, 2, 3, alice}, {2, 3, 1, alice}, {2, 3, 2, bob}, {3, 3, 4, bob}], [{alice, @eth, 7}, {alice, @eth, 3}, {bob, @eth, 7}, {bob, @eth, 3}]} ] Enum.map(transaction_list, ¶metrized_tester/1) end end describe "encoding/decoding is done properly" do @tag fixtures: [:alice] test "decoding malformed signed payment transaction", %{alice: alice} do payment = Transaction.Payment.new([{1, 0, 0}, {2, 0, 0}], [{alice.addr, @eth, 12}]) tx = DevCrypto.sign(payment, [alice.priv, alice.priv]) %Transaction.Signed{sigs: sigs} = tx [_payment_marker, inputs, outputs, _txdata, _metadata] = tx |> Transaction.raw_txbytes() |> ExRLP.decode() # sanity assert {:ok, _} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, 0, <<0::256>>]) ) assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(<<192>>) assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(<<0x80>>) assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(<<>>) assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(ExRLP.encode(23)) assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, 1])) # looks like a payment transaction but type points to a `Transaction.Fee`, hence malformed not unrecognized assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, 3, inputs, outputs, 0, <<0::256>>])) assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, 1, outputs, 0, <<0::256>>])) assert {:error, :unrecognized_transaction_type} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, ["bad"], inputs, outputs, 0, <<0::256>>])) assert {:error, :unrecognized_transaction_type} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, []])) assert {:error, :unrecognized_transaction_type} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, 234_567, inputs, outputs, 0, <<0::256>>])) assert {:error, :malformed_witnesses} == Transaction.Recovered.recover_from( ExRLP.encode([ [ExPlasma.payment_v1(), ExPlasma.payment_v1()], @payment_tx_type, inputs, outputs, 0, <<0::256>> ]) ) assert {:error, :malformed_witnesses} == Transaction.Recovered.recover_from( ExRLP.encode([ExPlasma.payment_v1(), @payment_tx_type, inputs, outputs, 0, <<0::256>>]) ) assert {:error, :malformed_witnesses} == Transaction.Recovered.recover_from( ExRLP.encode([[sigs], @payment_tx_type, inputs, outputs, 0, <<0::256>>]) ) assert {:error, :malformed_inputs} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, @payment_tx_type, 42, outputs, 0, <<0::256>>])) assert {:error, :malformed_inputs} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, [[1, 2]], outputs, 0, <<0::256>>]) ) assert {:error, :malformed_inputs} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, [[1, 2, 'a']], outputs, 0, <<0::256>>]) ) assert {:error, :malformed_outputs} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, @payment_tx_type, inputs, 42, 0, <<0::256>>])) assert {:error, :malformed_outputs} = Transaction.Recovered.recover_from( ExRLP.encode([ sigs, @payment_tx_type, inputs, [[@payment_output_type, alice.addr, alice.addr, 1]], 0, <<0::256>> ]) ) assert {:error, :malformed_outputs} = Transaction.Recovered.recover_from( ExRLP.encode([ sigs, @payment_tx_type, inputs, [[@payment_output_type, [alice.addr, alice.addr]]], 0, <<0::256>> ]) ) assert {:error, :malformed_outputs} = Transaction.Recovered.recover_from( ExRLP.encode([ sigs, @payment_tx_type, inputs, [[@payment_output_type, [alice.addr, alice.addr, 'a']]], 0, <<0::256>> ]) ) assert {:error, :unrecognized_output_type} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, [[<<232>>, [alice.addr, alice.addr, 1]]], 0, <<0::256>>]) ) assert {:error, :malformed_tx_data} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, 1, <<0::256>>]) ) assert {:error, :malformed_uint256} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, [<<6>>], <<0::256>>]) ) assert {:error, :leading_zeros_in_encoded_uint} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, <<0::256>>, <<0::256>>]) ) assert {:error, :leading_zeros_in_encoded_uint} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, <<1::256>>, <<0::256>>]) ) assert {:error, :malformed_metadata} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, 0, ""])) assert {:error, :malformed_metadata} = Transaction.Recovered.recover_from(ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, 0, []])) assert {:error, :malformed_metadata} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, 0, <<1::224>>]) ) assert {:error, :malformed_metadata} = Transaction.Recovered.recover_from( ExRLP.encode([sigs, @payment_tx_type, inputs, outputs, 0, <<2::288>>]) ) end @tag fixtures: [:alice, :bob] test "rlp encoding of a transaction is corrupt", %{alice: alice, bob: bob} do encoded_signed_tx = TestHelper.create_encoded([{1, 2, 3, alice}, {2, 3, 4, bob}], @eth, [{alice, 7}]) malformed2 = "A" <> encoded_signed_tx assert {:error, :malformed_transaction_rlp} = Transaction.Recovered.recover_from(malformed2) <<_, malformed3::binary>> = encoded_signed_tx assert {:error, :malformed_transaction_rlp} = Transaction.Recovered.recover_from(malformed3) cropped_size = byte_size(encoded_signed_tx) - 1 <> = encoded_signed_tx assert {:error, :malformed_transaction_rlp} = Transaction.Recovered.recover_from(malformed4) end @tag fixtures: [:alice, :bob] test "address in encoded transaction malformed", %{alice: alice, bob: bob} do malformed_alice = %{addr: "0x00000000000000000"} malformed_eth = "0x00000000000000000" malformed_signed1 = TestHelper.create_signed([{1, 2, 3, alice}, {2, 3, 4, bob}], @eth, [{malformed_alice, 7}]) malformed_signed2 = TestHelper.create_signed([{1, 2, 3, alice}, {2, 3, 4, bob}], malformed_eth, [{alice, 7}]) malformed_signed3 = TestHelper.create_signed([{1, 2, 3, alice}, {2, 3, 4, bob}], @eth, [{alice, 7}, {malformed_alice, 3}]) malformed1 = Transaction.Signed.encode(malformed_signed1) malformed2 = Transaction.Signed.encode(malformed_signed2) malformed3 = Transaction.Signed.encode(malformed_signed3) assert {:error, :malformed_address} = Transaction.Recovered.recover_from(malformed1) assert {:error, :malformed_address} = Transaction.Recovered.recover_from(malformed2) assert {:error, :malformed_address} = Transaction.Recovered.recover_from(malformed3) end @tag fixtures: [:alice] test "transactions with corrupt signatures don't do harm - one signature", %{alice: alice} do full_signed_tx = TestHelper.create_signed([{1, 2, 3, alice}], @eth, [{alice, 7}]) assert {:error, :signature_corrupt} == %Transaction.Signed{full_signed_tx | sigs: [<<1::size(520)>>]} |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from() end @tag fixtures: [:alice] test "transactions with corrupt signatures don't do harm - one of many signatures", %{alice: alice} do full_signed_tx = TestHelper.create_signed([{1, 2, 3, alice}, {1, 2, 4, alice}], @eth, [{alice, 7}]) %Transaction.Signed{sigs: [sig1, sig2 | _]} = full_signed_tx assert {:error, :signature_corrupt} == %Transaction.Signed{full_signed_tx | sigs: [sig1, <<1::size(520)>>]} |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from() assert {:error, :signature_corrupt} == %Transaction.Signed{full_signed_tx | sigs: [<<1::size(520)>>, sig2]} |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from() end end describe "stateless validity critical to the ledger is checked" do @tag fixtures: [:alice] test "transaction must have distinct inputs", %{alice: alice} do duplicate_inputs = TestHelper.create_encoded([{1, 2, 3, alice}, {1, 2, 3, alice}], @eth, [{alice, 7}]) assert {:error, :duplicate_inputs} = Transaction.Recovered.recover_from(duplicate_inputs) end end describe "formal protocol rules are enforced" do test "Decoding transaction with a zero input fails" do inputs_index_in_rlp = 2 assert {:error, :malformed_inputs} = good_tx_rlp_items() |> List.replace_at(inputs_index_in_rlp, [<<0::256>>, <<1::256>>]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end @tag fixtures: [:alice] test "Decoding deposit transaction without inputs is successful", %{alice: alice} do encoded_transaction = TestHelper.create_encoded([], @eth, [{alice, 100}]) assert {:ok, _} = Transaction.Recovered.recover_from(encoded_transaction) end @tag fixtures: [:alice] test "Decoding transaction with zero blknum works as long as input non-zero", %{alice: alice} do encoded_transaction = TestHelper.create_encoded([{0, 0, 1, alice}], [{alice, @zero_address, 10}]) assert {:ok, _} = Transaction.Recovered.recover_from(encoded_transaction) end test "Decoding transaction with list as transaction type fails" do tx_type_index_in_rlp = 1 assert {:error, :unrecognized_transaction_type} = good_tx_rlp_items() |> List.replace_at(tx_type_index_in_rlp, [ExPlasma.payment_v1()]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with too many inputs fails" do inputs_index_in_rlp = 2 [input | _] = Enum.at(good_tx_rlp_items(), inputs_index_in_rlp) assert {:error, :too_many_inputs} = good_tx_rlp_items() |> List.replace_at(inputs_index_in_rlp, List.duplicate(input, 5)) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with shorter input fails" do inputs_index_in_rlp = 2 [input | _] = Enum.at(good_tx_rlp_items(), inputs_index_in_rlp) assert {:error, :malformed_inputs} = good_tx_rlp_items() |> List.replace_at(inputs_index_in_rlp, [binary_part(input, 1, 31)]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with shorter/longer/malformed address fails" do outputs_index_in_rlp = 3 [[type, [owner, currency, amount]]] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) checker = fn bad_output -> assert {:error, :malformed_address} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, [bad_output]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end transaction_list = [ [type, [binary_part(owner, 1, 19), currency, amount]], [type, [binary_part(owner, 0, 19), currency, amount]], [type, [owner, binary_part(currency, 1, 19), amount]], [type, [owner, binary_part(currency, 0, 19), amount]], [type, [owner, <<1>>, amount]], [type, [<<1>>, currency, amount]], [type, [owner, "", amount]], [type, ["", currency, amount]], [type, [<<1>>, currency, amount]], # address-like (21 bytes encoded) items being lists [type, [owner, [<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>, <<3>>, <<1>>, <<1>>], amount]], [type, [[<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>, <<3>>, <<1>>, <<1>>], currency, amount]] ] Enum.map(transaction_list, checker) end @tag fixtures: [:alice] test "Decoding transaction with zero amount in outputs fails ", %{alice: alice} do encoded_transaction = TestHelper.create_encoded([{1000, 0, 0, alice}], @eth, [{alice, 0}, {alice, 100}]) assert {:error, :amount_cant_be_zero} = Transaction.Recovered.recover_from(encoded_transaction) end @tag fixtures: [:alice] test "Decoding transaction with zero output guard in outputs fails ", %{alice: alice} do no_account = %{addr: @zero_address} assert {:error, :output_guard_cant_be_zero} = Transaction.Recovered.recover_from( TestHelper.create_encoded([{1000, 0, 0, alice}], @eth, [{no_account, 10}, {alice, 100}]) ) end @tag fixtures: [:alice] test "Decoding transaction with zero output fails", %{alice: alice} do no_account = %{addr: @zero_address} assert {:error, :output_guard_cant_be_zero} = Transaction.Recovered.recover_from( TestHelper.create_encoded([{1000, 0, 0, alice}], [{no_account, @zero_address, 0}]) ) end test "Decoding transaction with zero output type fails" do outputs_index_in_rlp = 3 [[_type, output_fields]] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) bad_output = [0, output_fields] assert {:error, :unrecognized_output_type} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, [bad_output]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with list as output type fails" do outputs_index_in_rlp = 3 [[_type, output_fields]] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) bad_output = [[<<1>>], output_fields] assert {:error, :unrecognized_output_type} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, [bad_output]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with malformed output fails" do outputs_index_in_rlp = 3 [output] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) assert {:error, :malformed_outputs} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, output) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with leading-zeros in output amount fails" do outputs_index_in_rlp = 3 [[type, [owner, currency, _amount]]] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) checker = fn bad_amount -> assert {:error, :leading_zeros_in_encoded_uint} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, [[type, [owner, currency, bad_amount]]]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end Enum.map([<<1::288>>, <<1::224>>, <<1::64>>, <<0, 1>>], checker) end test "Decoding transaction with not-a-uint256 in output amount fails" do outputs_index_in_rlp = 3 [[type, [owner, currency, _amount]]] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) assert {:error, :malformed_outputs} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, [[type, [owner, currency, [<<6>>]]]]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with a bad RLP (non-optimal encoding) fails" do # NOTE: it's hard to build a bad RLP encoding of a full transaction, so just check if invalidity of RLP is a # specific error. This is a regression test for the underlying RLP implementation for # https://github.com/mana-ethereum/ex_rlp/issues/26 # sanity check - correct RLP but nonsense assert {:error, :malformed_transaction} = Transaction.Recovered.recover_from(<<10>>) # non-optimally encoded `<<10>>` in RLP, a specific error is returned assert {:error, :malformed_transaction_rlp} = Transaction.Recovered.recover_from(<<129, 10>>) end test "Decoding transaction with >32 bytes in output amount fails" do outputs_index_in_rlp = 3 [[type, [owner, currency, _amount]]] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) bad_amount = :binary.copy(<<1>>, 33) assert {:error, :encoded_uint_too_big} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, [[type, [owner, currency, bad_amount]]]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction a list in output amount fails" do outputs_index_in_rlp = 3 [[type, [owner, currency, _amount]]] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) bad_amount = [<<1>>] assert {:error, :malformed_outputs} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, [[type, [owner, currency, bad_amount]]]) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end test "Decoding transaction with too many outputs fails" do outputs_index_in_rlp = 3 [output] = Enum.at(good_tx_rlp_items(), outputs_index_in_rlp) assert {:error, :too_many_outputs} = good_tx_rlp_items() |> List.replace_at(outputs_index_in_rlp, List.duplicate(output, 5)) |> ExRLP.encode() |> Transaction.Recovered.recover_from() end @tag fixtures: [:alice] test "Decoding transaction without outputs fails", %{alice: alice} do assert {:error, :empty_outputs} = Transaction.Recovered.recover_from(TestHelper.create_encoded([{1000, 0, 0, alice}], @eth, [])) end @tag fixtures: [:alice, :bob] test "transaction is not allowed to have input and empty sigs", %{alice: alice} do tx = TestHelper.create_signed([{1, 2, 3, alice}, {2, 3, 4, alice}], @eth, [{alice, 7}]) tx_no_sigs = %{tx | sigs: [@empty_signature, @empty_signature]} tx_hash = Transaction.Signed.encode(tx_no_sigs) assert {:error, :missing_signature} == Transaction.Recovered.recover_from(tx_hash) end @tag fixtures: [:alice] test "transactions with superfluous signatures don't do harm", %{alice: alice} do full_signed_tx = TestHelper.create_signed([{1, 2, 3, alice}], @eth, [{alice, 7}]) %Transaction.Signed{sigs: [sig1 | _]} = full_signed_tx assert {:error, :superfluous_signature} == %Transaction.Signed{full_signed_tx | sigs: [sig1, sig1]} |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from() end end defp assert_tx_usable(signed, state_core) do fee = %{@eth => [1]} {:ok, transaction} = signed |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from() assert {:ok, {_, _, _}, _state} = Core.exec(state_core, transaction, fee) end defp parametrized_tester({inputs, outputs}) do tx = TestHelper.create_signed(inputs, outputs) encoded_signed_tx = Transaction.Signed.encode(tx) witnesses = inputs |> Enum.filter(fn {_, _, _, %{addr: addr}} -> addr != nil end) |> Enum.map(fn {_, _, _, spender} -> spender.addr end) |> Enum.with_index() |> Enum.into(%{}, fn {witness, index} -> {index, witness} end) assert {:ok, %Transaction.Recovered{ signed_tx: ^tx, witnesses: ^witnesses }} = Transaction.Recovered.recover_from(encoded_signed_tx) end # provides one with RLP items (ready for `ExRLP.encode/1`) representing a valid transaction defp good_tx_rlp_items() do alice = TestHelper.generate_entity() good_tx_rlp_items = TestHelper.create_encoded([{1000, 0, 0, alice}, {1000, 0, 1, alice}], [{alice, @eth, 10}]) |> ExRLP.decode() # sanity check just in case assert {:ok, _} = good_tx_rlp_items |> ExRLP.encode() |> Transaction.Recovered.recover_from() good_tx_rlp_items end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/transaction/witness_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.Transaction.WitnessTest do @moduledoc false use ExUnit.Case, async: true alias OMG.Watcher.State.Transaction.Witness describe "valid?/1" do test "returns true when is binary and 65 bytes long" do assert Witness.valid?(<<0::520>>) end test "returns false when not a binary" do refute Witness.valid?([<<0>>]) end test "returns false when not 65 bytes long" do refute Witness.valid?(<<0>>) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/transaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.TransactionTest do @moduledoc """ This test the public-most APIs regarging the transaction, being mainly centered around: - creation and encoding of raw transactions - some basic checks of internal APIs used elsewhere - getting inputs/outputs, spend authorization, hashing, encoding """ use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo @eth <<0::160>> @payment_output_type OMG.Watcher.WireFormatTypes.output_type_for(:output_payment_v1) @utxo_positions [{20, 42, 1}, {2, 21, 0}, {1000, 0, 0}, {10_001, 0, 0}] @transaction Transaction.Payment.new( [{1, 1, 0}, {1, 2, 1}], [{"alicealicealicealice", @eth, 1}, {"carolcarolcarolcarol", @eth, 2}], <<0::256>> ) test "create transaction with metadata" do tx_with_metadata = Transaction.Payment.new(@utxo_positions, [{"Joe Black", @eth, 53}], <<0::256>>) tx_without_metadata = Transaction.Payment.new(@utxo_positions, [{"Joe Black", @eth, 53}]) assert Transaction.raw_txhash(tx_with_metadata) == Transaction.raw_txhash(tx_without_metadata) assert byte_size(Transaction.raw_txbytes(tx_with_metadata)) == byte_size(Transaction.raw_txbytes(tx_without_metadata)) end test "raw transaction hash is invariant" do assert <<21, 94, 181, 22, 125, 2, 47, 124, 113>> <> _ = Transaction.raw_txhash(@transaction) end test "create transaction with different number inputs and outputs" do check_input1 = Utxo.position(20, 42, 1) output1 = {"Joe Black", @eth, 99} check_output2 = %{amount: 99, currency: @eth, owner: "Joe Black", output_type: @payment_output_type} # 1 - input, 1 - output tx1_1 = Transaction.Payment.new([hd(@utxo_positions)], [output1]) assert 1 == tx1_1 |> Transaction.get_inputs() |> length() assert 1 == tx1_1 |> Transaction.get_outputs() |> length() assert [^check_input1 | _] = Transaction.get_inputs(tx1_1) assert ^check_output2 = Transaction.get_outputs(tx1_1) |> hd() |> Map.from_struct() # 4 - input, 4 - outputs tx4_4 = Transaction.Payment.new(@utxo_positions, [output1, {"J", @eth, 929}, {"J", @eth, 929}, {"J", @eth, 199}]) assert 4 == tx4_4 |> Transaction.get_inputs() |> length() assert 4 == tx4_4 |> Transaction.get_outputs() |> length() assert [^check_input1 | _] = Transaction.get_inputs(tx4_4) assert ^check_output2 = Transaction.get_outputs(tx4_4) |> hd() |> Map.from_struct() end test "Decode raw transaction, a low level encode/decode parity check" do {:ok, decoded} = @transaction |> Transaction.raw_txbytes() |> Transaction.decode() assert decoded == @transaction assert decoded == @transaction |> Transaction.raw_txbytes() |> Transaction.decode!() end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state/utxo_set_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.State.UtxoSetTest do @moduledoc """ Low-level unit test of `OMG.Watcher.State.UtxoSet` """ use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.State.Transaction alias OMG.Watcher.State.UtxoSet alias OMG.Watcher.Utxo import OMG.Watcher.TestHelper, only: [generate_entity: 0, create_recovered: 2] require Utxo @eth <<0::160>> setup do [alice, bob] = 1..2 |> Enum.map(fn _ -> generate_entity() end) transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, bob}], [{bob, @eth, 1}, {bob, @eth, 2}]) inputs = Transaction.get_inputs(transaction) outputs = Transaction.get_outputs(transaction) db_query_result = inputs |> Enum.zip(outputs) |> Enum.map(fn {input, output} -> {input, %Utxo{output: output, creating_txhash: <<1>>}} end) |> Enum.map(fn {input, utxo} -> {Utxo.Position.to_input_db_key(input), Utxo.to_db_value(utxo)} end) utxo_set = UtxoSet.init(db_query_result) {:ok, %{alice: alice, bob: bob, inputs: inputs, outputs: outputs, db_query_result: db_query_result, utxo_set: utxo_set}} end describe "init/1" do test "can initialize empty", %{inputs: inputs} do assert {:error, :utxo_not_found} = [] |> UtxoSet.init() |> UtxoSet.get_by_inputs(inputs) end test "can initialize with db query result", %{inputs: inputs, outputs: outputs, db_query_result: db_query_result} do assert {:ok, ^outputs} = db_query_result |> UtxoSet.init() |> UtxoSet.get_by_inputs(inputs) end test "ignores not_founds from db query results", %{inputs: inputs, outputs: outputs, db_query_result: db_query_result} do db_results_with_missings = Enum.intersperse(db_query_result, :not_found) assert {:ok, ^outputs} = db_results_with_missings |> UtxoSet.init() |> UtxoSet.get_by_inputs(inputs) end end describe "get_by_inputs/2" do test "will get all by inputs in input order", %{inputs: inputs, utxo_set: utxo_set} do {:ok, result1} = UtxoSet.get_by_inputs(utxo_set, inputs) assert {:ok, Enum.reverse(result1)} == UtxoSet.get_by_inputs(utxo_set, Enum.reverse(inputs)) assert {:ok, result1 ++ result1} == UtxoSet.get_by_inputs(utxo_set, inputs ++ inputs) end test "will get for empty inputs", %{utxo_set: utxo_set} do assert {:ok, []} = UtxoSet.get_by_inputs(utxo_set, []) end test "will get for subset of inputs", %{inputs: [input | _], outputs: [output | _], utxo_set: utxo_set} do assert {:ok, [^output]} = UtxoSet.get_by_inputs(utxo_set, [input]) end end describe "apply_effects/3" do test "will apply effects of spends", %{inputs: [input1, input2 | _], outputs: [output1 | _], utxo_set: utxo_set} do assert {:ok, [^output1]} = utxo_set |> UtxoSet.apply_effects([input2], %{}) |> UtxoSet.get_by_inputs([input1]) assert {:error, :utxo_not_found} = utxo_set |> UtxoSet.apply_effects([input2], %{}) |> UtxoSet.get_by_inputs([input2]) end test "will apply effects of new utxos being created", %{inputs: [input | _], outputs: [output | _]} do utxo_map = %{input => %Utxo{output: output, creating_txhash: <<1>>}} assert {:ok, [^output]} = [] |> UtxoSet.init() |> UtxoSet.apply_effects([], utxo_map) |> UtxoSet.get_by_inputs([input]) end test "will create first, spend second", %{inputs: [input | _], outputs: [output | _]} do # this would not happen now, since `apply_effects/3` is called per tx, which cannot spend it's own input # nevertheless, let's make sure this is catered for on this level too utxo_map = %{input => %Utxo{output: output, creating_txhash: <<1>>}} assert {:error, :utxo_not_found} = [] |> UtxoSet.init() |> UtxoSet.apply_effects([input], utxo_map) |> UtxoSet.get_by_inputs([input]) end end describe "db_updates/2" do test "will write to db, creating first, spending second", %{inputs: [input | _], outputs: [output | _]} do # this would not happen now, since `apply_effects/3` is called per tx, which cannot spend it's own input # nevertheless, let's make sure this is catered for on this level too utxo_map = %{input => %Utxo{output: output, creating_txhash: <<1>>}} assert [{:put, :utxo, {key, _}}, {:delete, :utxo, key}] = UtxoSet.db_updates([input], utxo_map) end end describe "exists?/2" do test "false if input absent", %{inputs: [input | _]} do refute [] |> UtxoSet.init() |> UtxoSet.exists?(input) end test "true if present", %{inputs: [input | _], utxo_set: utxo_set} do assert UtxoSet.exists?(utxo_set, input) end end describe "find_matching_utxo/3" do test "will find pair if matches", %{inputs: [input | _], outputs: [output | _]} do utxo_map = %{input => %Utxo{output: output, creating_txhash: <<1>>}} assert hd(Map.to_list(utxo_map)) == [] |> UtxoSet.init() |> UtxoSet.apply_effects([], utxo_map) |> UtxoSet.find_matching_utxo(<<1>>, 0) end test "won't find if none matches", %{inputs: [input | _], outputs: [output | _]} do utxo_map = %{input => %Utxo{output: output, creating_txhash: <<1>>}} refute [] |> UtxoSet.init() |> UtxoSet.apply_effects([], utxo_map) |> UtxoSet.find_matching_utxo(<<1>>, 1) refute [] |> UtxoSet.init() |> UtxoSet.find_matching_utxo(<<1>>, 0) end end describe "filter_owned_by/2" do test "will find Bob's utxos", %{bob: bob, utxo_set: utxo_set} do assert [_, _] = UtxoSet.filter_owned_by(utxo_set, bob.addr) |> Enum.to_list() end test "will NOT find Alice's utxos, b/c she doesn't have any", %{alice: alice, utxo_set: utxo_set} do assert [] = UtxoSet.filter_owned_by(utxo_set, alice.addr) |> Enum.to_list() end end describe "zip_with_positions/1" do test "will zip all utxos with their positions", %{utxo_set: utxo_set} do # for now a trivial test case. When input pointers other than `utxo_pos` are used this becomes relevant assert [{_, Utxo.position(1, 0, 0)}, {_, Utxo.position(2, 0, 0)}] = UtxoSet.zip_with_positions(utxo_set) |> Enum.to_list() end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/state_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.StateTest do @moduledoc """ Smoke tests the imperative shell - runs a happy path on `OMG.Watcher.State`. Logic tested elsewhere """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.DB.Fixtures alias Ecto.Adapters.SQL.Sandbox alias OMG.Eth.Configuration alias OMG.Watcher.State alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo @eth <<0::160>> @fee_claimer_address Base.decode16!("DEAD000000000000000000000000000000000000") deffixture standalone_state_server(db_initialized) do # match variables to hide "unused var" warnings (can't be fixed by underscoring in line above, breaks macro): _ = db_initialized # need to override that to very often, so that many checks fall in between a single child chain block submission {:ok, started_apps} = Application.ensure_all_started(:omg_db) # the pubsub is required, because `OMG.Watcher.State` is broadcasting to the `OMG.Bus` {:ok, bus_apps} = Application.ensure_all_started(:omg_bus) Application.ensure_all_started(:postgrex) Application.ensure_all_started(:spandex_ecto) Application.ensure_all_started(:ecto) {:ok, _} = Supervisor.start_link( [ %{ id: OMG.WatcherInfo.DB.Repo, start: {OMG.WatcherInfo.DB.Repo, :start_link, []}, type: :supervisor } ], strategy: :one_for_one, name: WatcherInfo.Supervisor ) :ok = Sandbox.checkout(OMG.WatcherInfo.DB.Repo) Sandbox.mode(OMG.WatcherInfo.DB.Repo, {:shared, self()}) on_exit(fn -> (started_apps ++ bus_apps) |> Enum.reverse() |> Enum.map(fn app -> :ok = Application.stop(app) end) end) child_block_interval = Configuration.child_block_interval() metrics_collection_interval = 60_000 {:ok, _} = Supervisor.start_link( [ {OMG.Watcher.State, [ fee_claimer_address: @fee_claimer_address, child_block_interval: child_block_interval, metrics_collection_interval: metrics_collection_interval ]} ], strategy: :one_for_one ) :ok end @tag fixtures: [:alice, :standalone_state_server] test "can execute various calls on OMG.Watcher.State, one happy path only", %{alice: alice} do fee = %{@eth => [1]} # deposits, transactions, utxo existence assert {:ok, _} = State.deposit([ %{ owner: alice.addr, currency: @eth, amount: 10, blknum: 1, root_chain_txhash: <<1::256>>, eth_height: 1, log_index: 0 } ]) assert true == State.utxo_exists?(Utxo.position(1, 0, 0)) assert {:ok, _} = State.exec(TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 9}]), fee) # block forming & status assert {blknum, _} = State.get_status() # exits, with invalid ones assert {:ok, _db, _} = State.exit_utxos([Utxo.position(blknum, 0, 0)]) # close block assert {:ok, _db} = State.close_block() end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/supervisor_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.SupervisorTest do @moduledoc """ This test is here mainly to test the logic-rich part of the supervisor setup, namely the config of `OMG.Watcher.RootChainCoordinator.Core` supplied therein """ use ExUnit.Case, async: true alias OMG.Watcher.RootChainCoordinator.Core setup do {_args, config_services} = OMG.Watcher.CoordinatorSetup.coordinator_setup(1, 1, 1, 1) init = Core.init(config_services, 10) pid = config_services |> Map.keys() |> Enum.with_index(1) |> Enum.into(%{}, fn {key, idx} -> {key, :c.pid(0, idx, 0)} end) {:ok, %{state: initial_check_in(init, Map.keys(config_services), pid), pid: pid}} end test "syncs services correctly", %{state: state, pid: pid} do # NOTE: this assumes some finality margins embedded in `config/test.exs`. Consider refactoring if these # needs to change and break this test, instead of modifying this test! # start - only depositor and getter allowed to move assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:depositor]) assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:exit_processor]) assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:in_flight_exit_processor]) assert %{sync_height: 1, root_chain_height: 10} = Core.get_synced_info(state, pid[:block_getter]) # depositor advances assert {:ok, state} = Core.check_in(state, pid[:depositor], 9, :depositor) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:exit_processor]) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:in_flight_exit_processor]) # exit_processor advances assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:exit_challenger]) assert {:ok, state} = Core.check_in(state, pid[:exit_processor], 9, :exit_processor) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:exit_challenger]) # in flights advance assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:piggyback_processor]) assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:competitor_processor]) assert {:ok, state} = Core.check_in(state, pid[:in_flight_exit_processor], 9, :in_flight_exit_processor) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:piggyback_processor]) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:competitor_processor]) assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:piggyback_challenges_processor]) assert {:ok, state} = Core.check_in(state, pid[:piggyback_processor], 9, :piggyback_processor) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:piggyback_challenges_processor]) assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:challenges_responds_processor]) assert {:ok, state} = Core.check_in(state, pid[:competitor_processor], 9, :competitor_processor) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:challenges_responds_processor]) # BlockGetter advances assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:exit_finalizer]) assert %{sync_height: 0, root_chain_height: 9} = Core.get_synced_info(state, pid[:ife_exit_finalizer]) assert {:ok, state} = Core.check_in(state, pid[:block_getter], 10, :block_getter) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:exit_finalizer]) assert %{sync_height: 9, root_chain_height: 9} = Core.get_synced_info(state, pid[:ife_exit_finalizer]) # root chain advances assert {:ok, state} = Core.update_root_chain_height(state, 100) assert %{sync_height: 9, root_chain_height: 99} = Core.get_synced_info(state, pid[:exit_finalizer]) assert %{sync_height: 9, root_chain_height: 99} = Core.get_synced_info(state, pid[:ife_exit_finalizer]) assert %{sync_height: 99, root_chain_height: 99} = Core.get_synced_info(state, pid[:depositor]) assert %{sync_height: 10, root_chain_height: 100} = Core.get_synced_info(state, pid[:block_getter]) end defp initial_check_in(state, services, pid) do {:ok, state} = Enum.reduce(services, {:ok, state}, fn service, {:ok, state} -> Core.check_in(state, pid[service], 0, service) end) state end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/typed_data_hash_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.TypedDataHashTest do @moduledoc """ Idea behind testing functionality like this (which produces random byte-strings) is 4-tiered test suite. * tier 1: acknowledged third party (Metamask) signatures we can verify (recover address from) * tier 2: final structural hash on prepared transaction that gives the same signatures as above * tier 3: intermediate results of hashing (domain separator, structural hashes of inputs & outputs) * tier 4: end-to-end test of generating signatures in elixir code and verifying them in solidity library ( done in `OMG.Watcher.DependencyConformance.SignatureTest`) """ use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash alias OMG.Watcher.TypedDataHash.Tools alias OMG.Watcher.Utxo require Utxo require OMG.Watcher.TypedDataHash.Tools @test_domain_separator Tools.domain_separator(%{ name: "OMG Network", version: "1", verifyingContract: Base.decode16!("44de0ec539b8c4a4b530c78620fe8320167f2f74", case: :mixed), salt: Base.decode16!("fad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", case: :mixed ) }) setup_all do null_addr = <<0::160>> owner = Base.decode16!("2258a5279850f6fb78888a7e45ea2a5eb1b3c436", case: :mixed) token = Base.decode16!("0123456789abcdef000000000000000000000000", case: :mixed) {:ok, %{ inputs: [ {1, 0, 0}, {1000, 2, 3}, {101_000, 1337, 3} ], outputs: [ {owner, null_addr, 100}, {token, null_addr, 111}, {owner, token, 1337}, {null_addr, null_addr, 0} ], metadata: Base.decode16!("853a8d8af99c93405a791b97d57e819e538b06ffaa32ad70da2582500bc18d43", case: :mixed) }} end describe "Compliance with contract code" do test "EIP domain type is encoded correctly" do eip_domain = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" expected_hash = "d87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472" assert expected_hash == eip_domain |> Crypto.hash() |> Base.encode16(case: :lower) end test "Input type hash is computed correctly" do expected = "5f0e06e50b513a68a090818949172483acfec769d9b7756cad7c00b26b52178c" assert expected == "Input(uint256 blknum,uint256 txindex,uint256 oindex)" |> Crypto.hash() |> Base.encode16(case: :lower) end test "Output type hash is computed correctly" do expected = "9fd642c2bbaa2f3431add55df5d3932807048fb41b6b07d65c59e0f9ad3a8eb7" assert expected == "Output(uint256 outputType,bytes20 outputGuard,address currency,uint256 amount)" |> Crypto.hash() |> Base.encode16(case: :lower) end test "Transaction type hash is computed correctly" do expected = "186aebaa7ec9e4abef44830c07670c034d8efb44e91542dc63df2f65205e61cc" full_type = "Transaction(" <> "Input input0,Input input1,Input input2,Input input3," <> "Output output0,Output output1,Output output2,Output output3," <> "uint256 txdata,bytes32 metadata)" <> "Input(uint256 blknum,uint256 txindex,uint256 oindex)" <> "Output(uint256 outputType,bytes20 outputGuard,address currency,uint256 amount)" assert expected == full_type |> Crypto.hash() |> Base.encode16(case: :lower) end test "domain separator is computed correctly" do expected = "b542beb7bafc6796b8439716a4e460a2634ac432216cebc524e54f8789e2924c" assert expected == Base.encode16(@test_domain_separator, case: :lower) end test "Input is hashed properly" do assert "1a5933eb0b3223b0500fbbe7039cab9badc006adda6cf3d337751412fd7a4b61" == Utxo.position(0, 0, 0) |> Tools.hash_input() |> Base.encode16(case: :lower) assert "7377afcd24fdc685fd8c6ea2b5d15a74f2c898c3d5bcce3499f448a4d68db290" == Utxo.position(1, 0, 0) |> Tools.hash_input() |> Base.encode16(case: :lower) assert "c198a0ab9b12c3f225195cf0f7870c7ab12c316b33eb99771dfd0f3f7da455a5" == Utxo.position(101_000, 1337, 3) |> Tools.hash_input() |> Base.encode16(case: :lower) end test "Output is hashed properly", %{outputs: [output1, output2, output3, output4]} do to_output = fn {owner, currency, amount} -> [output] = Transaction.get_outputs(Transaction.Payment.new([], [{owner, currency, amount}])) output end assert "4b85fe2caac41f533c3d3b56ec75ca3363d0205e4dde63ca16b0d377fa79364d" == to_output.(output1) |> Tools.hash_output() |> Base.encode16(case: :lower) assert "27962e5f1453285204261a3b2fe420be5ee504f3606d857e5c3120e1fc7aac3f" == to_output.(output2) |> Tools.hash_output() |> Base.encode16(case: :lower) assert "257ce332ccd9571fb364f8abd0b22ca53cd3d7e4ba9a14fd208cdf25caf8854f" == to_output.(output3) |> Tools.hash_output() |> Base.encode16(case: :lower) assert "168031cd8ed05efce595276a59045cabf7a33d14a4dcad1ea16fdd0c98ad7598" == to_output.(output4) |> Tools.hash_output() |> Base.encode16(case: :lower) end test "Metadata is hashed properly", %{metadata: metadata} do empty = <<0::256>> assert "290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563" == empty |> Crypto.hash() |> Base.encode16(case: :lower) assert "f32aecc93539658c0e9f102ad05b1f37ec4692366142955451b7e432f59a513a" == metadata |> Crypto.hash() |> Base.encode16(case: :lower) end test "Transaction is hashed correctly", %{inputs: inputs, outputs: outputs, metadata: metadata} do assert "3f5b24d7cf1db32c34ae2921a3537b7af40ad7e787fb7e8e03f88715a861dfe7" == Transaction.Payment.new([], []) |> TypedDataHash.hash_transaction() |> Base.encode16(case: :lower) assert "d5fb24437003566da84b8948fde09c367bbf93da39cdd23390ecaa98e3054f2d" == Transaction.Payment.new(inputs, outputs) |> TypedDataHash.hash_transaction() |> Base.encode16(case: :lower) assert "7c3f89120b00c4b1ca433811b544e8177f109c5a4ca27ff434e08b02d66e77f4" == Transaction.Payment.new(inputs, outputs, metadata) |> TypedDataHash.hash_transaction() |> Base.encode16(case: :lower) end test "Structured hash is computed correctly", %{inputs: inputs, outputs: outputs, metadata: metadata} do assert "47f83702c496c7ebb6ec639cb11d6c8b81eb64f6d818cb087e3ed2cb92ccf1ae" == Transaction.Payment.new([], []) |> TypedDataHash.hash_struct(@test_domain_separator) |> Base.encode16(case: :lower) assert "aeaa272f4460436415f377cb0cefe8f2646f4457f60827519a7edf86d30c0bf0" == Transaction.Payment.new(inputs, outputs) |> TypedDataHash.hash_struct(@test_domain_separator) |> Base.encode16(case: :lower) assert "e1fcd0b07d8034ac039c15c544436a95e92879689a456604cbc0e8420e6e342a" == Transaction.Payment.new(inputs, outputs, metadata) |> TypedDataHash.hash_struct(@test_domain_separator) |> Base.encode16(case: :lower) end end describe "Eip-712 types" do test "align with encodeType format" do assert "EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)" == TypedDataHash.Types.encode_type(:EIP712Domain) assert "Transaction(" <> "uint256 txType," <> "Input input0,Input input1,Input input2,Input input3," <> "Output output0,Output output1,Output output2,Output output3," <> "uint256 txData,bytes32 metadata)" == TypedDataHash.Types.encode_type(:Transaction) assert "Input(uint256 blknum,uint256 txindex,uint256 oindex)" == TypedDataHash.Types.encode_type(:Input) assert "Output(uint256 outputType,bytes20 outputGuard,address currency,uint256 amount)" == TypedDataHash.Types.encode_type(:Output) end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/utxo/position_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Utxo.PositionTest do @moduledoc false use ExUnit.Case, async: true doctest OMG.Watcher.Utxo.Position end ================================================ FILE: apps/omg_watcher/test/omg_watcher/utxo_exit/core_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.UtxoExit.CoreTest do use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.Block alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias OMG.Watcher.Utxo.Position alias OMG.Watcher.UtxoExit.Core require OMG.Watcher.Utxo @eth <<0::160>> setup do alice = TestHelper.generate_entity() %{alice: alice} end describe "compose_deposit_standard_exit/1" do test "creates deposit exit", %{alice: alice} do position = Utxo.position(1003, 0, 0) encode_utxo = position |> Utxo.Position.encode() [output] = Transaction.get_outputs(Transaction.Payment.new([], [{alice.addr, @eth, 10}])) fake_utxo_db_kv = {Position.to_input_db_key(position), Utxo.to_db_value(%Utxo{output: output})} assert {:ok, %{ utxo_pos: ^encode_utxo, txbytes: txbytes, proof: proof }} = Core.compose_deposit_standard_exit({:ok, fake_utxo_db_kv}) assert [%{amount: 10}] = txbytes |> Transaction.decode!() |> Transaction.get_outputs() assert byte_size(proof) == 32 * 16 end test "fails to create deposit exit when UTXO missing in DB" do assert {:error, :no_deposit_for_given_blknum} = Core.compose_deposit_standard_exit(:not_found) end end describe "compose_utxo_exit/2" do test "composes output exit from tx inside a block", %{alice: alice} do blknum = 4000 tx_exit = TestHelper.create_recovered([{1_000, 1, 0, alice}], @eth, [{alice, 10}]) tx_exit_raw_tx_bytes = Transaction.raw_txbytes(tx_exit) position = Utxo.position(blknum, 1, 0) encode_utxo = position |> Utxo.Position.encode() block = [ TestHelper.create_recovered([{1_000, 2, 0, alice}], @eth, [{alice, 10}]), tx_exit, TestHelper.create_recovered([{1_000, 3, 0, alice}], @eth, [{alice, 10}]) ] |> Block.hashed_txs_at(blknum) |> Block.to_db_value() assert {:ok, %{ proof: proof, txbytes: ^tx_exit_raw_tx_bytes, utxo_pos: ^encode_utxo }} = Core.compose_block_standard_exit(block, position) # hash byte_size * merkle tree depth assert byte_size(proof) == 32 * 16 end test "doesn't find utxo for the output exit, tx position exceeding the block tx count", %{alice: alice} do blknum = 4000 position = Utxo.position(blknum, 1, 0) block = [TestHelper.create_recovered([{1_000, 1, 0, alice}], @eth, [{alice, 10}])] |> Block.hashed_txs_at(blknum) |> Block.to_db_value() assert {:error, :utxo_not_found} = Core.compose_block_standard_exit(block, position) end test "doesn't find utxo for the output exit, output position exceeding the output count", %{alice: alice} do blknum = 4000 position = Utxo.position(blknum, 0, 3) block = [TestHelper.create_recovered([{1_000, 1, 0, alice}], @eth, [{alice, 10}])] |> Block.hashed_txs_at(blknum) |> Block.to_db_value() assert {:error, :utxo_not_found} = Core.compose_block_standard_exit(block, position) end test "throws when composing output exit, mismatch blknum and utxo pos (should never occur)", %{alice: alice} do position = Utxo.position(3000, 0, 0) block = [TestHelper.create_recovered([{1_000, 1, 0, alice}], @eth, [{alice, 10}])] |> Block.hashed_txs_at(4000) |> Block.to_db_value() assert_raise MatchError, fn -> Core.compose_block_standard_exit(block, position) end end end end ================================================ FILE: apps/omg_watcher/test/omg_watcher/utxo_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.UtxoTest do @moduledoc false use ExUnit.Case, async: true doctest OMG.Watcher.Utxo end ================================================ FILE: apps/omg_watcher/test/omg_watcher/wire_format_types_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.WireFormatTypesTest do use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Watcher.WireFormatTypes describe "tx_type_for/1" do test "returns the tx type for the given atom" do assert WireFormatTypes.tx_type_for(:tx_payment_v1) == 1 end end describe "input_pointer_type_for/1" do test "returns the input type for the given input" do assert WireFormatTypes.input_pointer_type_for(:input_pointer_utxo_position) == 1 end end describe "output_type_for/1" do test "returns the output type for the given output" do assert WireFormatTypes.output_type_for(:output_payment_v1) == 1 end end end ================================================ FILE: apps/omg_watcher/test/support/dev_crypto.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.DevCrypto do @moduledoc """ Non-production crypto code like: - anything that touches private keys """ alias OMG.Watcher.Crypto alias OMG.Watcher.SignatureHelper alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash @doc """ Generates private key. Internally uses OpenSSL RAND_bytes. May throw if there is not enough entropy. """ @spec generate_private_key() :: {:ok, Crypto.priv_key_t()} def generate_private_key(), do: {:ok, :crypto.strong_rand_bytes(32)} @doc """ Given a private key, returns public key. """ @spec generate_public_key(Crypto.priv_key_t()) :: {:ok, Crypto.pub_key_t()} def generate_public_key(<>) do {:ok, der_pub} = get_public_key(priv) {:ok, der_to_raw(der_pub)} end @doc """ Signs transaction using private keys private keys are in the binary form, e.g.: ```<<54, 43, 207, 67, 140, 160, 190, 135, 18, 162, 70, 120, 36, 245, 106, 165, 5, 101, 183, 55, 11, 117, 126, 135, 49, 50, 12, 228, 173, 219, 183, 175>>``` """ @spec sign(Transaction.Protocol.t(), list(Crypto.priv_key_t())) :: Transaction.Signed.t() def sign(%{} = tx, private_keys) do sigs = Enum.map(private_keys, fn pk -> signature(tx, pk) end) %Transaction.Signed{raw_tx: tx, sigs: sigs} end @doc """ Produces a stand-alone, 65 bytes long, signature for message hash. """ @spec signature_digest(<<_::256>>, <<_::256>>) :: <<_::520>> def signature_digest(digest, priv) when is_binary(digest) and byte_size(digest) == 32 do {v, r, s} = SignatureHelper.sign_hash(digest, priv) pack_signature(v, r, s) end @doc """ Produces a stand-alone, 65 bytes long, signature for a given transaction. """ @spec signature(Transaction.Protocol.t(), Crypto.priv_key_t()) :: Crypto.sig_t() def signature(tx, priv), do: do_signature(tx, priv) defp do_signature(%{} = tx, priv) do tx |> TypedDataHash.hash_struct() |> signature_digest(priv) end # Pack a {v,r,s} signature as 65-bytes binary. defp pack_signature(v, r, s) do <> end defp der_to_raw(<<4::integer-size(8), data::binary>>), do: data defp get_public_key(private_key) do case ExSecp256k1.create_public_key(private_key) do {:ok, public_key} -> {:ok, public_key} {:error, reason} -> {:error, to_string(reason)} end end end ================================================ FILE: apps/omg_watcher/test/support/exit_processor/case.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.Case do @moduledoc """ `ExUnit` test case for a shared setup used in `ExitProcessor.Core` logic tests """ use ExUnit.CaseTemplate alias OMG.Watcher.Block alias OMG.Watcher.ExitProcessor alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo require Utxo import OMG.Watcher.ExitProcessor.TestHelper @default_min_exit_period_seconds 120 @default_child_block_interval 1000 @eth <<0::160>> @not_eth <<1::size(160)>> setup do [alice, bob, carol] = 1..3 |> Enum.map(fn _ -> TestHelper.generate_entity() end) transactions = [ TestHelper.create_recovered([{1, 0, 0, alice}, {1, 2, 1, carol}], [{alice, @eth, 1}, {carol, @eth, 2}]), TestHelper.create_recovered([{2, 1, 0, alice}, {2, 2, 1, carol}], [{alice, @not_eth, 1}, {carol, @not_eth, 2}]) ] competing_tx = TestHelper.create_recovered([{10, 2, 1, alice}, {1, 0, 0, alice}], [{bob, @eth, 2}, {carol, @eth, 1}]) unrelated_tx = TestHelper.create_recovered([{20, 1, 0, alice}, {20, 20, 1, alice}], [{bob, @eth, 2}, {carol, @eth, 1}]) {:ok, processor_empty} = Core.init([], [], [], @default_min_exit_period_seconds, @default_child_block_interval) in_flight_exit_events = transactions |> Enum.zip([2, 4]) |> Enum.map(fn {tx, eth_height} -> ife_event(tx, eth_height: eth_height) end) ife_tx_hashes = transactions |> Enum.map(&Transaction.raw_txhash/1) processor_filled = transactions |> Enum.zip([1, 4]) |> Enum.reduce(processor_empty, fn {tx, idx}, processor -> # use the idx as both two distinct ethereum heights and two distinct exit_ids arriving from the root chain start_ife_from(processor, tx, eth_height: idx, exit_id: idx) end) {:ok, %{ alice: alice, bob: bob, carol: carol, transactions: transactions, competing_tx: competing_tx, unrelated_tx: unrelated_tx, processor_empty: processor_empty, in_flight_exit_events: in_flight_exit_events, ife_tx_hashes: ife_tx_hashes, processor_filled: processor_filled, invalid_piggyback_on_input: invalid_piggyback_on_input(processor_filled, transactions, ife_tx_hashes, competing_tx), invalid_piggyback_on_output: invalid_piggyback_on_output(alice, processor_filled, transactions, ife_tx_hashes) }} end defp invalid_piggyback_on_input(state, [tx | _], [ife_id | _], competing_tx) do request = %ExitProcessor.Request{ blknum_now: 4000, eth_height_now: 5, ife_input_spending_blocks_result: [Block.hashed_txs_at([tx], 3000)] } state = state |> start_ife_from(competing_tx) |> piggyback_ife_from(ife_id, 0, :input) |> Core.find_ifes_in_blocks(request) %{ state: state, request: request, ife_input_index: 0, ife_txbytes: txbytes(tx), spending_txbytes: txbytes(competing_tx), spending_input_index: 1, spending_sig: sig(competing_tx) } end defp invalid_piggyback_on_output(alice, state, [tx | _], [ife_id | _]) do # the piggybacked-output-spending tx is going to be included in a block, which requires more back&forth # 1. transaction which is, ife'd, output piggybacked, and included in a block # 2. transaction which spends that piggybacked output comp = TestHelper.create_recovered([{3000, 0, 0, alice}], [{alice, @eth, 1}]) tx_blknum = 3000 comp_blknum = 4000 block = Block.hashed_txs_at([tx], tx_blknum) request = %ExitProcessor.Request{ blknum_now: 5000, eth_height_now: 5, blocks_result: [block], ife_input_spending_blocks_result: [block, Block.hashed_txs_at([comp], comp_blknum)] } # 3. stuff happens in the contract; output #4 is a double-spend; #5 is OK state = state |> piggyback_ife_from(ife_id, 0, :output) |> piggyback_ife_from(ife_id, 1, :output) |> Core.find_ifes_in_blocks(request) %{ state: state, request: request, ife_good_pb_index: 5, ife_txbytes: txbytes(tx), ife_output_pos: Utxo.position(tx_blknum, 0, 0), ife_proof: Block.inclusion_proof(block, 0), spending_txbytes: txbytes(comp), spending_input_index: 0, spending_sig: sig(comp), ife_input_index: 4 } end end ================================================ FILE: apps/omg_watcher/test/support/exit_processor/test_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.ExitProcessor.TestHelper do @moduledoc """ Common utilities to manipulate the `ExitProcessor` """ import ExUnit.Assertions alias OMG.Watcher.ExitProcessor.Core alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo require Utxo # default exit_id used when starting exits using `start_se_from` and `start_ife_from` @exit_id 9876 def start_se_from(%Core{} = processor, tx, exiting_pos, opts \\ []) do {event, status} = se_event_status(tx, exiting_pos, opts) {processor, _} = Core.new_exits(processor, [event], [status]) processor end def se_event_status(tx, exiting_pos, opts \\ []) do Utxo.position(_, _, oindex) = exiting_pos txbytes = Transaction.raw_txbytes(tx) enc_pos = Utxo.Position.encode(exiting_pos) owner = tx |> Transaction.get_outputs() |> Enum.at(oindex) |> Map.get(:owner) eth_height = Keyword.get(opts, :eth_height, 2) exit_id = Keyword.get(opts, :exit_id, @exit_id) call_data = %{utxo_pos: enc_pos, output_tx: txbytes} root_chain_txhash = <<1::256>> block_timestamp = :os.system_time(:second) scheduled_finalization_time = block_timestamp + 100 event = %{ owner: owner, eth_height: eth_height, exit_id: exit_id, call_data: call_data, root_chain_txhash: root_chain_txhash, block_timestamp: block_timestamp, scheduled_finalization_time: scheduled_finalization_time } exitable = not Keyword.get(opts, :inactive, false) # those should be unused so setting to `nil` fake_output_id = enc_pos amount = nil bond_size = nil status = Keyword.get(opts, :status) || {exitable, enc_pos, fake_output_id, owner, amount, bond_size} {event, status} end def start_ife_from(%Core{} = processor, tx, opts \\ []) do exit_id = Keyword.get(opts, :exit_id, @exit_id) status = Keyword.get(opts, :status, active_ife_status()) status = if status == :inactive, do: inactive_ife_status(), else: status {processor, _} = Core.new_in_flight_exits(processor, [ife_event(tx, opts)], [{status, exit_id}]) processor end # See `OMG.Eth.RootChain.get_in_flight_exits_structs/2` for reference of where this comes from # `nil`s are unused portions of the returns data from the contract def active_ife_status(), do: {nil, 1, nil, nil, nil, nil, nil} def inactive_ife_status(), do: {nil, 0, nil, nil, nil, nil, nil} def piggyback_ife_from(%Core{} = processor, tx_hash, output_index, piggyback_type) do {processor, _} = Core.new_piggybacks(processor, [ %{ tx_hash: tx_hash, output_index: output_index, omg_data: %{piggyback_type: piggyback_type} } ]) processor end def ife_event(tx, opts \\ []) do sigs = Keyword.get(opts, :sigs) || sigs(tx) input_utxos_pos = Transaction.get_inputs(tx) |> Enum.map(&Utxo.Position.encode/1) input_txs = Keyword.get(opts, :input_txs) || List.duplicate("input_tx", length(input_utxos_pos)) eth_height = Keyword.get(opts, :eth_height, 2) %{ call_data: %{ in_flight_tx: Transaction.raw_txbytes(tx), input_txs: input_txs, input_utxos_pos: input_utxos_pos, in_flight_tx_sigs: sigs }, eth_height: eth_height } end def ife_response(tx, position), do: %{tx_hash: Transaction.raw_txhash(tx), challenge_position: Utxo.Position.encode(position)} def ife_challenge(tx, comp, opts \\ []) do competitor_position = Keyword.get(opts, :competitor_position) competitor_position = if competitor_position, do: Utxo.Position.encode(competitor_position), else: not_included_competitor_pos() %{ tx_hash: Transaction.raw_txhash(tx), competitor_position: competitor_position, call_data: %{ competing_tx: txbytes(comp), competing_tx_input_index: Keyword.get(opts, :competing_tx_input_index, 0), competing_tx_sig: Keyword.get(opts, :competing_tx_sig, sig(comp)) } } end def txbytes(tx), do: Transaction.raw_txbytes(tx) def sigs(tx), do: tx.signed_tx.sigs def sig(tx, idx \\ 0), do: tx |> sigs() |> Enum.at(idx) def assert_proof_sound(proof_bytes) do # NOTE: checking of actual proof working up to the contract integration test assert is_binary(proof_bytes) # hash size * merkle tree depth assert byte_size(proof_bytes) == 32 * 16 end def assert_events(events, expected_events) do assert MapSet.new(events) == MapSet.new(expected_events) end def check_validity_filtered(request, processor, opts) do exclude_events = Keyword.get(opts, :exclude, []) only_events = Keyword.get(opts, :only, []) {result, events} = Core.check_validity(request, processor) any? = fn filtering_events, event -> Enum.any?(filtering_events, fn filtering_event -> event.__struct__ == filtering_event end) end filtered_events = events |> Enum.filter(fn event -> Enum.empty?(exclude_events) or not any?.(exclude_events, event) end) |> Enum.filter(fn event -> Enum.empty?(only_events) or any?.(only_events, event) end) {result, filtered_events} end defp not_included_competitor_pos() do <> = List.duplicate(<<255::8>>, 32) |> Enum.reduce(fn val, acc -> val <> acc end) long end end ================================================ FILE: apps/omg_watcher/test/support/integration/bad_child_chain_server.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.BadChildChainServer do @moduledoc """ Module useful for creating integration tests where we want to simulate byzantine child chain server which is returning a bad block for a particular block hash. """ alias OMG.Utils.HttpRPC.Encoding alias OMG.Utils.HttpRPC.Response alias OMG.Watcher.Block alias OMG.Watcher.HttpRPC.Adapter alias OMG.Watcher.Integration.TestServer @doc """ Adds a route to TestServer which responded with prepared bad block when asked for known hash all other requests are redirected to `real` Child Chain API """ def prepare_route_to_inject_bad_block(context, bad_block, bad_block_hash) do TestServer.with_route( context, "/block.get", fn %{body: %{"hash" => req_hash}} -> if {:ok, bad_block_hash} == Encoding.from_hex(req_hash) do bad_block |> Block.to_api_format() |> Response.sanitize() |> TestServer.make_response() else {:ok, block} = %{hash: req_hash} |> Adapter.rpc_post("block.get", context.real_addr) |> Adapter.get_response_body() TestServer.make_response(block) end end ) end @doc """ Version of `prepare_route_to_inject_bad_block/3` when we want to serve the block under it's real hash """ def prepare_route_to_inject_bad_block(context, %{hash: bad_block_hash} = bad_block) do prepare_route_to_inject_bad_block(context, bad_block, bad_block_hash) end end ================================================ FILE: apps/omg_watcher/test/support/integration/deposit_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.Integration.DepositHelper do @moduledoc """ Common helper functions that are useful when integration-testing the child chain and watcher requiring deposits """ alias OMG.Eth.Encoding alias OMG.Eth.Token alias OMG.Watcher.Configuration alias OMG.Watcher.State.Transaction alias Support.DevHelper alias Support.RootChainHelper @eth <<0::160>> def deposit_to_child_chain(to, value, token \\ @eth) def deposit_to_child_chain(to, value, @eth) do {:ok, receipt} = Transaction.Payment.new([], [{to, @eth, value}]) |> Transaction.raw_txbytes() |> RootChainHelper.deposit(value, to) |> DevHelper.transact_sync!() process_deposit(receipt) end def deposit_to_child_chain(to, value, token_addr) when is_binary(token_addr) and byte_size(token_addr) == 20 do contract_addr = Encoding.from_hex(OMG.Eth.Configuration.contracts().erc20_vault) {:ok, _} = DevHelper.transact_sync!(Token.approve(to, contract_addr, value, token_addr)) {:ok, receipt} = Transaction.Payment.new([], [{to, token_addr, value}]) |> Transaction.raw_txbytes() |> RootChainHelper.deposit_from(to) |> DevHelper.transact_sync!() process_deposit(receipt) end defp process_deposit(%{"blockNumber" => deposit_eth_height} = receipt) do _ = wait_deposit_recognized(deposit_eth_height) RootChainHelper.deposit_blknum_from_receipt(receipt) end defp wait_deposit_recognized(deposit_eth_height) do post_event_block_finality = deposit_eth_height + Configuration.deposit_finality_margin() {:ok, _} = DevHelper.wait_for_root_chain_block(post_event_block_finality + 1) # sleeping until the deposit is spendable Process.sleep(Application.fetch_env!(:omg_watcher, :ethereum_events_check_interval_ms) * 2) :ok end end ================================================ FILE: apps/omg_watcher/test/support/integration/fixtures.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.Fixtures do use ExUnitFixtures.FixtureModule use OMG.Watcher.Fixtures alias OMG.Eth alias OMG.Eth.Encoding alias Support.DevHelper alias Support.Integration.DepositHelper deffixture alice_deposits(alice, token) do prepare_deposits(alice, token) end deffixture stable_alice_deposits(stable_alice, token) do prepare_deposits(stable_alice, token) end defp prepare_deposits(alice, token_addr) do some_value = 10 {:ok, _} = DevHelper.import_unlock_fund(alice) deposit_blknum = DepositHelper.deposit_to_child_chain(alice.addr, some_value) token_addr = Encoding.from_hex(token_addr) {:ok, _} = Eth.Token.mint(alice.addr, some_value, token_addr) |> DevHelper.transact_sync!() token_deposit_blknum = DepositHelper.deposit_to_child_chain(alice.addr, some_value, token_addr) {deposit_blknum, token_deposit_blknum} end end ================================================ FILE: apps/omg_watcher/test/support/integration/test_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.TestHelper do @moduledoc """ Common helper functions that are useful when integration-testing the watcher """ require OMG.Watcher.Utxo alias OMG.Watcher.Configuration alias OMG.Watcher.State alias Support.DevHelper alias Support.RootChainHelper alias Support.WaitFor alias Support.WatcherHelper def wait_for_byzantine_events(event_names, timeout) do fn -> %{"byzantine_events" => emitted_events} = WatcherHelper.success?("/status.get") emitted_event_names = Enum.map(emitted_events, &String.to_atom(&1["event"])) if Enum.sort(emitted_event_names) == Enum.sort(event_names), do: {:ok, emitted_event_names}, else: :repeat end |> WaitFor.ok(timeout) end def wait_for_block_fetch(block_number, timeout) do # TODO query to State used in tests instead of an event system, remove when event system is here fn -> case elem(State.get_status(), 0) do blknum when blknum < block_number -> :repeat _ -> {:ok, block_number} end end |> WaitFor.ok(timeout) # write to db seems to be async and wait_for_block_fetch would return too early, so sleep # leverage `block` events if they get implemented Process.sleep(100) end @doc """ The above wait_for_block_fetch/2 function only waits for a block to appear in the state and add some "random" sleep to give the database time to process and write the block. This function will instead poll for block.get until found (or timeout). """ def wait_for_block_inserted_in_db(block_number, timeout) do func = fn -> case WatcherHelper.get_block(block_number) do {:error, _} -> :repeat {:ok, %{"blknum" => ^block_number}} -> {:ok, block_number} end end WaitFor.ok(func, timeout) end @doc """ We need to wait on both a margin of eth blocks and exit processing """ def wait_for_exit_processing(exit_eth_height, timeout \\ 5_000) do exit_finality = Configuration.exit_finality_margin() + 1 DevHelper.wait_for_root_chain_block(exit_eth_height + exit_finality, timeout) # wait some more to ensure exit is processed Process.sleep(Configuration.ethereum_events_check_interval_ms() * 2) end def process_exits(vault_id, token, user) do min_exit_period_ms = OMG.Eth.Configuration.min_exit_period_seconds() * 1000 # enough to wait out the exit period on the contract Process.sleep(2 * min_exit_period_ms) {:ok, %{"status" => "0x1", "blockNumber" => process_eth_height, "logs" => logs}} = RootChainHelper.process_exits(vault_id, token, 0, 1, user.addr) |> Support.DevHelper.transact_sync!() # status 0x1 doesn't yet mean much. To smoke test the success of the processing (exits actually processed) we # take a look at the logs. Single entry means no logs were processed (it is the `ProcessedExitsNum`, that always # gets emmitted) true = length(logs) > 1 || {:error, :looks_like_no_exits_were_processed} # to have the new event fully acknowledged by the services, wait the finality margin exit_finality_margin = Configuration.exit_finality_margin() DevHelper.wait_for_root_chain_block(process_eth_height + exit_finality_margin + 1) # just a little more to ensure events are recognized by services check_interval_ms = Configuration.ethereum_events_check_interval_ms() Process.sleep(3 * check_interval_ms) :ok end end ================================================ FILE: apps/omg_watcher/test/support/integration/test_server.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.Integration.TestServer do @moduledoc """ Helper functions to provide behavior to FakeServer without using FakeServer defined macros. Use with :test_server fixture which provides context variables For now it's strictly tied with child chain api and handles env variable changes """ @doc """ Configures route for fake server to respond for given path with given response **Please note: ** When the route is configured with a list of FakeServer.HTTP.Responses, the server will respond with the first element in the list and then remove it. This will be repeated for each request made for this route. Use `fn req -> response end` when you need to return always the same or modified response on every request Also first use of `with_route` changes configuration variable to child chain api to fake server, so invoke this function when fake response is needed. """ def with_route(%{fake_addr: fake_addr, server_pid: server_pid, server_id: server_id} = _context, path, response_block) do Application.put_env(:omg_watcher, :child_chain_url, fake_addr) FakeServer.put_route(server_pid, path, response_block) {:ok, port} = FakeServer.port(server_id) %{port: port} end def make_response(data) when is_map(data) do WatcherTestServerResponseFactory.build(:json_rpc, data: data, success: not Map.has_key?(data, :code)) end end defmodule WatcherTestServerResponseFactory do @moduledoc false use FakeServer.ResponseFactory def json_rpc_response() do ok( %{ version: "1.0", success: true, data: %{} }, %{"Content-Type" => "application/json"} ) end end ================================================ FILE: apps/omg_watcher/test/support/signature_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.SignatureHelper do @moduledoc false @doc """ Returns a ECDSA signature (v,r,s) for a given hashed value. This implementes Eq.(207) of the Yellow Paper. """ @base_recovery_id 27 @base_recovery_id_eip_155 35 @type keccak_hash :: binary() @type private_key :: <<_::256>> @type hash_v :: integer() @type hash_r :: integer() @type hash_s :: integer() @spec sign_hash(keccak_hash(), private_key, integer() | nil) :: {hash_v, hash_r, hash_s} def sign_hash(hash, private_key, chain_id \\ nil) do {:ok, {<>, recovery_id}} = ExSecp256k1.sign_compact(hash, private_key) # Fork Ψ EIP-155 recovery_id = if chain_id do chain_id * 2 + @base_recovery_id_eip_155 + recovery_id else @base_recovery_id + recovery_id end {recovery_id, r, s} end end ================================================ FILE: apps/omg_watcher/test/support/test_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.Watcher.TestHelper do @moduledoc false alias OMG.Watcher.Crypto alias OMG.Watcher.DevCrypto alias OMG.Watcher.State.Core alias OMG.Watcher.State.Transaction @type entity :: %{priv: Crypto.priv_key_t(), addr: Crypto.pub_key_t()} @empty_metadata <<0::256>> # Deterministic entities. Use only when truly needed. def entities_stable(), do: %{ stable_alice: %{ priv: <<54, 43, 207, 67, 140, 160, 190, 135, 18, 162, 70, 120, 36, 245, 106, 165, 5, 101, 183, 55, 11, 117, 126, 135, 49, 50, 12, 228, 173, 219, 183, 175>>, addr: <<59, 159, 76, 29, 210, 110, 11, 229, 147, 55, 59, 29, 54, 206, 226, 0, 140, 190, 184, 55>> }, stable_bob: %{ priv: <<208, 253, 134, 150, 198, 155, 175, 125, 158, 156, 21, 108, 208, 7, 103, 242, 9, 139, 26, 140, 118, 50, 144, 21, 226, 19, 156, 2, 210, 97, 84, 128>>, addr: <<207, 194, 79, 222, 88, 128, 171, 217, 153, 41, 195, 239, 138, 178, 227, 16, 72, 173, 118, 35>> }, stable_mallory: %{ priv: <<89, 253, 200, 245, 173, 195, 234, 62, 168, 206, 213, 19, 136, 51, 147, 209, 1, 14, 180, 107, 106, 8, 133, 131, 75, 157, 81, 109, 102, 19, 91, 130>>, addr: <<48, 120, 88, 246, 235, 202, 79, 121, 216, 73, 40, 199, 165, 186, 120, 113, 36, 119, 87, 207>> } } def entities(), do: Map.merge( %{ alice: generate_entity(), bob: generate_entity(), carol: generate_entity() }, entities_stable() ) @spec generate_entity :: entity() def generate_entity() do {:ok, priv} = DevCrypto.generate_private_key() {:ok, pub} = DevCrypto.generate_public_key(priv) {:ok, address} = Crypto.generate_address(pub) %{priv: priv, addr: address} end def do_deposit(state, owner, %{amount: amount, currency: cur, blknum: blknum}) do {:ok, _, new_state} = Core.deposit([%{owner: owner.addr, currency: cur, amount: amount, blknum: blknum}], state) new_state end @doc """ convenience function around Transaction.new to create recovered transactions, by allowing to provider private keys of utxo owners along with the inputs """ @spec create_recovered( list({pos_integer, non_neg_integer, 0 | 1, map}), Transaction.Payment.currency(), list({map, pos_integer}), Transaction.metadata() ) :: Transaction.Recovered.t() def create_recovered(inputs, currency, outputs, metadata \\ @empty_metadata) do create_encoded(inputs, currency, outputs, metadata) |> Transaction.Recovered.recover_from!() end @spec create_recovered( list({pos_integer, non_neg_integer, 0 | 1, map}), list({map, Transaction.Payment.currency(), pos_integer}) ) :: Transaction.Recovered.t() def create_recovered(inputs, outputs), do: create_encoded(inputs, outputs) |> Transaction.Recovered.recover_from!() def create_encoded(inputs, currency, outputs, metadata \\ @empty_metadata) do create_signed(inputs, currency, outputs, metadata) |> Transaction.Signed.encode() end def create_encoded(inputs, outputs) do create_signed(inputs, outputs) |> Transaction.Signed.encode() end def create_encoded_fee_tx(blknum, owner, currency, amount) do %Transaction.Signed{ raw_tx: Transaction.Fee.new(blknum, {owner, currency, amount}), sigs: [] } |> Transaction.Signed.encode() end def create_recovered_fee_tx(blknum, owner, currency, amount), do: create_encoded_fee_tx(blknum, owner, currency, amount) |> Transaction.Recovered.recover_from!() @doc """ convenience function around Transaction.new to create signed transactions (see create_recovered) """ @spec create_signed( list({pos_integer, non_neg_integer, 0 | 1, map}), Transaction.Payment.currency(), list({map, pos_integer}), Transaction.metadata() ) :: Transaction.Signed.t() def create_signed(inputs, currency, outputs, metadata \\ @empty_metadata) do raw_tx = Transaction.Payment.new( inputs |> Enum.map(fn {blknum, txindex, oindex, _} -> {blknum, txindex, oindex} end), outputs |> Enum.map(fn {owner, amount} -> {owner.addr, currency, amount} end), metadata ) privs = get_private_keys(inputs) DevCrypto.sign(raw_tx, privs) end @spec create_signed( list({pos_integer, non_neg_integer, 0 | 1, map}), list({map, Transaction.Payment.currency(), pos_integer}) ) :: Transaction.Signed.t() def create_signed(inputs, outputs) do raw_tx = Transaction.Payment.new( inputs |> Enum.map(fn {blknum, txindex, oindex, _} -> {blknum, txindex, oindex} end), outputs |> Enum.map(fn {owner, currency, amount} -> {owner.addr, currency, amount} end) ) privs = get_private_keys(inputs) DevCrypto.sign(raw_tx, privs) end def sign_encode(%{} = tx, priv_keys), do: tx |> DevCrypto.sign(priv_keys) |> Transaction.Signed.encode() def sign_recover!(%{} = tx, priv_keys) do tx |> sign_encode(priv_keys) |> Transaction.Recovered.recover_from!() end defp get_private_keys(inputs), do: Enum.map(inputs, fn {_, _, _, owner} -> owner.priv end) end ================================================ FILE: apps/omg_watcher/test/support/watcher_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Support.WatcherHelper do @moduledoc """ Module provides common testing functions used by App's tests. """ alias ExUnit.CaptureLog alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.Utxo require Utxo import ExUnit.Assertions import Plug.Conn import Phoenix.ConnTest @endpoint OMG.WatcherRPC.Web.Endpoint def wait_for_process(pid, timeout \\ :infinity) when is_pid(pid) do ref = Process.monitor(pid) receive do {:DOWN, ^ref, :process, _, _} -> :ok after timeout -> throw({:timeouted_waiting_for, pid}) end end def success?(path, body \\ nil) do response_body = rpc_call(path, body, 200) version = Map.get(response_body, "version") %{"version" => ^version, "success" => true, "data" => data} = response_body data end def no_success?(path, body \\ nil) do response_body = rpc_call(path, body, 200) version = Map.get(response_body, "version") %{"version" => ^version, "success" => false, "data" => data} = response_body data end def server_error?(path, body \\ nil) do response_body = rpc_call(path, body, 500) version = Map.get(response_body, "version") %{"version" => ^version, "success" => false, "data" => data} = response_body data end def rpc_call(path, body \\ nil, expected_resp_status \\ 200) do response = build_conn() |> put_req_header("content-type", "application/json") |> post(path, body) # CORS check assert ["*"] == get_resp_header(response, "access-control-allow-origin") required_headers = [ "access-control-allow-origin", "access-control-expose-headers", "access-control-allow-credentials" ] for header <- required_headers do assert header in Enum.map(response.resp_headers, &elem(&1, 0)) end # CORS check assert response.status == expected_resp_status Jason.decode!(response.resp_body) end def create_topic(main_topic, subtopic), do: main_topic <> ":" <> subtopic @doc """ Decodes specified keys in map from hex to binary """ @spec decode16(map(), list()) :: map() def decode16(data, keys) do keys |> Enum.into(%{}, &decode16_for_key(data, &1)) |> (&Map.merge(data, &1)).() end defp decode16_for_key(data, key) do case data[key] do value when is_binary(value) -> {key, decode_binary!(value)} value when is_list(value) -> bin_list = value |> Enum.map(&Encoding.from_hex/1) |> Enum.map(fn {:ok, bin} -> bin end) {key, bin_list} end end defp decode_binary!(value) do {:ok, bin} = Encoding.from_hex(value) bin end def get_balance(address, token) do encoded_token = Encoding.to_hex(token) address |> get_balance() |> Enum.find(%{"amount" => 0}, fn %{"currency" => currency} -> encoded_token == currency end) |> Map.get("amount") end def get_utxos(params) when is_map(params) do hex_string_address = Encoding.to_hex(params.address) success?("/account.get_utxos", %{params | address: hex_string_address}) end @doc """ shortcut helper for get_utxos that inject pagination data for you """ def get_utxos(address, page \\ 1, limit \\ 100) do success?("/account.get_utxos", %{"address" => Encoding.to_hex(address), "page" => page, "limit" => limit}) end def get_exitable_utxos(address) do success?("/account.get_exitable_utxos", %{"address" => Encoding.to_hex(address)}) end def get_balance(address) do success?("/account.get_balance", %{"address" => Encoding.to_hex(address)}) end def get_block(blknum) do response_body = rpc_call("block.get", %{blknum: blknum}, 200) case response_body do %{"success" => false, "data" => error} -> {:error, error} %{"success" => true, "data" => block} -> {:ok, block} end end def get_exit_data(blknum, txindex, oindex) do get_exit_data(Utxo.Position.encode(Utxo.position(blknum, txindex, oindex))) end def get_exit_challenge(blknum, txindex, oindex) do utxo_pos = Utxo.position(blknum, txindex, oindex) |> Utxo.Position.encode() data = success?("utxo.get_challenge_data", %{utxo_pos: utxo_pos}) decode16(data, ["exiting_tx", "txbytes", "sig"]) end def get_in_flight_exit(transaction) do exit_data = success?("in_flight_exit.get_data", %{txbytes: Encoding.to_hex(transaction)}) decode16(exit_data, ["in_flight_tx", "input_txs", "input_txs_inclusion_proofs", "in_flight_tx_sigs"]) end def get_in_flight_exit_competitors(transaction) do competitor_data = success?("in_flight_exit.get_competitor", %{txbytes: Encoding.to_hex(transaction)}) decode16(competitor_data, ["in_flight_txbytes", "competing_txbytes", "competing_sig", "competing_proof", "input_tx"]) end def get_prove_canonical(transaction) do competitor_data = success?("in_flight_exit.prove_canonical", %{txbytes: Encoding.to_hex(transaction)}) decode16(competitor_data, ["in_flight_txbytes", "in_flight_proof"]) end def submit(transaction) do submission_info = success?("transaction.submit", %{transaction: Encoding.to_hex(transaction)}) decode16(submission_info, ["txhash"]) end def get_input_challenge_data(transaction, input_index) do proof_data = success?("in_flight_exit.get_input_challenge_data", %{ txbytes: Encoding.to_hex(transaction), input_index: input_index }) decode16(proof_data, [ "in_flight_txbytes", "spending_txbytes", "spending_sig", "input_tx" ]) end def get_output_challenge_data(transaction, output_index) do proof_data = success?("in_flight_exit.get_output_challenge_data", %{ txbytes: Encoding.to_hex(transaction), output_index: output_index }) decode16(proof_data, [ "in_flight_txbytes", "in_flight_proof", "spending_txbytes", "spending_sig" ]) end def capture_log(function, max_waiting_ms \\ 2_000) do CaptureLog.capture_log(fn -> logs = CaptureLog.capture_log(fn -> function.() end) case logs do "" -> wait_for_log(max_waiting_ms) logs -> logs end end) end defp wait_for_log(max_waiting_ms, sleep_time_ms \\ 20) do steps = :erlang.ceil(max_waiting_ms / sleep_time_ms) Enum.reduce_while(1..steps, nil, fn _, _ -> logs = CaptureLog.capture_log(fn -> Process.sleep(sleep_time_ms) end) case logs do "" -> {:cont, ""} logs -> {:halt, logs} end end) end defp get_exit_data(encoded_position) do data = success?("utxo.get_exit_data", %{utxo_pos: encoded_position}) decode16(data, ["txbytes", "proof"]) end end ================================================ FILE: apps/omg_watcher/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.configure( exclude: [mix_based_child_chain: true, watcher: true, common: true, integration: true, property: true, wrappers: true] ) ExUnitFixtures.load_fixture_files(Path.join([Mix.Project.build_path(), "../../", "apps/*/test/**/fixtures.exs"])) ExUnitFixtures.start() ExUnit.start() {:ok, _} = Application.ensure_all_started(:httpoison) {:ok, _} = Application.ensure_all_started(:fake_server) # TODO: even though watcher does not have postgres, this needs to be here # because tests breach scope Mix.Task.run("ecto.create", ~w(--quiet)) Mix.Task.run("ecto.migrate", ~w(--quiet)) {:ok, _} = Application.ensure_all_started(:briefly) {:ok, _} = Application.ensure_all_started(:erlexec) ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/api/account.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.Account do @moduledoc """ Module provides operations related to plasma accounts. """ alias OMG.Utils.Paginator alias OMG.Watcher.Crypto alias OMG.WatcherInfo.DB @doc """ Returns a list of amounts of currencies that a given address owns """ @spec get_balance(Crypto.address_t()) :: list(DB.TxOutput.balance()) def get_balance(address) do DB.TxOutput.get_balance(address) end @doc """ Gets all utxos belonging to the given address. """ @spec get_utxos(Keyword.t()) :: Paginator.t(%DB.TxOutput{}) def get_utxos(params) do DB.TxOutput.get_utxos(params) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/api/block.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.Block do @moduledoc """ Module provides operations related to plasma blocks. """ alias OMG.Utils.Paginator alias OMG.WatcherInfo.DB @default_blocks_limit 100 @doc """ Retrieves a specific block by block number """ @spec get(pos_integer()) :: {:ok, %DB.Block{}} | {:error, :block_not_found} def get(blknum) do case DB.Block.get(blknum) do nil -> {:error, :block_not_found} block -> {:ok, block} end end @doc """ Retrieves a list of blocks. Length of the list is limited by `limit` and `page` arguments. """ @spec get_blocks(Keyword.t()) :: Paginator.t(%DB.Block{}) def get_blocks(constraints) do paginator = Paginator.from_constraints(constraints, @default_blocks_limit) DB.Block.get_blocks(paginator) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/api/deposit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.Deposit do @moduledoc """ Module provides operations related to deposits. """ alias OMG.Utils.Paginator alias OMG.WatcherInfo.DB @default_events_limit 100 @doc """ Retrieves a list of deposits. Length of the list is limited by `limit` and `page` arguments. """ @spec get_deposits(Keyword.t()) :: Paginator.t(%DB.EthEvent{}) def get_deposits(constraints) do {:ok, address} = Keyword.fetch(constraints, :address) constraints |> Paginator.from_constraints(@default_events_limit) |> DB.EthEvent.get_deposits(address) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/api/stats.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.Stats do @moduledoc """ Module provides operations related to network statistics. """ alias OMG.WatcherInfo.DB.Block alias OMG.WatcherInfo.DB.Transaction @seconds_in_twenty_four_hours 86_400 @doc """ Retrieves network statistics. """ def get() do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime_24_hours = end_datetime - @seconds_in_twenty_four_hours response = %{ transaction_count: %{ all_time: Transaction.count_all(), last_24_hours: Transaction.count_all_between_timestamps(start_datetime_24_hours, end_datetime) }, block_count: %{ all_time: Block.count_all(), last_24_hours: Block.count_all_between_timestamps(start_datetime_24_hours, end_datetime) }, average_block_interval_seconds: %{ all_time: get_average_block_interval_all_time(), last_24_hours: get_average_block_interval_between(start_datetime_24_hours, end_datetime) } } {:ok, response} end @doc """ Calculates the all-time average block interval. """ @spec get_average_block_interval_all_time() :: float() | nil def get_average_block_interval_all_time() do block_count = Block.count_all() case block_count do n when n < 2 -> nil _ -> %{:max => max, :min => min} = Block.get_timestamp_range_all() # Formula for average of difference max |> Kernel.-(min) |> Kernel./(block_count - 1) end end @doc """ Calculates the average block interval between two given timestamps. """ @spec get_average_block_interval_between(non_neg_integer(), non_neg_integer()) :: float() | nil def get_average_block_interval_between(start_datetime, end_datetime) do block_count = Block.count_all_between_timestamps(start_datetime, end_datetime) case block_count do n when n < 2 -> nil _ -> %{:max => max, :min => min} = Block.get_timestamp_range_between(start_datetime, end_datetime) # Formula for average of difference max |> Kernel.-(min) |> Kernel./(block_count - 1) end end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/api/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.Transaction do @moduledoc """ Module provides API for transactions """ alias OMG.Utils.Paginator alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.HttpRPC.Client alias OMG.WatcherInfo.Transaction, as: TransactionCreator require Utxo require Transaction.Payment @default_transactions_limit 200 @type create_t() :: {:ok, %{ result: :complete | :intermediate, transactions: nonempty_list(TransactionCreator.transaction_with_typed_data_t()) }} | {:error, :too_many_outputs} | {:error, :empty_transaction} | {:error, {:insufficient_funds, list(map())}} @doc """ Retrieves a specific transaction by id """ @spec get(binary()) :: {:ok, %DB.Transaction{}} | {:error, :transaction_not_found} def get(transaction_id) do if transaction = DB.Transaction.get(transaction_id), do: {:ok, transaction}, else: {:error, :transaction_not_found} end @doc """ Retrieves a list of transactions that: - (optionally) a given address is involved as input or output owner. - (optionally) belong to a given child block number Length of the list is limited by `limit` argument """ @spec get_transactions(Keyword.t()) :: Paginator.t(%DB.Transaction{}) def get_transactions(constraints) do paginator = Paginator.from_constraints(constraints, @default_transactions_limit) constraints |> Keyword.drop([:limit, :page]) |> DB.Transaction.get_by_filters(paginator) end @doc """ Passes the signed transaction to the child chain. Caution: This function is unaware of the child chain's security status, e.g.: * Watcher is fully synced, * all operator blocks have been verified, * transaction doesn't spend funds not yet mined * etc... """ @spec submit(Transaction.Signed.t()) :: Client.response_t() | {:error, atom()} def submit(%Transaction.Signed{} = signed_tx) do url = Application.get_env(:omg_watcher_info, :child_chain_url) signed_tx |> Transaction.Signed.encode() |> Client.submit(url) end @doc """ Given order finds spender's inputs sufficient to perform a payment. If also provided with receiver's address, creates and encodes a transaction. """ @spec create(TransactionCreator.order_t()) :: create_t() def create(order) do owner_inputs = order.owner |> DB.TxOutput.get_sorted_grouped_utxos(:desc) |> TransactionCreator.select_inputs(order) case owner_inputs do {:ok, inputs} -> inputs |> get_utxos_count() |> create_transaction(inputs, order) err -> err end end @doc """ Converts parameter keyword list to a map before passing it to multi-clause "handle_merge/`1" """ @spec merge(Keyword.t()) :: create_t() def merge(parameters) do parameters |> Map.new() |> handle_merge() end @spec handle_merge(map()) :: create_t() defp handle_merge(%{address: address, currency: currency}) do merge_inputs = address |> DB.TxOutput.get_sorted_grouped_utxos(:asc) |> Map.get(currency, []) case merge_inputs do [] -> {:error, :no_inputs_found} [_single_input] -> {:error, :single_input} inputs -> {:ok, TransactionCreator.generate_merge_transactions(inputs)} end end defp handle_merge(%{utxo_positions: utxo_positions}) do with {:ok, inputs} <- get_merge_inputs(utxo_positions), :ok <- no_duplicates(inputs), :ok <- single_owner(inputs), :ok <- single_currency(inputs) do {:ok, TransactionCreator.generate_merge_transactions(inputs)} end end defp get_utxos_count(currencies) do Enum.reduce(currencies, 0, fn {_, currency_inputs}, acc -> acc + length(currency_inputs) end) end defp create_transaction(utxos_count, inputs, _order) when utxos_count > Transaction.Payment.max_inputs() do transactions = inputs |> Enum.reduce([], fn {_, token_inputs}, acc -> merged_transactions = token_inputs |> TransactionCreator.generate_merge_transactions() |> Enum.reverse() merged_transactions ++ acc end) |> Enum.reverse() respond({:ok, transactions}, :intermediate) end defp create_transaction(_utxos_count, inputs, order) do inputs |> TransactionCreator.create(order) |> respond(:complete) end @spec get_merge_inputs(list()) :: {:ok, list()} | {:error, atom()} defp get_merge_inputs(utxo_positions) do Enum.reduce_while(utxo_positions, {:ok, []}, fn encoded_position, {:ok, acc} -> case encoded_position |> Utxo.Position.decode!() |> DB.TxOutput.get_by_position() do nil -> {:halt, {:error, :position_not_found}} input -> {:cont, {:ok, [input | acc]}} end end) end @spec no_duplicates(list()) :: :ok | {:error, :duplicate_input_positions} defp no_duplicates(inputs) do inputs |> Enum.uniq() |> length() |> case do n when n == length(inputs) -> :ok _ -> {:error, :duplicate_input_positions} end end @spec single_owner(list()) :: :ok | {:error, :multiple_input_owners} defp single_owner(inputs) do case inputs |> Enum.uniq_by(fn input -> input.owner end) |> length() do 1 -> :ok _ -> {:error, :multiple_input_owners} end end @spec single_currency(list()) :: :ok | {:error, :multiple_currencies} defp single_currency(inputs) do case inputs |> Enum.uniq_by(fn input -> input.currency end) |> length() do 1 -> :ok _ -> {:error, :multiple_currencies} end end defp respond({:ok, transactions}, result), do: {:ok, %{result: result, transactions: transactions}} defp respond(error, _), do: error end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Application do @moduledoc false use Application require Logger def start(_type, _args) do _ = Logger.info("Starting #{inspect(__MODULE__)}") _ = attach_ecto_telemetry() start_root_supervisor() end def start_root_supervisor() do # root supervisor must stop whenever any of its children supervisors goes down (children carry the load of restarts) children = [ %{ id: OMG.WatcherInfo.Supervisor, start: {OMG.WatcherInfo.Supervisor, :start_link, []}, restart: :permanent, type: :supervisor } ] opts = [ strategy: :one_for_one, # whenever any of supervisor's children goes down, so it does max_restarts: 0, name: OMG.WatcherInfo.RootSupervisor ] Supervisor.start_link(children, opts) end def start_phase(:attach_telemetry, :normal, _phase_args) do handlers = [ ["measure-watcher-info", OMG.WatcherInfo.Measure.supported_events(), &OMG.WatcherInfo.Measure.handle_event/4, nil] ] Enum.each(handlers, fn handler -> case apply(:telemetry, :attach_many, handler) do :ok -> :ok {:error, :already_exists} -> :ok end end) end defp attach_ecto_telemetry() do event = [:omg_watcher, :watcher_info, :db, :repo, :query] :telemetry.attach("spandex-query-tracer", event, &SpandexEcto.TelemetryAdapter.handle_event/4, nil) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/block_applicator.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.BlockApplicator do @moduledoc """ Handles new block applications from Watcher's `BlockGetter` and persists them for further processing """ alias OMG.WatcherInfo.DB @type block_application_t :: %{ eth_height: pos_integer(), hash: binary(), number: pos_integer(), timestamp: pos_integer(), transactions: [OMG.Watcher.State.Transaction.Recovered.t()] } @doc """ Inserts a block along with transactions and outputs, does not break when block already exists. """ @spec insert_block!(block_application_t()) :: :ok def insert_block!(block) do block |> DB.Block.insert_from_block_application() |> case do {:ok, _} -> :ok # Ensures insert idempotency. Trying to add block with the same `blknum` that already exists takes no effect. # See also [comment](https://github.com/omgnetwork/elixir-omg/pull/1769#discussion_r528700434) {:error, "current_block", changeset, _explain} -> [{:blknum, {_msg, [constraint: :unique, constraint_name: _name]}}] = changeset.errors() :ok end end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/block.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.Block do @moduledoc """ Ecto schema for Plasma Chain block """ use Ecto.Schema require Logger use Spandex.Decorators import Ecto.Changeset alias OMG.Utils.Paginator alias OMG.Watcher.State alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.DB.Block.Chunk import Ecto.Query, only: [from: 2] @type mined_block() :: %{ transactions: [State.Transaction.Recovered.t()], blknum: pos_integer(), blkhash: <<_::256>>, timestamp: pos_integer(), eth_height: pos_integer() } @primary_key {:blknum, :integer, []} @derive {Phoenix.Param, key: :blknum} schema "blocks" do field(:hash, :binary) field(:eth_height, :integer) field(:timestamp, :integer) field(:tx_count, :integer, virtual: true, default: nil) has_many(:transactions, DB.Transaction, foreign_key: :blknum) timestamps(type: :utc_datetime_usec) end @spec get_max_blknum() :: non_neg_integer() @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_max_blknum() do DB.Repo.aggregate(__MODULE__, :max, :blknum) end @doc """ Gets a block specified by a block number. """ @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get(blknum) do query = from( block in base_query(), where: [blknum: ^blknum] ) DB.Repo.one(query) end def base_query() do from( block in __MODULE__, left_join: tx in assoc(block, :transactions), group_by: block.blknum, select: %{block | tx_count: count(tx.txhash)} ) end @doc """ Returns a list of blocks """ @spec get_blocks(Paginator.t(%DB.Block{})) :: Paginator.t(%DB.Block{}) @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_blocks(paginator) do query_get_last(paginator.data_paging) |> DB.Repo.all() |> Paginator.set_data(paginator) end @spec query_timestamp_between(Ecto.Query.t(), non_neg_integer(), non_neg_integer()) :: Ecto.Query.t() def query_timestamp_between(query, start_datetime, end_datetime) do from(block in query, where: block.timestamp >= ^start_datetime and block.timestamp <= ^end_datetime ) end @doc """ Returns the total number of blocks in between given timestamps """ @spec count_all_between_timestamps(non_neg_integer(), non_neg_integer()) :: non_neg_integer() @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def count_all_between_timestamps(start_datetime, end_datetime) do query_count() |> query_timestamp_between(start_datetime, end_datetime) |> DB.Repo.one!() end @doc """ Returns the total number of blocks """ @spec count_all() :: non_neg_integer() @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def count_all() do DB.Repo.one!(query_count()) end @doc """ Returns a map with the timestamps of the earliest and latest blocks of all time. """ @spec get_timestamp_range_all :: %{min: non_neg_integer(), max: non_neg_integer()} @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_timestamp_range_all() do DB.Repo.one!(query_timestamp_range()) end @doc """ Returns a map with the timestamps of the earliest and latest blocks within a given time range. """ @spec get_timestamp_range_between(non_neg_integer(), non_neg_integer()) :: %{ min: non_neg_integer(), max: non_neg_integer() } @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_timestamp_range_between(start_datetime, end_datetime) do query_timestamp_range() |> query_timestamp_between(start_datetime, end_datetime) |> DB.Repo.one!() end @spec insert(map()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()} @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def insert(params) do %__MODULE__{} |> changeset(params) |> DB.Repo.insert() end @doc """ Takes a block application and inserts block into the database. """ # sobelow_skip ["Misc.BinToTerm"] @spec insert_from_block_application(map()) :: {:ok, %__MODULE__{}} | {:error, binary(), Ecto.Changeset.t(), any()} @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def insert_from_block_application(block_application) do %{number: block_number, transactions: transactions} = block_application {db_txs, db_outputs, db_inputs} = prepare_db_transactions(transactions, block_number) current_block = %{ blknum: block_number, hash: block_application.hash, timestamp: block_application.timestamp, eth_height: block_application.eth_height } db_txs_stream = Chunk.chunk(db_txs) db_outputs_stream = Chunk.chunk(db_outputs) multi = Ecto.Multi.new() |> Ecto.Multi.insert("current_block", changeset(%__MODULE__{}, current_block), []) |> prepare_inserts(db_txs_stream, "db_txs_", DB.Transaction) |> prepare_inserts(db_outputs_stream, "db_outputs_", DB.TxOutput) |> DB.TxOutput.spend_utxos(db_inputs) {insert_duration, result} = :timer.tc(DB.Repo, :transaction, [multi]) case result do {:ok, _} -> _ = Logger.info("Block \##{block_number} persisted in WatcherDB, done in #{insert_duration / 1000}ms") result {:error, name, changeset, explain} -> _ = Logger.info("Block \##{block_number} not persisted in WatcherDB, done in #{insert_duration / 1000}ms") _ = Logger.info("Error in transaction #{name}: #{inspect(changeset.errors)} #{inspect(explain)}") result end end defp prepare_inserts(multi, stream, name, schema) do {ecto_multi, _} = Enum.reduce(stream, {multi, 0}, fn action, {multi, index} -> {Ecto.Multi.insert_all(multi, name <> "#{index}", schema, action), index + 1} end) ecto_multi end @spec prepare_db_transactions(State.Transaction.Recovered.t(), pos_integer()) :: {[map()], [%DB.TxOutput{}], [%DB.TxOutput{}]} defp prepare_db_transactions(mined_transactions, block_number) do mined_transactions |> Stream.with_index() |> Enum.reduce({[], [], []}, fn {tx, txindex}, {tx_list, output_list, input_list} -> {tx, outputs, inputs} = prepare_db_transaction(tx, block_number, txindex) {[tx | tx_list], outputs ++ output_list, inputs ++ input_list} end) end @spec prepare_db_transaction(State.Transaction.Recovered.t(), pos_integer(), integer()) :: [ {map(), [%DB.TxOutput{}], [%DB.TxOutput{}]} ] defp prepare_db_transaction(recovered_tx, block_number, txindex) do tx = Map.fetch!(recovered_tx, :signed_tx) raw_tx = Map.fetch!(tx, :raw_tx) tx_type = Map.fetch!(raw_tx, :tx_type) metadata = Map.get(raw_tx, :metadata) signed_tx_bytes = Map.fetch!(recovered_tx, :signed_tx_bytes) tx_hash = State.Transaction.raw_txhash(tx) transaction = create(block_number, txindex, tx_hash, tx_type, signed_tx_bytes, metadata) outputs = DB.TxOutput.create_outputs(block_number, txindex, tx_hash, tx) inputs = DB.TxOutput.create_inputs(tx, tx_hash) {transaction, outputs, inputs} end @spec create(pos_integer(), integer(), binary(), pos_integer(), binary(), State.Transaction.metadata()) :: map() defp create(block_number, txindex, txhash, txtype, txbytes, metadata) do %{ txhash: txhash, txtype: txtype, txbytes: txbytes, blknum: block_number, txindex: txindex, metadata: metadata } end @spec query_timestamp_range() :: Ecto.Query.t() defp query_timestamp_range() do from(block in __MODULE__, select: %{ max: max(block.timestamp), min: min(block.timestamp) } ) end @spec query_count() :: Ecto.Query.t() defp query_count() do from(block in __MODULE__, select: count()) end defp changeset(block, params) do block |> cast(params, [:blknum, :hash, :timestamp, :eth_height]) |> unique_constraint(:blknum, name: :blocks_pkey) |> validate_required([:blknum, :hash, :timestamp, :eth_height]) |> validate_number(:blknum, greater_than: 0) |> validate_number(:timestamp, greater_than: 0) |> validate_number(:eth_height, greater_than: 0) end defp query_get_last(%{limit: limit, page: page}) do offset = (page - 1) * limit from( block in base_query(), order_by: [desc: :blknum], limit: ^limit, offset: ^offset ) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/eth_event.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.EthEvent do @moduledoc """ Ecto schema for events logged by Ethereum """ import Ecto.Query, only: [from: 2] import Ecto.Changeset use Ecto.Schema use Spandex.Decorators alias OMG.Eth.Encoding alias OMG.Utils.Paginator alias OMG.Watcher.Crypto alias OMG.Watcher.Utxo alias OMG.Watcher.Utxo.Position alias OMG.Watcher.WireFormatTypes alias OMG.WatcherInfo.DB require Utxo @typep available_event_type_t() :: :standard_exit | :in_flight_exit @typep output_pointer_t() :: %{utxo_pos: pos_integer()} | %{txhash: Crypto.hash_t(), oindex: non_neg_integer()} @typep event_data_t() :: %{ call_data: output_pointer_t(), root_chain_txhash: charlist(), log_index: non_neg_integer(), eth_height: pos_integer() } @primary_key false schema "ethevents" do field(:root_chain_txhash, :binary, primary_key: true) field(:log_index, :integer, primary_key: true) field(:event_type, OMG.WatcherInfo.DB.Types.AtomType) field(:eth_height, :integer) field(:root_chain_txhash_event, :binary) many_to_many( :txoutputs, DB.TxOutput, join_through: DB.EthEventTxOutput, join_keys: [root_chain_txhash_event: :root_chain_txhash_event, child_chain_utxohash: :child_chain_utxohash] ) timestamps(type: :utc_datetime_usec) end @doc """ Inserts deposits based on a list of event maps (if not already inserted before) """ @spec insert_deposits!([OMG.Watcher.State.Core.deposit()]) :: :ok def insert_deposits!(deposits) do Enum.each(deposits, &insert_deposit!/1) end @spec insert_deposit!(OMG.Watcher.State.Core.deposit()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()} @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) defp insert_deposit!(params) do %{ root_chain_txhash: root_chain_txhash, log_index: log_index, blknum: blknum, owner: owner, eth_height: eth_height, currency: currency, amount: amount } = params event_type = :deposit position = Utxo.position(blknum, 0, 0) root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, log_index) case get(root_chain_txhash_event) do nil -> deposit = %__MODULE__{ root_chain_txhash_event: root_chain_txhash_event, log_index: log_index, root_chain_txhash: root_chain_txhash, event_type: event_type, eth_height: eth_height, # a deposit from the root chain will only ever have 1 childchain txoutput object txoutputs: [ %DB.TxOutput{ child_chain_utxohash: generate_child_chain_utxohash(position), blknum: blknum, txindex: 0, oindex: 0, otype: WireFormatTypes.output_type_for(:output_payment_v1), owner: owner, currency: currency, amount: amount } ] } {:ok, _} = DB.Repo.insert(deposit) # an ethevents row just got inserted, now return the ethevent with all populated fields including # those populated by the DB (eg: inserted_at, updated_at, ...) {:ok, get(root_chain_txhash_event)} existing_deposit -> {:ok, existing_deposit} end end @doc """ Uses a list of encoded `Utxo.Position`s to insert the exits (if not already inserted before) """ @spec insert_exits!([event_data_t()], available_event_type_t(), atom()) :: :ok def insert_exits!(exits, event_type, event_type_detailed) do ensure_output = expect_output_existence?(event_type, event_type_detailed) exits |> Stream.map(&utxo_exit_from_exit_event/1) |> Enum.each(&insert_exit!(&1, event_type, ensure_output)) end def txoutput_changeset(txoutput, params, ethevent) do fields = [:blknum, :txindex, :oindex, :owner, :amount, :currency, :child_chain_utxohash] txoutput |> cast(params, fields) |> put_assoc(:ethevents, txoutput.ethevents ++ [ethevent]) |> validate_required(fields) end @doc """ Generate a unique child_chain_utxohash from the Utxo.position """ @spec generate_child_chain_utxohash(Utxo.Position.t()) :: OMG.Watcher.Crypto.hash_t() def generate_child_chain_utxohash(position) do Crypto.hash("<#{Position.encode(position)}>") end def generate_root_chain_txhash_event(root_chain_txhash, log_index) do (Encoding.to_hex(root_chain_txhash) <> Integer.to_string(log_index)) |> Crypto.hash() end @doc """ Retrieves event by `root_chain_txhash_event` (unique identifier). Preload txoutputs in a single query as there will not be a large number of them. """ @spec get(binary()) :: %__MODULE__{} def get(root_chain_txhash_event) do DB.Repo.one( from(ethevent in base_query(), where: ethevent.root_chain_txhash_event == ^root_chain_txhash_event ) ) end @doc """ Gets a paginated list of deposits filtered by address. """ @spec get_deposits( paginator :: Paginator.t(%DB.EthEvent{}), address :: Crypto.address_t() ) :: Paginator.t(%DB.EthEvent{}) @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_deposits(paginator, address) do base_query() |> query_deposits() |> query_by_address(address) |> query_paginated(paginator.data_paging) |> DB.Repo.all() |> Paginator.set_data(paginator) end @spec utxo_exit_from_exit_event(event_data_t()) :: %{ root_chain_txhash: binary(), log_index: non_neg_integer(), eth_height: pos_integer(), output_pointer: tuple() } defp utxo_exit_from_exit_event(%{ call_data: output_pointer, root_chain_txhash: root_chain_txhash, log_index: log_index, eth_height: eth_height }) do %{ root_chain_txhash: root_chain_txhash, log_index: log_index, output_pointer: transform_output_pointer(output_pointer), eth_height: eth_height } end defp transform_output_pointer(%{utxo_pos: utxo_pos}), do: {:utxo_position, Position.decode!(utxo_pos)} defp transform_output_pointer(%{txhash: txhash, oindex: oindex}), do: {:output_id, {txhash, oindex}} @spec insert_exit!( %{ root_chain_txhash: binary(), log_index: non_neg_integer(), output_pointer: {:utxo_position, Position.t()} | {:output_id, tuple()}, eth_height: pos_integer() }, available_event_type_t(), boolean() ) :: :ok | :noop defp insert_exit!(event, event_type, ensure_output) do %{ root_chain_txhash: root_chain_txhash, log_index: log_index, eth_height: eth_height, output_pointer: output_pointer } = event root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, log_index) ethevent = case get(root_chain_txhash_event) do nil -> %__MODULE__{ root_chain_txhash_event: root_chain_txhash_event, log_index: log_index, root_chain_txhash: root_chain_txhash, eth_height: eth_height, event_type: event_type } event -> event end case resolve_tx_output(output_pointer) do %DB.TxOutput{} = tx_output -> :ok = insert_exit_if_not_exist(ethevent, tx_output) # The transaction's output is expected to be found in the DB unless explicitly allowed by `ensure_output = false` # More explanation can be found in [the issue discussion](https://github.com/omgnetwork/elixir-omg/issues/1760#issuecomment-722313713). nil when not ensure_output -> :noop end end @spec resolve_tx_output(tuple()) :: %DB.TxOutput{} | nil defp resolve_tx_output({:utxo_position, utxo_pos}), do: DB.TxOutput.get_by_position(utxo_pos) defp resolve_tx_output({:output_id, {txhash, oindex}}), do: DB.TxOutput.get_by_output_id(txhash, oindex) @spec insert_exit_if_not_exist(%__MODULE__{}, %DB.TxOutput{} | nil) :: :ok defp insert_exit_if_not_exist(ethevent, tx_output) do # if TxOutput is already assiociated with this (or any other) spending event no action is needed if output_spent?(tx_output), do: :ok, else: do_insert_exit(ethevent, tx_output) end @spec do_insert_exit(%__MODULE__{}, %DB.TxOutput{}) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()} @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) defp do_insert_exit(ethevent, tx_output) when ethevent != nil and tx_output != nil do # sanity check false = output_spent?(tx_output) decoded_utxo_position = Utxo.position(tx_output.blknum, tx_output.txindex, tx_output.oindex) tx_output |> txoutput_changeset(%{child_chain_utxohash: generate_child_chain_utxohash(decoded_utxo_position)}, ethevent) |> DB.Repo.update!() :ok end defp base_query() do from( ethevent in __MODULE__, order_by: [desc: :eth_height], left_join: txoutputs in assoc(ethevent, :txoutputs), preload: [txoutputs: txoutputs] ) end defp query_by_address(query, address) do from( [ethevent, txoutputs] in query, where: txoutputs.owner == ^address ) end defp query_deposits(query) do from( ethevent in query, where: ethevent.event_type == ^:deposit ) end defp query_paginated(query, paginator) do offset = (paginator.page - 1) * paginator.limit from( event in query, limit: ^paginator.limit, offset: ^offset ) end # Tells whether `TxOutput` was already spent # NOTE: it looks a little too deep into DB.TxOutput module, but I don't want to extent its API @spec output_spent?(%DB.TxOutput{}) :: boolean() defp output_spent?(%DB.TxOutput{spending_txhash: nil} = tx_output) do Enum.any?(tx_output.ethevents, &(&1.event_type != :deposit)) end defp output_spent?(%DB.TxOutput{}), do: true # Tells whether processing exit event, exited output has to be present in the database # For more see: https://github.com/omgnetwork/elixir-omg/issues/1760#issuecomment-722313713 defp expect_output_existence?(:standard_exit, _), do: true defp expect_output_existence?(:in_flight_exit, :InFlightTxOutputPiggybacked), do: false defp expect_output_existence?(:in_flight_exit, _any), do: true end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/eth_event_txoutput.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.EthEventTxOutput do @moduledoc """ Ecto Schema representing a many-to-many ethevents <-> txoutputs association """ use Ecto.Schema alias OMG.WatcherInfo.DB @primary_key false schema "ethevents_txoutputs" do belongs_to(:ethevents, DB.EthEvent, references: :root_chain_txhash_event, foreign_key: :root_chain_txhash_event, type: :binary ) belongs_to(:txoutputs, DB.TxOutput, references: :child_chain_utxohash, foreign_key: :child_chain_utxohash, type: :binary ) timestamps(type: :utc_datetime_usec) end def changeset(params \\ %{}) do %__MODULE__{} |> Ecto.Changeset.cast(params, [:root_chain_txhash_event, :child_chain_utxohash]) |> Ecto.Changeset.validate_required([:root_chain_txhash_event, :child_chain_utxohash]) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/repo.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.Repo do use Ecto.Repo, otp_app: :omg_watcher_info, adapter: Ecto.Adapters.Postgres end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.Transaction do @moduledoc """ Ecto Schema representing a transaction """ use Ecto.Schema use Spandex.Decorators require Logger alias OMG.Utils.Paginator alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB require Utxo import Ecto.Query, only: [from: 2, where: 2, where: 3, select: 3, join: 5, distinct: 2] @primary_key {:txhash, :binary, []} @derive {Phoenix.Param, key: :txhash} @derive {Jason.Encoder, except: [:__meta__]} schema "transactions" do field(:txindex, :integer) field(:txtype, :integer) field(:txbytes, :binary) field(:metadata, :binary) has_many(:inputs, DB.TxOutput, foreign_key: :spending_txhash) has_many(:outputs, DB.TxOutput, foreign_key: :creating_txhash) belongs_to(:block, DB.Block, foreign_key: :blknum, references: :blknum, type: :integer) timestamps(type: :utc_datetime_usec) end @doc """ Gets a transaction specified by a hash. Optionally, fetches block which the transaction was included in. """ @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get(hash) do query = from( __MODULE__, where: [txhash: ^hash], preload: [ block: ^DB.Block.base_query(), inputs: ^from(txo in DB.TxOutput, order_by: :spending_tx_oindex), outputs: ^from(txo in DB.TxOutput, order_by: :oindex) ] ) DB.Repo.one(query) end @doc """ Returns transactions possibly filtered by constraints * constraints - accepts keyword in the form of [schema_field: value] """ @spec get_by_filters(Keyword.t(), Paginator.t(%__MODULE__{})) :: Paginator.t(%__MODULE__{}) @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_by_filters(constraints, paginator) do allowed_constraints = [:address, :blknum, :txindex, :txtypes, :metadata, :end_datetime] constraints = filter_constraints(constraints, allowed_constraints) # we need to handle complex constraints with dedicated modifier function {address, constraints} = Keyword.pop(constraints, :address) {txtypes, constraints} = Keyword.pop(constraints, :txtypes) {end_datetime, constraints} = Keyword.pop(constraints, :end_datetime) query_get_last(paginator.data_paging) |> query_get_by_address(address) |> query_get_by_txtypes(txtypes) |> query_get_by(constraints) |> query_get_by_end_datetime(end_datetime) |> DB.Repo.all() |> Paginator.set_data(paginator) end defp query_get_last(%{limit: limit, page: page}) do offset = (page - 1) * limit from( __MODULE__, order_by: [desc: :blknum, desc: :txindex], limit: ^limit, offset: ^offset, preload: [ :block, inputs: ^from(txo in DB.TxOutput, order_by: :spending_tx_oindex), outputs: ^from(txo in DB.TxOutput, order_by: :oindex) ] ) end @spec query_count() :: Ecto.Query.t() defp query_count() do from(transaction in __MODULE__, select: count()) end @spec query_timestamp_between(Ecto.Query.t(), non_neg_integer(), non_neg_integer()) :: Ecto.Query.t() def query_timestamp_between(query, start_datetime, end_datetime) do from(transaction in query, join: block in assoc(transaction, :block), where: block.timestamp >= ^start_datetime and block.timestamp <= ^end_datetime ) end @doc """ Returns the total number of transactions between the given timestamps. """ @spec count_all_between_timestamps(non_neg_integer(), non_neg_integer()) :: non_neg_integer() @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def count_all_between_timestamps(start_datetime, end_datetime) do query_count() |> query_timestamp_between(start_datetime, end_datetime) |> DB.Repo.one!() end @doc """ Returns the total number of transactions """ @spec count_all() :: non_neg_integer() @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def count_all() do DB.Repo.one!(query_count()) end defp query_get_by_address(query, nil), do: query defp query_get_by_address(query, address) do query |> join(:inner, [t], o in DB.TxOutput, on: t.txhash == o.creating_txhash or t.txhash == o.spending_txhash) |> where([t, o], o.owner == ^address) |> select([t, o], t) |> distinct(true) end defp query_get_by_txtypes(query, nil), do: query defp query_get_by_txtypes(query, []), do: query defp query_get_by_txtypes(query, txtypes) do where(query, [t], t.txtype in ^txtypes) end defp query_get_by(query, constraints) when is_list(constraints), do: query |> where(^constraints) @spec get_by_blknum(pos_integer) :: list(%__MODULE__{}) @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_by_blknum(blknum) do __MODULE__ |> query_get_by(blknum: blknum) |> from(order_by: [asc: :txindex]) |> DB.Repo.all() end defp query_get_by_end_datetime(query, nil), do: query defp query_get_by_end_datetime(query, end_datetime) do from(transaction in query, join: block in assoc(transaction, :block), where: block.timestamp <= ^end_datetime ) end @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_by_position(blknum, txindex) do DB.Repo.one(from(__MODULE__, where: [blknum: ^blknum, txindex: ^txindex])) end defp filter_constraints(constraints, allowed_constraints) do case Keyword.drop(constraints, allowed_constraints) do [{out_of_schema, _} | _] -> _ = Logger.warn("Constraint on #{inspect(out_of_schema)} does not exist in schema and was dropped from the query") constraints |> Keyword.take(allowed_constraints) [] -> constraints end end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/txoutput.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.TxOutput do @moduledoc """ Ecto schema for transaction's output or input """ use Ecto.Schema use Spandex.Decorators alias OMG.Utils.Paginator alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.DB.Repo require Utxo import Ecto.Query, only: [from: 2] @default_get_utxos_limit 200 @type balance() :: %{ currency: binary(), amount: non_neg_integer() } @type exit_t() :: %{ utxo_pos: pos_integer(), txbytes: binary(), proof: binary(), sigs: binary() } @type order_t() :: :asc | :desc @primary_key false schema "txoutputs" do field(:blknum, :integer, primary_key: true) field(:txindex, :integer, primary_key: true) field(:oindex, :integer, primary_key: true) field(:owner, :binary) field(:otype, :integer) field(:amount, OMG.WatcherInfo.DB.Types.IntegerType) field(:currency, :binary) field(:proof, :binary) field(:spending_tx_oindex, :integer) field(:child_chain_utxohash, :binary) belongs_to(:creating_transaction, DB.Transaction, foreign_key: :creating_txhash, references: :txhash, type: :binary) belongs_to(:spending_transaction, DB.Transaction, foreign_key: :spending_txhash, references: :txhash, type: :binary) many_to_many( :ethevents, DB.EthEvent, join_through: DB.EthEventTxOutput, join_keys: [child_chain_utxohash: :child_chain_utxohash, root_chain_txhash_event: :root_chain_txhash_event] ) timestamps(type: :utc_datetime_usec) end # preload ethevents in a single query as there will not be a large number of them @spec get_by_position(Utxo.Position.t()) :: map() | nil @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_by_position(Utxo.position(blknum, txindex, oindex)) do Repo.one( from(txoutput in __MODULE__, preload: [:ethevents], where: txoutput.blknum == ^blknum and txoutput.txindex == ^txindex and txoutput.oindex == ^oindex ) ) end @spec get_by_output_id(txhash :: OMG.Watcher.Crypto.hash_t(), oindex :: non_neg_integer()) :: map() | nil @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_by_output_id(txhash, oindex) do Repo.one( from(txoutput in __MODULE__, preload: [:ethevents], where: txoutput.creating_txhash == ^txhash and txoutput.oindex == ^oindex ) ) end @spec get_utxos(keyword) :: OMG.Utils.Paginator.t(%__MODULE__{}) @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_utxos(params) do address = Keyword.fetch!(params, :address) paginator = Paginator.from_constraints(params, @default_get_utxos_limit) %{limit: limit, page: page} = paginator.data_paging offset = (page - 1) * limit address |> query_get_utxos() |> from(limit: ^limit, offset: ^offset) |> Repo.all() |> Paginator.set_data(paginator) end @spec get_balance(Crypto.address_t()) :: list(balance()) @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) def get_balance(owner) do query = from( txoutput in __MODULE__, left_join: ethevent in assoc(txoutput, :ethevents), # select txoutputs by owner that have neither been spent nor have a corresponding ethevents exit events where: txoutput.owner == ^owner and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment( " NOT EXISTS (SELECT 1 FROM ethevents_txoutputs AS etfrag JOIN ethevents AS efrag ON etfrag.root_chain_txhash_event=efrag.root_chain_txhash_event AND efrag.event_type = ANY(?) AND etfrag.child_chain_utxohash = ?)", ["standard_exit", "in_flight_exit"], txoutput.child_chain_utxohash )), group_by: txoutput.currency, select: {txoutput.currency, sum(txoutput.amount)} ) Repo.all(query) |> Enum.map(fn {currency, amount} -> # defends against sqlite that returns integer here amount = amount |> Decimal.new() |> Decimal.to_integer() %{currency: currency, amount: amount} end) end @spec spend_utxos(Ecto.Multi.t(), [map()]) :: Ecto.Multi.t() def spend_utxos(multi, db_inputs) do utc_now = DateTime.utc_now() {multi0, _} = Enum.reduce(db_inputs, {multi, 0}, fn data, {multi, index} -> {Utxo.position(blknum, txindex, oindex), spending_oindex, spending_txhash} = data {Ecto.Multi.update_all( multi, "spend_utxos_#{index}", from(p in DB.TxOutput, where: p.blknum == ^blknum and p.txindex == ^txindex and p.oindex == ^oindex ), set: [ spending_tx_oindex: spending_oindex, spending_txhash: spending_txhash, updated_at: utc_now ] ), index + 1} end) multi0 end @spec create_outputs(pos_integer(), integer(), binary(), Transaction.any_flavor_t()) :: [map()] def create_outputs(blknum, txindex, txhash, tx) do # zero-value outputs are not inserted, tx can have no outputs at all outputs = tx |> Transaction.get_outputs() |> Enum.with_index() |> Enum.flat_map(fn {%{currency: currency, owner: owner, amount: amount, output_type: otype}, oindex} -> create_output(otype, blknum, txindex, oindex, txhash, owner, currency, amount) end) outputs end defp create_output(_otype, _blknum, _txindex, _txhash, _oindex, _owner, _currency, 0), do: [] defp create_output(otype, blknum, txindex, oindex, txhash, owner, currency, amount) when amount > 0 do [ %{ otype: otype, blknum: blknum, txindex: txindex, oindex: oindex, owner: owner, amount: amount, currency: currency, creating_txhash: txhash } ] end @spec create_inputs(Transaction.any_flavor_t(), binary()) :: [tuple()] def create_inputs(tx, spending_txhash) do tx |> Transaction.get_inputs() |> Enum.with_index() |> Enum.map(fn {Utxo.position(_, _, _) = input_utxo_pos, index} -> {input_utxo_pos, index, spending_txhash} end) end @spec get_sorted_grouped_utxos(Crypto.address_t(), order_t()) :: %{Crypto.address_t() => list(%__MODULE__{})} def get_sorted_grouped_utxos(owner, order) do # TODO: use clever DB query to get following out of DB owner |> get_all_utxos() |> Enum.group_by(fn utxo -> utxo.currency end) |> Enum.map(fn {currency, utxos} -> {currency, Enum.sort_by(utxos, fn utxo -> utxo.amount end, order)} end) |> Map.new() end defp query_get_utxos(address) do from( txoutput in __MODULE__, preload: [:ethevents], left_join: ethevent in assoc(txoutput, :ethevents), # select txoutputs by owner that have neither been spent nor have a corresponding ethevents exit events where: txoutput.owner == ^address and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment( " NOT EXISTS (SELECT 1 FROM ethevents_txoutputs AS etfrag JOIN ethevents AS efrag ON etfrag.root_chain_txhash_event=efrag.root_chain_txhash_event AND efrag.event_type = ANY(?) AND etfrag.child_chain_utxohash = ?)", ["standard_exit", "in_flight_exit"], txoutput.child_chain_utxohash )), order_by: [asc: :blknum, asc: :txindex, asc: :oindex] ) end @spec get_all_utxos(Crypto.address_t()) :: list() @decorate trace(service: :ecto, type: :db, tracer: OMG.WatcherInfo.Tracer) defp get_all_utxos(address) do query = query_get_utxos(address) Repo.all(query) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/types/atom_type.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.Types.AtomType do @moduledoc """ Custom Ecto type that converts DB's string value into atom. """ @behaviour Ecto.Type def type(), do: :string def cast(value), do: {:ok, value} def load(value), do: {:ok, String.to_existing_atom(value)} def dump(value) when is_atom(value), do: {:ok, Atom.to_string(value)} def dump(_), do: :error # https://hexdocs.pm/ecto/Ecto.Type.html#c:embed_as/1 def embed_as(_), do: :self def equal?(value1, value2), do: value1 == value2 end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/types/block/chunk.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.Block.Chunk do @moduledoc """ La chunk. """ @max_params_count 0xFFFF # Prepares entries to the database in chunks to avoid `too many parameters` error. # Accepts the same parameters that `Repo.insert_all/3`. @spec chunk(Enumerable.t()) :: Enumerable.t() def chunk(entries) do utc_now = DateTime.utc_now() entries = Enum.map(entries, fn entry -> Map.merge(entry, %{inserted_at: utc_now, updated_at: utc_now}) end) chunk_size = entries |> hd() |> chunk_size() Stream.chunk_every(entries, chunk_size) end # Note: an entry with 0 fields will cause a divide-by-zero error. # # DB.Repo.chunk_size(%{}) ==> (ArithmeticError) bad argument in arithmetic expression # # But we could not think of a case where this code happen, so no defensive # checks here. defp chunk_size(entry), do: div(@max_params_count, fields_count(entry)) defp fields_count(map), do: Kernel.map_size(map) end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/db/types/integer_type.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.Types.IntegerType do @moduledoc """ Custom Ecto type that converts DB's decimal value into integer. Ecto supports `:decimal` type out of the box (via `decimal` package). However, since this `:decimal` type requires its own functions to operate, e.g. `Decimal.add/2`, and we only work with whole numbers, we can safely convert to Elixir's primitive integer for easier operations. """ @behaviour Ecto.Type def type(), do: :integer def cast(value) do {:ok, value} end def load(value) do {:ok, Decimal.to_integer(value)} end def load!(nil), do: 0 def load!(value), do: Decimal.to_integer(value) def dump(value) do {:ok, value} end # https://hexdocs.pm/ecto/Ecto.Type.html#c:embed_as/1 def embed_as(_), do: :self def equal?(value1, value2), do: value1 == value2 end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/http_rpc/adapter.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.HttpRPC.Adapter do @moduledoc """ Provides functions to communicate with Child Chain API """ alias OMG.Utils.AppVersion require Logger @doc """ Makes HTTP POST request to the API """ def rpc_post(body, path, url) do addr = "#{url}/#{path}" headers = [{"content-type", "application/json"}, {"X-Watcher-Version", AppVersion.version(:omg_watcher_info)}] with {:ok, body} <- Jason.encode(body), {:ok, %HTTPoison.Response{} = response} <- HTTPoison.post(addr, body, headers) do _ = Logger.debug("rpc post #{inspect(addr)} completed successfully") response else err -> _ = Logger.warn("rpc post #{inspect(addr)} failed with #{inspect(err)}") err end end @doc """ Retrieves body from response structure but don't deserialize it. """ def get_unparsed_response_body(%HTTPoison.Response{status_code: 200, body: body}) do with {:ok, response} <- Jason.decode(body), %{"success" => true, "data" => data} <- response do {:ok, data} else %{"success" => false, "data" => data} -> {:error, {:client_error, data}} match_err -> {:error, {:malformed_response, match_err}} end end def get_unparsed_response_body(%HTTPoison.Response{body: error}), do: {:error, {:server_error, error}} def get_unparsed_response_body({:error, %HTTPoison.Error{reason: :econnrefused}}) do {:error, :childchain_unreachable} end def get_unparsed_response_body({:error, %HTTPoison.Error{reason: reason}}) do {:error, reason} end def get_unparsed_response_body(error), do: error @doc """ Retrieves body from response structure. When response is successful the structure in body is known, so we can try to deserialize it. """ def get_response_body(response) do case get_unparsed_response_body(response) do {:ok, data} -> {:ok, convert_keys_to_atoms(data)} error -> error end end defp convert_keys_to_atoms(data) when is_list(data), do: Enum.map(data, &convert_keys_to_atoms/1) defp convert_keys_to_atoms(data) when is_map(data) do data |> Stream.map(fn {k, v} -> {String.to_existing_atom(k), v} end) |> Map.new() end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/http_rpc/client.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.HttpRPC.Client do @moduledoc """ Provides functions to communicate with Child Chain API """ alias OMG.Utils.HttpRPC.Encoding alias OMG.WatcherInfo.HttpRPC.Adapter require Logger @type response_t() :: {:ok, %{required(atom()) => any()}} | {:error, {:client_error | :server_error, any()} | {:malformed_response, any() | {:error, :invalid}}} @doc """ Gets Block of given hash """ @spec get_block(binary(), binary()) :: response_t() def get_block(hash, url), do: call(%{hash: Encoding.to_hex(hash)}, "block.get", url) @doc """ Gets supported fees """ @spec get_fees(map(), binary()) :: {:ok, map()} | {:error, {:client_error | :server_error, any()} | {:malformed_response, any() | {:error, :invalid}}} def get_fees(params, url) do params |> Adapter.rpc_post("fees.all", url) |> Adapter.get_unparsed_response_body() end @doc """ Submits transaction """ @spec submit(binary(), binary()) :: response_t() def submit(tx, url), do: call(%{transaction: Encoding.to_hex(tx)}, "transaction.submit", url) defp call(params, path, url), do: params |> Adapter.rpc_post(path, url) |> Adapter.get_response_body() |> decode_response() # Translates response's body to known elixir structure, either block or tx submission response or error. defp decode_response({:ok, %{transactions: transactions, blknum: number, hash: hash}}) do {:ok, %{ number: number, hash: decode16!(hash), transactions: Enum.map(transactions, &decode16!/1) }} end defp decode_response({:ok, %{txhash: _hash} = response}) do {:ok, Map.update!(response, :txhash, &decode16!/1)} end defp decode_response(error), do: error defp decode16!(hexstr) do {:ok, bin} = Encoding.from_hex(hexstr) bin end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/measure.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Measure do @moduledoc """ Counting business metrics sent to Datadog. """ import OMG.Status.Metric.Event, only: [name: 1] alias OMG.Status.Metric.Datadog alias OMG.WatcherInfo.PendingBlockQueueLengthChecker @supported_events [ [:pending_block_queue_length, PendingBlockQueueLengthChecker] ] def supported_events(), do: @supported_events def handle_event([:pending_block_queue_length, PendingBlockQueueLengthChecker], %{length: length}, _state, _config) do _ = Datadog.gauge(name(:pending_block_queue_length), length) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/order_fee_fetcher.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.OrderFeeFetcher do @moduledoc """ Handle fetching and formatting of fees for an order """ alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.WireFormatTypes alias OMG.WatcherInfo.HttpRPC.Client alias OMG.WatcherInfo.Transaction, as: TransactionCreator # Note: Hardcoding the tx_type for now, until we need to support more types of transactions # that require fetching fees from the child chain. # For now, only transaction.create uses this module and it's used only for payment type transactions. @tx_type WireFormatTypes.tx_type_for(:tx_payment_v1) @str_tx_type Integer.to_string(@tx_type) @type order_without_fee_amount_t() :: %{ owner: Crypto.address_t(), payments: nonempty_list(TransactionCreator.payment_t()), fee: %{currency: Transaction.Payment.currency()}, metadata: binary() | nil } @doc """ Fetch the correct fee amount for the given fee currency from the childchain and adds it to the order map. """ @spec add_fee_to_order(order_without_fee_amount_t(), String.t() | nil) :: {:ok, TransactionCreator.order_t()} | {:error, atom()} def add_fee_to_order(%{fee: %{currency: currency}} = order, url \\ nil) do child_chain_url = url || Application.get_env(:omg_watcher_info, :child_chain_url) encoded_currency = Encoding.to_hex(currency) params = %{"currencies" => [encoded_currency], "tx_types" => [@tx_type]} with {:ok, fees} <- Client.get_fees(params, child_chain_url), {:ok, amount} <- validate_child_chain_fees(fees, encoded_currency) do {:ok, Kernel.put_in(order, [:fee, :amount], amount)} end end defp validate_child_chain_fees(fees, currency) do case fees do # Ensuring that the child chain response is correct, we should only have # 1 currency for the given tx type as we filter it in the params of the request. # We also take this opportunity to pattern match the amount. %{@str_tx_type => [%{"amount" => amount, "currency" => ^currency}]} -> {:ok, amount} _ -> {:error, :unexpected_fee_currency} end end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/release_tasks/init_postgresql_db.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.ReleaseTasks.InitPostgresqlDB do @moduledoc false @app :omg_watcher_info def migrate() do for repo <- repos() do {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) end end def rollback(repo, version) do {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) end defp repos() do _ = Application.load(@app) Application.fetch_env!(@app, :ecto_repos) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/release_tasks/set_tracer.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.ReleaseTasks.SetTracer do @moduledoc false @behaviour Config.Provider require Logger alias OMG.WatcherInfo.Tracer @app :omg_watcher_info def init(args) do args end def load(config, args) do _ = on_load() adapter = Keyword.get(args, :system_adapter, System) _ = Process.put(:system_adapter, adapter) dd_disabled = get_dd_disabled() tracer_config = @app |> Application.get_env(Tracer) |> Keyword.put(:disabled?, dd_disabled) tracer_config = case dd_disabled do false -> app_env = get_app_env() Keyword.put(tracer_config, :env, app_env) true -> Keyword.put(tracer_config, :env, "") end Config.Reader.merge(config, omg_watcher_info: [{Tracer, tracer_config}]) end defp get_dd_disabled() do dd_disabled = Application.get_env(@app, Tracer)[:disabled?] dd_disabled? = validate_bool(get_env("DD_DISABLED"), dd_disabled) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DD_DISABLED Value: #{inspect(dd_disabled?)}.") dd_disabled? end defp get_app_env() do env = validate_app_env(get_env("APP_ENV")) _ = Logger.info("CONFIGURATION: App: #{@app} Key: APP_ENV Value: #{inspect(env)}.") env end defp get_env(key) do Process.get(:system_adapter).get_env(key) end defp validate_bool(value, _default) when is_binary(value), do: to_bool(String.upcase(value)) defp validate_bool(_, default), do: default defp to_bool("TRUE"), do: true defp to_bool("FALSE"), do: false defp to_bool(_), do: exit("DD_DISABLED either true or false.") defp validate_app_env(value) when is_binary(value), do: value defp validate_app_env(nil), do: exit("APP_ENV must be set.") defp on_load() do _ = Application.ensure_all_started(:logger) Application.load(@app) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/supervisor.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Supervisor do @moduledoc """ Starts and supervises Watcher Informational services such as the watcher informational database, block consumer, deposit consumer, exit consumer, etc. """ use Supervisor require Logger alias OMG.WatcherInfo if Mix.env() == :test do defmodule Sandbox do @moduledoc """ Must be start after WatcherInfo.DB.Repo, that no data will be downloaded/inserted before setting the sandbox option. """ use GenServer alias Ecto.Adapters.SQL def start_link(_args) do GenServer.start_link(__MODULE__, :ok, name: __MODULE__) end def init(stack) do :ok = SQL.Sandbox.checkout(WatcherInfo.DB.Repo) SQL.Sandbox.mode(WatcherInfo.DB.Repo, {:shared, self()}) {:ok, stack} end end end @children_run_after_repo if(Mix.env() == :test, do: [{__MODULE__.Sandbox, []}], else: []) def start_link() do Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) end def init(:ok) do # why sandbox is in this code # https://github.com/omgnetwork/elixir-omg/pull/562 top_children = [ %{ id: WatcherInfo.DB.Repo, start: {WatcherInfo.DB.Repo, :start_link, []}, type: :supervisor } ] ++ @children_run_after_repo opts = [strategy: :one_for_one] _ = Logger.info("Starting #{inspect(__MODULE__)}") Supervisor.init(top_children, opts) end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/tracer.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Tracer do @moduledoc """ Trace Ecto requests and reports information to Datadog via Spandex """ use Spandex.Tracer, otp_app: :omg_watcher_info end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Transaction do @moduledoc """ Module creates transaction from selected utxos and order. """ alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.UtxoSelection require Transaction.Payment @empty_metadata <<0::256>> @max_outputs Transaction.Payment.max_outputs() @merge_fee 0 @type create_t() :: {:ok, nonempty_list(transaction_t())} | {:error, :too_many_outputs} | {:error, :empty_transaction} @type create_typed_data_t() :: {:ok, nonempty_list(transaction_with_typed_data_t())} | {:error, :too_many_outputs} | {:error, :empty_transaction} @type fee_t() :: %{ currency: UtxoSelection.currency_t(), amount: non_neg_integer() } @type payment_t() :: %{ owner: Crypto.address_t() | nil, currency: UtxoSelection.currency_t(), amount: pos_integer() } @type transaction_t() :: %{ inputs: nonempty_list(%DB.TxOutput{}), outputs: nonempty_list(payment_t()), fee: fee_t(), txbytes: Transaction.tx_bytes() | nil, metadata: Transaction.metadata(), sign_hash: Crypto.hash_t() | nil } @type transaction_with_typed_data_t() :: %{ inputs: nonempty_list(%DB.TxOutput{}), outputs: nonempty_list(payment_t()), fee: fee_t(), txbytes: Transaction.tx_bytes() | nil, metadata: Transaction.metadata(), sign_hash: Crypto.hash_t() | nil, typed_data: TypedDataHash.Types.typedDataSignRequest_t() } @type order_t() :: %{ owner: Crypto.address_t(), payments: list(payment_t()), metadata: binary() | nil, fee: fee_t() } @type utxos_map_t() :: %{UtxoSelection.currency_t() => UtxoSelection.utxo_list_t()} @type inputs_t() :: {:ok, utxos_map_t()} | {:error, {:insufficient_funds, list(map())}} @doc """ Given an `order`, finds spender's inputs sufficient to perform a payment. If also provided with receiver's address, creates and encodes a transaction. """ @spec select_inputs(utxos_map_t(), order_t()) :: inputs_t() def select_inputs(utxos, %{payments: payments, fee: fee}) do reviewed_selected_utxos = payments # calculates net amount to satisfy payments and fee. |> UtxoSelection.calculate_net_amount(fee) # tries to select utxos that satisfy net amount. |> UtxoSelection.select_utxos(utxos) # reviews if selected utxos satisfy net amount. |> UtxoSelection.review_selected_utxos() case reviewed_selected_utxos do {:ok, funds} -> stealth_merge_utxos = utxos |> UtxoSelection.prioritize_merge_utxos(funds) |> UtxoSelection.add_utxos_for_stealth_merge(funds) {:ok, stealth_merge_utxos} err -> err end end @doc """ Given selected utxos and order, create inputs and outputs, then returns either {:error, reason} or transactions. - Returns transactions when the inputs look good. - Returns an error when any of the following conditions is met: 1. A number of outputs overs maximum. 2. An inputs are empty. """ @spec create(utxos_map_t(), order_t()) :: create_t() def create(utxos_per_token, order) do inputs = build_inputs(utxos_per_token) outputs = build_outputs(utxos_per_token, order) cond do Enum.count(outputs) > @max_outputs -> {:error, :too_many_outputs} Enum.empty?(inputs) -> {:error, :empty_transaction} true -> raw_tx = create_raw_transaction(inputs, outputs, order.metadata) {:ok, [ %{ inputs: inputs, outputs: outputs, fee: order.fee, metadata: order.metadata, txbytes: Transaction.raw_txbytes(raw_tx), sign_hash: TypedDataHash.hash_struct(raw_tx) } ]} end end @spec include_typed_data(create_t()) :: create_typed_data_t() def include_typed_data({:error, _} = err), do: err def include_typed_data({:ok, %{result: result, transactions: txs}}) do { :ok, %{result: result, transactions: Enum.map(txs, fn tx -> Map.put_new(tx, :typed_data, add_type_specs(tx)) end)} } end def include_typed_data({:ok, txs}) do { :ok, %{transactions: Enum.map(txs, fn tx -> Map.put_new(tx, :typed_data, add_type_specs(tx)) end)} } end @spec generate_merge_transactions(UtxoSelection.utxo_list_t()) :: list(transaction_t()) def generate_merge_transactions(merge_inputs) do merge_inputs |> Stream.chunk_every(@max_outputs) |> Enum.flat_map(fn input_set -> case input_set do [_single_input] -> [] inputs -> create_merge(inputs) end end) end @spec create_merge(UtxoSelection.utxo_list_t()) :: list(transaction_t()) defp create_merge(inputs) do %{currency: currency, owner: owner} = List.first(inputs) case create(%{currency => inputs}, %{ fee: %{amount: @merge_fee, currency: currency}, metadata: @empty_metadata, owner: owner, payments: [] }) do {:error, :empty_transaction} -> [] {:error, :too_many_outputs} -> [] {:ok, transactions} -> transactions end end defp build_inputs(utxos_per_token) do utxos_per_token |> Enum.reduce([], fn {_, utxos}, acc -> Enum.reverse(utxos) ++ acc end) |> Enum.reverse() end defp build_outputs(utxos_per_token, order) do rests = utxos_per_token |> Stream.map(fn {token, utxos} -> outputs = [order.fee | order.payments] |> Stream.filter(fn %{currency: currency} -> currency == token end) |> Stream.map(fn %{amount: amount} -> amount end) |> Enum.sum() inputs = utxos |> Stream.map(fn %{amount: amount} -> amount end) |> Enum.sum() %{amount: inputs - outputs, owner: order.owner, currency: token} end) |> Enum.filter(fn %{amount: amount} -> amount > 0 end) order.payments ++ rests end defp create_raw_transaction(inputs, outputs, metadata) do Transaction.Payment.new( Enum.map(inputs, fn input -> {input.blknum, input.txindex, input.oindex} end), Enum.map(outputs, fn output -> {output.owner, output.currency, output.amount} end), metadata || @empty_metadata ) end defp add_type_specs(%{inputs: inputs, outputs: outputs, metadata: metadata}) do message = [ create_inputs(inputs), create_outputs(outputs), [metadata: metadata || @empty_metadata] ] |> Enum.concat() |> Map.new() Map.merge( %{ domain: TypedDataHash.Config.domain_data_from_config(), message: message }, TypedDataHash.Types.eip712_types_specification() ) end defp create_inputs(inputs) do inputs |> Stream.map(fn input -> %{blknum: input.blknum, txindex: input.txindex, oindex: input.oindex} end) |> Stream.concat(Stream.repeatedly(fn -> %{blknum: 0, txindex: 0, oindex: 0} end)) |> (fn input -> Enum.zip([:input0, :input1, :input2, :input3], input) end).() end defp create_outputs(outputs) do zero_addr = <<0::160>> empty_gen = fn -> %{owner: zero_addr, currency: zero_addr, amount: 0} end outputs |> Stream.concat(Stream.repeatedly(empty_gen)) |> (fn output -> Enum.zip([:output0, :output1, :output2, :output3], output) end).() end end ================================================ FILE: apps/omg_watcher_info/lib/omg_watcher_info/utxo_selection.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.UtxoSelection do @moduledoc """ Provides Utxos selection and merging algorithms. """ alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.State.Transaction alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.Transaction, as: TransactionCreator require Transaction require Transaction.Payment @type currency_t() :: Transaction.Payment.currency() @type utxos_map_t() :: %{currency_t() => utxo_list_t()} @type utxo_list_t() :: list(%DB.TxOutput{}) @doc """ Defines and prioritises available UTXOs for stealth merge based on the available and selected sets. - Excludes currencies not already used in the transaction and UTXOs in the selected set. - Prioritises currencies that have the largest number of UTXOs - Sorts by ascending order of UTXO value within the currency groupings ("dust first"). """ @spec prioritize_merge_utxos(utxos_map_t(), utxos_map_t()) :: utxo_list_t() def prioritize_merge_utxos(utxos, selected_utxos) do utxos_hash = selected_utxos |> Enum.flat_map(fn {_ccy, utxos} -> utxos end) |> Enum.reduce(%{}, fn utxo, acc -> Map.put(acc, utxo.child_chain_utxohash, true) end) case utxos_hash do hashes_map when map_size(hashes_map) == 0 -> [] hashes_map -> selected_utxos |> Enum.map(&prioritize_utxos_by_currency(&1, utxos, hashes_map)) |> Enum.sort_by(&length/1, :desc) |> Enum.map(fn currency_utxos -> currency_utxos |> Enum.slice(0, 3) |> Enum.reverse() end) |> Enum.reduce(fn utxos, acc -> utxos ++ acc end) |> Enum.reverse() end end @doc """ Given a map of UTXOs sufficient for the transaction and a set of available UTXOs, adds UTXOs to the transaction for "stealth merge" until the limit is reached or no UTXOs are available. Agnostic to the priority ordering of available UTXOs. Returns an updated map of UTXOs for the transaction. """ @spec add_utxos_for_stealth_merge(utxo_list_t(), utxos_map_t()) :: utxos_map_t() def add_utxos_for_stealth_merge([], selected_utxos), do: selected_utxos def add_utxos_for_stealth_merge(available_utxos, selected_utxos) do case get_number_of_utxos(selected_utxos) do Transaction.Payment.max_inputs() -> selected_utxos _ -> [priority_utxo | remaining_available_utxos] = available_utxos stealth_merge_utxos = Map.update!(selected_utxos, priority_utxo.currency, fn current_utxos -> [priority_utxo | current_utxos] end) add_utxos_for_stealth_merge(remaining_available_utxos, stealth_merge_utxos) end end @doc """ Given the available set of UTXOs and the net amount by currency, tries to find a UTXO that satisfies the payment with no change. If this fails, starts to collect UTXOs (starting from the largest amount) until the payment is covered. Returns {currency, { variance, [utxos] }}. A `variance` greater than zero means insufficient funds. The ordering of UTXOs in descending order of amount is implicitly assumed for this algorithm to work deterministically. """ @spec select_utxos(%{currency_t() => pos_integer()}, utxos_map_t()) :: list({currency_t(), {integer, utxo_list_t()}}) def select_utxos(net_amount, utxos) do Enum.map(net_amount, fn {token, need} -> selected_utxos = utxos |> Map.get(token, []) |> find_utxos_by_token(need) {token, selected_utxos} end) end @doc """ Sums up payable amount by token, including the fee. """ @spec calculate_net_amount(list(TransactionCreator.payment_t()), %{amount: pos_integer(), currency: currency_t()}) :: %{currency_t() => pos_integer()} def calculate_net_amount(payments, %{currency: fee_currency, amount: fee_amount}) do net_amount_map = payments |> Enum.group_by(fn payment -> payment.currency end) |> Stream.map(fn {token, payment} -> {token, payment |> Stream.map(fn payment -> payment.amount end) |> Enum.sum()} end) |> Map.new() Map.update(net_amount_map, fee_currency, fee_amount, fn amount -> amount + fee_amount end) end @doc """ Checks if the result of `select_utxos/2` covers the amount(s) of the transaction order. """ @spec review_selected_utxos([ {currency :: currency_t(), {variance :: integer(), selected_utxos :: utxo_list_t()}} ]) :: {:ok, utxos_map_t()} | {:error, {:insufficient_funds, [%{token: String.t(), missing: pos_integer()}]}} def review_selected_utxos(utxo_selection) do missing_funds = utxo_selection |> Stream.filter(fn {_currency, {variance, _selected_utxos}} -> variance > 0 end) |> Enum.map(fn {currency, {missing, _selected_utxos}} -> %{token: Encoding.to_hex(currency), missing: missing} end) case Enum.empty?(missing_funds) do true -> {:ok, Enum.reduce(utxo_selection, %{}, fn {token, {_missing_amount, utxos}}, acc -> Map.put(acc, token, utxos) end)} _ -> {:error, {:insufficient_funds, missing_funds}} end end defp recursively_find_utxos(_, need, selected_utxos) when need <= 0, do: {need, selected_utxos} defp recursively_find_utxos([], need, _), do: {need, []} defp recursively_find_utxos([utxo | utxos], need, selected_utxos), do: recursively_find_utxos(utxos, need - utxo.amount, [utxo | selected_utxos]) defp find_utxos_by_token(token_utxos, need) do case Enum.find(token_utxos, fn %DB.TxOutput{amount: amount} -> amount == need end) do nil -> recursively_find_utxos(token_utxos, need, []) utxo -> {0, [utxo]} end end defp prioritize_utxos_by_currency({currency, _utxos}, utxos, selected_utxo_hashes) do utxos[currency] |> filter_unselected(selected_utxo_hashes) |> Enum.sort_by(fn utxo -> utxo.amount end, :asc) end @spec filter_unselected(utxo_list_t(), %{currency_t() => boolean()}) :: utxo_list_t() defp filter_unselected(available_utxos, selected_utxo_hashes) do Enum.filter(available_utxos, fn utxo -> !Map.has_key?(selected_utxo_hashes, utxo.child_chain_utxohash) end) end @spec get_number_of_utxos(utxos_map_t()) :: integer() defp get_number_of_utxos(utxos_by_currency) do Enum.reduce(utxos_by_currency, 0, fn {_currency, utxos}, acc -> length(utxos) + acc end) end end ================================================ FILE: apps/omg_watcher_info/lib/watcher_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo do @moduledoc """ WatcherInfo is responsible for the non-security-critical part of the Watcher. """ end ================================================ FILE: apps/omg_watcher_info/mix.exs ================================================ defmodule OMG.WatcherInfo.MixProject do use Mix.Project def project() do [ app: :omg_watcher_info, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end def application() do [ mod: {OMG.WatcherInfo.Application, []}, start_phases: [{:attach_telemetry, []}], extra_applications: [:logger, :runtime_tools, :telemetry, :omg_watcher] ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end # Specifies which paths to compile per environment. defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp deps() do [ {:postgrex, "~> 0.15"}, {:ecto_sql, "~> 3.4"}, {:telemetry, "~> 0.4.1"}, {:spandex_ecto, "~> 0.6.0"}, # there's no apparent reason why libsecp256k1, spandex need to be included as dependencies # to this umbrella application apart from mix ecto.gen.migration not working, so here they are, copied from # the parent (main) mix.exs {:spandex, "~> 3.0.2"}, {:jason, "~> 1.0"}, # UMBRELLA {:omg_status, in_umbrella: true}, {:omg_utils, in_umbrella: true}, # TEST ONLY # here only to leverage common test helpers and code {:fake_server, "~> 2.1", only: [:dev, :test], runtime: false}, {:briefly, "~> 0.3.0", only: [:dev, :test]}, {:phoenix, "~> 1.5", runtime: false}, {:poison, "~> 4.0"}, {:ex_machina, "~> 2.3", only: [:test], runtime: false} ] end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20180813131000_create_block_table.exs ================================================ defmodule OMG.WatcherInfo.Repo.Migrations.CreateBlockTable do use Ecto.Migration def change() do create table(:blocks, primary_key: false) do add :blknum, :bigint, null: false, primary_key: true add :hash, :binary, null: false add :timestamp, :integer, null: false add :eth_height, :bigint, null: false end end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20180813131706_create_transaction_table.exs ================================================ defmodule OMG.WatcherInfo.Repo.Migrations.CreateTransactionTable do use Ecto.Migration def change() do create table(:transactions, primary_key: false) do add :txhash, :binary, primary_key: true add :txindex, :integer, null: false add :txbytes, :binary, null: false add :sent_at, :timestamp add :blknum, references(:blocks, column: :blknum, type: :bigint) end # TODO: this will work as long as there will be not nulls here create unique_index(:transactions, [:blknum, :txindex], name: :unq_transaction_blknum_txindex) end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20180813133000_create_ethevent_table.exs ================================================ defmodule OMG.WatcherInfo.Repo.Migrations.CreateEtheventTable do use Ecto.Migration def change() do create table(:ethevents, primary_key: false) do add :hash, :binary, primary_key: true add :blknum, :bigint add :txindex, :integer add :event_type, :string, size: 124, null: false end end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20180813143343_create_txoutput_table.exs ================================================ defmodule OMG.WatcherInfo.Repo.Migrations.CreateTxoutputTable do use Ecto.Migration def change() do create table(:txoutputs, primary_key: false) do add :blknum, :bigint, null: false, primary_key: true add :txindex, :integer, null: false, primary_key: true add :oindex, :integer, null: false, primary_key: true add :creating_txhash, references(:transactions, column: :txhash, type: :binary) add :creating_deposit, references(:ethevents, column: :hash, type: :binary) add :spending_txhash, references(:transactions, column: :txhash, type: :binary) add :spending_exit, references(:ethevents, column: :hash, type: :binary) add :spending_tx_oindex, :integer add :owner, :binary, null: false add :amount, :decimal, precision: 81, scale: 0, null: false add :currency, :binary, null: false add :proof, :binary end end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20190314105410_alter_transactions_table_add_metadata_field.exs ================================================ defmodule OMG.WatcherInfo.DB.Repo.Migrations.AlterTransactionsTableAddMetadataField do use Ecto.Migration def change() do alter table(:transactions) do add(:metadata, :binary) end end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20190315095855_alter_transactions_table_add_partitial_index.exs ================================================ defmodule OMG.WatcherInfo.DB.Repo.Migrations.AlterTransactionsTableAddPartialIndex do use Ecto.Migration def up() do execute("CREATE INDEX transactions_metadata_index ON transactions(metadata) WHERE metadata IS NOT NULL") end def down() do execute("DROP INDEX transactions_metadata_index") end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20190408131000_add_missing_indices_to_txoutputs.exs ================================================ defmodule OMG.WatcherInfo.Repo.Migrations.AddMissingIndicesToTxOuputs do use Ecto.Migration def change() do create index(:txoutputs, [:creating_txhash, :spending_txhash]) create index(:txoutputs, [:creating_deposit]) create index(:txoutputs, [:spending_txhash]) create index(:txoutputs, [:spending_exit], where: "spending_exit IS NOT NULL") create index(:txoutputs, [:owner]) end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20190806111817_alter_txoutputs_ethevents_make_many_to_many_relation.exs ================================================ defmodule OMG.WatcherInfo.DB.Repo.Migrations.AlterTxOutputsTableAddRootchainTxnHashDepositAndExitColumns do use Ecto.Migration # non-backward compatible migration, thus cannot use `change/0` def up() do drop constraint(:txoutputs, "txoutputs_creating_deposit_fkey") drop constraint(:txoutputs, "txoutputs_spending_exit_fkey") drop constraint(:ethevents, "ethevents_pkey") flush() # drop ethevents table and rebuild it as this table is currently unused for all practical purposes. # when getting utxos we filter on txoutputs.creating_deposit is nil and txoutputs.spending is nil and # never query/join with the ethevents table. # dropping is easiest here because we are altering what the primary key is drop table(:ethevents) create table(:ethevents, primary_key: false) do add(:root_chain_txhash, :binary, primary_key: true) add(:log_index, :int, primary_key: true) add(:event_type, :string, size: 124) add(:root_chain_txhash_event, :binary) timestamps([type: :utc_datetime]) end create index(:ethevents, :root_chain_txhash) create index(:ethevents, :log_index) create unique_index(:ethevents, :root_chain_txhash_event) # how to do this in ecto correctly? do it manually execute("ALTER TABLE ethevents ALTER COLUMN inserted_at SET DEFAULT (now() at time zone 'utc');") execute("ALTER TABLE ethevents ALTER COLUMN updated_at SET DEFAULT (now() at time zone 'utc');") alter table(:txoutputs) do add(:child_chain_utxohash, :binary) end create unique_index(:txoutputs, :child_chain_utxohash) flush() # backfill child_chain_utxohash with values from either creating_deposit or spending_exit execute """ UPDATE txoutputs as t SET child_chain_utxohash = (SELECT CASE WHEN creating_deposit IS NULL THEN spending_exit WHEN spending_exit IS NULL THEN creating_deposit ELSE creating_deposit || spending_exit END AS txhash FROM txoutputs as t_inner WHERE t.creating_deposit = t_inner.creating_deposit OR t.spending_exit = t_inner.spending_exit); """ alter table(:txoutputs) do remove(:creating_deposit) remove(:spending_exit) timestamps(type: :utc_datetime, default: fragment("(now() at time zone 'utc')"), null: true) end create table(:ethevents_txoutputs, primary_key: false) do add(:root_chain_txhash_event, references(:ethevents, column: :root_chain_txhash_event, type: :binary, on_delete: :restrict), primary_key: true) add(:child_chain_utxohash, references(:txoutputs, column: :child_chain_utxohash, type: :binary, on_delete: :restrict), primary_key: true) timestamps([type: :utc_datetime]) end # how to do this in ecto correctly? do it manually execute("ALTER TABLE ethevents_txoutputs ALTER COLUMN inserted_at SET DEFAULT (now() at time zone 'utc');") execute("ALTER TABLE ethevents_txoutputs ALTER COLUMN updated_at SET DEFAULT (now() at time zone 'utc');") create index(:ethevents_txoutputs, :root_chain_txhash_event) create index(:ethevents_txoutputs, :child_chain_utxohash) end def down() do # non-backward compatible migration, thus cannot use `change/0` # no-op end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20190917165912_set_inserted_at_updated_at_to_epoc.exs ================================================ defmodule OMG.WatcherInfo.Repo.Migrations.SetInsertedAtUpdatedAtToEpoch do use Ecto.Migration def change() do execute("UPDATE txoutputs SET inserted_at = 'epoch' at time zone 'utc';") execute("UPDATE txoutputs SET updated_at = 'epoch' at time zone 'utc';") execute("ALTER TABLE txoutputs ALTER COLUMN inserted_at SET NOT NULL;") execute("ALTER TABLE txoutputs ALTER COLUMN updated_at SET NOT NULL;") end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20200129051756_index_block_timestamp.exs ================================================ defmodule OMG.WatcherInfo.DB.Repo.Migrations.IndexBlockTimestamp do use Ecto.Migration def change() do create(index(:blocks, [:timestamp])) end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20200211064454_add_txtype_to_transaction_and_output.exs ================================================ defmodule OMG.WatcherInfo.DB.Repo.Migrations.AddTxtypeToTransactionAndOutput do use Ecto.Migration import Ecto.Query, only: [from: 2] alias Ecto.Adapters.SQL alias OMG.Watcher.State.Transaction alias OMG.Watcher.WireFormatTypes alias OMG.WatcherInfo.DB.Repo def up() do alter table(:transactions) do add :txtype, :integer end alter table(:txoutputs) do add :otype, :integer end create index(:transactions, :txtype) create index(:txoutputs, :otype) flush() set_txtypes() alter table(:transactions) do modify(:txtype, :integer, null: false) end alter table(:txoutputs) do modify(:otype, :integer, null: false) end end def down() do # This migration only supports outputs of type 1 and 2, we prevent rollback so we # don't have problems if new types are introduced in the future. raise "can't rollback this migration" end # Update existing transactions and output that don't have a type defp set_txtypes() do :ok = update_transaction_types() {_, nil} = update_fee_outputs() {_, nil} = update_payment_outputs() end defp update_transaction_types() do Repo |> SQL.query!("SELECT txhash, txbytes FROM transactions") |> Map.get(:rows) |> Enum.reduce(%{}, &reduce_txhash_txbytes/2) |> Enum.each(&update_txtype_for_txhashes/1) end defp reduce_txhash_txbytes([txhash, txbytes], txtype_to_txhashes) do %{raw_tx: %{tx_type: txtype}} = Transaction.Signed.decode!(txbytes) hashes = case txtype_to_txhashes[txtype] do nil -> [txhash] hashes -> [txhash | hashes] end Map.put(txtype_to_txhashes, txtype, hashes) end defp update_txtype_for_txhashes({txtype, txhashes}) do count = length(txhashes) {^count, nil} = Repo.update_all( from(t in "transactions", where: t.txhash in ^txhashes), set: [txtype: txtype] ) end defp update_fee_outputs() do fee_tx_type = WireFormatTypes.tx_type_for(:tx_fee_token_claim) Repo.update_all( from( o in "txoutputs", join: t in "transactions", on: o.creating_txhash == t.txhash, where: t.txtype == ^fee_tx_type ), set: [otype: WireFormatTypes.output_type_for(:output_fee_token_claim)] ) end defp update_payment_outputs() do Repo.update_all( from(o in "txoutputs", where: is_nil(o.otype)), set: [otype: WireFormatTypes.output_type_for(:output_payment_v1)] ) end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20200214132000_add_and_fix_timestamps.exs ================================================ defmodule OMG.Watcher.Repo.Migrations.AddAndFixTimestamps do use Ecto.Migration # this is a non-backward compatible change as the the `transactions.sent_at` column # is being removed, so only an `up()` function is defined def up() do Enum.each(["ethevents", "txoutputs", "ethevents_txoutputs"], fn table_name -> # backfill tables that may have null values for `inserted_at` and `updated_at` before adding NOT NULL constraint backfill_null_timestamps(table_name) update_timestamps_ddl(table_name) end) alter table(:transactions) do remove(:sent_at) timestamps([type: :timestamptz, default: fragment("('epoch'::TIMESTAMPTZ AT TIME ZONE 'UTC')")]) end alter table(:blocks) do timestamps([type: :timestamptz, default: fragment("('epoch'::TIMESTAMPTZ AT TIME ZONE 'UTC')")]) end end defp backfill_null_timestamps(table_name) do execute("UPDATE #{table_name} SET inserted_at=('epoch'::TIMESTAMPTZ AT TIME ZONE 'UTC'), updated_at=('epoch'::TIMESTAMPTZ AT TIME ZONE 'UTC') WHERE inserted_at IS NULL AND updated_at IS NULL") end defp update_timestamps_ddl(table_name) do # change column type. this must be done before column default can be set execute("ALTER TABLE #{table_name} ALTER COLUMN inserted_at TYPE TIMESTAMPTZ USING inserted_at AT TIME ZONE 'UTC'") execute("ALTER TABLE #{table_name} ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC'") # add column default value and not null constraint execute("ALTER TABLE #{table_name} ALTER COLUMN inserted_at SET DEFAULT ('epoch'::TIMESTAMPTZ AT TIME ZONE 'UTC'), ALTER COLUMN inserted_at SET NOT NULL") execute("ALTER TABLE #{table_name} ALTER COLUMN updated_at SET DEFAULT ('epoch'::TIMESTAMPTZ AT TIME ZONE 'UTC'), ALTER COLUMN updated_at SET NOT NULL") end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20200514115919_add_eth_height_to_eth_events.exs ================================================ defmodule OMG.WatcherInfo.DB.Repo.Migrations.AddEthHeightToEthEvents do use Ecto.Migration def up() do alter table(:ethevents) do add(:eth_height, :integer) end end end ================================================ FILE: apps/omg_watcher_info/priv/repo/migrations/20200529085008_create_pending_block_table.exs ================================================ defmodule OMG.WatcherInfo.DB.Repo.Migrations.CreatePendingBlockTable do use Ecto.Migration def change() do create table(:pending_blocks, primary_key: false) do add :blknum, :bigint, null: false, primary_key: true add :data, :binary, null: false timestamps([type: :timestamptz, default: fragment("('epoch'::TIMESTAMPTZ AT TIME ZONE 'UTC')")]) end end end ================================================ FILE: apps/omg_watcher_info/test/fixtures.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Fixtures do use ExUnitFixtures.FixtureModule use OMG.Eth.Fixtures use OMG.DB.Fixtures use OMG.Watcher.Fixtures alias Ecto.Adapters.SQL alias OMG.Watcher.Crypto alias OMG.WatcherInfo alias OMG.WatcherInfo.DB @eth <<0::160>> deffixture in_beam_watcher(db_initialized, contract) do :ok = db_initialized _ = contract {:ok, started_apps} = Application.ensure_all_started(:omg_db) {:ok, started_watcher} = Application.ensure_all_started(:omg_watcher_info) {:ok, started_watcher_api} = Application.ensure_all_started(:omg_watcher_rpc) [] = DB.Repo.all(DB.Block) on_exit(fn -> Application.put_env(:omg_db, :path, nil) (started_apps ++ started_watcher ++ started_watcher_api) |> Enum.reverse() |> Enum.map(fn app -> :ok = Application.stop(app) end) end) end deffixture web_endpoint do Application.ensure_all_started(:spandex_ecto) Application.ensure_all_started(:telemetry) :telemetry.attach( "spandex-query-tracer", [:omg_watcher, :watcher, :db, :repo, :query], &SpandexEcto.TelemetryAdapter.handle_event/4, nil ) {:ok, pid} = ensure_web_started(OMG.WatcherRPC.Web.Endpoint, :start_link, [], 100) _ = Application.load(:omg_watcher_rpc) on_exit(fn -> wait_for_process(pid) :ok end) end @doc "run only database in sandbox and endpoint to make request" deffixture phoenix_ecto_sandbox(web_endpoint) do :ok = web_endpoint Supervisor.start_link( [%{id: DB.Repo, start: {DB.Repo, :start_link, []}, type: :supervisor}], strategy: :one_for_one, name: WatcherInfo.Supervisor ) :ok = SQL.Sandbox.checkout(DB.Repo) SQL.Sandbox.mode(DB.Repo, {:shared, self()}) end deffixture initial_blocks(alice, bob, blocks_inserter, initial_deposits) do :ok = initial_deposits blocks = [ {1000, [ OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 300}]), OMG.Watcher.TestHelper.create_recovered([{1000, 0, 0, bob}], @eth, [{alice, 100}, {bob, 200}]) ]}, {2000, [ OMG.Watcher.TestHelper.create_recovered( [{1000, 1, 0, alice}], @eth, [{bob, 99}, {alice, 1}], <<1337::256>> ) ]}, {3000, [ OMG.Watcher.TestHelper.create_recovered([], @eth, [{alice, 150}]), OMG.Watcher.TestHelper.create_recovered([{1000, 1, 1, bob}], @eth, [{bob, 150}, {alice, 50}]) ]} ] blocks_inserter.(blocks) end deffixture initial_deposits(alice, bob, phoenix_ecto_sandbox) do :ok = phoenix_ecto_sandbox deposits = [ %{ root_chain_txhash: Crypto.hash(<<1000::256>>), log_index: 0, owner: alice.addr, currency: @eth, amount: 333, eth_height: 1, otype: 1, blknum: 1 }, %{ root_chain_txhash: Crypto.hash(<<2000::256>>), log_index: 0, owner: bob.addr, currency: @eth, amount: 100, otype: 1, eth_height: 2, blknum: 2 } ] # Initial data depending tests can reuse DB.EthEvent.insert_deposits!(deposits) :ok end deffixture blocks_inserter(phoenix_ecto_sandbox) do :ok = phoenix_ecto_sandbox fn blocks -> Enum.flat_map(blocks, &prepare_one_block/1) end end defp prepare_one_block({blknum, recovered_txs}) do block_application = %{ transactions: recovered_txs, number: blknum, hash: "##{blknum}", timestamp: 1_540_465_606, eth_height: 1 } {:ok, _} = DB.Block.insert_from_block_application(block_application) recovered_txs |> Enum.with_index() |> Enum.map(fn {recovered_tx, txindex} -> {blknum, txindex, recovered_tx.tx_hash, recovered_tx} end) end defp ensure_web_started(module, function, args, counter) do _ = Process.flag(:trap_exit, true) do_ensure_web_started(module, function, args, counter) end defp do_ensure_web_started(module, function, args, 0), do: apply(module, function, args) defp do_ensure_web_started(module, function, args, counter) do {:ok, _pid} = result = apply(module, function, args) result rescue e in MatchError -> case e do %MatchError{ term: {:error, {:shutdown, {:failed_to_start_child, {:ranch_listener_sup, OMG.WatcherRPC.Web.Endpoint.HTTP}, _}}} } -> :ok = Process.sleep(5) ensure_web_started(module, function, args, counter - 1) %MatchError{term: {:error, {:already_started, pid}}} -> {:ok, pid} end end defp wait_for_process(pid, timeout \\ :infinity) when is_pid(pid) do ref = Process.monitor(pid) receive do {:DOWN, ^ref, :process, _, _} -> :ok after timeout -> throw({:timeouted_waiting_for, pid}) end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/api/block_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.BlockTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures import OMG.WatcherInfo.Factory alias OMG.Utils.Paginator alias OMG.WatcherInfo.API alias OMG.WatcherInfo.DB describe "get_block/1" do @tag fixtures: [:initial_blocks] test "returns block by block number" do blknum = 1000 block = DB.Block.get(blknum) assert {:ok, block} == API.Block.get(blknum) end @tag fixtures: [:initial_blocks] test "returns expected error if block not found" do non_existent_block = 5000 assert {:error, :block_not_found} == API.Block.get(non_existent_block) end end describe "get_blocks/1" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns a paginator with a list of blocks" do _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: 200) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: 300) constraints = [] results = API.Block.get_blocks(constraints) assert %Paginator{} = results assert length(results.data) == 3 assert Enum.all?(results.data, fn block -> %DB.Block{} = block end) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a paginator according to the provided paginator constraints" do _inserted_1 = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: 100) inserted_2 = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: 200) inserted_3 = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: 300) constraints = [limit: 2, page: 1] results = API.Block.get_blocks(constraints) assert %Paginator{} = results assert length(results.data) == 2 assert results.data |> Enum.at(0) |> Map.get(:blknum) == inserted_3.blknum assert results.data |> Enum.at(1) |> Map.get(:blknum) == inserted_2.blknum end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/api/deposit_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.DepositTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures import OMG.WatcherInfo.Factory alias OMG.Utils.Paginator alias OMG.WatcherInfo.API alias OMG.WatcherInfo.DB describe "get_deposits/1" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns a paginator with a list of deposits" do owner = <<1::160>> _ = insert(:ethevent, event_type: :deposit, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, event_type: :deposit, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, event_type: :standard_exit, txoutputs: [build(:txoutput, %{owner: owner})]) constraints = [address: owner] results = API.Deposit.get_deposits(constraints) assert %Paginator{} = results assert length(results.data) == 2 assert Enum.all?(results.data, fn ethevent -> %DB.EthEvent{event_type: :deposit} = ethevent end) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a paginator according to the provided paginator constraints" do owner = <<1::160>> _ = insert(:ethevent, event_type: :deposit, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, event_type: :deposit, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, event_type: :deposit, txoutputs: [build(:txoutput, %{owner: owner})]) assert [page: 1, limit: 2, address: owner] |> API.Deposit.get_deposits() |> Map.get(:data) |> length() == 2 assert [page: 2, limit: 2, address: owner] |> API.Deposit.get_deposits() |> Map.get(:data) |> length() == 1 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns results filtered by address" do owner_1 = <<1::160>> owner_2 = <<2::160>> deposit_output_1 = build(:txoutput, %{owner: owner_1}) deposit_output_2 = build(:txoutput, %{owner: owner_2}) _ = insert(:ethevent, event_type: :deposit, txoutputs: [deposit_output_1]) _ = insert(:ethevent, event_type: :deposit, txoutputs: [deposit_output_2]) constraint = [address: owner_1] result = API.Deposit.get_deposits(constraint) assert %Paginator{data: [%DB.EthEvent{} = deposit]} = result assert deposit |> Map.get(:txoutputs) |> Enum.at(0) |> Map.get(:owner) == owner_1 end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/api/stats_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.StatsTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures alias OMG.WatcherInfo.API.Stats import OMG.WatcherInfo.Factory @seconds_in_twenty_four_hours 86_400 describe "get/0" do @tag fixtures: [:phoenix_ecto_sandbox] test "retrieves expected statistics" do now = DateTime.to_unix(DateTime.utc_now()) within_today = now - @seconds_in_twenty_four_hours + 100 before_today = now - @seconds_in_twenty_four_hours - 100 block_1 = insert(:block, blknum: 1000, timestamp: within_today) _ = insert(:transaction, block: block_1, txindex: 0) _ = insert(:transaction, block: block_1, txindex: 1) block_2 = insert(:block, blknum: 2000, timestamp: before_today) _ = insert(:transaction, block: block_2, txindex: 0) _ = insert(:transaction, block: block_2, txindex: 1) result = Stats.get() expected = {:ok, %{ block_count: %{all_time: 2, last_24_hours: 1}, transaction_count: %{all_time: 4, last_24_hours: 2}, average_block_interval_seconds: %{all_time: 200.0, last_24_hours: nil} }} assert result == expected end end describe "get_average_block_interval_all_time/0" do @tag fixtures: [:phoenix_ecto_sandbox] test "correctly returns the average difference of block timestamps for all time" do base = 100 [diff_1, diff_2, diff_3] = [10, 10, 30] _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: base) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: base + diff_1) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: base + diff_1 + diff_2) _ = insert(:block, blknum: 4000, hash: "0x4000", eth_height: 4, timestamp: base + diff_1 + diff_2 + diff_3) expected = (diff_1 + diff_2 + diff_3) / 3 actual = Stats.get_average_block_interval_all_time() assert expected == actual end @tag fixtures: [:phoenix_ecto_sandbox] test "returns nil if the number of blocks is less than 2" do result_1 = Stats.get_average_block_interval_all_time() assert result_1 == nil _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: 100) result_2 = Stats.get_average_block_interval_all_time() assert result_2 == nil _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: 200) result_3 = Stats.get_average_block_interval_all_time() assert result_3 == 100 end end describe "get_average_block_interval_between/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "correctly returns the average difference of block timestamps in the given time range" do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime = end_datetime - @seconds_in_twenty_four_hours [diff_1, diff_2] = [80, 90] _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: start_datetime - 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: start_datetime) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: start_datetime + diff_1) _ = insert(:block, blknum: 4000, hash: "0x4000", eth_height: 4, timestamp: start_datetime + diff_1 + diff_2) expected = (diff_1 + diff_2) / 2 actual = Stats.get_average_block_interval_between(start_datetime, end_datetime) assert expected == actual end @tag fixtures: [:phoenix_ecto_sandbox] test "returns nil if the number of blocks in the given time range is less than 2" do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime = end_datetime - @seconds_in_twenty_four_hours _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: start_datetime - 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: start_datetime - 50) result_1 = Stats.get_average_block_interval_between(start_datetime, end_datetime) assert result_1 == nil _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: start_datetime) result_2 = Stats.get_average_block_interval_between(start_datetime, end_datetime) assert result_2 == nil _ = insert(:block, blknum: 4000, hash: "0x4000", eth_height: 4, timestamp: start_datetime + 100) result_2 = Stats.get_average_block_interval_between(start_datetime, end_datetime) assert result_2 == 100 end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/api/transaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.API.TransactionTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures alias OMG.Watcher.Utxo.Position alias OMG.WatcherInfo.API.Transaction import OMG.WatcherInfo.Factory @alice <<1::160>> @bob <<2::160>> @currency_1 <<3::160>> @currency_2 <<4::160>> describe "merge/1 with address and currency parameters" do @tag fixtures: [:phoenix_ecto_sandbox] test "merge with address and currency forms multiple merge transactions if possible" do insert_initial_utxo() _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) assert {:ok, [%{inputs: [_, _, _, _], outputs: [output_1]}, %{inputs: [_, _, _], outputs: [output_2]}]} = Transaction.merge(%{address: @alice, currency: @currency_1}) assert output_1 === %{amount: 4, currency: @currency_1, owner: @alice} assert output_2 === %{amount: 3, currency: @currency_1, owner: @alice} end @tag fixtures: [:phoenix_ecto_sandbox] test "fetches inputs for the given addresss only" do insert_initial_utxo() _ = insert(:txoutput, currency: @currency_1, owner: @bob, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @bob, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) assert {:ok, [alice_merge]} = Transaction.merge(%{address: @alice, currency: @currency_1}) assert %{inputs: [_, _, _, _], outputs: [%{amount: 4, currency: @currency_1, owner: @alice}]} = alice_merge end @tag fixtures: [:phoenix_ecto_sandbox] test "fetches inputs for the given currency only" do insert_initial_utxo() _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_2, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_2, owner: @alice, amount: 1) assert {:ok, [alice_merge]} = Transaction.merge(%{address: @alice, currency: @currency_2}) assert %{inputs: [_, _], outputs: [%{amount: 2, currency: @currency_2, owner: @alice}]} = alice_merge end @tag fixtures: [:phoenix_ecto_sandbox] test "returns one merge transaction if five UTXOs are available – prioritising the lowest value inputs" do insert_initial_utxo() _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 2) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 3) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 4) _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 5) assert {:ok, [merge_tx]} = Transaction.merge(%{address: @alice, currency: @currency_1}) assert %{ inputs: [%{amount: 1}, %{amount: 2}, %{amount: 3}, %{amount: 4}], outputs: [%{amount: 10, currency: @currency_1, owner: @alice}] } = merge_tx end @tag fixtures: [:phoenix_ecto_sandbox] test "merge with address and currency fails on single input" do insert_initial_utxo() _ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1) _ = insert(:txoutput, currency: @currency_2, owner: @alice, amount: 1) assert Transaction.merge(%{address: @alice, currency: @currency_1}) == {:error, :single_input} end @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected error when no inputs are found" do assert Transaction.merge(%{address: @alice, currency: @currency_1}) == {:error, :no_inputs_found} end end describe "merge/1 with utxo_positions parameter" do @tag fixtures: [:phoenix_ecto_sandbox] test "given valid `utxo_positions` parameters, forms a merge transaction correctly" do insert_initial_utxo() position_1 = :txoutput |> insert(owner: @alice, currency: @currency_1, amount: 1) |> encoded_position_from_insert() position_2 = :txoutput |> insert(owner: @alice, currency: @currency_1, amount: 1) |> encoded_position_from_insert() position_3 = :txoutput |> insert(owner: @alice, currency: @currency_1, amount: 1) |> encoded_position_from_insert() {:ok, [%{inputs: inputs, outputs: outputs}]} = Transaction.merge([{:utxo_positions, [position_1, position_2, position_3]}]) assert length(inputs) == 3 assert [%{amount: 3, currency: @currency_1, owner: @alice}] = outputs end @tag fixtures: [:phoenix_ecto_sandbox] test "returns an error if any duplicate positions are in the list" do insert_initial_utxo() position_1 = :txoutput |> insert() |> encoded_position_from_insert() assert Transaction.merge([{:utxo_positions, [position_1, position_1]}]) == {:error, :duplicate_input_positions} end @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected error if any position is not found" do insert_initial_utxo() position_1 = :txoutput |> insert() |> encoded_position_from_insert() position_2 = :txoutput |> insert() |> encoded_position_from_insert() position_3 = :txoutput |> insert() |> encoded_position_from_insert() empty_position = insert(:txoutput) |> Map.update!(:blknum, fn n -> n + 1 end) |> encoded_position_from_insert() assert Transaction.merge([{:utxo_positions, [position_1, position_2, position_3, empty_position]}]) == {:error, :position_not_found} end @tag fixtures: [:phoenix_ecto_sandbox] test "returns an error if there is more than one owner for the given set of UTXO positions" do insert_initial_utxo() position_1 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert() position_2 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert() position_3 = :txoutput |> insert(owner: @bob, currency: @currency_1) |> encoded_position_from_insert() assert Transaction.merge([{:utxo_positions, [position_1, position_2, position_3]}]) == {:error, :multiple_input_owners} end @tag fixtures: [:phoenix_ecto_sandbox] test "returns an error if there is more than one currency for the given set of UTXO positions" do insert_initial_utxo() position_1 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert() position_2 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert() position_3 = :txoutput |> insert(owner: @alice, currency: @currency_2) |> encoded_position_from_insert() assert Transaction.merge([{:utxo_positions, [position_1, position_2, position_3]}]) == {:error, :multiple_currencies} end end defp encoded_position_from_insert(%{oindex: oindex, txindex: txindex, blknum: blknum}) do Position.encode({:utxo_position, blknum, txindex, oindex}) end # This is needed so that UTXOs inserted subsequently can have a proper (non-zero) position defp insert_initial_utxo() do insert(:txoutput) end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/block_applicator_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.BlockApplicatorTest do use ExUnitFixtures use ExUnit.Case, async: false alias OMG.Watcher.BlockGetter.BlockApplication alias OMG.WatcherInfo.BlockApplicator alias OMG.WatcherInfo.DB import Ecto.Query, only: [where: 2] setup do eth = <<0::160>> alice = OMG.Watcher.TestHelper.generate_entity() tx = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], eth, [{alice, 100}]) block_application = %BlockApplication{ number: 1_000, eth_height: 1, eth_height_done: true, hash: "0x1000", transactions: [tx], timestamp: 1_576_500_000 } {:ok, block_application: block_application} end describe "insert_block!" do @tag fixtures: [:phoenix_ecto_sandbox] test "inserts the given block application into pending block", %{block_application: block_application} do assert :ok = BlockApplicator.insert_block!(block_application) assert [%DB.Block{blknum: 1_000}] = DB.Repo.all(DB.Block) end @tag fixtures: [:phoenix_ecto_sandbox] test "insert block operation is idempotent", %{block_application: block_application} do blknum = block_application.number :ok = BlockApplicator.insert_block!(block_application) assert :ok = BlockApplicator.insert_block!(block_application) assert %DB.Block{blknum: ^blknum} = DB.Block |> where(blknum: ^blknum) |> DB.Repo.one() end @tag fixtures: [:phoenix_ecto_sandbox] test "breaks when block application is invalid", %{block_application: block_application} do block_application = %BlockApplication{block_application | number: "not an integer"} assert_raise MatchError, fn -> BlockApplicator.insert_block!(block_application) end end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/db/block/chunk_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.Block.ChunkTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures import OMG.WatcherInfo.Factory alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.DB.Block.Chunk require Utxo describe "Chunk.chunk/3" do # The current number of columns on the transaction table allow up to 8191 # transactions to be inserted using `DB.Repo.insert_all/3` before chunking must # be done to avoid hitting postgres limits. The test `DB.Repo.insert_all for # transactions (via postgres INSERT)...` below shows how this number is derived # and asserts the number is correct # # This is the chunk_size for the transactions table. @max_txns_before_chunking 8191 # A special test for insert_all_chunked/3 is here because under the hood it calls insert_all/2. Using # insert_all/3 with a queryable means that certain autogenerated columns, such as inserted_at and # updated_at, will not be inserted as they would be if you used a plain insert. More info # is here: https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert_all/3 @tag fixtures: [:phoenix_ecto_sandbox] test "insert_all_chunked adds inserted_at and updated_at timestamps correctly" do txoutput = :txoutput |> params_for() |> Map.drop([:ethevents]) Enum.each(Chunk.chunk([txoutput]), &DB.Repo.insert_all(DB.TxOutput, &1)) txoutput_with_dates = DB.TxOutput.get_by_position(Utxo.position(txoutput.blknum, txoutput.txindex, txoutput.oindex)) assert txoutput_with_dates.inserted_at != nil assert DateTime.compare(txoutput_with_dates.inserted_at, txoutput_with_dates.updated_at) == :eq end @tag fixtures: [:phoenix_ecto_sandbox] test "insert_all_chunked/3 does not exceed postgres' max of 65535 parameters" do block = insert(:block) # Create an array of transactions beyond postgres limits where chunking # is required. transactions = new_transactions(block.blknum, @max_txns_before_chunking + 1) assert Enum.map(Chunk.chunk(transactions), &DB.Repo.insert_all(DB.Transaction, &1)) == [{8191, nil}, {1, nil}] end end @tag fixtures: [:phoenix_ecto_sandbox] test "DB.Repo.insert_all for transactions (via postgres INSERT) is limited to #{@max_txns_before_chunking} transactions" do utc_now = DateTime.utc_now() # test that transaction inseration at the max limit succeeds block = insert(:block) transactions = new_transactions(block.blknum, @max_txns_before_chunking, utc_now) {transactions_inserted, _} = DB.Repo.insert_all(OMG.WatcherInfo.DB.Transaction, transactions) assert transactions_inserted == @max_txns_before_chunking # test that transaction inseration above the max limit raises an exception block = insert(:block) transactions = new_transactions(block.blknum, @max_txns_before_chunking + 1, utc_now) assert_raise( Postgrex.QueryError, "postgresql protocol can not handle 65536 parameters, the maximum is 65535", fn -> DB.Repo.insert_all(OMG.WatcherInfo.DB.Transaction, transactions) end ) end describe "DB.Repo timestamps" do @tag fixtures: [:phoenix_ecto_sandbox] test "all tables have inserted_at and updated_at timestamps set correctly on inserts and udpates" do Enum.each([:block, :transaction, :txoutput, :ethevent], fn row -> row = insert(row) assert row.inserted_at != nil assert DateTime.compare(row.inserted_at, row.updated_at) == :eq {:ok, row} = DB.Repo.update(Ecto.Changeset.change(row), [{:force, true}]) assert DateTime.compare(row.inserted_at, row.updated_at) == :lt end) end end # Prefer using `ExMachina.build_list/3 which uses `OMG.WatcherInfo.Factory.Transaction` # over this function. This function is built to be fast and simple. defp new_transactions(blknum, count, utc_now \\ nil) do Enum.reduce(1..count, [], fn index, acc -> [new_transaction(blknum, index, utc_now) | acc] end) end # Prefer using `ExMachina.build/2 which uses `OMG.WatcherInfo.Factory.Transaction` # over this function. This function is built to be fast and simple. # # `ExMachina.params_for/2` could be used here to make use of `OMG.WatcherInfo.Factory.Transaction`. # But the transaction factory does a lot of extra stuff unnecessary for this test. This stripped # down version is about 15x faster. Also using `ExMachina.params_for/2` here also requires some # tweaking of the map it returns because `OMG.WatcherInfo.DB.Block.chunk/1` is the code # being tested rather than `Ecto.Repo.insert_all/3`. The 2 functions differ in the inputs they # expect. defp new_transaction(blknum, index, utc_now) do transaction = %{ txhash: to_string(index), txindex: index, txbytes: to_string(index), metadata: to_string(index), txtype: 1, blknum: blknum } if utc_now != nil do Map.merge(transaction, %{inserted_at: utc_now, updated_at: utc_now}) else transaction end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/db/block_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.BlockTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures import OMG.WatcherInfo.Factory import Ecto.Query, only: [from: 2] alias OMG.Utils.Paginator alias OMG.WatcherInfo.DB @eth <<0::160>> @seconds_in_twenty_four_hours 86_400 describe "base_query" do @tag fixtures: [:phoenix_ecto_sandbox] test "can be used to retrieve all blocks" do _ = insert(:block, blknum: 1000, hash: <<1000>>, eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: <<2000>>, eth_height: 2, timestamp: 200) _ = insert(:block, blknum: 3000, hash: <<3000>>, eth_height: 3, timestamp: 300) result = DB.Repo.all(DB.Block.base_query()) assert length(result) == 3 assert Enum.all?(result, fn block -> %DB.Block{} = block end) end @tag fixtures: [:phoenix_ecto_sandbox] test "can be used with a 'where' query expression to retrieve a specific block" do _ = insert(:block, blknum: 1000, hash: <<1000>>, eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: <<2000>>, eth_height: 2, timestamp: 200) _ = insert(:block, blknum: 3000, hash: <<3000>>, eth_height: 3, timestamp: 300) target_blknum = 1000 query = from( block in DB.Block.base_query(), where: [blknum: ^target_blknum] ) result = DB.Repo.one(query) assert %DB.Block{} = result assert result.blknum == target_blknum end @tag fixtures: [:phoenix_ecto_sandbox] test "includes the transaction count corresponding to a block" do block = insert(:block) _ = insert(:transaction, block: block, txindex: 0) _ = insert(:transaction, block: block, txindex: 1) tx_count = DB.Block.base_query() |> DB.Repo.all() |> Enum.at(0) |> Map.get(:tx_count) assert tx_count == 2 end end describe "get/1" do @tag fixtures: [:phoenix_ecto_sandbox] test "retrieves a block by block number" do blknum = 1000 _ = insert(:block, blknum: blknum, hash: "0x#{blknum}", eth_height: 1, timestamp: 100) block = DB.Block.get(blknum) assert %DB.Block{} = block assert block.blknum == blknum end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a correct transaction count if block contains transactions" do blknum = 1000 block = insert(:block, blknum: 1000) _ = insert(:transaction, block: block, txindex: 0) _ = insert(:transaction, block: block, txindex: 1) result = DB.Block.get(blknum) assert result.tx_count == 2 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a tx_count of zero if block has no transactions" do blknum = 1000 _ = insert(:block, blknum: blknum, hash: "0x#{blknum}", eth_height: 1, timestamp: 100) result = DB.Block.get(blknum) assert result.tx_count == 0 end end describe "get_max_blknum/0" do @tag fixtures: [:phoenix_ecto_sandbox] test "last consumed block is not set in empty database" do assert nil == DB.Block.get_max_blknum() end @tag fixtures: [:phoenix_ecto_sandbox] test "last consumed block returns correct block number" do _ = insert(:block, blknum: 1000, hash: <<1000>>, eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: <<2000>>, eth_height: 2, timestamp: 200) _ = insert(:block, blknum: 3000, hash: <<3000>>, eth_height: 3, timestamp: 300) assert 3000 == DB.Block.get_max_blknum() end end describe "get_blocks/1" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns a list of blocks" do _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: 200) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: 300) paginator = %Paginator{ data: [], data_paging: %{ limit: 10, page: 1 } } results = DB.Block.get_blocks(paginator) assert length(results.data) == 3 assert Enum.all?(results.data, fn block -> %DB.Block{} = block end) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a list of blocks sorted by descending blknum" do _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: 200) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: 300) paginator = %Paginator{ data: [], data_paging: %{ limit: 10, page: 1 } } results = DB.Block.get_blocks(paginator) assert length(results.data) == 3 assert results.data |> Enum.at(0) |> Map.get(:blknum) == 3000 assert results.data |> Enum.at(1) |> Map.get(:blknum) == 2000 assert results.data |> Enum.at(2) |> Map.get(:blknum) == 1000 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns an empty list when given limit: 0" do paginator = %Paginator{ data: [], data_paging: %{ limit: 0, page: 1 } } results = DB.Block.get_blocks(paginator) assert results.data == [] end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a correct transaction count if block contains transactions" do block = insert(:block, blknum: 1000) _ = insert(:transaction, block: block, txindex: 0) _ = insert(:transaction, block: block, txindex: 1) paginator = %Paginator{ data: [], data_paging: %{ limit: 10, page: 1 } } tx_count = DB.Block.get_blocks(paginator) |> Map.get(:data) |> Enum.at(0) |> Map.get(:tx_count) assert tx_count == 2 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a tx_count of zero if block has no transactions" do _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: 100) paginator = %Paginator{ data: [], data_paging: %{ limit: 10, page: 1 } } tx_count = DB.Block.get_blocks(paginator) |> Map.get(:data) |> Enum.at(0) |> Map.get(:tx_count) assert tx_count == 0 end end describe "count_all/0" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns correct number of blocks" do _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: 200) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: 300) block_count = DB.Block.count_all() assert block_count == 3 end end describe "count_all_between_timestamps/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns correct count if blocks have been produced between the two given timestamps" do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime = end_datetime - @seconds_in_twenty_four_hours _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: start_datetime + 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: start_datetime) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: start_datetime - 100) block_count = DB.Block.count_all_between_timestamps(start_datetime, end_datetime) assert block_count == 2 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns correct count if no blocks have been produced between the two given timestamps" do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime = end_datetime - @seconds_in_twenty_four_hours _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: start_datetime - 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: start_datetime - 100) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: start_datetime - 100) block_count = DB.Block.count_all_between_timestamps(start_datetime, end_datetime) assert block_count == 0 end end describe "get_timestamp_range_all/0" do @tag fixtures: [:phoenix_ecto_sandbox] test "retrieves the timestamps of the earliest and latest block of all time correctly" do earliest_datetime = 100 _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: earliest_datetime) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: earliest_datetime + 100) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: earliest_datetime + 200) _ = insert(:block, blknum: 4000, hash: "0x4000", eth_height: 3, timestamp: earliest_datetime + 300) expected = %{ max: earliest_datetime + 300, min: earliest_datetime } actual = DB.Block.get_timestamp_range_all() assert expected == actual end end describe "get_timestamp_range_between/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "retrieves the timestamps of the earliest and latest block within a given time range correctly" do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime = end_datetime - @seconds_in_twenty_four_hours _ = insert(:block, blknum: 1000, hash: "0x1000", eth_height: 1, timestamp: start_datetime - 100) _ = insert(:block, blknum: 2000, hash: "0x2000", eth_height: 2, timestamp: start_datetime) _ = insert(:block, blknum: 3000, hash: "0x3000", eth_height: 3, timestamp: start_datetime + 100) _ = insert(:block, blknum: 4000, hash: "0x4000", eth_height: 4, timestamp: start_datetime + 200) expected = %{ max: start_datetime + 200, min: start_datetime } actual = DB.Block.get_timestamp_range_between(start_datetime, end_datetime) assert expected == actual end end describe "insert_from_block_application/1" do @tag fixtures: [:phoenix_ecto_sandbox, :alice, :bob] test "inserts the block, its transactions and transaction outputs", %{alice: alice, bob: bob} do tx_1 = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 300}]) tx_2 = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 500}]) block_application = %{ transactions: [tx_1, tx_2], number: 1000, hash: "0x1000", timestamp: 1_576_500_000, eth_height: 1 } {:ok, block} = DB.Block.insert_from_block_application(block_application) assert %DB.Block{} = block["current_block"] current_block_hash = block["current_block"].hash assert block_application.hash == current_block_hash assert DB.Repo.get(DB.Transaction, tx_1.tx_hash) assert DB.Repo.get(DB.Transaction, tx_2.tx_hash) end @tag fixtures: [:phoenix_ecto_sandbox, :alice, :bob] test "returns an error when inserting with an existing blknum", %{alice: alice, bob: bob} do tx_1 = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 300}]) tx_2 = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 500}]) block_application = %{ transactions: [tx_1, tx_2], number: 1000, hash: "0x1000", timestamp: 1_576_500_000, eth_height: 1 } {:ok, _block} = DB.Block.insert_from_block_application(block_application) assert {:error, "current_block", changeset, %{}} = DB.Block.insert_from_block_application(block_application) assert changeset.errors == [ blknum: {"has already been taken", [constraint: :unique, constraint_name: "blocks_pkey"]} ] end @tag fixtures: [:phoenix_ecto_sandbox, :alice, :bob] @tag :watcher_info @tag timeout: :infinity test "full block test", %{alice: alice, bob: bob} do tx_1 = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 1}]) tx_2 = OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 2}]) transactions = Enum.map(3..64_000, fn _index -> a = OMG.Watcher.TestHelper.generate_entity() b = OMG.Watcher.TestHelper.generate_entity() amount = 5 OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, a}], @eth, [{b, amount}]) end) block_application = %{ transactions: [tx_1, tx_2] ++ transactions, number: 1000, hash: "0x1000", timestamp: 1_576_500_000, eth_height: 1 } {:ok, block} = DB.Block.insert_from_block_application(block_application) assert %DB.Block{} = block["current_block"] current_block_hash = block["current_block"].hash assert block_application.hash == current_block_hash assert DB.Repo.get(DB.Transaction, tx_1.tx_hash) assert DB.Repo.get(DB.Transaction, tx_2.tx_hash) end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/db/eth_event_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.EthEventTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures alias OMG.Utils.Paginator alias OMG.Watcher.Crypto alias OMG.Watcher.Utxo alias OMG.Watcher.Utxo.Position alias OMG.WatcherInfo.DB import OMG.WatcherInfo.Factory require Utxo @eth <<0::160>> @default_paginator %Paginator{ data: [], data_paging: %{ limit: 10, page: 1 } } @tag fixtures: [:phoenix_ecto_sandbox] test "insert deposits: creates deposit event and utxo" do expected_root_chain_txnhash = Crypto.hash(<<1::256>>) expected_log_index = 0 expected_event_type = :deposit expected_eth_height = 1 expected_blknum = 10_000 expected_txindex = 0 expected_oindex = 0 expected_owner = <<1::160>> expected_currency = @eth expected_amount = 1 root_chain_txhash_event = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txnhash, expected_log_index) expected_child_chain_utxohash = DB.EthEvent.generate_child_chain_utxohash(Utxo.position(expected_blknum, expected_txindex, expected_oindex)) assert :ok = DB.EthEvent.insert_deposits!([ %{ root_chain_txhash: expected_root_chain_txnhash, log_index: expected_log_index, blknum: expected_blknum, owner: expected_owner, eth_height: expected_eth_height, currency: expected_currency, amount: expected_amount } ]) event = DB.EthEvent.get(root_chain_txhash_event) assert %DB.EthEvent{ root_chain_txhash: ^expected_root_chain_txnhash, log_index: ^expected_log_index, event_type: ^expected_event_type, eth_height: ^expected_eth_height } = event # check ethevent side of relationship assert length(event.txoutputs) == 1 assert [ %DB.TxOutput{ blknum: ^expected_blknum, txindex: ^expected_txindex, oindex: ^expected_oindex, owner: ^expected_owner, amount: ^expected_amount, currency: ^expected_currency, creating_txhash: nil, spending_txhash: nil, spending_tx_oindex: nil, proof: nil, child_chain_utxohash: ^expected_child_chain_utxohash } | _tail ] = event.txoutputs # check txoutput side of relationship txoutput = DB.TxOutput.get_by_position(Utxo.position(expected_blknum, expected_txindex, expected_oindex)) assert %DB.TxOutput{ blknum: ^expected_blknum, txindex: ^expected_txindex, oindex: ^expected_oindex, owner: ^expected_owner, amount: ^expected_amount, currency: ^expected_currency, creating_txhash: nil, spending_txhash: nil, spending_tx_oindex: nil, proof: nil, child_chain_utxohash: ^expected_child_chain_utxohash } = txoutput assert length(txoutput.ethevents) == 1 assert [ %DB.EthEvent{ root_chain_txhash: ^expected_root_chain_txnhash, log_index: ^expected_log_index, event_type: ^expected_event_type, eth_height: ^expected_eth_height } | _tail ] = txoutput.ethevents end @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "insert deposits: creates deposits and retrieves them by hash", %{alice: alice} do expected_event_type = :deposit expected_owner = alice.addr expected_currency = @eth expected_log_index = 0 expected_amount = 1 expected_eth_height = 1 expected_root_chain_txhash_1 = Crypto.hash(<<2::256>>) expected_root_chain_txhash_event_1 = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txhash_1, expected_log_index) expected_blknum_1 = 20_000 expected_root_chain_txhash_2 = Crypto.hash(<<3::256>>) expected_root_chain_txhash_event_2 = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txhash_2, expected_log_index) expected_blknum_2 = 30_000 expected_root_chain_txhash_3 = Crypto.hash(<<4::256>>) expected_root_chain_txhash_event_3 = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txhash_3, expected_log_index) expected_blknum_3 = 40_000 assert :ok = DB.EthEvent.insert_deposits!([ %{ root_chain_txhash: expected_root_chain_txhash_1, log_index: expected_log_index, blknum: expected_blknum_1, owner: expected_owner, currency: expected_currency, amount: expected_amount, eth_height: expected_eth_height }, %{ root_chain_txhash: expected_root_chain_txhash_2, log_index: expected_log_index, blknum: expected_blknum_2, owner: expected_owner, currency: expected_currency, amount: expected_amount, eth_height: expected_eth_height }, %{ root_chain_txhash: expected_root_chain_txhash_3, log_index: expected_log_index, blknum: expected_blknum_3, owner: expected_owner, currency: expected_currency, amount: expected_amount, eth_height: expected_eth_height } ]) assert %DB.EthEvent{ root_chain_txhash: ^expected_root_chain_txhash_1, event_type: ^expected_event_type, root_chain_txhash_event: ^expected_root_chain_txhash_event_1, eth_height: ^expected_eth_height } = DB.EthEvent.get(expected_root_chain_txhash_event_1) assert %DB.EthEvent{ root_chain_txhash: ^expected_root_chain_txhash_2, event_type: ^expected_event_type, root_chain_txhash_event: ^expected_root_chain_txhash_event_2, eth_height: ^expected_eth_height } = DB.EthEvent.get(expected_root_chain_txhash_event_2) assert %DB.EthEvent{ root_chain_txhash: ^expected_root_chain_txhash_3, event_type: ^expected_event_type, root_chain_txhash_event: ^expected_root_chain_txhash_event_3, eth_height: ^expected_eth_height } = DB.EthEvent.get(expected_root_chain_txhash_event_3) %{data: alice_utxos} = DB.TxOutput.get_utxos(address: alice.addr) assert [^expected_root_chain_txhash_1, ^expected_root_chain_txhash_2, ^expected_root_chain_txhash_3] = Enum.map(alice_utxos, fn txoutput -> [head | _tail] = txoutput.ethevents head.root_chain_txhash end) end @tag fixtures: [:phoenix_ecto_sandbox] test "insert exits: creates exit event and marks utxo as spent" do expected_owner = <<1::160>> expected_log_index = 0 expected_amount = 1 expected_currency = @eth expected_blknum = 50_000 expected_txindex = 0 expected_oindex = 0 expected_utxo_encoded_position = Position.encode(Utxo.position(expected_blknum, expected_txindex, expected_oindex)) expected_deposit_root_chain_txhash = Crypto.hash(<<5::256>>) expected_exit_root_chain_txhash = Crypto.hash(<<6::256>>) expected_deposit_eth_height = 1 expected_exit_eth_height = 2 assert :ok = DB.EthEvent.insert_deposits!([ %{ root_chain_txhash: expected_deposit_root_chain_txhash, log_index: expected_log_index, blknum: expected_blknum, owner: expected_owner, currency: expected_currency, amount: expected_amount, eth_height: expected_deposit_eth_height } ]) %{data: utxos} = DB.TxOutput.get_utxos(address: expected_owner) assert length(utxos) == 1 assert :ok = DB.EthEvent.insert_exits!( [ %{ call_data: %{utxo_pos: expected_utxo_encoded_position}, root_chain_txhash: expected_exit_root_chain_txhash, log_index: expected_log_index, eth_height: expected_exit_eth_height } ], :standard_exit, nil ) %{data: utxos_after_exit} = DB.TxOutput.get_utxos(address: expected_owner) assert Enum.empty?(utxos_after_exit) end @tag fixtures: [:alice, :initial_blocks] test "Writes of deposits and exits are idempotent", %{alice: alice} do # try to insert again existing deposit (from initial_blocks) assert :ok = DB.EthEvent.insert_deposits!([ %{ root_chain_txhash: Crypto.hash(<<1000::256>>), eth_height: 1, log_index: 0, owner: alice.addr, currency: @eth, amount: 333, blknum: 1 } ]) exits = [ %{ root_chain_txhash: Crypto.hash(<<1000::256>>), log_index: 1, call_data: %{utxo_pos: Utxo.Position.encode(Utxo.position(1, 0, 0))}, eth_height: 2 }, %{ root_chain_txhash: Crypto.hash(<<1000::256>>), log_index: 1, call_data: %{utxo_pos: Utxo.Position.encode(Utxo.position(1, 0, 0))}, eth_height: 2 } ] assert :ok = DB.EthEvent.insert_exits!(exits, :in_flight_exit, :InFlightExitStarted) end @tag fixtures: [:alice, :initial_blocks] test "Can spend multiple outputs with single start_ife event", %{alice: alice} do expected_log_index = 0 expected_eth_height = 0 expected_eth_txhash = Crypto.hash(<<6::256>>) expected_event_type = :in_flight_exit %{data: utxos} = DB.TxOutput.get_utxos(address: alice.addr) [ %DB.TxOutput{blknum: blknum1, txindex: txindex1, oindex: oindex1}, %DB.TxOutput{blknum: blknum2, txindex: txindex2, oindex: oindex2} | _ ] = utxos utxo_pos1 = Utxo.position(blknum1, txindex1, oindex1) utxo_pos2 = Utxo.position(blknum2, txindex2, oindex2) exits = [ %{ root_chain_txhash: expected_eth_txhash, log_index: expected_log_index, eth_height: expected_eth_height, call_data: %{utxo_pos: Utxo.Position.encode(utxo_pos1)} }, %{ root_chain_txhash: expected_eth_txhash, log_index: expected_log_index, eth_height: expected_eth_height, call_data: %{utxo_pos: Utxo.Position.encode(utxo_pos2)} } ] assert :ok = DB.EthEvent.insert_exits!(exits, expected_event_type, :InFlightExitStarted) txo1 = DB.TxOutput.get_by_position(utxo_pos1) assert txo1 != nil assert [ %DB.EthEvent{ log_index: ^expected_log_index, root_chain_txhash: ^expected_eth_txhash, event_type: ^expected_event_type } ] = txo1.ethevents txo2 = DB.TxOutput.get_by_position(utxo_pos2) assert txo2 != nil assert [ %DB.EthEvent{ log_index: ^expected_log_index, root_chain_txhash: ^expected_eth_txhash, event_type: ^expected_event_type } ] = txo2.ethevents end @tag fixtures: [:alice, :initial_blocks] test "Can spend ife piggybacked output", %{alice: alice} do expected_log_index1 = 0 expected_log_index2 = 1 expected_eth_height1 = 0 expected_eth_height2 = 1 expected_eth_txhash1 = Crypto.hash(<<6::256>>) expected_eth_txhash2 = Crypto.hash(<<7::256>>) expected_event_type = :in_flight_exit %{data: utxos} = DB.TxOutput.get_utxos(address: alice.addr) [ %DB.TxOutput{creating_txhash: txhash1, oindex: oindex1}, %DB.TxOutput{creating_txhash: txhash2, oindex: oindex2} ] = utxos |> Enum.drop(1) |> Enum.take(2) exits = [ %{ root_chain_txhash: expected_eth_txhash1, log_index: expected_log_index1, eth_height: expected_eth_height1, call_data: %{txhash: txhash1, oindex: oindex1} }, %{ root_chain_txhash: expected_eth_txhash2, log_index: expected_log_index2, eth_height: expected_eth_height2, call_data: %{txhash: txhash2, oindex: oindex2} } ] assert :ok = DB.EthEvent.insert_exits!(exits, expected_event_type, :InFlightExitOutputWithdrawn) assert_txoutput_spent_by_event( txhash1, oindex1, expected_log_index1, expected_eth_txhash1, expected_eth_height1, expected_event_type ) assert_txoutput_spent_by_event( txhash2, oindex2, expected_log_index2, expected_eth_txhash2, expected_eth_height2, expected_event_type ) end @tag fixtures: [:initial_blocks] test "Allows for missing output when the event explicitly allows it" do max_blknum = DB.Repo.aggregate(DB.TxOutput, :max, :blknum) pos_from_future = Utxo.position(max_blknum + 1, 0, 0) exits = [ %{ root_chain_txhash: Crypto.hash(<<6::256>>), log_index: 0, eth_height: 0, call_data: %{utxo_pos: Utxo.Position.encode(pos_from_future)} } ] assert :ok = DB.EthEvent.insert_exits!(exits, :in_flight_exit, :InFlightTxOutputPiggybacked) end @tag fixtures: [:initial_blocks] test "Fails when missing output disallowed" do max_blknum = DB.Repo.aggregate(DB.TxOutput, :max, :blknum) pos_from_future = Utxo.position(max_blknum + 1, 0, 0) exits = [ %{ root_chain_txhash: Crypto.hash(<<6::256>>), log_index: 0, eth_height: 0, call_data: %{utxo_pos: Utxo.Position.encode(pos_from_future)} } ] assert_raise CaseClauseError, fn -> DB.EthEvent.insert_exits!(exits, :in_flight_exit, :InFlightExitOutputWithdrawn) end end @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "deposited and exited utxo is retrievable by position", %{alice: alice} do assert :ok = DB.EthEvent.insert_deposits!([ %{ root_chain_txhash: Crypto.hash(<<1000::256>>), eth_height: 1, log_index: 0, owner: alice.addr, currency: @eth, amount: 333, blknum: 1 } ]) assert :ok = DB.EthEvent.insert_exits!( [ %{ root_chain_txhash: Crypto.hash(<<1001::256>>), eth_height: 2, log_index: 1, call_data: %{utxo_pos: Utxo.Position.encode(Utxo.position(1, 0, 0))} } ], :in_flight_exit, :InFlightExitStarted ) assert %DB.TxOutput{ethevents: events} = DB.TxOutput.get_by_position(Utxo.position(1, 0, 0)) assert [ %DB.EthEvent{event_type: :deposit}, %DB.EthEvent{event_type: :in_flight_exit} ] = Enum.sort(events, &(&1.eth_height < &2.eth_height)) end @tag fixtures: [:initial_blocks] test "only one exit type can be associated to output which is retrievable by output_id", %{initial_blocks: blocks} do # Get the transaction with _unspent_ output {blknum, txindex, txhash, _tx} = Enum.find(blocks, fn {blknum, _, _, _} -> blknum == 2000 end) utxo_pos = Utxo.Position.encode(Utxo.position(blknum, txindex, 0)) assert :ok = DB.EthEvent.insert_exits!( [ %{ root_chain_txhash: Crypto.hash(<<1001::256>>), eth_height: 1, log_index: 0, call_data: %{utxo_pos: utxo_pos} } ], :standard_exit, nil ) assert :ok = DB.EthEvent.insert_exits!( [ %{ root_chain_txhash: Crypto.hash(<<1002::256>>), eth_height: 2, log_index: 1, call_data: %{utxo_pos: utxo_pos} } ], :in_flight_exit, :InFlightExitStarted ) assert %DB.TxOutput{ethevents: events} = DB.TxOutput.get_by_output_id(txhash, 0) assert [%DB.EthEvent{event_type: :standard_exit}] = events end defp assert_txoutput_spent_by_event(txhash, oindex, log_index, eth_txhash, eth_height, event_type) do txo = DB.TxOutput.get_by_output_id(txhash, oindex) assert txo != nil assert [ %DB.EthEvent{ log_index: ^log_index, root_chain_txhash: ^eth_txhash, eth_height: ^eth_height, event_type: ^event_type } ] = txo.ethevents end describe "get_deposits" do @tag fixtures: [:phoenix_ecto_sandbox] test "filters deposits by address" do owner_1 = <<1::160>> owner_2 = <<2::160>> deposit_output_1 = build(:txoutput, %{owner: owner_1}) deposit_output_2 = build(:txoutput, %{owner: owner_2}) _ = insert(:ethevent, event_type: :deposit, txoutputs: [deposit_output_1]) _ = insert(:ethevent, event_type: :deposit, txoutputs: [deposit_output_2]) %{data: [deposit_1]} = DB.EthEvent.get_deposits(@default_paginator, owner_1) %{data: [deposit_2]} = DB.EthEvent.get_deposits(@default_paginator, owner_2) assert deposit_1 |> Map.get(:txoutputs) |> Enum.at(0) |> Map.get(:owner) == owner_1 assert deposit_2 |> Map.get(:txoutputs) |> Enum.at(0) |> Map.get(:owner) == owner_2 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns deposits sorted by descending eth_height" do owner = <<1::160>> _ = insert(:ethevent, eth_height: 1, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, eth_height: 3, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, eth_height: 2, txoutputs: [build(:txoutput, %{owner: owner})]) results = DB.EthEvent.get_deposits(@default_paginator, owner) assert results.data |> Enum.at(0) |> Map.get(:eth_height) == 3 assert results.data |> Enum.at(1) |> Map.get(:eth_height) == 2 assert results.data |> Enum.at(2) |> Map.get(:eth_height) == 1 end @tag fixtures: [:phoenix_ecto_sandbox] test "pagination - correctly paginates responses" do owner = <<1::160>> _ = insert(:ethevent, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, txoutputs: [build(:txoutput, %{owner: owner})]) paginator_1 = %Paginator{ data: [], data_paging: %{ limit: 2, page: 1 } } paginator_2 = %Paginator{ data: [], data_paging: %{ limit: 2, page: 2 } } %{data: data_page_1} = DB.EthEvent.get_deposits(paginator_1, owner) %{data: data_page_2} = DB.EthEvent.get_deposits(paginator_2, owner) assert length(data_page_1) == 2 assert length(data_page_2) == 1 end @tag fixtures: [:phoenix_ecto_sandbox] test "pagination - returns empty array if given limit 0" do owner = <<1::160>> _ = insert(:ethevent, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, txoutputs: [build(:txoutput, %{owner: owner})]) _ = insert(:ethevent, txoutputs: [build(:txoutput, %{owner: owner})]) paginator = %Paginator{ data: [], data_paging: %{ limit: 0, page: 1 } } %{data: data} = DB.EthEvent.get_deposits(paginator, owner) assert Enum.empty?(data) end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/db/transaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.TransactionTest do @moduledoc """ Currently, this test focuses on testing behaviors not testable via Controllers.TransactionTest. The reason is that we are treating the DB schema etc. as implementation detail. In case testing through controllers becomes hard/slow or otherwise unreasnable, refactor these two kinds of tests appropriately """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use Plug.Test alias OMG.Utils.Paginator alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB require Utxo import ExUnit.CaptureLog import OMG.WatcherInfo.Factory @seconds_in_twenty_four_hours 86_400 @tag fixtures: [:initial_blocks] test "the associated block can be preloaded" do preloaded = DB.Transaction.get_by_position(3000, 1) |> DB.Repo.preload(:block) assert %DB.Transaction{ blknum: 3000, txindex: 1, block: %DB.Block{blknum: 3000} } = preloaded end @tag fixtures: [:initial_blocks] test "gets all transactions from a block", %{initial_blocks: initial_blocks} do # this test is here to ensure that calls coming from places other than `transaction` controllers are covered [tx0, tx1] = DB.Transaction.get_by_blknum(3000) tx_hashes = initial_blocks |> Enum.filter(&(elem(&1, 0) == 3000)) |> Enum.map(&elem(&1, 2)) assert tx_hashes == [tx0, tx1] |> Enum.map(& &1.txhash) assert [] == DB.Transaction.get_by_blknum(5000) end @tag fixtures: [:initial_blocks] test "passing constrains out of allowed takes no effect and print a warning" do assert capture_log([level: :warn], fn -> DB.Transaction.get_by_filters( [blknum: 2000, nothing: "there's no such thing"], %Paginator{} ) end) =~ "Constraint on :nothing does not exist in schema and was dropped from the query" end describe "count_all/0" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns a correct transaction count" do block = insert(:block, blknum: 1000) _ = insert(:transaction, block: block, txindex: 0) _ = insert(:transaction, block: block, txindex: 1) tx_count = DB.Transaction.count_all() assert tx_count == 2 end end describe "get/1" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns the transaction from its hash with all data" do block = insert(:block, blknum: 1000) %{txhash: txhash} = insert(:transaction, block: block, txindex: 0, txtype: 1) _ = insert(:transaction, block: block, txindex: 1, txtype: 3) tx = DB.Transaction.get(txhash) assert tx.txindex == 0 assert tx.txtype == 1 assert tx.txhash == txhash end end describe "count_all_between_timestamp/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns correct count if transactions have been made between the given timestamps" do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime = end_datetime - @seconds_in_twenty_four_hours block = insert(:block, blknum: 1000, timestamp: start_datetime + 100) _ = insert(:transaction, block: block, txindex: 0) _ = insert(:transaction, block: block, txindex: 1) tx_count = DB.Transaction.count_all_between_timestamps(start_datetime, end_datetime) assert tx_count == 2 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns correct count if no transactions have been made between the given timestamps" do end_datetime = DateTime.to_unix(DateTime.utc_now()) start_datetime = end_datetime - @seconds_in_twenty_four_hours block = insert(:block, blknum: 1000, timestamp: start_datetime - 100) _ = insert(:transaction, block: block, txindex: 0) _ = insert(:transaction, block: block, txindex: 1) tx_count = DB.Transaction.count_all_between_timestamps(start_datetime, end_datetime) assert tx_count == 0 end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/db/txoutput_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.DB.TxOutputTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures import OMG.WatcherInfo.Factory alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB require Utxo @eth <<0::160>> @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "transaction output schema handles big numbers properly", %{alice: alice} do power_of_2 = fn n -> :lists.duplicate(n, 2) |> Enum.reduce(&(&1 * &2)) end assert 16 == power_of_2.(4) big_amount = power_of_2.(256) - 1 block_application = %{ transactions: [OMG.Watcher.TestHelper.create_recovered([], @eth, [{alice, big_amount}])], number: 11_000, hash: <>, timestamp: :os.system_time(:second), eth_height: 10 } {:ok, _} = DB.Block.insert_from_block_application(block_application) utxo = DB.TxOutput.get_by_position(Utxo.position(11_000, 0, 0)) assert not is_nil(utxo) assert utxo.amount == big_amount end describe "create_outputs/4" do @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "create outputs according to params", %{alice: alice} do blknum = 11_000 amount_1 = 1000 amount_2 = 2000 tx = OMG.Watcher.TestHelper.create_recovered([], @eth, [{alice, amount_1}, {alice, amount_2}]) assert [ %{ amount: amount_1, blknum: blknum, creating_txhash: tx.tx_hash, currency: @eth, oindex: 0, otype: 1, owner: alice.addr, txindex: 0 }, %{ amount: amount_2, blknum: blknum, creating_txhash: tx.tx_hash, currency: @eth, oindex: 1, otype: 1, owner: alice.addr, txindex: 0 } ] == DB.TxOutput.create_outputs(blknum, 0, tx.tx_hash, tx) end end describe "OMG.WatcherInfo.DB.TxOutput.spend_utxos/3" do # a special test for spend_utxos/3 is here because under the hood it calls update_all/3. using # update_all/3 with a queryable means that certain autogenerated columns, such as inserted_at and # updated_at, will not be updated as they would be if you used a plain update. More info # is here: https://hexdocs.pm/ecto/Ecto.Repo.html#c:update_all/3 @tag fixtures: [:phoenix_ecto_sandbox] test "spend_utxos updates the updated_at timestamp correctly" do deposit = :txoutput |> build() |> with_deposit() :transaction |> insert() |> with_inputs([deposit]) txinput = DB.TxOutput.get_by_position(Utxo.position(deposit.blknum, deposit.txindex, deposit.oindex)) spend_utxo_params = spend_uxto_params_from_txoutput(txinput) _ = DB.Repo.transaction(DB.TxOutput.spend_utxos(Ecto.Multi.new(), [spend_utxo_params])) spent_txoutput = DB.TxOutput.get_by_position(Utxo.position(txinput.blknum, txinput.txindex, txinput.oindex)) assert :eq == DateTime.compare(txinput.inserted_at, spent_txoutput.inserted_at) assert :lt == DateTime.compare(txinput.updated_at, spent_txoutput.updated_at) end end describe "get_utxos_grouped_by_currency/1" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns outputs grouped by currency" do alice = TestHelper.generate_entity() currency_1 = <<1::160>> currency_2 = <<2::160>> _ = insert(:txoutput, currency: currency_1, owner: alice.addr) _ = insert(:txoutput, currency: currency_2, owner: alice.addr) _ = insert(:txoutput, currency: currency_1, owner: alice.addr) _ = insert(:txoutput, currency: currency_2, owner: alice.addr) result = DB.TxOutput.get_sorted_grouped_utxos(alice.addr, :desc) assert Map.keys(result) == [currency_1, currency_2] Enum.each(result, fn {currency, outputs} -> assert length(outputs) == 2 Enum.each(outputs, fn output -> assert output.currency == currency end) end) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns outputs for the given address only" do alice = <<1::160>> bob = <<2::160>> _ = insert(:txoutput, owner: alice) _ = insert(:txoutput, owner: alice) _ = insert(:txoutput, owner: bob) result = DB.TxOutput.get_sorted_grouped_utxos(alice, :desc) Enum.each(result, fn {_currency, outputs} -> Enum.each(outputs, fn output -> assert output.owner == alice end) end) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns outputs for a given currency in descending amount order if specified" do alice = <<1::160>> _ = insert(:txoutput, amount: 100, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 200, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 300, currency: @eth, owner: alice) result = alice |> DB.TxOutput.get_sorted_grouped_utxos(:desc) |> Map.get(@eth) assert result |> Enum.at(0) |> Map.get(:amount) == 300 assert result |> Enum.at(1) |> Map.get(:amount) == 200 assert result |> Enum.at(2) |> Map.get(:amount) == 100 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns outputs for a given currency in ascending amount order if specified" do alice = <<1::160>> _ = insert(:txoutput, amount: 100, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 200, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 300, currency: @eth, owner: alice) result = alice |> DB.TxOutput.get_sorted_grouped_utxos(:asc) |> Map.get(@eth) assert result |> Enum.at(0) |> Map.get(:amount) == 100 assert result |> Enum.at(1) |> Map.get(:amount) == 200 assert result |> Enum.at(2) |> Map.get(:amount) == 300 end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/http_rpc/adapter_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.HttpRPC.AdapterTest do use ExUnit.Case, async: true import FakeServer alias OMG.Utils.AppVersion alias OMG.WatcherInfo.HttpRPC.Adapter describe "get_unparsed_response_body/1" do test "returns an unparsed body when successful" do body = "{\r\n \"success\": true,\r\n \"data\": {\r\n \"test\": \"something\"\r\n }\r\n}" assert {:ok, response} = Adapter.get_unparsed_response_body(%HTTPoison.Response{status_code: 200, body: body}) assert response == %{"test" => "something"} end test "returns a `client_error` error with the data when failed" do body = "{\r\n \"success\": false,\r\n \"data\": {\r\n \"test\": \"something\"\r\n }\r\n}" assert {:error, response} = Adapter.get_unparsed_response_body(%HTTPoison.Response{status_code: 200, body: body}) assert response == {:client_error, %{"test" => "something"}} end test "returns a `malformed_response` error with the data when the body is not recognized" do body = "{\r\n \"malformed\": \"body\"\r\n}" assert {:error, response} = Adapter.get_unparsed_response_body(%HTTPoison.Response{status_code: 200, body: body}) assert response == {:malformed_response, %{"malformed" => "body"}} end test "returns a `childchain_unreachable` error when `econnrefused` is returned" do assert {:error, :childchain_unreachable} = Adapter.get_unparsed_response_body({:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}) end test "returns the HTTPoison error reason when present" do assert {:error, :a_reason} = Adapter.get_unparsed_response_body({:error, %HTTPoison.Error{id: nil, reason: :a_reason}}) end end describe "get_response_body/1" do test "returns a body with the key parsed when successful" do body = "{\r\n \"success\": true,\r\n \"data\": {\r\n \"test\": \"something\"\r\n }\r\n}" assert {:ok, response} = Adapter.get_response_body(%HTTPoison.Response{status_code: 200, body: body}) assert response == %{test: "something"} end test "returns a `client_error` error with the data when failed" do body = "{\r\n \"success\": false,\r\n \"data\": {\r\n \"test\": \"something\"\r\n }\r\n}" assert {:error, response} = Adapter.get_response_body(%HTTPoison.Response{status_code: 200, body: body}) assert response == {:client_error, %{"test" => "something"}} end test "returns a `malformed_response` error with the data when the body is not recognized" do body = "{\r\n \"malformed\": \"body\"\r\n}" assert {:error, response} = Adapter.get_response_body(%HTTPoison.Response{status_code: 200, body: body}) assert response == {:malformed_response, %{"malformed" => "body"}} end end describe "rpc_post/3" do test_with_server "includes X-Watcher-Version header" do route("/path", FakeServer.Response.ok()) _ = Adapter.rpc_post(%{}, "path", FakeServer.address()) expected_watcher_version = AppVersion.version(:omg_watcher_info) assert request_received( "/path", method: "POST", headers: %{"content-type" => "application/json", "x-watcher-version" => expected_watcher_version} ) end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/order_fee_fetcher_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.OrderFeeFetcherTest do use ExUnitFixtures use ExUnit.Case, async: true use OMG.WatcherInfo.Fixtures alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.WireFormatTypes alias OMG.WatcherInfo.OrderFeeFetcher alias OMG.WatcherInfo.TestServer @eth <<0::160>> @not_eth <<1::160>> @tx_type WireFormatTypes.tx_type_for(:tx_payment_v1) @str_tx_type Integer.to_string(@tx_type) setup do context = TestServer.start() on_exit(fn -> TestServer.stop(context) end) context end describe "add_fee_to_order/2" do test "adds the correct amount to the order", context do prepare_test_server(context, %{ @str_tx_type => [ %{ "currency" => Encoding.to_hex(@eth), "amount" => 2, "subunit_to_unit" => 1_000_000_000_000_000_000, "pegged_amount" => 4, "pegged_currency" => "USD", "pegged_subunit_to_unit" => 100, "updated_at" => "2019-01-01T10:10:00+00:00" } ] }) order = %{ fee: %{currency: @eth} } assert OrderFeeFetcher.add_fee_to_order(order, context.fake_addr) == {:ok, Kernel.put_in(order, [:fee, :amount], 2)} end test "returns an `unexpected_fee_currency` error when the child chain returns an unexpected fee value", context do prepare_test_server(context, %{ @str_tx_type => [ %{ "currency" => Encoding.to_hex(@not_eth), "amount" => 2, "subunit_to_unit" => 1_000_000_000_000_000_000, "pegged_amount" => 4, "pegged_currency" => "USD", "pegged_subunit_to_unit" => 100, "updated_at" => "2019-01-01T10:10:00+00:00" } ] }) assert OrderFeeFetcher.add_fee_to_order(%{fee: %{currency: @eth}}, context.fake_addr) == {:error, :unexpected_fee_currency} end test "forwards the childchain error", context do prepare_test_server(context, %{ code: "fees.all:some_error", description: "Some errors" }) assert OrderFeeFetcher.add_fee_to_order(%{fee: %{currency: @eth}}, context.fake_addr) == {:error, {:client_error, %{"code" => "fees.all:some_error", "description" => "Some errors"}}} end end defp prepare_test_server(context, response) do response |> TestServer.make_response() |> TestServer.with_response(context, "/fees.all") end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/release_tasks/set_tracer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.ReleaseTasks.SetTracerTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.WatcherInfo.ReleaseTasks.SetTracer alias OMG.WatcherInfo.Tracer @app :omg_watcher_info setup do {:ok, pid} = __MODULE__.System.start_link([]) nil = Process.put(__MODULE__.System, pid) :ok end test "if environment variables get applied in the configuration" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUE") :ok = __MODULE__.System.put_env("APP_ENV", "YOLO") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) disabled = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:disabled?) env = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:env) assert disabled == true # if it's disabled, env doesn't matter, so we set it to an empty string assert env == "" end) end test "if default configuration is used when there's no environment variables" do :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 3") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) # we set env to an empty string because disabled? is set to true! configuration = @app |> Application.get_env(Tracer) |> Keyword.put(:env, "") |> Enum.sort() tracer_config = config |> Keyword.get(@app) |> Keyword.get(Tracer) |> Enum.sort() assert configuration == tracer_config end) end test "if exit is thrown when faulty configuration is used" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUEeee") catch_exit(SetTracer.load([], system_adapter: __MODULE__.System)) end defmodule System do def start_link(args), do: GenServer.start_link(__MODULE__, args, []) def get_env(key), do: __MODULE__ |> Process.get() |> GenServer.call({:get_env, key}) def put_env(key, value), do: __MODULE__ |> Process.get() |> GenServer.call({:put_env, key, value}) def init(_), do: {:ok, %{}} def handle_call({:get_env, key}, _, state) do {:reply, state[key], state} end def handle_call({:put_env, key, value}, _, state) do {:reply, :ok, Map.put(state, key, value)} end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/transaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.TransactionTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.Transaction import OMG.WatcherInfo.Factory require Utxo @eth <<0::160>> @alice <<27::160>> @bob <<28::160>> describe "select_inputs/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns {:ok, transctions} when able to select utxos to satisfy payments and fee" do _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) utxos_per_token = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) order = %{ owner: @alice, payments: [ %{amount: 10, currency: @eth, owner: @bob} ], fee: %{currency: @eth, amount: 5}, metadata: nil } assert {:ok, %{ @eth => [ %{amount: 10, currency: @eth}, %{amount: 10, currency: @eth} ] }} = Transaction.select_inputs(utxos_per_token, order) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns error when the funds are not sufficient to satisfy payments and fee" do _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) utxos_per_token = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) order = %{ owner: @alice, payments: [ %{amount: 30, currency: @eth, owner: @bob} ], fee: %{currency: @eth, amount: 10}, metadata: nil } assert {:error, {:insufficient_funds, [%{missing: 10}]}} = Transaction.select_inputs(utxos_per_token, order) end end describe "create/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns {:error, :too_many_outputs} when a number of outputs > maximum" do _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) utxos_per_token = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) order = %{ owner: @alice, payments: [ %{amount: 9, currency: @eth, owner: @bob}, %{amount: 9, currency: @eth, owner: <<29::160>>}, %{amount: 9, currency: @eth, owner: <<30::160>>}, %{amount: 9, currency: @eth, owner: <<31::160>>}, %{amount: 9, currency: @eth, owner: <<32::160>>} ], fee: %{currency: @eth, amount: 9}, metadata: nil } assert {:error, :too_many_outputs} == Transaction.create(utxos_per_token, order) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns {:error, :empty_inputs} when a inputs of inputs = 0" do order = %{ owner: @alice, payments: [ %{amount: 45, currency: @eth, owner: @bob} ], fee: %{currency: @eth, amount: 5}, metadata: nil } assert {:error, :empty_transaction} == Transaction.create([], order) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns {:ok, transactions} when 0 < inputs <= 4 and 0 < outputs <= 4" do _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) utxos_per_token = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) order = %{ owner: @alice, payments: [ %{amount: 35, currency: @eth, owner: @bob} ], fee: %{currency: @eth, amount: 5}, metadata: nil } assert {:ok, [ %{ fee: %{currency: @eth, amount: 5}, inputs: [ %{amount: 10, currency: @eth}, %{amount: 10, currency: @eth}, %{amount: 10, currency: @eth}, %{amount: 10, currency: @eth} ], outputs: [ %{amount: 35, currency: @eth, owner: @bob} ] } ]} = Transaction.create(utxos_per_token, order) end end describe "include_typed_data/1" do test "returns an original error when the param is matched with {:error, _}" do assert {:error, :any} == Transaction.include_typed_data({:error, :any}) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns transactions with :typed_data" do _ = insert(:txoutput, amount: 10, currency: @eth, owner: @alice) utxos_per_token = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) order = %{ owner: @alice, payments: [ %{amount: 5, currency: @eth, owner: @bob} ], fee: %{currency: @eth, amount: 5}, metadata: nil } {:ok, transactions} = Transaction.create(utxos_per_token, order) assert {:ok, %{transactions: transactions}} = Transaction.include_typed_data({:ok, %{transactions: transactions, result: :complete}}) assert Enum.all?(transactions, &Map.has_key?(&1, :typed_data)) end end end ================================================ FILE: apps/omg_watcher_info/test/omg_watcher_info/utxo_selection_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and defmodule OMG.WatcherInfo.UtxoSelectionTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures alias OMG.Eth.Encoding alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.UtxoSelection import OMG.WatcherInfo.Factory require Utxo @alice <<27::160>> @eth <<0::160>> @other_token <<127::160>> describe "calculate_net_amount/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns a correct map when payment_currency != fee_currency" do payment_currency = @eth fee_currency = @other_token payments = [ %{ owner: @alice, currency: payment_currency, amount: 1_000 } ] fee = %{ currency: fee_currency, amount: 2_000 } assert %{ payment_currency => 1_000, fee_currency => 2_000 } == UtxoSelection.calculate_net_amount(payments, fee) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a correct map when payment_currency == fee_currency" do payment_currency = @eth payments = [ %{ owner: @alice, currency: payment_currency, amount: 1_000 } ] fee = %{ currency: payment_currency, amount: 2_000 } assert %{ payment_currency => 3_000 } == UtxoSelection.calculate_net_amount(payments, fee) end end describe "review_selected_utxos/1" do test "should return the expected error if selected UTXOs do not cover the amount of the transaction order" do variances = %{@eth => 5, @other_token => 10} # UTXO list is empty for simplicty as the error response does not need it. utxo_list = [] constructed_argument = Enum.map([@eth, @other_token], fn ccy -> {ccy, {variances[ccy], utxo_list}} end) assert UtxoSelection.review_selected_utxos(constructed_argument) == {:error, {:insufficient_funds, [ %{missing: variances[@eth], token: Encoding.to_hex(@eth)}, %{missing: variances[@other_token], token: Encoding.to_hex(@other_token)} ]}} end @tag fixtures: [:phoenix_ecto_sandbox] test "should return the expected response if UTXOs cover the amount of the transaction order" do variances = %{@eth => -5, @other_token => 0} _ = insert(:txoutput, amount: 100, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 100, currency: @other_token, owner: @alice) %{@eth => [eth_utxo], @other_token => [other_token_utxo]} = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) constructed_argument = [ {@eth, {variances[@eth], [eth_utxo]}}, {@other_token, {variances[@other_token], [other_token_utxo]}} ] assert {:ok, %{ @eth => [_eth_utxo], @other_token => [^other_token_utxo] }} = UtxoSelection.review_selected_utxos(constructed_argument) end end describe "select_utxos/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns the expected utxos if UTXOs cover `net_amount`" do net_amount = %{ @eth => 2_000 } _ = insert(:txoutput, amount: 1_200, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 1_000, currency: @eth, owner: @alice) utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) assert [{@eth, {-200, _utxos}}] = UtxoSelection.select_utxos(net_amount, utxos) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns the expected utxos if any of UTXOs exactly matched `net_amount`" do net_amount = %{ @eth => 2_000 } _ = insert(:txoutput, amount: 2_000, currency: @eth, owner: @alice) utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) assert [{@eth, {0, _utxos}}] = UtxoSelection.select_utxos(net_amount, utxos) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns positive variance if UTXOs don't cover `net_amount`" do net_amount = %{ @eth => 2_000 } _ = insert(:txoutput, amount: 500, currency: @eth, owner: @alice) _ = insert(:txoutput, amount: 500, currency: @eth, owner: @alice) utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) assert [{@eth, {1_000, _utxos}}] = UtxoSelection.select_utxos(net_amount, utxos) end end describe "add_utxos_for_stealth_merge/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns selected UTXOs with no additions if the maximum has already been selected" do for _i <- 1..5 do _ = insert(:txoutput, owner: @alice) end [not_included | included] = @alice |> DB.TxOutput.get_sorted_grouped_utxos(:desc) |> Map.get(@eth) inputs = %{ @eth => included } assert UtxoSelection.add_utxos_for_stealth_merge([not_included], inputs) == inputs end @tag fixtures: [:phoenix_ecto_sandbox] test "returns selected UTXOs with no additions if no other UTXOs are available" do for _i <- 1..4 do _ = insert(:txoutput, owner: @alice) end inputs = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) other_available = [] assert UtxoSelection.add_utxos_for_stealth_merge(other_available, inputs) == inputs end @tag fixtures: [:phoenix_ecto_sandbox] test "adds UTXOs until the limit is reached in the case of one currency" do for _i <- 1..5 do _ = insert(:txoutput, owner: @alice) end [included | available] = @alice |> DB.TxOutput.get_sorted_grouped_utxos(:desc) |> Map.get(@eth) [available_1, available_2, available_3 | _not_for_inclusion] = available inputs = %{ @eth => [included] } expected = %{ @eth => [available_3, available_2, available_1, included] } assert UtxoSelection.add_utxos_for_stealth_merge(available, inputs) == expected end @tag fixtures: [:phoenix_ecto_sandbox] test "adds UTXOs until the limit is reached in the case of multiple currencies" do _ = insert(:txoutput, currency: @eth, owner: @alice) _ = insert(:txoutput, currency: @eth, owner: @alice) _ = insert(:txoutput, currency: @other_token, owner: @alice) _ = insert(:txoutput, currency: @other_token, owner: @alice) utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) [input_1, merge_1] = Map.get(utxos, @eth) [input_2, merge_2] = Map.get(utxos, @other_token) inputs = %{ @eth => [input_1], @other_token => [input_2] } assert %{ @eth => [^merge_1, ^input_1], @other_token => [^merge_2, ^input_2] } = UtxoSelection.add_utxos_for_stealth_merge([merge_1, merge_2], inputs) end end describe "prioritize_merge_utxos/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns only UTXOs that have not already been selected for the transaction" do _ = insert(:txoutput, currency: @eth, owner: @alice) _ = insert(:txoutput, currency: @eth, owner: @alice) _ = insert(:txoutput, currency: @eth, owner: @alice) _ = insert(:txoutput, currency: @eth, owner: @alice) %{ @eth => [selected_eth | available_eth] } = utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) inputs = %{ @eth => [selected_eth] } result = UtxoSelection.prioritize_merge_utxos(utxos, inputs) assert result == available_eth assert Enum.member?(result, selected_eth) == false end @tag fixtures: [:phoenix_ecto_sandbox] test "returns UTXOs only for currencies already in the transaction" do token_a = <<65::160>> token_b = <<66::160>> token_c = <<67::160>> _ = insert(:txoutput, currency: token_a, owner: @alice) _ = insert(:txoutput, currency: token_a, owner: @alice) _ = insert(:txoutput, currency: token_b, owner: @alice) _ = insert(:txoutput, currency: token_b, owner: @alice) _ = insert(:txoutput, currency: token_c, owner: @alice) _ = insert(:txoutput, currency: token_c, owner: @alice) %{ ^token_a => [utxo_a_1, utxo_a_2], ^token_b => [utxo_b_1, utxo_b_2], ^token_c => [_, _] } = utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) inputs = %{ token_a => [utxo_a_1], token_b => [utxo_b_1] } result = UtxoSelection.prioritize_merge_utxos(utxos, inputs) assert result == [utxo_a_2, utxo_b_2] assert Enum.find(result, fn utxo -> utxo.currency == token_c end) == nil end @tag fixtures: [:phoenix_ecto_sandbox] test "orders UTXOs by currency in descending order of set size" do token_a = <<65::160>> token_b = <<66::160>> token_c = <<67::160>> for _i <- 1..4 do _ = insert(:txoutput, currency: token_a, owner: @alice) end for _i <- 1..3 do _ = insert(:txoutput, currency: token_b, owner: @alice) end for _i <- 1..2 do _ = insert(:txoutput, currency: token_c, owner: @alice) end %{ ^token_a => [selected_a | available_a], ^token_b => [selected_b | available_b], ^token_c => [selected_c | available_c] } = utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) inputs = %{ token_a => [selected_a], token_b => [selected_b], token_c => [selected_c] } result = UtxoSelection.prioritize_merge_utxos(utxos, inputs) assert result == available_a ++ available_b ++ available_c end @tag fixtures: [:phoenix_ecto_sandbox] test "orders UTXOs by currency in descending order of set size, excluding already selected UTXOs" do token_a = <<65::160>> token_b = <<66::160>> for _i <- 1..3 do _ = insert(:txoutput, currency: token_a, owner: @alice) _ = insert(:txoutput, currency: token_b, owner: @alice) end %{ ^token_a => [a_1, a_2, a_3], ^token_b => [b_1, b_2, b_3] } = utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) inputs = %{ token_a => [a_1, a_2], token_b => [b_1] } assert UtxoSelection.prioritize_merge_utxos(utxos, inputs) == [b_2, b_3, a_3] end @tag fixtures: [:phoenix_ecto_sandbox] test "within the currency grouping, orders UTXOs in ascending order of value ('dust first')" do token_a = <<65::160>> token_b = <<66::160>> _ = insert(:txoutput, amount: 10, currency: token_a, owner: @alice) _ = insert(:txoutput, amount: 30, currency: token_b, owner: @alice) _ = insert(:txoutput, amount: 20, currency: token_a, owner: @alice) _ = insert(:txoutput, amount: 40, currency: token_b, owner: @alice) _ = insert(:txoutput, amount: 40, currency: token_a, owner: @alice) _ = insert(:txoutput, amount: 20, currency: token_b, owner: @alice) _ = insert(:txoutput, amount: 30, currency: token_a, owner: @alice) _ = insert(:txoutput, amount: 10, currency: token_b, owner: @alice) %{ ^token_a => [a_1 | available_a], ^token_b => [b_1, b_2 | available_b] } = utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) inputs = %{ token_a => [a_1], token_b => [b_1, b_2] } sorted_available_a = Enum.sort_by(available_a, fn utxo -> utxo.amount end, :asc) sorted_available_b = Enum.sort_by(available_b, fn utxo -> utxo.amount end, :asc) assert UtxoSelection.prioritize_merge_utxos(utxos, inputs) == Enum.concat(sorted_available_a, sorted_available_b) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns no more than 3 UTXOs per currency grouping" do for _i <- 1..5 do _ = insert(:txoutput, currency: @eth, owner: @alice) end %{ @eth => [eth_1 | available_eth] } = utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) assert length(available_eth) > 3 inputs = %{ @eth => [eth_1] } assert UtxoSelection.prioritize_merge_utxos(utxos, inputs) |> Enum.filter(fn utxo -> utxo.currency == @eth end) |> length() == 3 end @tag fixtures: [:phoenix_ecto_sandbox] test "returns empty list when original utxos are empty" do _ = insert(:txoutput, currency: @eth, owner: @alice) _ = insert(:txoutput, currency: @eth, owner: @alice) utxos = DB.TxOutput.get_sorted_grouped_utxos(@alice, :desc) assert [] == UtxoSelection.prioritize_merge_utxos(utxos, %{}) end end end ================================================ FILE: apps/omg_watcher_info/test/support/factories/block_factory.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Factory.Block do @moduledoc """ Block factory. Generates an empty test block with no transactions. Two ways to generate a block with transactions are: 1. Build a transaction first: ``` build(:transaction) ``` which will automatically create a block that the transaction belongs to. 2. Build an empty block and then build a transaction passing in the empty block to the transaction factory: ``` block = build(:block) transaction = build(:transaction, block: block) ``` Note that `tx_count` is an aggregate sum(block.transactions) field and does not automatically get setup in the tests. In most cases `tx_count` will need to be managed manually. """ defmacro __using__(_opts) do quote do alias OMG.WatcherInfo.DB def block_factory() do block = %DB.Block{ blknum: sequence(:block_blknum, fn seq -> seq * 1000 end), hash: insecure_random_bytes(32), eth_height: sequence(:block_eth_height, fn seq -> seq end), timestamp: sequence(:block_timestamp, fn seq -> seq end), transactions: [], tx_count: 0 } end end end end ================================================ FILE: apps/omg_watcher_info/test/support/factories/data_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Factory.DataHelper do @moduledoc """ A data helper module with functions to generate useful data for testing. Unlike the factories, the data generated in this module is not constrained to the sructures defined in the DB models. """ defmacro __using__(_opts) do quote do alias OMG.Eth.Encoding alias OMG.Watcher.Utxo require Utxo # Generates a certain length of random bytes. Uniqueness not guaranteed so it's not recommended for identifiers. def insecure_random_bytes(num_bytes) when num_bytes >= 0 and num_bytes <= 255 do 0..255 |> Enum.shuffle() |> Enum.take(num_bytes) |> :erlang.list_to_binary() end # creates event data specifically for the TxOutput.spend_utxos/3function def spend_uxto_params_from_txoutput(txoutput) do {Utxo.position(txoutput.blknum, txoutput.txindex, txoutput.oindex), txoutput.spending_tx_oindex, txoutput.spending_txhash} end end end end ================================================ FILE: apps/omg_watcher_info/test/support/factories/eth_event_factory.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Factory.EthEvent do @moduledoc """ EthEvent factory. Generates an ethevent. For testing flexibility, an ethevent can be created with 0 txoutputs. Although this does not conform to the business logic, this violates no database constraints. To associate an ethevent with one or more txoutputs, an array of txoutputs can be passed in via by overriding `txoutputs`. Most scenarios will have a only a 1-1 relationship between ethevents an txoutputs or a one-to-many (txoutput -> ethevents) relationship. However, with an ExitFinalized ethevent (processExits()) scenario, an ethevent may have many txoutputs. A txoutput for every utxo in the exit queue when processExits() was called. The default event type is `:deposit`, but can be overridden by setting `event_type`. """ defmacro __using__(_opts) do quote do alias OMG.WatcherInfo.DB def ethevent_factory() do ethevent = %DB.EthEvent{ root_chain_txhash: insecure_random_bytes(32), # within a log there may be 0 or more ethereum events, this is the index of the # event within the log log_index: 0, eth_height: 1, event_type: :deposit, txoutputs: [] } root_chain_txhash_event = DB.EthEvent.generate_root_chain_txhash_event(ethevent.root_chain_txhash, ethevent.log_index) Map.put(ethevent, :root_chain_txhash_event, root_chain_txhash_event) end end end end ================================================ FILE: apps/omg_watcher_info/test/support/factories/transaction_factory.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Factory.Transaction do @moduledoc """ Transaction factory. Generates a transaction without any transaction inputs or outputs. To generate a transaction with closest data to production, consider generating transaction inputs and/or outputs and associate them with this transaction. """ defmacro __using__(_opts) do quote do alias OMG.WatcherInfo.DB alias OMG.Watcher.Utxo require Utxo def transaction_factory(attrs \\ %{}) do {block, attrs} = case attrs[:block] do nil -> {build(:block, tx_count: 1), attrs} block -> block = Map.put(block, :tx_count, Map.get(block, :tx_count) + 1) {block, Map.delete(attrs, :block)} end transaction = %DB.Transaction{ txhash: insecure_random_bytes(32), txindex: block.tx_count - 1, txbytes: insecure_random_bytes(32), metadata: insecure_random_bytes(32), txtype: 1, block: block, inputs: [], outputs: [] } # not returning `merge_attributes(transaction, attrs)` directly to avoid dialyzer errors transaction = merge_attributes(transaction, attrs) transaction end def with_inputs(transaction, txoutputs) do {_, transaction} = txoutputs |> Enum.with_index() |> Enum.map_reduce(transaction, fn {txoutput, index}, transaction -> input_fields = %{ spending_transaction: transaction, spending_tx_oindex: index } utxo_pos = Utxo.position(transaction.block.blknum, txoutput.txindex, txoutput.oindex) txoutput = DB.TxOutput.get_by_position(utxo_pos) || txoutput {:ok, txoutput} = txoutput |> Ecto.Changeset.change(input_fields) |> DB.Repo.insert_or_update() {{txoutput, index}, Map.put(transaction, :inputs, transaction.inputs ++ [txoutput])} end) transaction end def with_outputs(transaction, txoutputs) do {_, transaction} = txoutputs |> Enum.with_index() |> Enum.map_reduce(transaction, fn {txoutput, index}, transaction -> output_fields = %{ creating_transaction: transaction, blknum: transaction.block.blknum, txindex: transaction.txindex, oindex: index } child_chain_utxohash = DB.EthEvent.generate_child_chain_utxohash( Utxo.position(txoutput.blknum, txoutput.txindex, txoutput.oindex) ) output_fields = Map.put(output_fields, :child_chain_utxohash, child_chain_utxohash) txoutput = insert(struct(txoutput, output_fields)) {{txoutput, index}, Map.put(transaction, :outputs, transaction.outputs ++ [txoutput])} end) transaction end end end end ================================================ FILE: apps/omg_watcher_info/test/support/factories/txoutput_factory.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Factory.TxOutput do @moduledoc """ TxOutput factory. Generates a txoutput with a `blknum` using blknum sequence from the block factory.the 1, 1001, 2001, etc... In most test use cases `blknum` should be overridden. If you are overriding some values, also consider its relation to other values. E.g: - To override `blknum`, also consider overriding `txindex`. - To override `creating_transaction`, also consider overriding `txindex` and `oindex`. - To override `spending_transaction`, also consider overriding `spending_tx_oindex` """ defmacro __using__(_opts) do quote do alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB require Utxo @eth <<0::160>> def txoutput_factory(attrs \\ %{}) do attrs = Map.put_new_lazy(attrs, :blknum, fn -> build(:block).blknum end) txoutput = %DB.TxOutput{ blknum: 0, txindex: 0, oindex: 0, otype: 1, owner: insecure_random_bytes(20), amount: 100, currency: @eth, creating_transaction: nil, spending_transaction: nil, spending_tx_oindex: nil, proof: insecure_random_bytes(32), ethevents: [] } txoutput = merge_attributes(txoutput, attrs) child_chain_utxohash = DB.EthEvent.generate_child_chain_utxohash(Utxo.position(txoutput.blknum, txoutput.txindex, txoutput.oindex)) Map.put(txoutput, :child_chain_utxohash, child_chain_utxohash) end def with_deposit(txoutput) do ethevent = build(:ethevent) Map.put(txoutput, :ethevents, [ethevent] ++ txoutput.ethevents) end def with_standard_exit(txoutput) do ethevent = build(:ethevent, event_type: :standard_exit) Map.put(txoutput, :ethevents, [ethevent] ++ txoutput.ethevents) end # if testing with a transaction containing multiple txoutput outputs then consider using the transaction # factory's `with_outputs()` function instead def with_creating_transaction(txoutput, transaction \\ nil) do transaction = case transaction do nil -> build(:transaction) transaction -> transaction end txoutput = struct(txoutput, %{ blknum: transaction.block.blknum, creating_txhash: transaction.txhash, txindex: length(transaction.outputs) }) Map.put( txoutput, :child_chain_utxohash, DB.EthEvent.generate_child_chain_utxohash(Utxo.position(txoutput.blknum, txoutput.txindex, txoutput.oindex)) ) end # if testing with a transaction containing multiple txoutput inputs then consider using the transaction # factory's `with_inputs()` function instead def with_spending_transaction(txoutput, transaction \\ nil) do transaction = case transaction do nil -> build(:transaction) transaction -> transaction end txoutput |> Map.put(:proof, insecure_random_bytes(32)) |> Map.put(:spending_transaction, transaction) |> Map.put(:spending_tx_oindex, length(transaction.inputs)) end end end end ================================================ FILE: apps/omg_watcher_info/test/support/factory.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.Factory do @moduledoc """ Test data factory for OMG.WatcherInfo. Use this module to build structs or insert data into WatcherInfo's database. ## Usage Import this factory into your test module with `import OMG.WatcherInfo.Factory`. Importing only this factory and you will be able to use all the factories in the factories directory. To specify which factory ExMachina should use pass an atom of the factory name to the `build()` and `insert()` functions. For example, to build a block using the block factory use: `block = build(:block)` To build a transaction using the transaction factory use: `transaction = build(:transaction)` Use [`build/1`](https://hexdocs.pm/ex_machina/ExMachina.html#c:build/1) to build a struct without inserting them to the database, or [`build/2`](https://hexdocs.pm/ex_machina/ExMachina.html#c:build/2) to override default data. Or use [`insert/1`](https://hexdocs.pm/ex_machina/ExMachina.html#c:insert/1) to build and insert the struct to database or [`build/2`](https://hexdocs.pm/ex_machina/ExMachina.html#c:build/2) to insert with overrides. See all available APIs at https://hexdocs.pm/ex_machina/ExMachina.html. ## Example defmodule MyTest do use ExUnit.Case import OMG.WatcherInfo.Factory test ... do # Returns %OMG.WatcherInfo.DB.Block{blknum: ..., hash: ...} build(:block) # Returns %OMG.WatcherInfo.DB.Block{blknum: 1234, hash: ...} build(:block, blknum: 1234) # Inserts and returns %OMG.WatcherInfo.DB.Block{blknum: ..., hash: ...} insert(:block) # Inserts and returns %OMG.WatcherInfo.DB.Block{blknum: 1234, hash: ...} insert(:block, blknum: 1234) end end """ use ExMachina.Ecto, repo: OMG.WatcherInfo.DB.Repo use OMG.WatcherInfo.Factory.Block use OMG.WatcherInfo.Factory.DataHelper use OMG.WatcherInfo.Factory.EthEvent use OMG.WatcherInfo.Factory.Transaction use OMG.WatcherInfo.Factory.TxOutput end ================================================ FILE: apps/omg_watcher_info/test/support/test_server.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherInfo.TestServer do @moduledoc """ Helper functions to provide behavior to FakeServer without using FakeServer defined macros. For now it's strictly tied with child chain api and handles env variable changes """ @doc """ Starts a mock server that will handle child chain requests """ def start() do server_id = :watcher_info_test_server {:ok, pid} = FakeServer.start(server_id) real_addr = Application.fetch_env!(:omg_watcher_info, :child_chain_url) {:ok, port} = FakeServer.port(server_id) fake_addr = "http://localhost:#{port}" %{ real_addr: real_addr, fake_addr: fake_addr, server_id: server_id, server_pid: pid } end @doc """ Stops a server and put back the original child chain address to the env. """ def stop(%{real_addr: real_addr, server_id: server_id}) do Application.put_env(:omg_watcher_info, :child_chain_url, real_addr) FakeServer.stop(server_id) end @doc """ Configures route for fake server to respond for given path with given response **Please note: ** When the route is configured with a list of FakeServer.HTTP.Responses, the server will respond with the first element in the list and then remove it. This will be repeated for each request made for this route. Use `fn req -> response end` when you need to return always the same or modified response on every request Also first use of `with_response` changes configuration variable to child chain api to fake server, so invoke this function when fake response is needed. """ def with_response(response_block, %{fake_addr: fake_addr, server_pid: server_pid} = _context, path) do Application.put_env(:omg_watcher_info, :child_chain_url, fake_addr) FakeServer.put_route(server_pid, path, fn _ -> response_block end) end def make_response(data) when is_map(data) do TestServerResponseFactory.build(:json_rpc, data: data, success: not Map.has_key?(data, :code)) end end defmodule TestServerResponseFactory do @moduledoc false use FakeServer.ResponseFactory def json_rpc_response() do ok( %{ version: "1.0", success: true, data: %{} }, %{"Content-Type" => "application/json"} ) end end ================================================ FILE: apps/omg_watcher_info/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.configure(exclude: [mix_based_child_chain: true, integration: true, property: true, wrappers: true]) ExUnitFixtures.start() ExUnit.start() {:ok, _} = Application.ensure_all_started(:httpoison) {:ok, _} = Application.ensure_all_started(:fake_server) {:ok, _} = Application.ensure_all_started(:briefly) {:ok, _} = Application.ensure_all_started(:erlexec) {:ok, _} = Application.ensure_all_started(:ex_machina) Mix.Task.run("ecto.create", ~w(--quiet)) Mix.Task.run("ecto.migrate", ~w(--quiet)) ================================================ FILE: apps/omg_watcher_rpc/lib/application.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Application do @moduledoc false use Application require Logger def start(_type, _args) do _ = Logger.info("Starting #{inspect(__MODULE__)}") start_root_supervisor() end def start_root_supervisor() do # root supervisor must stop whenever any of its children supervisors goes down (children carry the load of restarts) _ = SpandexPhoenix.Telemetry.install( endpoint_telemetry_prefix: [:watcher_rpc, :endpoint], tracer: OMG.WatcherRPC.Tracer, customize_metadata: &OMG.WatcherRPC.Tracer.add_trace_metadata/1 ) children = [ %{ id: OMG.WatcherRPC.Web.Endpoint, start: {OMG.WatcherRPC.Web.Endpoint, :start_link, []}, type: :supervisor } ] opts = [ strategy: :one_for_one, # whenever any of supervisor's children goes down, so it does name: OMG.WatcherRPC.RootSupervisor ] Supervisor.start_link(children, opts) end # Tell Phoenix to update the endpoint configuration # whenever the application is updated. def config_change(changed, _new, removed) do OMG.WatcherRPC.Web.Endpoint.config_change(changed, removed) :ok end end ================================================ FILE: apps/omg_watcher_rpc/lib/configuration.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Configuration do @moduledoc """ Provides access to applications configuration """ @app :omg_watcher_rpc @spec version() :: String.t() def version() do OMG.Utils.AppVersion.version(@app) end @spec service_name() :: atom() def service_name() do Application.get_env(@app, :api_mode) end end ================================================ FILE: apps/omg_watcher_rpc/lib/release_tasks/set_api_mode.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.ReleaseTasks.SetApiMode do @moduledoc false @behaviour Config.Provider require Logger def init(nil) do exit("WatcherRPC's API mode is not provided.") end def init(args) do args end def load(config, api_mode) do Config.Reader.merge(config, omg_watcher_rpc: [api_mode: api_mode]) end end ================================================ FILE: apps/omg_watcher_rpc/lib/release_tasks/set_endpoint.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.ReleaseTasks.SetEndpoint do @moduledoc false @behaviour Config.Provider require Logger @app :omg_watcher_rpc def init(args) do args end def load(config, _args) do _ = on_load() endpoint_config = Application.get_env(@app, OMG.WatcherRPC.Web.Endpoint) endpoint_config = Keyword.put( endpoint_config, :http, List.foldl(endpoint_config[:http], [], fn {:port, _num}, acc -> [get_port() | acc] other, acc -> [other | acc] end) ) endpoint_config = Keyword.put( endpoint_config, :url, List.foldl(endpoint_config[:url], [], fn {:host, _num}, acc -> [get_hostname() | acc] other, acc -> [other | acc] end) ) Config.Reader.merge(config, omg_watcher_rpc: [{OMG.WatcherRPC.Web.Endpoint, Enum.sort(endpoint_config)}]) end defp get_port() do port = validate_integer( get_env("PORT"), Keyword.get(Application.get_env(@app, OMG.WatcherRPC.Web.Endpoint)[:http], :port) ) _ = Logger.info("CONFIGURATION: App: #{@app} Key: PORT Value: #{inspect(port)}.") {:port, port} end defp get_hostname() do hostname = validate_string( get_env("HOSTNAME"), Keyword.get(Application.get_env(@app, OMG.WatcherRPC.Web.Endpoint)[:url], :host) ) _ = Logger.info("CONFIGURATION: App: #{@app} Key: HOSTNAME Value: #{inspect(hostname)}.") {:host, hostname} end defp get_env(key), do: System.get_env(key) defp validate_integer(value, _default) when is_binary(value), do: String.to_integer(value) defp validate_integer(_, default), do: default defp validate_string(value, _default) when is_binary(value), do: value defp validate_string(_, default), do: default defp on_load() do _ = Application.ensure_all_started(:logger) _ = Application.load(@app) end end ================================================ FILE: apps/omg_watcher_rpc/lib/release_tasks/set_tracer.ex ================================================ # Copyright 2019-2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.ReleaseTasks.SetTracer do @moduledoc false @behaviour Config.Provider alias OMG.WatcherRPC.Tracer require Logger @app :omg_watcher_rpc def init(args) do args end def load(config, args) do _ = on_load() adapter = Keyword.get(args, :system_adapter, System) _ = Process.put(:system_adapter, adapter) dd_disabled = get_dd_disabled() tracer_config = @app |> Application.get_env(Tracer) |> Keyword.put(:disabled?, dd_disabled) tracer_config = case dd_disabled do false -> app_env = get_app_env() Keyword.put(tracer_config, :env, app_env) true -> Keyword.put(tracer_config, :env, "") end Config.Reader.merge(config, omg_watcher_rpc: [{Tracer, tracer_config}], spandex_phoenix: [tracer: Tracer] ) end defp get_dd_disabled() do dd_disabled = Application.get_env(@app, OMG.WatcherRPC.Tracer)[:disabled?] dd_disabled? = validate_bool(get_env("DD_DISABLED"), dd_disabled) _ = Logger.info("CONFIGURATION: App: #{@app} Key: DD_DISABLED Value: #{inspect(dd_disabled?)}.") dd_disabled? end defp get_app_env() do env = validate_string(get_env("APP_ENV"), Application.get_env(@app, OMG.WatcherRPC.Tracer)[:env]) _ = Logger.info("CONFIGURATION: App: #{@app} Key: APP_ENV Value: #{inspect(env)}.") env end defp get_env(key) do Process.get(:system_adapter).get_env(key) end defp validate_bool(value, _default) when is_binary(value), do: to_bool(String.upcase(value)) defp validate_bool(_, default), do: default defp to_bool("TRUE"), do: true defp to_bool("FALSE"), do: false defp to_bool(_), do: exit("DD_DISABLED either true or false.") defp validate_string(value, _default) when is_binary(value), do: value defp validate_string(_, default), do: default defp on_load() do _ = Application.ensure_all_started(:logger) Application.load(@app) end end ================================================ FILE: apps/omg_watcher_rpc/lib/tracer.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Tracer do @moduledoc """ Trace Phoenix requests and reports information to Datadog via Spandex """ use Spandex.Tracer, otp_app: :omg_watcher_rpc alias OMG.WatcherRPC.Configuration def add_trace_metadata(%{assigns: %{error_type: error_type, error_msg: error_msg}} = conn) do service_name = Configuration.service_name() version = Configuration.version() conn |> SpandexPhoenix.default_metadata() |> Keyword.put(:service, service_name) |> Keyword.put(:error, [{:error, true}]) |> Keyword.put(:tags, [{:version, version}, {:"error.type", error_type}, {:"error.msg", error_msg}]) end def add_trace_metadata(conn) do service_name = Configuration.service_name() version = Configuration.version() conn |> SpandexPhoenix.default_metadata() |> Keyword.put(:service, service_name) |> Keyword.put(:tags, [{:version, version}]) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/account.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Account do @moduledoc """ Module provides operation related to plasma accounts. """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.API, as: SecurityAPI alias OMG.WatcherInfo.API, as: InfoAPI alias OMG.WatcherRPC.Web.Validator.AccountConstraints @doc """ Gets plasma account balance """ def get_balance(conn, params) do with {:ok, address} <- expect(params, "address", :address) do address |> InfoAPI.Account.get_balance() |> api_response(conn, :balance) end end def get_utxos(conn, params) do with {:ok, constraints} <- AccountConstraints.parse(params) do constraints |> InfoAPI.Account.get_utxos() |> api_response(conn, :utxos) end end def get_exitable_utxos(conn, params) do with {:ok, address} <- expect(params, "address", :address) do address |> SecurityAPI.Account.get_exitable_utxos() |> api_response(conn, :exitable_utxos) end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/alarm.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Alarm do @moduledoc """ Module provides operation related to the watcher raised alarms that might point to faulty watcher node. """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.API.Alarm def get_alarms(conn, _params) do {:ok, alarms} = Alarm.get_alarms() api_response(alarms, conn, :alarm) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/block.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Block do @moduledoc """ Operations related to block. """ require Logger use OMG.WatcherRPC.Web, :controller alias OMG.Watcher alias OMG.WatcherInfo.API.Block, as: InfoApiBlock alias OMG.WatcherRPC.Web.Validator @doc """ Retrieves a specific block by block number. """ def get_block(conn, params) do with {:ok, blknum} <- expect(params, "blknum", :pos_integer) do blknum |> InfoApiBlock.get() |> api_response(conn, :block) end end @doc """ Retrieves a list of most recent blocks """ def get_blocks(conn, params) do with {:ok, constraints} <- Validator.BlockConstraints.parse(params) do constraints |> InfoApiBlock.get_blocks() |> api_response(conn, :blocks) end end @doc """ Executes stateful and stateless validation of a block. """ def validate_block(conn, params) do with {:ok, block} <- Validator.BlockConstraints.parse_to_validate(params) do case Watcher.BlockValidator.stateless_validate(block) do {:ok, true} -> api_response(%{valid: true}, conn, :validate_block) {:error, reason} -> Logger.info("Block #{block.number} is invalid due to #{reason}") api_response(%{valid: false}, conn, :validate_block) end end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/challenge.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Challenge do @moduledoc """ Handles exit challenges """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.API alias OMG.Watcher.Utxo @doc """ Challenges exits """ def get_utxo_challenge(conn, params) do with {:ok, utxo_pos} <- expect(params, "utxo_pos", :pos_integer), {:ok, utxo} <- Utxo.Position.decode(utxo_pos) do utxo |> API.Utxo.create_challenge() |> api_response(conn, :challenge) end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/configuration.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Configuration do @moduledoc """ Module provides operation related to the watcher raised alarms that might point to faulty watcher node. """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.API.Configuration def get_configuration(conn, _params) do {:ok, configuration} = Configuration.get_configuration() api_response(configuration, conn, :configuration) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/deposit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Deposit do @moduledoc """ Operations related to deposits. """ use OMG.WatcherRPC.Web, :controller alias OMG.WatcherInfo.API.Deposit, as: InfoApiDeposit alias OMG.WatcherRPC.Web.Validator @doc """ Retrieves a list of deposits. """ def get_deposits(conn, params) do case Validator.DepositConstraints.parse(params) do {:ok, constraints} -> constraints |> InfoApiDeposit.get_deposits() |> api_response(conn, :deposits) error -> error end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/fallback.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Fallback do @moduledoc """ The fallback handler. """ use Phoenix.Controller import Plug.Conn alias OMG.WatcherRPC.Web.Views @errors %{ exit_not_found: %{ code: "challenge:exit_not_found", description: "The challenge of particular exit is impossible because exit is inactive or missing" }, utxo_not_spent: %{ code: "challenge:utxo_not_spent", description: "The challenge of particular exit is impossible because provided utxo is not spent" }, transaction_not_found: %{ code: "transaction:not_found", description: "Transaction doesn't exist for provided search criteria" }, utxo_not_found: %{ code: "exit:invalid", description: "Utxo was spent or does not exist." }, tx_for_input_not_found: %{ code: "in_flight_exit:tx_for_input_not_found", description: "No transaction that created input." }, deposit_input_spent_ife_unsupported: %{ code: "in_flight_exit:deposit_input_spent_ife_unsupported", description: "Retrieving IFE data of a transaction with a spent deposit is unsupported." }, econnrefused: %{ code: "connection:econnrefused", description: "Cannot connect to the Ethereum node." }, childchain_unreachable: %{ code: "connection:childchain_unreachable", description: "Cannot communicate with the childchain." }, insufficient_funds: %{ code: "transaction.create:insufficient_funds", description: "Account balance is too low to satisfy the payment." }, too_many_inputs: %{ code: "transaction.create:too_many_inputs", description: "The number of inputs required to cover the payment and fee exceeds the maximum allowed." }, too_many_outputs: %{ code: "transaction.create:too_many_outputs", description: "Total number of payments + change + fees exceed maximum allowed outputs." }, single_input: %{ code: "merge:single_input", description: "Only one input found for the given address and currency." }, no_inputs_found: %{ code: "merge:no_inputs_found", description: "No inputs found for the given address and currency." }, multiple_currencies: %{ code: "merge:multiple_currencies", description: "All inputs must have the same currency." }, multiple_input_owners: %{ code: "merge:multiple_input_owners", description: "All inputs must have the same owner." }, duplicate_input_positions: %{ code: "merge:duplicate_input_positions", description: "Duplicate input positions provided." }, empty_transaction: %{ code: "transaction.create:empty_transaction", description: "Requested payment transfers no funds." }, self_transaction_not_supported: %{ code: "transaction.create:self_transaction_not_supported", description: "This endpoint cannot be used to create merge or split transactions." }, invalid_merkle_root: %{ code: "block.validate:invalid_merkle_root", description: "Block hash does not match reconstructed Merkle root." }, missing_signature: %{ code: "submit_typed:missing_signature", description: "Signatures should correspond to inputs owner. When all non-empty inputs has the same owner, " <> "signatures should be duplicated." }, superfluous_signature: %{ code: "submit_typed:superfluous_signature", description: "Number of non-empty inputs should match signatures count. Remove redundant signatures." }, no_deposit_for_given_blknum: %{ code: "exit:invalid", description: "Utxo was spent or does not exist." }, operation_not_found: %{ code: "operation:not_found", description: "Operation cannot be found. Check request URL." }, operation_bad_request: %{ code: "operation:bad_request", description: "Parameters required by this operation are missing or incorrect." } } def call(conn, {:error, {:validation_error, param_name, validator}}) do error = error_info(conn, :operation_bad_request) :telemetry.execute([:web, :fallback], %{error: 1}, %{error_code: error.code, route: current_route(conn)}) conn |> assign_error_metadata_to_conn(error) |> put_view(Views.Error) |> render( :error, %{ code: error.code, description: error.description, messages: %{ validation_error: %{ parameter: param_name, validator: inspect(validator) } } } ) end def call(conn, {:error, {reason, data}}) do error = error_info(conn, reason) :telemetry.execute([:web, :fallback], %{error: 1}, %{error_code: error.code, route: current_route(conn)}) conn |> assign_error_metadata_to_conn(error) |> put_view(Views.Error) |> render(:error, %{code: error.code, description: error.description, messages: data}) end def call(conn, {:error, reason}) do error = error_info(conn, reason) :telemetry.execute([:web, :fallback], %{error: 1}, %{error_code: error.code, route: current_route(conn)}) conn |> assign_error_metadata_to_conn(error) |> put_view(Views.Error) |> render(:error, %{code: error.code, description: error.description}) end def call(conn, :error), do: call(conn, {:error, :unknown_error}) # Controller's action with expression has no match, e.g. on guard def call(conn, _), do: call(conn, {:error, :unknown_error}) defp error_info(conn, reason) do case Map.get(@errors, reason) do nil -> %{code: "#{action_name(conn)}#{inspect(reason)}", description: nil} error -> error end end defp assign_error_metadata_to_conn(conn, error) do conn |> assign(:error_type, error.code) |> assign(:error_msg, error.description) end # There isn't a way to get the route directly from a conn, so we need this roundabout way. # We want the route instead of the request path because it's of limited cardinality for the metric tags. defp current_route(%{private: %{phoenix_router: phoenix_router}} = conn) do case Phoenix.Router.route_info(phoenix_router, conn.method, conn.request_path, conn.host) do %{:route => route} -> route :error -> nil end end defp current_route(_) do nil end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/fee.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Fee do @moduledoc """ Operations related to fees. """ use OMG.WatcherRPC.Web, :controller alias OMG.WatcherInfo.HttpRPC.Client def fees_all(conn, params) do with {:ok, _} <- expect(params, "currencies", list: &to_currency/1, optional: true), {:ok, _} <- expect(params, "tx_types", list: &to_tx_type/1, optional: true), child_chain_url <- Application.get_env(:omg_watcher_info, :child_chain_url), {:ok, fees} <- Client.get_fees(params, child_chain_url) do api_response(fees, conn, :fees_all) end end defp to_currency(currency_str) do expect(%{"currency" => currency_str}, "currency", :address) end defp to_tx_type(tx_type_str) do expect(%{"tx_type" => tx_type_str}, "tx_type", :non_neg_integer) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/in_flight_exit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.InFlightExit do @moduledoc """ Operations related to in flight exits starting and handling. """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.API @doc """ For a given transaction provided in params, responds with arguments for plasma contract function that starts in-flight exit. """ def get_in_flight_exit(conn, params) do handle_txbytes_based_request(conn, params, &API.InFlightExit.get_in_flight_exit/1, :in_flight_exit) end def get_competitor(conn, params) do handle_txbytes_based_request(conn, params, &API.InFlightExit.get_competitor/1, :competitor) end def prove_canonical(conn, params) do handle_txbytes_based_request(conn, params, &API.InFlightExit.prove_canonical/1, :prove_canonical) end def get_input_challenge_data(conn, params) do with {:ok, txbytes} <- expect(params, "txbytes", :hex), {:ok, input_index} <- expect(params, "input_index", :non_neg_integer) do API.InFlightExit.get_input_challenge_data(txbytes, input_index) |> api_response(conn, :get_input_challenge_data) end end def get_output_challenge_data(conn, params) do with {:ok, txbytes} <- expect(params, "txbytes", :hex), {:ok, output_index} <- expect(params, "output_index", :non_neg_integer) do API.InFlightExit.get_output_challenge_data(txbytes, output_index) |> api_response(conn, :get_output_challenge_data) end end # NOTE: don't overdo this DRYing here - if the above controller functions evolve and diverge, it might be better to # un-DRY defp handle_txbytes_based_request(conn, params, api_function, template) do with {:ok, txbytes} <- expect(params, "txbytes", :hex) do txbytes |> api_function.() |> api_response(conn, template) end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/stats.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Stats do @moduledoc """ Operations related to network statistics. """ use OMG.WatcherRPC.Web, :controller alias OMG.WatcherInfo.API.Stats, as: InfoApiStats @doc """ Retrieves network statistics """ def get_statistics(conn, _params) do response = InfoApiStats.get() api_response(response, conn, :stats) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/status.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Status do @moduledoc """ Module provides operation related to the child chain health status, like: geth syncing status, last minned block number and time and last block verified by watcher. """ use OMG.WatcherRPC.Web, :controller # check for health before calling action plug(OMG.WatcherRPC.Web.Plugs.Health) alias OMG.Watcher.API.Status @doc """ Gets plasma network and Watcher status """ def get_status(conn, _params) do api_response(Status.get_status(), conn, :status) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Transaction do @moduledoc """ Operations related to transaction. """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.API.Transaction, as: SecurityApiTransaction alias OMG.Watcher.State.Transaction alias OMG.WatcherInfo.API.Transaction, as: InfoApiTransaction alias OMG.WatcherInfo.OrderFeeFetcher alias OMG.WatcherInfo.Transaction, as: TransactionCreator alias OMG.WatcherRPC.Web.Validator @doc """ Retrieves a specific transaction by id. """ def get_transaction(conn, params) do with {:ok, id} <- expect(params, "id", :hash) do id |> InfoApiTransaction.get() |> api_response(conn, :transaction) end end @doc """ Retrieves a list of transactions """ def get_transactions(conn, params) do with {:ok, constraints} <- Validator.TransactionConstraints.parse(params) do constraints |> InfoApiTransaction.get_transactions() |> api_response(conn, :transactions) end end @doc """ Submits transaction to child chain """ def submit(conn, params) do with {:ok, txbytes} <- expect(params, "transaction", :hex) do submit_tx_sec(txbytes, conn) end end @doc """ Submits transaction to child chain """ def batch_submit(conn, params) do with {:ok, txbytes} <- expect(params, "transactions", list: &to_transaction/1, optional: false) do submit_tx_sec(txbytes, conn) end end @doc """ Thin-client version of `/transaction.submit` that accepts json encoded transaction """ def submit_typed(conn, params) do with {:ok, signed_tx} <- Validator.TypedDataSigned.parse(params) do # it's tempting to skip the unnecessary encoding-decoding part, but it gain broader # validation and communicates with API layer with known structures than bytes signed_tx |> Transaction.Signed.encode() |> submit_tx_inf(conn) end end @doc """ Given token, amount and spender, finds spender's inputs sufficient to perform a payment. If also provided with receiver's address, creates and encodes a transaction. """ def create(conn, params) do with {:ok, order} <- Validator.Order.parse(params), {:ok, order} <- OrderFeeFetcher.add_fee_to_order(order) do order |> InfoApiTransaction.create() |> TransactionCreator.include_typed_data() |> api_response(conn, :create) end end @doc """ Creates and encodes a merge transaction. Can be called with either an array of utxo positions or an address currency pair. """ def merge(conn, params) do with {:ok, constraints} <- Validator.MergeConstraints.parse(params) do constraints |> InfoApiTransaction.merge() |> TransactionCreator.include_typed_data() |> api_response(conn, :merge) end end # Provides extra validation (recover_from) and passes transaction to API layer defp submit_tx_inf(txbytes, conn) do with {:ok, recovered_tx} <- Transaction.Recovered.recover_from(txbytes), :ok <- is_supported(recovered_tx) do recovered_tx |> Map.get(:signed_tx) |> InfoApiTransaction.submit() |> api_response(conn, :submission) end end # Provides extra validation (recover_from) and passes transaction to API layer defp submit_tx_sec(txbytes, conn) when is_list(txbytes) do txbytes |> SecurityApiTransaction.batch_submit() |> api_response(conn, :batch_submission) end defp submit_tx_sec(txbytes, conn) do with {:ok, recovered_tx} <- Transaction.Recovered.recover_from(txbytes), :ok <- is_supported(recovered_tx) do recovered_tx |> Map.get(:signed_tx) |> SecurityApiTransaction.submit() |> api_response(conn, :submission) end end defp is_supported(%Transaction.Recovered{signed_tx: %Transaction.Signed{raw_tx: %Transaction.Fee{}}}) do {:error, :transaction_not_supported} end defp is_supported(%Transaction.Recovered{}), do: :ok defp to_transaction(transaction) do expect(%{"transaction" => transaction}, "transaction", :hex) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/controllers/utxo.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.Utxo do @moduledoc """ Operations related to utxo. Modify the state in the database. """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.API alias OMG.Watcher.Utxo def get_utxo_exit(conn, params) do with {:ok, utxo_pos} <- expect(params, "utxo_pos", :pos_integer), {:ok, utxo} <- Utxo.Position.decode(utxo_pos) do utxo |> API.Utxo.compose_utxo_exit() |> api_response(conn, :utxo_exit) end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/endpoint.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Endpoint do use Sentry.PlugCapture use Phoenix.Endpoint, otp_app: :omg_watcher_rpc plug(OMG.Utils.RemoteIP) plug(Plug.RequestId) plug(Plug.Logger, log: :debug) plug(Plug.Telemetry, event_prefix: [:watcher_rpc, :endpoint]) if code_reloading? do plug(Phoenix.CodeReloader) end plug( Plug.Parsers, parsers: [:json], pass: [], json_decoder: Jason ) plug(Sentry.PlugContext) plug(Plug.MethodOverride) plug(Plug.Head) if Application.get_env(:omg_watcher_rpc, OMG.WatcherRPC.Web.Endpoint)[:enable_cors], do: plug(CORSPlug) plug(OMG.WatcherRPC.Web.Plugs.MethodParamFilter) plug(OMG.WatcherRPC.Web.Router) end ================================================ FILE: apps/omg_watcher_rpc/lib/web/plugs/health.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Plugs.Health do @moduledoc """ Observes the systems alarms and prevents calls towards an unhealthy one. """ alias OMG.Status alias OMG.Utils.HttpRPC.Error alias Phoenix.Controller import Plug.Conn require Logger use GenServer ### ### PLUG ### def init(options), do: options def call(conn, _params) do # is anything raised? if Status.is_healthy() do conn else data = Error.serialize( "operation:service_unavailable", "The server is not ready to handle the request. Check the alarms for more info." ) conn |> Controller.json(data) |> halt() end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/plugs/method_param_filter.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Plugs.MethodParamFilter do @moduledoc """ Filters the `query_params`, `body_params` and `params` of the conn depending on the HTTP method used. For a POST: `query_params` will be ignored and `body_params` will be set to `params`. For a GET: `body_params` will be ignored and `query_params` will be set to `params`. For other http method, the original `conn` is returned. """ def init(args), do: args def call(%Plug.Conn{method: "POST", body_params: params} = conn, _) do conn |> Map.put(:query_params, %{}) |> Map.put(:params, params) end def call(%Plug.Conn{method: "GET", query_params: params} = conn, _) do conn |> Map.put(:body_params, %{}) |> Map.put(:params, params) end def call(conn, _), do: conn end ================================================ FILE: apps/omg_watcher_rpc/lib/web/plugs/supported_watcher_modes.ex ================================================ # Copyright 2019 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Plugs.SupportedWatcherModes do @moduledoc """ Ensures that unsupported endpoints return an appropriate error, depending on watcher's mode. """ alias OMG.WatcherRPC.Web.Controller @behaviour Plug @app :omg_watcher_rpc @spec init([atom()]) :: [atom()] def init(supported_modes), do: supported_modes @spec call(Plug.Conn.t(), [atom()]) :: Plug.Conn.t() def call(conn, supported_modes) do case Application.get_env(@app, :api_mode) in supported_modes do true -> conn false -> conn |> Controller.Fallback.call({:error, :operation_not_found}) |> Plug.Conn.halt() end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/response.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Response do @moduledoc """ Prepares the response into the expected result/data format. Contains only the behaviours specific to the watcher. For the generic response, see `OMG.Utils.HttpRPC.Response`. """ alias OMG.WatcherRPC.Configuration @doc """ Adds "version" and "service_name" to the response map. """ @spec add_app_infos(map()) :: %{version: String.t(), service_name: String.t()} def add_app_infos(response) do response |> Map.put(:version, Configuration.version()) |> Map.put(:service_name, "#{Configuration.service_name()}") end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/router.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Router do use OMG.WatcherRPC.Web, :router alias OMG.WatcherRPC.Web.Plugs.SupportedWatcherModes pipeline :api do plug(:accepts, ["json"]) end pipeline :security_api do plug(:accepts, ["json"]) plug(SupportedWatcherModes, [:watcher, :watcher_info]) end pipeline :info_api do plug(:accepts, ["json"]) plug(SupportedWatcherModes, [:watcher_info]) end # A note on scope ordering. # # The scopes are order-sensitive. Due to the way that plug works sequentially, # once a plug halts, the rest of the router does not get evaluated, even if it is # outside the scope of the used plug. # # Therefore, always put the more permissive scope first, e.g. put the scope with # `plug(SupportedWatcherModes, [:watcher, :watcher_info])` before the scope with # plug(SupportedWatcherModes, [:watcher_info]) # # Endpoints allowed on both Watcher Security-Critical and Info API # scope "/", OMG.WatcherRPC.Web do pipe_through([:security_api]) post("/status.get", Controller.Status, :get_status) get("/alarm.get", Controller.Alarm, :get_alarms) get("/configuration.get", Controller.Configuration, :get_configuration) post("/account.get_exitable_utxos", Controller.Account, :get_exitable_utxos) post("/block.validate", Controller.Block, :validate_block) post("/utxo.get_exit_data", Controller.Utxo, :get_utxo_exit) post("/utxo.get_challenge_data", Controller.Challenge, :get_utxo_challenge) post("/transaction.submit", Controller.Transaction, :submit) post("/transaction.batch_submit", Controller.Transaction, :batch_submit) post("/in_flight_exit.get_data", Controller.InFlightExit, :get_in_flight_exit) post("/in_flight_exit.get_competitor", Controller.InFlightExit, :get_competitor) post("/in_flight_exit.prove_canonical", Controller.InFlightExit, :prove_canonical) post("/in_flight_exit.get_input_challenge_data", Controller.InFlightExit, :get_input_challenge_data) post("/in_flight_exit.get_output_challenge_data", Controller.InFlightExit, :get_output_challenge_data) end # # Extra endpoints allowed only on Watcher Info API # scope "/", OMG.WatcherRPC.Web do pipe_through([:info_api]) post("/account.get_balance", Controller.Account, :get_balance) post("/account.get_utxos", Controller.Account, :get_utxos) post("/account.get_transactions", Controller.Transaction, :get_transactions) post("/block.all", Controller.Block, :get_blocks) post("/deposit.all", Controller.Deposit, :get_deposits) post("/transaction.all", Controller.Transaction, :get_transactions) post("/transaction.get", Controller.Transaction, :get_transaction) post("/transaction.create", Controller.Transaction, :create) post("/transaction.submit_typed", Controller.Transaction, :submit_typed) post("/transaction.merge", Controller.Transaction, :merge) post("/block.get", Controller.Block, :get_block) post("/fees.all", Controller.Fee, :fees_all) post("/stats.get", Controller.Stats, :get_statistics) end # Fallbacks # NOTE: This *has to* be the last route, catching all unhandled paths scope "/", OMG.WatcherRPC.Web do pipe_through([:api]) match(:*, "/*path", Controller.Fallback, {:error, :operation_not_found}) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/serializers/base.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Serializer.Base do @moduledoc """ Common structure formatters module. """ def to_utxo(%{blknum: blknum, txindex: txindex, oindex: oindex} = db_entry) do alias OMG.Watcher.Utxo require Utxo db_entry |> Map.take([ :amount, :currency, :blknum, :txindex, :oindex, :otype, :owner, :creating_txhash, :spending_txhash, :inserted_at, :updated_at ]) |> Map.put(:utxo_pos, Utxo.position(blknum, txindex, oindex) |> Utxo.Position.encode()) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/sockets/socket.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Socket do @moduledoc """ This module is the entry points for websocket connections to the watcher API. It contains the channels to which providers/clients can connect to listen and receive events. """ use Phoenix.Socket, log: :debug # Socket params are passed from the client and can # be used to verify and authenticate a user. After # verification, you can put default assigns into # the socket that will be set for all channels, ie # # {:ok, assign(socket, :user_id, verified_user_id)} # # To deny connection, return `:error`. # # See `Phoenix.Token` documentation for examples in # performing token verification on connect. def connect(_params, socket) do {:ok, socket} end # Socket id's are topics that allow you to identify all sockets for a given user: # # def id(socket), do: "user_socket:#{socket.assigns.user_id}" # # Would allow you to broadcast a "disconnect" event and terminate # all active sockets and channels for a given user: # # OMG.WatcherRPC.Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) # # Returning `nil` makes this socket anonymous. def id(_socket), do: nil end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/account_constraints.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.AccountConstraints do @moduledoc """ Validates `/account.get_utxos` query parameters """ alias OMG.WatcherRPC.Web.Validator.Helpers @doc """ Validates possible query constraints, stops on first error. """ @spec parse(%{binary() => any()}) :: {:ok, Keyword.t()} | {:error, any()} def parse(params) do constraints = [ {"limit", [pos_integer: true, lesser: 1000, optional: true], :limit}, {"page", [:pos_integer, :optional], :page}, {"address", [:address], :address} ] Helpers.validate_constraints(params, constraints) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.BlockConstraints do @moduledoc """ Validates `/block.all` query parameters """ use OMG.WatcherRPC.Web, :controller alias OMG.Watcher.Block alias OMG.WatcherRPC.Web.Validator.Helpers @doc """ Validates possible query constraints, stops on first error. """ @spec parse(%{binary() => any()}) :: {:ok, Keyword.t()} | {:error, any()} def parse(params) do constraints = [ {"limit", [pos_integer: true, lesser: 1000, optional: true], :limit}, {"page", [:pos_integer, :optional], :page} ] Helpers.validate_constraints(params, constraints) end @doc """ Validates that a block submitted for validation is correctly formed. """ @spec parse_to_validate(Block.t()) :: {:error, {:validation_error, binary, any}} | {:ok, Block.t()} def parse_to_validate(block) do with {:ok, hash} <- expect(block, "hash", :hash), {:ok, transactions} <- expect(block, "transactions", list: &is_hex/1), {:ok, number} <- expect(block, "number", :pos_integer), do: {:ok, %Block{hash: hash, transactions: transactions, number: number}} end defp is_hex(original) do expect(%{"hash" => original}, "hash", :hex) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/deposit_constraints.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.DepositConstraints do @moduledoc """ Validates query parameters for `deposit.all` """ alias OMG.WatcherRPC.Web.Validator.Helpers @doc """ Validates possible query constraints, stops on first error. """ @spec parse(%{binary() => any()}) :: {:ok, Keyword.t()} | {:error, any()} def parse(params) do constraints = [ {"limit", [pos_integer: true, lesser: 1000, optional: true], :limit}, {"page", [:pos_integer, :optional], :page}, {"address", :address, :address} ] Helpers.validate_constraints(params, constraints) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/helpers.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.Helpers do @moduledoc """ helper for validators """ import OMG.Utils.HttpRPC.Validator.Base, only: [expect: 3] @doc """ Validates possible params with query constraints, stops on first error. """ @spec validate_constraints(%{binary() => any()}, list()) :: {:ok, Keyword.t()} | {:error, any()} def validate_constraints(params, constraints) do Enum.reduce_while(constraints, {:ok, []}, fn {key, validators, atom}, {:ok, list} -> case expect(params, key, validators) do {:ok, nil} -> {:cont, {:ok, list}} {:ok, value} -> {:cont, {:ok, [{atom, value} | list]}} error -> {:halt, error} end end) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/merge_constraints.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.MergeConstraints do @moduledoc """ Validates `/transaction.merge` parameters """ alias OMG.Utils.HttpRPC.Validator.Base alias OMG.WatcherRPC.Web.Validator.Helpers import OMG.Utils.HttpRPC.Validator.Base require OMG.Watcher.State.Transaction.Payment @doc """ Parses and validates request body for `/transaction.merge` """ @spec parse(map()) :: {:ok, Keyword.t()} | Base.validation_error_t() def parse(params) do with {:ok, constraints} <- get_constraints(params), {:ok, result} <- Helpers.validate_constraints(params, constraints) do {:ok, result} end end defp get_constraints(params) do case params do %{"address" => _, "currency" => _} -> {:ok, [{"address", [:address], :address}, {"currency", [:currency], :currency}]} %{"utxo_positions" => _} -> {:ok, [{"utxo_positions", [min_length: 2, max_length: 4, list: &to_utxo_pos/1], :utxo_positions}]} _ -> {:error, :operation_bad_request} end end defp to_utxo_pos(utxo_pos_string) do expect(%{"utxo_pos" => utxo_pos_string}, "utxo_pos", :non_neg_integer) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/order.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.Order do @moduledoc """ Validates `/transaction.create` request body. """ require OMG.Watcher.State.Transaction.Payment import OMG.Utils.HttpRPC.Validator.Base alias OMG.Utils.HttpRPC.Validator.Base alias OMG.Watcher.State.Transaction alias OMG.WatcherInfo.Transaction, as: TransactionCreator @doc """ Parses and validates request body """ @spec parse(map()) :: {:ok, TransactionCreator.order_t()} | Base.validation_error_t() def parse(params) do with {:ok, owner} <- expect(params, "owner", :address), {:ok, metadata} <- expect(params, "metadata", [:hash, :optional]), {:ok, fee} <- expect(params, "fee", map: &parse_fee/1), {:ok, payments} <- expect(params, "payments", list: &parse_payment/1), {:ok, payments} <- fills_in_outputs?(payments), :ok <- ensure_not_self_transaction(owner, payments) do {:ok, %{ owner: owner, payments: payments, fee: fee, metadata: metadata }} end end defp ensure_not_self_transaction(owner, payments) when length(payments) > 0 do payments |> Enum.any?(fn payment -> owner != payment[:owner] end) |> handle_self_tx_result() end defp ensure_not_self_transaction(_, _), do: :ok defp handle_self_tx_result(true), do: :ok defp handle_self_tx_result(false), do: {:error, :self_transaction_not_supported} defp fills_in_outputs?(payments) do if length(payments) <= Transaction.Payment.max_outputs(), do: {:ok, payments}, else: error("payments", {:too_many_payments, Transaction.Payment.max_outputs()}) end defp parse_payment(raw_payment) do with {:ok, owner} <- expect(raw_payment, "owner", [:address, :optional]), {:ok, amount} <- expect(raw_payment, "amount", :pos_integer), {:ok, currency} <- expect(raw_payment, "currency", :address), do: {:ok, %{owner: owner, currency: currency, amount: amount}} end defp parse_fee(map) when is_map(map) do with {:ok, currency} <- expect(map, "currency", :address) do {:ok, %{currency: currency}} end end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/transaction_constraints.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.TransactionConstraints do @moduledoc """ Validates `/transaction.all` query parameters """ import OMG.Utils.HttpRPC.Validator.Base, only: [expect: 3] alias OMG.WatcherRPC.Web.Validator.Helpers @max_tx_types 16 @doc """ Validates possible query constraints, stops on first error. """ @spec parse(%{binary() => any()}) :: {:ok, Keyword.t()} | {:error, any()} def parse(params) do constraints = [ {"address", [:address, :optional], :address}, {"blknum", [:pos_integer, :optional], :blknum}, {"metadata", [:hash, :optional], :metadata}, {"txtypes", [list: &to_tx_type/1, max_length: @max_tx_types, optional: true], :txtypes}, {"limit", [pos_integer: true, lesser: 1000, optional: true], :limit}, {"page", [:pos_integer, :optional], :page}, {"end_datetime", [:pos_integer, :optional], :end_datetime} ] Helpers.validate_constraints(params, constraints) end defp to_tx_type(tx_type_str) do expect(%{"txtype" => tx_type_str}, "txtype", :non_neg_integer) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/validators/typed_data_signed.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.TypedDataSigned do @moduledoc """ Validates `/transaction.submit_typed` request body. """ alias OMG.Utils.HttpRPC.Validator.Base alias OMG.Watcher.Crypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.TypedDataHash.Config alias OMG.Watcher.TypedDataHash.Tools import OMG.Utils.HttpRPC.Validator.Base @doc """ Parses and validates request body for /transaction.submit_typed` """ @spec parse(map()) :: {:ok, Transaction.Signed.t()} | {:error, any()} def parse(params) do with {:ok, domain} <- expect(params, "domain", map: &parse_domain/1), :ok <- ensure_network_match(domain), {:ok, sigs} <- expect(params, "signatures", list: &to_signature/1), {:ok, raw_tx} <- parse_transaction(params) do {:ok, %Transaction.Signed{raw_tx: raw_tx, sigs: sigs}} end end @spec parse_transaction(map()) :: {:ok, Transaction.Payment.t()} | {:error, any} def parse_transaction(params) do with {:ok, msg} <- expect(params, "message", :map), inputs when is_list(inputs) <- parse_inputs(msg), outputs when is_list(outputs) <- parse_outputs(msg), {:ok, metadata} <- expect(msg, "metadata", :hash) do {:ok, Transaction.Payment.new(inputs, outputs, metadata)} end end @spec parse_domain(map()) :: {:ok, Tools.eip712_domain_t()} | Base.validation_error_t() def parse_domain(map) when is_map(map) do name = Map.get(map, "name") version = Map.get(map, "version") with {:ok, salt} <- expect(map, "salt", :hash), {:ok, contract} <- expect(map, "verifyingContract", :address), do: {:ok, %{name: name, version: version, salt: salt, verifyingContract: contract}} end @spec ensure_network_match(Tools.eip712_domain_t(), Tools.eip712_domain_t() | nil) :: :ok | Base.validation_error_t() def ensure_network_match(domain_from_params, network_domain \\ nil) do network_domain = case network_domain do nil -> Config.domain_separator_from_config() params when is_map(params) -> Tools.domain_separator(params) end if network_domain == Tools.domain_separator(domain_from_params), do: :ok, else: error("domain", :domain_separator_mismatch) end @spec to_signature(binary()) :: {:ok, <<_::520>>} | Base.validation_error_t() defp to_signature(sig_str), do: expect(%{"signature" => sig_str}, "signature", :signature) @spec parse_input(map()) :: {:ok, {integer(), integer(), integer()}} | Base.validation_error_t() defp parse_input(input) do with {:ok, blknum} <- expect(input, "blknum", :non_neg_integer), {:ok, txindex} <- expect(input, "txindex", :non_neg_integer), {:ok, oindex} <- expect(input, "oindex", :non_neg_integer), do: {:ok, {blknum, txindex, oindex}} end @spec parse_inputs(map()) :: [{integer(), integer(), integer()}] | {:error, any()} defp parse_inputs(message) do require Transaction.Payment 0..(Transaction.Payment.max_inputs() - 1) |> Enum.map(fn i -> expect(message, "input#{i}", map: &parse_input/1) end) |> Enum.reject(&empty_input?/1) |> all_success_or_error() end @spec parse_output(map()) :: {:ok, {Crypto.address_t(), Crypto.address_t(), integer()}} | Base.validation_error_t() defp parse_output(output) do with {:ok, owner} <- expect(output, "owner", :address), {:ok, currency} <- expect(output, "currency", :address), {:ok, amount} <- expect(output, "amount", :non_neg_integer), do: {:ok, {owner, currency, amount}} end @spec parse_outputs(map()) :: [{Crypto.address_t(), Crypto.address_t(), integer()}] | {:error, any()} defp parse_outputs(message) do require Transaction.Payment 0..(Transaction.Payment.max_outputs() - 1) |> Enum.map(fn i -> expect(message, "output#{i}", map: &parse_output/1) end) |> Enum.reject(&empty_output?/1) |> all_success_or_error() end # we do not longer pad with empty so we need to filter here, because typed data still require exact 4 in/outputs defp empty_input?({:ok, {0, 0, 0}}), do: true defp empty_input?(_input), do: false defp empty_output?({:ok, {_owner, _currency, 0}}), do: true defp empty_output?(_output), do: false end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/account.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Account do @moduledoc """ The account view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.Utils.Paginator alias OMG.Watcher.Utxo alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse require Utxo def render("balance.json", %{response: balance}) do balance |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("utxos.json", %{response: %Paginator{data: utxos, data_paging: data_paging}}) do utxos |> Enum.map(&to_utxo/1) |> Response.serialize_page(data_paging) |> WatcherRPCResponse.add_app_infos() end def render("exitable_utxos.json", %{response: utxos}) do utxos |> Enum.map(&to_utxo/1) |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/alarm.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Alarm do @moduledoc """ The alarm view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("alarm.json", %{response: alarms}) do alarms |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/block.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Block do @moduledoc """ The block view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.Utils.Paginator alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("block.json", %{response: block}) do block |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("blocks.json", %{response: %Paginator{data: blocks, data_paging: data_paging}}) do blocks |> Response.serialize_page(data_paging) |> WatcherRPCResponse.add_app_infos() end def render("validate_block.json", %{response: block}) do block |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/challenge.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Challenge do @moduledoc """ The challenge view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("challenge.json", %{response: challenge}) do challenge |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/configuration.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Configuration do @moduledoc """ The Configuration view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("configuration.json", %{response: configuration}) do configuration |> to_api_format() |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end defp to_api_format(%{contract_semver: contract_semver, network: network} = response) do response |> Map.put(:contract_semver, {:skip_hex_encode, contract_semver}) |> Map.put(:network, {:skip_hex_encode, network}) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/deposit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Deposit do @moduledoc """ The deposit view for rendering JSON. """ alias OMG.Utils.HttpRPC.Response alias OMG.Utils.Paginator alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse use OMG.WatcherRPC.Web, :view def render("deposits.json", %{response: %Paginator{data: ethevents, data_paging: data_paging}}) do ethevents |> Enum.map(&render_ethevent/1) |> Response.serialize_page(data_paging) |> WatcherRPCResponse.add_app_infos() end defp render_ethevent(event) do event |> Map.update!(:txoutputs, &render_txoutputs/1) |> Map.take([ :eth_height, :event_type, :log_index, :root_chain_txhash, :txoutputs, :inserted_at, :updated_at ]) end defp render_txoutputs(outputs) do Enum.map(outputs, &to_utxo/1) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/error.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Views.Error do @moduledoc """ The error view for rendering json """ use OMG.WatcherRPC.Web, :view require Logger alias OMG.Utils.HttpRPC.Error alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse @doc """ Handles client errors, e.g. malformed json in request body """ def render("400.json", _) do "operation:bad_request" |> Error.serialize("Server has failed to parse the request.") |> WatcherRPCResponse.add_app_infos() end @doc """ Handles invalid input parsing errors, e.g. Content-Type not application/json """ def render("415.json", _) do "operation:bad_request" |> Error.serialize("Invalid Content-Type header, use application/json.") |> WatcherRPCResponse.add_app_infos() end @doc """ Supports internal server error thrown by Phoenix. """ def render("500.json", %{reason: %{message: message}} = _conn) do "server:internal_server_error" |> Error.serialize(message) |> WatcherRPCResponse.add_app_infos() end @doc """ Renders the given error code, description and messages. """ def render("error.json", %{code: code, description: description, messages: messages}) do code |> Error.serialize(description, messages) |> WatcherRPCResponse.add_app_infos() end @doc """ Renders the given error code and description. """ def render("error.json", %{code: code, description: description}) do code |> Error.serialize(description) |> WatcherRPCResponse.add_app_infos() end # In case no render clause matches or no # template is found, let's render it as 500 def template_not_found(_template, _assigns) do "server:internal_server_error" |> Error.serialize("Server has failed to render the error.") |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/fee.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Fee do @moduledoc """ The challenge view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("fees_all.json", %{response: fees}) do fees |> Enum.map(&parse_for_type/1) |> Enum.into(%{}) |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end defp parse_for_type({tx_type, fees}) do {tx_type, Enum.map(fees, &parse_for_token/1)} end defp parse_for_token(fee) do fee |> Map.put("currency", {:skip_hex_encode, fee["currency"]}) |> Map.put("pegged_currency", {:skip_hex_encode, fee["pegged_currency"]}) |> Map.put("updated_at", {:skip_hex_encode, fee["updated_at"]}) end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/in_flight_exit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.InFlightExit do @moduledoc """ The transaction view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.Watcher.Utxo alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("in_flight_exit.json", %{response: in_flight_exit}) do in_flight_exit |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("competitor.json", %{response: competitor}) do competitor |> Map.update!(:competing_tx_pos, &Utxo.Position.encode/1) |> Map.update!(:input_utxo_pos, &Utxo.Position.encode/1) |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("prove_canonical.json", %{response: prove_canonical}) do prove_canonical |> Map.update!(:in_flight_tx_pos, &Utxo.Position.encode/1) |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("get_input_challenge_data.json", %{response: challenge_data}) do challenge_data |> Map.update!(:input_utxo_pos, &Utxo.Position.encode/1) |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("get_output_challenge_data.json", %{response: challenge_data}) do challenge_data |> Map.update!(:in_flight_output_pos, &Utxo.Position.encode/1) |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/stats.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Stats do @moduledoc """ The block view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("stats.json", %{response: stats}) do stats |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/status.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Status do @moduledoc """ The status view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("status.json", %{response: status}) do status |> format_byzantine_events() |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end defp format_byzantine_events(%{byzantine_events: byzantine_events, services_synced_heights: heights} = status) do prepared_events = Enum.map(byzantine_events, &format_byzantine_event/1) prepared_heights = Enum.map(heights, &format_synced_height/1) %{status | byzantine_events: prepared_events, services_synced_heights: prepared_heights} end defp format_byzantine_event(%{name: name} = event) do %{event: name, details: event} end defp format_synced_height({name, height}) do %{service: name, height: height} end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Transaction do @moduledoc """ The transaction view for rendering json """ alias OMG.Utils.HttpRPC.Response alias OMG.Utils.Paginator alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse use OMG.WatcherRPC.Web, :view def render("transaction.json", %{response: transaction}) do transaction |> render_transaction() |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("transactions.json", %{response: %Paginator{data: transactions, data_paging: data_paging}}) do transactions |> Enum.map(&render_transaction/1) |> Response.serialize_page(data_paging) |> WatcherRPCResponse.add_app_infos() end def render("submission.json", %{response: transaction}) do transaction |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("batch_submission.json", %{response: transactions}) do transactions |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end def render("merge.json", %{response: advice}) do render_transactions(advice) end def render("create.json", %{response: advice}) do render_transactions(advice) end defp render_transactions(advice) do transactions = advice.transactions |> Enum.map(fn tx -> Map.update!(tx, :inputs, &render_txoutputs/1) end) |> Enum.map(&skip_hex_encoding/1) advice |> Map.put(:transactions, transactions) |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end defp render_transaction(transaction) do transaction |> Map.take([:txindex, :txhash, :txtype, :block, :inputs, :outputs, :txbytes, :metadata, :inserted_at, :updated_at]) |> Map.update!(:inputs, &render_txoutputs/1) |> Map.update!(:outputs, &render_txoutputs/1) end defp render_txoutputs(inputs) do inputs |> Enum.map(&to_utxo/1) end defp skip_hex_encoding(%{typed_data: typed_data} = tx) do typed_data_esc = typed_data |> Kernel.put_in([:skip_hex_encode], [:types, :primaryType]) |> Kernel.put_in([:domain, :skip_hex_encode], [:name, :version]) %{tx | typed_data: typed_data_esc} end end ================================================ FILE: apps/omg_watcher_rpc/lib/web/views/utxo.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.Utxo do @moduledoc """ The utxo view for rendering json """ use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse def render("utxo_exit.json", %{response: utxo_exit}) do utxo_exit |> Response.serialize() |> WatcherRPCResponse.add_app_infos() end end ================================================ FILE: apps/omg_watcher_rpc/lib/web.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web do @moduledoc """ The entrypoint for defining your web interface, such as controllers, views, channels and so on. This can be used in your application as: use OMG.WatcherRPC.Web, :controller use OMG.WatcherRPC.Web, :view The definitions below will be executed for every view, controller, etc, so keep them short and clean, focused on imports, uses and aliases. Do NOT define functions inside the quoted expressions below. Instead, define any helper function in modules and import those modules here. """ def controller() do quote do use Phoenix.Controller, namespace: OMG.WatcherRPC.Web, log: :debug import Plug.Conn import OMG.WatcherRPC.Web.Router.Helpers import OMG.Utils.HttpRPC.Validator.Base action_fallback(OMG.WatcherRPC.Web.Controller.Fallback) @doc """ Passes result to the render process when successful or returns error result unchanged. Error tuple will be passed to the see: `OMG.WatcherRPC.Web.Controller.Fallback` """ def api_response(api_result, conn, template) when is_tuple(api_result), do: with({:ok, data} <- api_result, do: api_response(data, conn, template)) @doc """ Takes advantage of preset api response structure and module names conventions to discover parameters to Phoenix Controller's [render/3](https://hexdocs.pm/phoenix/Phoenix.Controller.html#render/3) """ def api_response(data, conn, template) do view_module = conn |> controller_module() |> Atom.to_string() |> String.replace("Controller", "View") |> String.to_existing_atom() conn |> put_view(view_module) |> render(template, response: data) end end end def view() do quote do use Phoenix.View, root: "lib/omg_watcher_rpc_web/templates", namespace: OMG.WatcherRPC.Web # Import convenience functions from controllers import Phoenix.Controller, only: [get_flash: 2, view_module: 1] import OMG.WatcherRPC.Web.Router.Helpers import OMG.WatcherRPC.Web.Serializer.Base end end def router() do quote do use Phoenix.Router import Plug.Conn import Phoenix.Controller end end def channel() do quote do use Phoenix.Channel end end @doc """ When used, dispatch to the appropriate controller/view/etc. """ defmacro __using__(which) when is_atom(which) do apply(__MODULE__, which, []) end end ================================================ FILE: apps/omg_watcher_rpc/mix.exs ================================================ defmodule OMG.WatcherRPC.Mixfile do use Mix.Project def project() do [ app: :omg_watcher_rpc, version: version(), build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, deps: deps(), test_coverage: [tool: ExCoveralls] ] end def application() do [ mod: {OMG.WatcherRPC.Application, []}, extra_applications: [:logger, :runtime_tools, :telemetry, :omg_watcher] ] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end # Specifies which paths to compile per environment. defp elixirc_paths(:prod), do: ["lib"] defp elixirc_paths(:dev), do: ["lib"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp deps() do [ {:phoenix, "~> 1.5"}, {:poison, "~> 4.0"}, {:plug_cowboy, "~> 2.3"}, {:cors_plug, "~> 2.0"}, {:spandex_phoenix, "~> 1.0"}, {:spandex_datadog, "~> 1.0"}, {:telemetry, "~> 0.4.1"}, # UMBRELLA {:omg_bus, in_umbrella: true}, {:omg_utils, in_umbrella: true}, {:omg_watcher, in_umbrella: true}, # UMBRELLA but test only {:omg_watcher_info, in_umbrella: true, only: [:test]}, # TEST ONLY {:ex_machina, "~> 2.3", only: [:test], runtime: false} ] end end ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/paths.yaml ================================================ account.get_balance: post: tags: - Account summary: Returns the balance of each currency for the given account address. operationId: account_get_balance requestBody: $ref: 'request_bodies.yaml#/AddressBodySchema' responses: 200: $ref: 'responses.yaml#/AccountBalanceResponse' 500: $ref: '../responses.yaml#/InternalServerError' account.get_utxos: post: tags: - Account summary: Gets all utxos belonging to the given address. operationId: account_get_utxos requestBody: $ref: 'request_bodies.yaml#/AddressBodySchema' responses: 200: $ref: 'responses.yaml#/AccountUtxoResponse' 500: $ref: '../responses.yaml#/InternalServerError' # This is served by transaction's controller, referencing there for definitions account.get_transactions: post: tags: - Account summary: Gets a list of transactions for given account address. operationId: account_get_transactions requestBody: $ref: '../transaction/request_bodies.yaml#/GetAllTransactionsBodySchema' responses: 200: $ref: '../transaction/responses.yaml#/GetAllTransactionsResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/request_bodies.yaml ================================================ AddressBodySchema: description: HEX-encoded address of the account and pagination fields required: true content: application/json: schema: title: 'AddressBodySchema' type: object properties: address: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' limit: 100 page: 2 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/response_schemas.yaml ================================================ AccountBalanceResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/AccountBalanceSchema' example: data: - currency: '0xbfdf85743ef16cfb1f8d4dd1dfc74c51dc496434' amount: 20 - currency: '0x0000000000000000000000000000000000000000' amount: 1000000000 AccountUtxoResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/AccountUtxoSchema' data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 example: data: - amount: 10 blknum: 123000 creating_txhash: '0x2c499b95ccb6bf7b923049b32b03a613d30882a448102136e544b302119eb722' currency: '0x0000000000000000000000000000000000000000' inserted_at: '2020-02-10T12:07:32Z' oindex: 0 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' spending_txhash: null txindex: 111 updated_at: '2020-02-15T04:07:57Z' utxo_pos: 123000001110000 data_paging: page: 1 limit: 200 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/responses.yaml ================================================ AccountBalanceResponse: description: Account balance successful response content: application/json: schema: $ref: 'response_schemas.yaml#/AccountBalanceResponseSchema' AccountUtxoResponse: description: Account utxos succcessful response content: application/json: schema: $ref: 'response_schemas.yaml#/AccountUtxoResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/schemas.yaml ================================================ AccountBalanceSchema: type: object properties: currency: type: string amount: type: integer format: int256 AccountUtxoSchema: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 otype: type: integer format: int16 oindex: type: integer format: int8 utxo_pos: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string owner: type: string currency: type: string amount: type: integer format: int256 inserted_at: type: string updated_at: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/alarm/alarms_schema.yml ================================================ components: schemas: ethereum_connection_error: type: object properties: ethereum_connection_error: type: object properties: node: type: string reporter: type: string ethereum_stalled_sync: type: object properties: ethereum_stalled_sync: type: object properties: ethereum_height: type: integer minimum: 0 synced_at: type: string format: date-time invalid_fee_source: type: object properties: invalid_fee_source: type: object properties: node: type: string reporter: type: string statsd_client_connection: type: object properties: statsd_client_connection: type: object properties: node: type: string reporter: type: string system_memory_high_watermark: type: object properties: statsd_client_connection: type: array items: type: string default: [] disk_almost_full: type: object properties: disk_almost_full: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/alarm/paths.yaml ================================================ alarm.get: get: tags: - Alarm summary: Provides alarms related to system memory, cpu and storage and application specific alarms. description: > **Note:** Service operator alarms. operationId: alarm_get responses: 200: $ref: 'responses.yaml#/AlarmResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/alarm/response_schemas.yaml ================================================ AlarmResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/AlarmSchema' example: data: - disk_almost_full: "/dev/null" ethereum_connection_error: {} ethereum_stalled_sync: {} system_memory_high_watermark: [] ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/alarm/responses.yaml ================================================ AlarmResponse: description: System alarms content: application/json: schema: $ref: 'response_schemas.yaml#/AlarmResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/alarm/schemas.yaml ================================================ AlarmSchema: type: array items: anyOf: - $ref: 'alarms_schema.yml#/components/schemas/ethereum_connection_error' - $ref: 'alarms_schema.yml#/components/schemas/ethereum_stalled_sync' - $ref: 'alarms_schema.yml#/components/schemas/invalid_fee_source' - $ref: 'alarms_schema.yml#/components/schemas/statsd_client_connection' - $ref: 'alarms_schema.yml#/components/schemas/system_memory_high_watermark' - $ref: 'alarms_schema.yml#/components/schemas/disk_almost_full' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/batch_transaction/paths.yaml ================================================ transaction.batch_submit: post: tags: - Transaction summary: This endpoint submits an array of signed transaction to the child chain. description: > Normally you should call the Watcher's Transaction - Submit instead of this. The Watcher's version performs various security and validation checks (TO DO) before submitting the transaction, so is much safer. However, if the Watcher is not available this version exists. operationId: batch_submit requestBody: $ref: 'request_bodies.yaml#/TransactionBatchSubmitBodySchema' responses: 200: $ref: 'responses.yaml#/TransactionBatchSubmitResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/batch_transaction/request_bodies.yaml ================================================ TransactionBatchSubmitBodySchema: description: Array of signed transactions, RLP-encoded to bytes, and HEX-encoded to string required: true content: application/json: schema: title: 'TransactionBatchSubmitBodySchema' type: object properties: transactions: type: array items: type: string required: - transactions example: transactions: ['0xf8d083015ba98080808080940000...', '0xf8d083a15ba98080808080920000...'] ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/batch_transaction/response_schemas.yaml ================================================ TransactionBatchSubmitResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: array $ref: 'schemas.yaml#/TransactionBatchSubmitSchema ' example: data: - blknum: 123000 txindex: 111 txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/batch_transaction/responses.yaml ================================================ TransactionBatchSubmitResponse: description: Transaction batch submission successful response content: application/json: schema: $ref: 'response_schemas.yaml#/TransactionBatchSubmitResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/batch_transaction/schemas.yaml ================================================ TransactionBatchSubmitSchema: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 txhash: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/block/paths.yaml ================================================ block.all: post: tags: - Block summary: Gets all blocks (can be limited with various filters). description: > Returns a list of blocks ordered by the highest block number first. Intended to be used for presenting an overview of most recent blocks. Note: Due to eventual consistency nature of the informational API, blocks may later than deposits, exits, etc. operationId: block_all requestBody: $ref: 'request_bodies.yaml#/AllBlocksBodySchema' responses: 200: $ref: 'responses.yaml#/BlocksAllResponse' 500: $ref: '../responses.yaml#/InternalServerError' block.get: post: tags: - Block summary: Retrieves a single block for the given block number. description: > Intended for operations requiring info for a specific block only. Returns a single block object for the given block number. The returned object includes transaction count but not associated transactions - for which you can use transaction.all with blknum in the request body. operationId: block_get requestBody: $ref: 'request_bodies.yaml#/GetBlockBodySchema' responses: 200: $ref: 'responses.yaml#/BlockResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/block/request_bodies.yaml ================================================ AllBlocksBodySchema: description: The supported request parameters for /block.all content: application/json: schema: title: 'AllBlocksBodySchema' type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: page: 2 limit: 100 GetBlockBodySchema: description: Block number required: true content: application/json: schema: title: 'GetBlockBodySchema' type: object properties: blknum: type: integer format: int64 example: blknum: 68290000 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/block/response_schemas.yaml ================================================ BlocksAllResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseListResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/BlockSchema' data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: data: - blknum: 68290000 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 timestamp: 1540365586 tx_count: 2 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' data_paging: page: 1 limit: 100 BlockResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/BlockSchema' example: data: timestamp: 1540365586 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 blknum: 68290000 tx_count: 2 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/block/responses.yaml ================================================ BlocksAllResponse: description: Blocks succcessful response content: application/json: schema: $ref: 'response_schemas.yaml#/BlocksAllResponseSchema' BlockResponse: description: Block succcessful response content: application/json: schema: $ref: 'response_schemas.yaml#/BlockResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/block/schemas.yaml ================================================ BlockSchema: type: object properties: blknum: type: integer format: int64 hash: type: string eth_height: type: integer format: int64 timestamp: type: integer format: int64 tx_count: type: integer format: int64 inserted_at: type: string updated_at: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/configuration/configuration_schema.yml ================================================ components: schemas: deposit_finality_margin: type: integer format: int256 contract_semver: type: string network: type: string exit_processor_sla_margin: type: integer format: int256 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/configuration/paths.yaml ================================================ configuration.get: get: tags: - Configuration summary: Provides configuration values description: > **Note:** Configuration values. operationId: configuration_get responses: 200: $ref: 'responses.yaml#/ConfigurationResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/configuration/response_schemas.yaml ================================================ ConfigurationResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/ConfigurationSchema' example: data: - deposit_finality_margin: 10 contract_semver: "1.0.0.1+a1s29s8" exit_processor_sla_margin: 5520 network: "RINKEBY" ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/configuration/responses.yaml ================================================ ConfigurationResponse: description: Configuration response content: application/json: schema: $ref: 'response_schemas.yaml#/ConfigurationResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/configuration/schemas.yaml ================================================ ConfigurationSchema: type: object properties: deposit_finality_margin: type: integer format: int256 contract_semver: type: string exit_processor_sla_margin: type: integer format: int256 network: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/deposit/paths.yaml ================================================ deposit.all: post: tags: - Deposit summary: Gets a paginated list of deposit for the given address. description: > Returns a list of deposits ordered by Ethereum height in descending order for the given address. operationId: deposit_all requestBody: $ref: 'request_bodies.yaml#/AllDepositsBodySchema' responses: 200: $ref: 'responses.yaml#/DepositsAllResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/deposit/request_bodies.yaml ================================================ AllDepositsBodySchema: description: The supported request parameters for /deposit.all. The "limit" and "page" parameters are optional. required: true content: application/json: schema: title: 'AllDepositsBodySchema' type: object properties: address: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: page: 2 limit: 100 address: "0xb01cb6f56d798a62d1e0bace406c73a122c39c9d" ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/deposit/response_schemas.yaml ================================================ DepositsAllResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseListResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/DepositSchema' data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: data: - event_type: "deposit" inserted_at: "2020-05-15T12:37:40Z" eth_height: 168637 log_index: 0 root_chain_txhash: "0x63c056f122f5bf30bf8119ec0a2184b73f975951229995a427ea58d904eaab85" txoutputs: - blknum: 1 txindex: 0 otype: 1 oindex: 0 utxo_pos: 1000000000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' creating_txhash: null spending_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' amount: 20000000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-10T12:07:32Z' data_paging: page: 1 limit: 100 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/deposit/responses.yaml ================================================ DepositsAllResponse: description: /deposit.all succcessful response content: application/json: schema: $ref: 'response_schemas.yaml#/DepositsAllResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/deposit/schemas.yaml ================================================ DepositSchema: type: object properties: event_type: type: string root_chain_txhash: type: string log_index: type: integer eth_height: type: integer format: int64 inserted_at: type: string updated_at: type: string txoutputs: type: array items: $ref: '#/TransactionOutputSchema' TransactionOutputSchema: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/fees/paths.yaml ================================================ fees.all: post: tags: - Fees summary: This endpoint retrieves the list of fee tokens currently supported by the childchain and the current amount needed to perform a transaction. operationId: fees_all requestBody: $ref: 'request_bodies.yaml#/FeesAllBodySchema' responses: 200: $ref: 'responses.yaml#/AllFeesResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/fees/request_bodies.yaml ================================================ FeesAllBodySchema: description: An optional array of currencies to filter, raises an error if one of the currencies is not supported. required: false content: application/json: schema: title: 'FeesAllBodySchema' type: object properties: currencies: type: array items: type: string tx_types: type: array items: type: integer example: currencies: ['0x0000000000000000000000000000000000000000'] tx_types: [1] ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/fees/response_schemas.yaml ================================================ AllFeesResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/FeeAllSchema' example: data: '1': - currency: '0x0000000000000000000000000000000000000000' amount: 220000000000000 subunit_to_unit: 1000000000000000000 pegged_currency: 'USD' pegged_amount: 4 pegged_subunit_to_unit: 100 updated_at: '2019-01-01T10:10:10+00:00' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/fees/responses.yaml ================================================ AllFeesResponse: description: List of all supported fees response content: application/json: schema: $ref: 'response_schemas.yaml#/AllFeesResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/fees/schemas.yaml ================================================ FeeAllSchema: type: object additionalProperties: type: array items: $ref: '#/FeeItemSchema' FeeItemSchema: type: object properties: currency: type: string amount: type: integer format: int256 subunit_to_unit: type: integer format: int256 pegged_currency: type: string pegged_amount: type: integer format: int256 pegged_subunit_to_unit: type: integer format: int256 updated_at: type: string format: date-time ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/response_schemas.yaml ================================================ WatcherInfoBaseResponseSchema: description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: '1.0.0+abcdefa' success: true data: {} WatcherInfoBaseListResponseSchema: description: The response schema for a successful list operation type: object properties: version: type: string success: type: boolean data: type: array items: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: '1.0+abcdefa' success: true data: [] WatcherInfoErrorResponseSchema: description: The response schema for an error allOf: - $ref: 'response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: $ref: '../shared/schemas.yaml#/ErrorSchema' required: - data example: success: false data: object: error code: server:internal_server_error description: Something went wrong on the server messages: {error_key: error_reason} ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/responses.yaml ================================================ InternalServerError: description: Returns an internal server error content: application/json: schema: $ref: 'response_schemas.yaml#/WatcherInfoErrorResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/stats/paths.yaml ================================================ stats.get: post: tags: - Stats summary: Retrieves network statistics description: > Retrieves transaction count, block count and average block interval, both for all time and the last 24 hours. operationId: stats_get responses: 200: $ref: 'responses.yaml#/StatsGetResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/stats/response_schemas.yaml ================================================ StatsGetResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseListResponseSchema' - type: object properties: data: type: object items: $ref: 'schemas.yaml#/StatsSchema' example: data: transaction_count: all_time: 4 last_24_hours: 2 block_count: all_time: 2 last_24_hours: 1 average_block_interval: all_time: 100 last_24_hours: null ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/stats/responses.yaml ================================================ StatsGetResponse: description: Stats Successful Response content: application/json: schema: $ref: 'response_schemas.yaml#/StatsGetResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/stats/schemas.yaml ================================================ StatsSchema: type: object properties: transaction_count: type: object properties: all_time: type: integer format: int64 last_24_hours: type: integer format: int64 block_count: type: object properties: all_time: type: integer format: int64 last_24_hours: type: integer format: int64 average_block_interval: type: object properties: all_time: type: number format: int64 last_24_hours: type: number format: int64 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/swagger.yaml ================================================ openapi: 3.0.0 info: version: '1.0.0' title: Watcher Info API description: > API specification of the Watcher's Informational Service Error codes are available in [html](https://github.com/omgnetwork/elixir-omg/blob/master/docs/api_specs/errors.md#error-codes-description) format. contact: name: OMG Network email: engineering@omg.network license: name: 'Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0' url: 'https://omg.network/' servers: - url: https://watcher-info.ropsten.v1.omg.network/ - url: http://localhost:7534/ tags: - name: Account description: Account related API. - name: Block description: Block related API. - name: Deposit description: Deposit related API. - name: Transaction description: Transaction related API. - name: Fees description: Fees related API. - name: Stats description: Stats related API. - name: Configuration description: Configuration related API. paths: /alarm.get: $ref: 'alarm/paths.yaml#/alarm.get' /configuration.get: $ref: 'configuration/paths.yaml#/configuration.get' /account.get_balance: $ref: 'account/paths.yaml#/account.get_balance' /account.get_utxos: $ref: 'account/paths.yaml#/account.get_utxos' /account.get_transactions: $ref: 'account/paths.yaml#/account.get_transactions' /block.get: $ref: 'block/paths.yaml#/block.get' /block.all: $ref: 'block/paths.yaml#/block.all' /deposit.all: $ref: 'deposit/paths.yaml#/deposit.all' /transaction.all: $ref: 'transaction/paths.yaml#/transaction.all' /transaction.create: $ref: 'transaction/paths.yaml#/transaction.create' /transaction.merge: $ref: 'transaction/paths.yaml#/transaction.merge' /transaction.get: $ref: 'transaction/paths.yaml#/transaction.get' /transaction.submit_typed: $ref: 'transaction/paths.yaml#/transaction.submit_typed' /fees.all: $ref: 'fees/paths.yaml#/fees.all' /stats.get: $ref: 'stats/paths.yaml#/stats.get' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/transaction/paths.yaml ================================================ transaction.all: post: tags: - Transaction summary: Gets all transactions (can be limited with various filters). description: > Digests the details of the transaction, by listing the value of outputs, aggregated by currency. Intended to be used when presenting the little details about multiple transactions. For all details queries to `/transaction.get` should be made using the transaction's hash provided. operationId: transactions_all requestBody: $ref: 'request_bodies.yaml#/GetAllTransactionsBodySchema' responses: 200: $ref: 'responses.yaml#/GetAllTransactionsResponse' 500: $ref: '../responses.yaml#/InternalServerError' transaction.create: post: tags: - Transaction summary: Finds an optimal way to construct a transaction spending particular amount. description: > Given token, amount and spender, finds spender's inputs sufficient to perform a payment. If also provided with receiver's address, creates and encodes a transaction. operationId: createTransaction requestBody: $ref: 'request_bodies.yaml#/CreateTransactionsBodySchema' responses: 200: $ref: 'responses.yaml#/CreateTransactionResponse' 500: $ref: '../responses.yaml#/InternalServerError' transaction.merge: post: tags: - Transaction summary: Constructs merge transactions. description: | - Given address and currency parameters, returns a list of merge transactions for correspondng UTXOs – grouped in ascending order of value. - Given between two and four UTXO positions, returns a merge transaction for the corresponding UTXOs. (These UTXOs must have the same owner and currency) operationId: mergeTransaction requestBody: $ref: 'request_bodies.yaml#/MergeTransactionsBodySchema' responses: 200: $ref: 'responses.yaml#/MergeTransactionResponse' 500: $ref: '../responses.yaml#/InternalServerError' transaction.get: post: tags: - Transaction summary: Gets a transaction with the given id. operationId: transaction_get requestBody: $ref: 'request_bodies.yaml#/GetTransactionBodySchema' responses: 200: $ref: 'responses.yaml#/GetTransactionResponse' 500: $ref: '../responses.yaml#/InternalServerError' transaction.submit_typed: post: tags: - Transaction summary: Sends EIP-712 formatted transaction to Child chain. description: > Request to this method is the same as to Web3 `eth_signTypedData` with additional `signatures` array. The `/transaction.create` `typed_data` field can be used to prepare transaction. The same conditions are met as with security-critical `/transaction.submit` operationId: submit_typed requestBody: $ref: 'request_bodies.yaml#/TransactionSubmitTypedBodySchema' responses: 200: $ref: '../../security_critical_api_specs/transaction/responses.yaml#/TransactionSubmitResponse' 500: $ref: '../responses.yaml#/InternalServerError' transaction.get_by_position: post: tags: - Transaction summary: Gets a transaction with the given position (block number, transaction index). description: __Not implemented yet, proposed in OMG-364__ operationId: get_transaction_by_pos requestBody: $ref: 'request_bodies.yaml#/GetTransactionByPosBodySchema' responses: 200: $ref: 'responses.yaml#/GetTransactionResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/transaction/request_bodies.yaml ================================================ GetAllTransactionsBodySchema: description: Account address, block number and other criteria required: true content: application/json: schema: title: 'GetAllTransactionsBodySchema' type: object properties: address: type: string blknum: type: integer format: int64 txtypes: type: array items: type: integer metadata: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 end_datetime: type: integer format: int32 required: - limit example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txtypes: [1] blknum: 68290000 limit: 100 page: 2 end_datetime: 1592476174 CreateTransactionsBodySchema: description: The description of transaction to be crafted. required: true content: application/json: schema: title: 'CreateTransactionsBodySchema' type: object properties: owner: type: string payments: type: array items: type: object properties: amount: type: integer format: int256 currency: type: string owner: type: string required: - amount - currency fee: type: object properties: currency: type: string required: - currency metadata: type: string required: - owner - payments - fee example: owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' payments: - amount: 100 currency: '0x0000000000000000000000000000000000000000' owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' fee: currency: '0x0000000000000000000000000000000000000000' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' MergeTransactionsBodySchema: description: The description of merge transaction to be crafted. content: application/json: schema: title: 'MergeTransactionsBodySchema' type: object properties: address: type: string currency: type: string utxo_positions: type: array items: type: string examples: UTXO positions: value: utxo_positions: ["811000000000001", "811000000000002"] address and currency: value: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' GetTransactionBodySchema: description: Id (hash) of the transaction required: true content: application/json: schema: title: 'GetTransactionBodySchema' type: object properties: id: type: string required: - id example: id: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' TransactionSubmitTypedBodySchema: description: Transaction as for `eth_signTypedData` along with signatures required: true content: application/json: schema: title: 'TransactionSubmitTypedBodySchema' allOf: - $ref: 'schemas.yaml#/Eip712SignRequestSchema' - type: object properties: signatures: type: array items: type: string required: - domain - message - signatures example: domain: name: 'OMG Network' salt: '0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83' verifyingContract: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' version: '1' message: input0: blknum: 1 txindex: 0 oindex: 0 input1: blknum: 1000 txindex: 1 oindex: 1 input2: blknum: 0 txindex: 0 oindex: 0 input3: blknum: 0 txindex: 0 oindex: 0 output0: owner: '0x0527a37aa7081efcf405bd7c8fe36b01e91df27d' currency: '0x0000000000000000000000000000000000000000' amount: 100 output1: owner: '0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837' currency: '0x0000000000000000000000000000000000000000' amount: 10 output2: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output3: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 metadata: '0x0000000000000000000000000000000000000000000000000000000000000000' signatures: - '0x6bfb9b2dbe32...' GetTransactionByPosBodySchema: description: Position of the transaction required: true content: application/json: schema: title: 'GetTransactionByPosBodySchema' type: object properties: blknum: type: string txindex: type: integer format: int16 required: - blknum - txindex example: blknum: 68290000 txindex: 100 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/transaction/response_schemas.yaml ================================================ GetAllTransactionsResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseListResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/TransactionSchema' data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 example: data: - block: timestamp: 1540365586 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 blknum: 68290000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' txindex: 0 txtype: 1 txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' metadata: '0x00000000000000000000000000000000000000000000000000000048656c6c6f' txbytes: '0x5df13a6bee20000...' inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' inputs: - blknum: 1000 txindex: 111 otype: 1 oindex: 0 utxo_pos: 1000001110000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' amount: 20000000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' outputs: - blknum: 68290000 txindex: 5113 otype: 1 oindex: 0 utxo_pos: 68290000051130000 owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' currency: '0x0000000000000000000000000000000000000000' amount: 15000000 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' - blknum: 68290000 txindex: 5113 otype: 1 oindex: 1 utxo_pos: 68290000051130001 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 5000000 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' data_paging: page: 1 limit: 200 CreateTransactionResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/CreateTransactionSchema' example: data: result: 'complete' transactions: - inputs: - blknum: 123000 txindex: 111 oindex: 0 utxo_pos: 123000001110000 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 50 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null - blknum: 277000 txindex: 2340 oindex: 3 utxo_pos: 277000023400003 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 75 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null outputs: - amount: 100 currency: '0x0000000000000000000000000000000000000000' owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' - amount: 20 currency: '0x0000000000000000000000000000000000000000' owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' fee: amount: 5 currency: '0x0000000000000000000000000000000000000000' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txbytes: '0x5df13a6bee20000...' sign_hash: '0x7851b951edb0b9e88f0fc80e83461f71d0f4b1d4e44fae7d25a5d4ab6adc5d3d' typed_data: types: EIP712Domain: - name: name type: string - name: version type: string - name: verifyingContract type: address - name: salt type: bytes32 Transaction: - name: input0 type: Input - name: input1 type: Input - name: input2 type: Input - name: input3 type: Input - name: output0 type: Output - name: output1 type: Output - name: output2 type: Output - name: output3 type: Output - name: metadata type: bytes32 Input: - name: blknum type: uint256 - name: txindex type: uint256 - name: oindex type: uint256 Output: - name: owner type: address - name: currency type: address - name: amount type: uint256 primaryType: 'Transaction' domain: name: 'OMG Network' salt: '0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83' verifyingContract: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' version: '1' message: input0: blknum: 123000 txindex: 111 oindex: 0 input1: blknum: 277000 txindex: 2340 oindex: 3 input2: blknum: 0 txindex: 0 oindex: 0 input3: blknum: 0 txindex: 0 oindex: 0 output0: owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' currency: '0x0000000000000000000000000000000000000000' amount: 100 output1: owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 20 output2: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output3: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' MergeTransactionResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/MergeTransactionSchema' example: data: result: 'complete' transactions: - inputs: - blknum: 123000 txindex: 111 oindex: 0 utxo_pos: 123000001110000 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 50 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null - blknum: 277000 txindex: 2340 oindex: 3 utxo_pos: 277000023400003 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 50 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null outputs: - amount: 100 currency: '0x0000000000000000000000000000000000000000' owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' fee: amount: 0 currency: '0x0000000000000000000000000000000000000000' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txbytes: '0x5df13a6bee20000...' sign_hash: '0x7851b951edb0b9e88f0fc80e83461f71d0f4b1d4e44fae7d25a5d4ab6adc5d3d' typed_data: types: EIP712Domain: - name: name type: string - name: version type: string - name: verifyingContract type: address - name: salt type: bytes32 Transaction: - name: input0 type: Input - name: input1 type: Input - name: input2 type: Input - name: input3 type: Input - name: output0 type: Output - name: output1 type: Output - name: output2 type: Output - name: output3 type: Output - name: metadata type: bytes32 Input: - name: blknum type: uint256 - name: txindex type: uint256 - name: oindex type: uint256 Output: - name: owner type: address - name: currency type: address - name: amount type: uint256 primaryType: 'Transaction' domain: name: 'OMG Network' salt: '0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83' verifyingContract: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' version: '1' message: input0: blknum: 123000 txindex: 111 oindex: 0 input1: blknum: 277000 txindex: 2340 oindex: 3 input2: blknum: 0 txindex: 0 oindex: 0 input3: blknum: 0 txindex: 0 oindex: 0 output0: owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 100 output1: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output2: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output3: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 metadata: '0x0000000000000000000000000000000000000000000000000000000000000000' GetTransactionResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherInfoBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/TransactionSchema' example: data: txindex: 5113 txtype: 1 txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' metadata: '0x00000000000000000000000000000000000000000000000000000048656c6c6f' txbytes: '0x5df13a6bee20000...' inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' block: timestamp: 1540365586 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 blknum: 68290000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' inputs: - blknum: 1000 txindex: 111 oindex: 0 otype: 1 utxo_pos: 1000001110000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 10 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' outputs: - blknum: 68290000 txindex: 5113 oindex: 0 otype: 1 utxo_pos: 68290000051130000 owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' currency: '0x0000000000000000000000000000000000000000' amount: 2 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' - blknum: 68290000 txindex: 5113 oindex: 1 otype: 1 utxo_pos: 68290000051130001 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 7 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/transaction/responses.yaml ================================================ GetAllTransactionsResponse: description: Transactions succcessful response content: application/json: schema: $ref: 'response_schemas.yaml#/GetAllTransactionsResponseSchema' CreateTransactionResponse: description: Transaction create successful response content: application/json: schema: $ref: 'response_schemas.yaml#/CreateTransactionResponseSchema' MergeTransactionResponse: description: Transaction merge successful response content: application/json: schema: $ref: 'response_schemas.yaml#/MergeTransactionResponseSchema' GetTransactionResponse: description: Transaction details succcessful response content: application/json: schema: $ref: 'response_schemas.yaml#/GetTransactionResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs/transaction/schemas.yaml ================================================ TransactionOutputSchema: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string TransactionSchema: type: object properties: txindex: type: integer format: int16 txtype: type: integer format: int16 txhash: type: string metadata: type: string txbytes: type: string inserted_at: type: string updated_at: type: string block: $ref: '../block/schemas.yaml#/BlockSchema' inputs: type: array items: $ref: '#/TransactionOutputSchema' outputs: type: array items: $ref: '#/TransactionOutputSchema' Eip712DomainSchema: type: object properties: name: type: string salt: type: string verifyingContract: type: string version: type: string required: - name - salt - verifyingContract - version Eip712MsgInputSchema: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 Eip712MsgOutputSchema: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 Eip712MsgTransactionSchema: type: object properties: input0: $ref: '#/Eip712MsgInputSchema' input1: $ref: '#/Eip712MsgInputSchema' input2: $ref: '#/Eip712MsgInputSchema' input3: $ref: '#/Eip712MsgInputSchema' output0: $ref: '#/Eip712MsgOutputSchema' output1: $ref: '#/Eip712MsgOutputSchema' output2: $ref: '#/Eip712MsgOutputSchema' output3: $ref: '#/Eip712MsgOutputSchema' metadata: type: string required: - input0 - input1 - input2 - input3 - output0 - output1 - output2 - output3 - metadata Eip712SignRequestSchema: type: object properties: types: type: object properties: EIP712Domain: type: array items: type: object properties: name: type: string type: type: string additionalProperties: type: array items: type: object properties: name: type: string type: type: string required: - name - type primaryType: type: string domain: $ref: '#/Eip712DomainSchema' message: $ref: '#/Eip712MsgTransactionSchema' CreateTransactionSchema: type: object properties: result: type: string enum: [complete, intermediate] transactions: type: array items: type: object properties: inputs: type: array items: $ref: '#/TransactionOutputSchema' outputs: type: array items: type: object properties: amount: type: integer format: int256 currency: type: string owner: type: string fee: type: object properties: amount: type: integer format: int256 currency: type: string metadata: type: string txbytes: type: string sign_hash: type: string typed_data: $ref: '#/Eip712SignRequestSchema' MergeTransactionSchema: type: object properties: transactions: type: array items: type: object properties: inputs: type: array items: $ref: '#/TransactionOutputSchema' outputs: type: array items: type: object properties: amount: type: integer format: int256 currency: type: string owner: type: string fee: type: object properties: amount: type: integer format: int256 currency: type: string metadata: type: string txbytes: type: string sign_hash: type: string typed_data: $ref: '#/Eip712SignRequestSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml ================================================ openapi: 3.0.0 info: version: 1.0.0 title: Watcher Info API description: | API specification of the Watcher's Informational Service Error codes are available in [html](https://github.com/omgnetwork/elixir-omg/blob/master/docs/api_specs/errors.md#error-codes-description) format. contact: name: OMG Network email: engineering@omg.network license: name: 'Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0' url: 'https://omg.network/' servers: - url: 'https://watcher-info.ropsten.v1.omg.network/' - url: 'http://localhost:7534/' tags: - name: Account description: Account related API. - name: Block description: Block related API. - name: Deposit description: Deposit related API. - name: Transaction description: Transaction related API. - name: Fees description: Fees related API. - name: Stats description: Stats related API. - name: Configuration description: Configuration related API. paths: /alarm.get: get: tags: - Alarm summary: 'Provides alarms related to system memory, cpu and storage and application specific alarms.' description: | **Note:** Service operator alarms. operationId: alarm_get responses: '200': description: System alarms content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: array items: anyOf: - type: object properties: ethereum_connection_error: type: object properties: node: type: string reporter: type: string - type: object properties: ethereum_stalled_sync: type: object properties: ethereum_height: type: integer minimum: 0 synced_at: type: string format: date-time - type: object properties: invalid_fee_source: type: object properties: node: type: string reporter: type: string - type: object properties: statsd_client_connection: type: object properties: node: type: string reporter: type: string - type: object properties: statsd_client_connection: type: array items: type: string default: [] - type: object properties: disk_almost_full: type: string example: data: - disk_almost_full: /dev/null ethereum_connection_error: {} ethereum_stalled_sync: {} system_memory_high_watermark: [] '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /configuration.get: get: tags: - Configuration summary: Provides configuration values description: | **Note:** Configuration values. operationId: configuration_get responses: '200': description: Configuration response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: object properties: deposit_finality_margin: type: integer format: int256 contract_semver: type: string exit_processor_sla_margin: type: integer format: int256 network: type: string example: data: - deposit_finality_margin: 10 contract_semver: 1.0.0.1+a1s29s8 exit_processor_sla_margin: 5520 network: RINKEBY '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /account.get_balance: post: tags: - Account summary: Returns the balance of each currency for the given account address. operationId: account_get_balance requestBody: description: HEX-encoded address of the account and pagination fields required: true content: application/json: schema: title: AddressBodySchema type: object properties: address: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' limit: 100 page: 2 responses: '200': description: Account balance successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: object properties: currency: type: string amount: type: integer format: int256 example: data: - currency: '0xbfdf85743ef16cfb1f8d4dd1dfc74c51dc496434' amount: 20 - currency: '0x0000000000000000000000000000000000000000' amount: 1000000000 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /account.get_utxos: post: tags: - Account summary: Gets all utxos belonging to the given address. operationId: account_get_utxos requestBody: description: HEX-encoded address of the account and pagination fields required: true content: application/json: schema: title: AddressBodySchema type: object properties: address: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' limit: 100 page: 2 responses: '200': description: Account utxos succcessful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 otype: type: integer format: int16 oindex: type: integer format: int8 utxo_pos: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string owner: type: string currency: type: string amount: type: integer format: int256 inserted_at: type: string updated_at: type: string data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 example: data: - amount: 10 blknum: 123000 creating_txhash: '0x2c499b95ccb6bf7b923049b32b03a613d30882a448102136e544b302119eb722' currency: '0x0000000000000000000000000000000000000000' inserted_at: '2020-02-10T12:07:32Z' oindex: 0 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' spending_txhash: null txindex: 111 updated_at: '2020-02-15T04:07:57Z' utxo_pos: 123000001110000 data_paging: page: 1 limit: 200 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /account.get_transactions: post: tags: - Account summary: Gets a list of transactions for given account address. operationId: account_get_transactions requestBody: description: 'Account address, block number and other criteria' required: true content: application/json: schema: title: GetAllTransactionsBodySchema type: object properties: address: type: string blknum: type: integer format: int64 txtypes: type: array items: type: integer metadata: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 end_datetime: type: integer format: int32 required: - limit example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txtypes: - 1 blknum: 68290000 limit: 100 page: 2 end_datetime: 1592476174 responses: '200': description: Transactions succcessful response content: application/json: schema: allOf: - description: The response schema for a successful list operation type: object properties: version: type: string success: type: boolean data: type: array items: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0+abcdefa success: true data: [] - type: object properties: data: type: array items: type: object properties: txindex: type: integer format: int16 txtype: type: integer format: int16 txhash: type: string metadata: type: string txbytes: type: string inserted_at: type: string updated_at: type: string block: type: object properties: blknum: type: integer format: int64 hash: type: string eth_height: type: integer format: int64 timestamp: type: integer format: int64 tx_count: type: integer format: int64 inserted_at: type: string updated_at: type: string inputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string outputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 example: data: - block: timestamp: 1540365586 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 blknum: 68290000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' txindex: 0 txtype: 1 txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' metadata: '0x00000000000000000000000000000000000000000000000000000048656c6c6f' txbytes: 0x5df13a6bee20000... inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' inputs: - blknum: 1000 txindex: 111 otype: 1 oindex: 0 utxo_pos: 1000001110000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' amount: 20000000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' outputs: - blknum: 68290000 txindex: 5113 otype: 1 oindex: 0 utxo_pos: 68290000051130000 owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' currency: '0x0000000000000000000000000000000000000000' amount: 15000000 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' - blknum: 68290000 txindex: 5113 otype: 1 oindex: 1 utxo_pos: 68290000051130000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 5000000 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' data_paging: page: 1 limit: 200 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /block.get: post: tags: - Block summary: Retrieves a single block for the given block number. description: | Intended for operations requiring info for a specific block only. Returns a single block object for the given block number. The returned object includes transaction count but not associated transactions - for which you can use transaction.all with blknum in the request body. operationId: block_get requestBody: description: Block number required: true content: application/json: schema: title: GetBlockBodySchema type: object properties: blknum: type: integer format: int64 example: blknum: 68290000 responses: '200': description: Block succcessful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: blknum: type: integer format: int64 hash: type: string eth_height: type: integer format: int64 timestamp: type: integer format: int64 tx_count: type: integer format: int64 inserted_at: type: string updated_at: type: string example: data: timestamp: 1540365586 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 blknum: 68290000 tx_count: 2 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /block.all: post: tags: - Block summary: Gets all blocks (can be limited with various filters). description: | Returns a list of blocks ordered by the highest block number first. Intended to be used for presenting an overview of most recent blocks. Note: Due to eventual consistency nature of the informational API, blocks may later than deposits, exits, etc. operationId: block_all requestBody: description: The supported request parameters for /block.all content: application/json: schema: title: AllBlocksBodySchema type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: page: 2 limit: 100 responses: '200': description: Blocks succcessful response content: application/json: schema: allOf: - description: The response schema for a successful list operation type: object properties: version: type: string success: type: boolean data: type: array items: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0+abcdefa success: true data: [] - type: object properties: data: type: array items: type: object properties: blknum: type: integer format: int64 hash: type: string eth_height: type: integer format: int64 timestamp: type: integer format: int64 tx_count: type: integer format: int64 inserted_at: type: string updated_at: type: string data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: data: - blknum: 68290000 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 timestamp: 1540365586 tx_count: 2 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' data_paging: page: 1 limit: 100 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /deposit.all: post: tags: - Deposit summary: Gets a paginated list of deposit for the given address. description: | Returns a list of deposits ordered by Ethereum height in descending order for the given address. operationId: deposit_all requestBody: description: The supported request parameters for /deposit.all. The "limit" and "page" parameters are optional. required: true content: application/json: schema: title: AllDepositsBodySchema type: object properties: address: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: page: 2 limit: 100 address: '0xb01cb6f56d798a62d1e0bace406c73a122c39c9d' responses: '200': description: /deposit.all succcessful response content: application/json: schema: allOf: - description: The response schema for a successful list operation type: object properties: version: type: string success: type: boolean data: type: array items: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0+abcdefa success: true data: [] - type: object properties: data: type: array items: type: object properties: event_type: type: string root_chain_txhash: type: string log_index: type: integer eth_height: type: integer format: int64 inserted_at: type: string updated_at: type: string txoutputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 100 example: data: - event_type: deposit inserted_at: '2020-05-15T12:37:40Z' eth_height: 168637 log_index: 0 root_chain_txhash: '0x63c056f122f5bf30bf8119ec0a2184b73f975951229995a427ea58d904eaab85' txoutputs: - blknum: 1 txindex: 0 otype: 1 oindex: 0 utxo_pos: 1000000000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' creating_txhash: null spending_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' amount: 20000000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-10T12:07:32Z' data_paging: page: 1 limit: 100 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /transaction.all: post: tags: - Transaction summary: Gets all transactions (can be limited with various filters). description: | Digests the details of the transaction, by listing the value of outputs, aggregated by currency. Intended to be used when presenting the little details about multiple transactions. For all details queries to `/transaction.get` should be made using the transaction's hash provided. operationId: transactions_all requestBody: description: 'Account address, block number and other criteria' required: true content: application/json: schema: title: GetAllTransactionsBodySchema type: object properties: address: type: string blknum: type: integer format: int64 txtypes: type: array items: type: integer metadata: type: string page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 end_datetime: type: integer format: int32 required: - limit example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txtypes: - 1 blknum: 68290000 limit: 100 page: 2 end_datetime: 1592476174 responses: '200': description: Transactions succcessful response content: application/json: schema: allOf: - description: The response schema for a successful list operation type: object properties: version: type: string success: type: boolean data: type: array items: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0+abcdefa success: true data: [] - type: object properties: data: type: array items: type: object properties: txindex: type: integer format: int16 txtype: type: integer format: int16 txhash: type: string metadata: type: string txbytes: type: string inserted_at: type: string updated_at: type: string block: type: object properties: blknum: type: integer format: int64 hash: type: string eth_height: type: integer format: int64 timestamp: type: integer format: int64 tx_count: type: integer format: int64 inserted_at: type: string updated_at: type: string inputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string outputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string data_paging: type: object properties: page: type: integer format: int32 default: 1 limit: type: integer format: int32 default: 200 example: data: - block: timestamp: 1540365586 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 blknum: 68290000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' txindex: 0 txtype: 1 txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' metadata: '0x00000000000000000000000000000000000000000000000000000048656c6c6f' txbytes: 0x5df13a6bee20000... inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' inputs: - blknum: 1000 txindex: 111 otype: 1 oindex: 0 utxo_pos: 1000001110000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' amount: 20000000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' outputs: - blknum: 68290000 txindex: 5113 otype: 1 oindex: 0 utxo_pos: 68290000051130000 owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' currency: '0x0000000000000000000000000000000000000000' amount: 15000000 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' - blknum: 68290000 txindex: 5113 otype: 1 oindex: 1 utxo_pos: 68290000051130000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 5000000 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' data_paging: page: 1 limit: 200 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /transaction.create: post: tags: - Transaction summary: Finds an optimal way to construct a transaction spending particular amount. description: | Given token, amount and spender, finds spender's inputs sufficient to perform a payment. If also provided with receiver's address, creates and encodes a transaction. operationId: createTransaction requestBody: description: The description of transaction to be crafted. required: true content: application/json: schema: title: CreateTransactionsBodySchema type: object properties: owner: type: string payments: type: array items: type: object properties: amount: type: integer format: int256 currency: type: string owner: type: string required: - amount - currency fee: type: object properties: currency: type: string required: - currency metadata: type: string required: - owner - payments - fee example: owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' payments: - amount: 100 currency: '0x0000000000000000000000000000000000000000' owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' fee: currency: '0x0000000000000000000000000000000000000000' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' responses: '200': description: Transaction create successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: result: type: string enum: - complete - intermediate transactions: type: array items: type: object properties: inputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string outputs: type: array items: type: object properties: amount: type: integer format: int256 currency: type: string owner: type: string fee: type: object properties: amount: type: integer format: int256 currency: type: string metadata: type: string txbytes: type: string sign_hash: type: string typed_data: type: object properties: types: type: object properties: EIP712Domain: type: array items: type: object properties: name: type: string type: type: string additionalProperties: type: array items: type: object properties: name: type: string type: type: string required: - name - type primaryType: type: string domain: type: object properties: name: type: string salt: type: string verifyingContract: type: string version: type: string required: - name - salt - verifyingContract - version message: type: object properties: input0: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input1: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input2: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input3: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 output0: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output1: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output2: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output3: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 metadata: type: string required: - input0 - input1 - input2 - input3 - output0 - output1 - output2 - output3 - metadata example: data: result: complete transactions: - inputs: - blknum: 123000 txindex: 111 oindex: 0 utxo_pos: 123000001110000 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 50 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null - blknum: 277000 txindex: 2340 oindex: 3 utxo_pos: 277000023400003 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 75 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null outputs: - amount: 100 currency: '0x0000000000000000000000000000000000000000' owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' - amount: 20 currency: '0x0000000000000000000000000000000000000000' owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' fee: amount: 5 currency: '0x0000000000000000000000000000000000000000' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txbytes: 0x5df13a6bee20000... sign_hash: '0x7851b951edb0b9e88f0fc80e83461f71d0f4b1d4e44fae7d25a5d4ab6adc5d3d' typed_data: types: EIP712Domain: - name: name type: string - name: version type: string - name: verifyingContract type: address - name: salt type: bytes32 Transaction: - name: input0 type: Input - name: input1 type: Input - name: input2 type: Input - name: input3 type: Input - name: output0 type: Output - name: output1 type: Output - name: output2 type: Output - name: output3 type: Output - name: metadata type: bytes32 Input: - name: blknum type: uint256 - name: txindex type: uint256 - name: oindex type: uint256 Output: - name: owner type: address - name: currency type: address - name: amount type: uint256 primaryType: Transaction domain: name: OMG Network salt: '0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83' verifyingContract: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' version: '1' message: input0: blknum: 123000 txindex: 111 oindex: 0 input1: blknum: 277000 txindex: 2340 oindex: 3 input2: blknum: 0 txindex: 0 oindex: 0 input3: blknum: 0 txindex: 0 oindex: 0 output0: owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' currency: '0x0000000000000000000000000000000000000000' amount: 100 output1: owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 20 output2: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output3: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /transaction.merge: post: tags: - Transaction summary: Constructs merge transactions. description: "- Given address and currency parameters, returns a list of merge transactions for correspondng \nUTXOs –\_grouped in ascending order of value. \n- Given between two and four UTXO positions, returns a merge transaction for the corresponding\nUTXOs. (These UTXOs must have the same owner and currency)\n" operationId: mergeTransaction requestBody: description: The description of merge transaction to be crafted. content: application/json: schema: title: MergeTransactionsBodySchema type: object properties: address: type: string currency: type: string utxo_positions: type: array items: type: string examples: UTXO positions: value: utxo_positions: - '811000000000001' - '811000000000002' address and currency: value: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' responses: '200': description: Transaction merge successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: transactions: type: array items: type: object properties: inputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string outputs: type: array items: type: object properties: amount: type: integer format: int256 currency: type: string owner: type: string fee: type: object properties: amount: type: integer format: int256 currency: type: string metadata: type: string txbytes: type: string sign_hash: type: string typed_data: type: object properties: types: type: object properties: EIP712Domain: type: array items: type: object properties: name: type: string type: type: string additionalProperties: type: array items: type: object properties: name: type: string type: type: string required: - name - type primaryType: type: string domain: type: object properties: name: type: string salt: type: string verifyingContract: type: string version: type: string required: - name - salt - verifyingContract - version message: type: object properties: input0: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input1: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input2: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input3: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 output0: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output1: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output2: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output3: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 metadata: type: string required: - input0 - input1 - input2 - input3 - output0 - output1 - output2 - output3 - metadata example: data: result: complete transactions: - inputs: - blknum: 123000 txindex: 111 oindex: 0 utxo_pos: 123000001110000 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 50 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null - blknum: 277000 txindex: 2340 oindex: 3 utxo_pos: 277000023400003 otype: 1 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 50 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: null outputs: - amount: 100 currency: '0x0000000000000000000000000000000000000000' owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' fee: amount: 0 currency: '0x0000000000000000000000000000000000000000' metadata: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txbytes: 0x5df13a6bee20000... sign_hash: '0x7851b951edb0b9e88f0fc80e83461f71d0f4b1d4e44fae7d25a5d4ab6adc5d3d' typed_data: types: EIP712Domain: - name: name type: string - name: version type: string - name: verifyingContract type: address - name: salt type: bytes32 Transaction: - name: input0 type: Input - name: input1 type: Input - name: input2 type: Input - name: input3 type: Input - name: output0 type: Output - name: output1 type: Output - name: output2 type: Output - name: output3 type: Output - name: metadata type: bytes32 Input: - name: blknum type: uint256 - name: txindex type: uint256 - name: oindex type: uint256 Output: - name: owner type: address - name: currency type: address - name: amount type: uint256 primaryType: Transaction domain: name: OMG Network salt: '0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83' verifyingContract: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' version: '1' message: input0: blknum: 123000 txindex: 111 oindex: 0 input1: blknum: 277000 txindex: 2340 oindex: 3 input2: blknum: 0 txindex: 0 oindex: 0 input3: blknum: 0 txindex: 0 oindex: 0 output0: owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 100 output1: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output2: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output3: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 metadata: '0x0000000000000000000000000000000000000000000000000000000000000000' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /transaction.get: post: tags: - Transaction summary: Gets a transaction with the given id. operationId: transaction_get requestBody: description: Id (hash) of the transaction required: true content: application/json: schema: title: GetTransactionBodySchema type: object properties: id: type: string required: - id example: id: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' responses: '200': description: Transaction details succcessful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: txindex: type: integer format: int16 txtype: type: integer format: int16 txhash: type: string metadata: type: string txbytes: type: string inserted_at: type: string updated_at: type: string block: type: object properties: blknum: type: integer format: int64 hash: type: string eth_height: type: integer format: int64 timestamp: type: integer format: int64 tx_count: type: integer format: int64 inserted_at: type: string updated_at: type: string inputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string outputs: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 otype: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 creating_txhash: type: string spending_txhash: type: string inserted_at: type: string updated_at: type: string example: data: txindex: 5113 txtype: 1 txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' metadata: '0x00000000000000000000000000000000000000000000000000000048656c6c6f' txbytes: 0x5df13a6bee20000... inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' block: timestamp: 1540365586 hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' eth_height: 97424 blknum: 68290000 inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' inputs: - blknum: 1000 txindex: 111 oindex: 0 otype: 1 utxo_pos: 1000001110000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 10 creating_txhash: '0x40d65df1c3b1156d813d6bf96d5bd3b5bcf6e6588fc18c2a2ba564c6a64d4320' spending_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' outputs: - blknum: 68290000 txindex: 5113 oindex: 0 otype: 1 utxo_pos: 68290000051130000 owner: '0xae8ae48796090ba693af60b5ea6be3686206523b' currency: '0x0000000000000000000000000000000000000000' amount: 2 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' - blknum: 68290000 txindex: 5113 oindex: 1 otype: 1 utxo_pos: 68290000051130000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 7 creating_txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' spending_txhash: null inserted_at: '2020-02-10T12:07:32Z' updated_at: '2020-02-15T04:07:57Z' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /transaction.submit_typed: post: tags: - Transaction summary: Sends EIP-712 formatted transaction to Child chain. description: | Request to this method is the same as to Web3 `eth_signTypedData` with additional `signatures` array. The `/transaction.create` `typed_data` field can be used to prepare transaction. The same conditions are met as with security-critical `/transaction.submit` operationId: submit_typed requestBody: description: Transaction as for `eth_signTypedData` along with signatures required: true content: application/json: schema: title: TransactionSubmitTypedBodySchema allOf: - type: object properties: types: type: object properties: EIP712Domain: type: array items: type: object properties: name: type: string type: type: string additionalProperties: type: array items: type: object properties: name: type: string type: type: string required: - name - type primaryType: type: string domain: type: object properties: name: type: string salt: type: string verifyingContract: type: string version: type: string required: - name - salt - verifyingContract - version message: type: object properties: input0: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input1: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input2: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 input3: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 oindex: type: integer format: int8 output0: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output1: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output2: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 output3: type: object properties: owner: type: string currency: type: string amount: type: integer format: int256 metadata: type: string required: - input0 - input1 - input2 - input3 - output0 - output1 - output2 - output3 - metadata - type: object properties: signatures: type: array items: type: string required: - domain - message - signatures example: domain: name: OMG Network salt: '0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83' verifyingContract: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' version: '1' message: input0: blknum: 1 txindex: 0 oindex: 0 input1: blknum: 1000 txindex: 1 oindex: 1 input2: blknum: 0 txindex: 0 oindex: 0 input3: blknum: 0 txindex: 0 oindex: 0 output0: owner: '0x0527a37aa7081efcf405bd7c8fe36b01e91df27d' currency: '0x0000000000000000000000000000000000000000' amount: 100 output1: owner: '0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837' currency: '0x0000000000000000000000000000000000000000' amount: 10 output2: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 output3: owner: '0x0000000000000000000000000000000000000000' currency: '0x0000000000000000000000000000000000000000' amount: 0 metadata: '0x0000000000000000000000000000000000000000000000000000000000000000' signatures: - 0x6bfb9b2dbe32... responses: '200': description: Transaction submission successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 txhash: type: string example: data: blknum: 123000 txindex: 111 txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /fees.all: post: tags: - Fees summary: This endpoint retrieves the list of fee tokens currently supported by the childchain and the current amount needed to perform a transaction. operationId: fees_all requestBody: description: 'An optional array of currencies to filter, raises an error if one of the currencies is not supported.' required: false content: application/json: schema: title: FeesAllBodySchema type: object properties: currencies: type: array items: type: string tx_types: type: array items: type: integer example: currencies: - '0x0000000000000000000000000000000000000000' tx_types: - 1 responses: '200': description: List of all supported fees response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object additionalProperties: type: array items: type: object properties: currency: type: string amount: type: integer format: int256 subunit_to_unit: type: integer format: int256 pegged_currency: type: string pegged_amount: type: integer format: int256 pegged_subunit_to_unit: type: integer format: int256 updated_at: type: string format: date-time example: data: '1': - currency: '0x0000000000000000000000000000000000000000' amount: 220000000000000 subunit_to_unit: 1000000000000000000 pegged_currency: USD pegged_amount: 4 pegged_subunit_to_unit: 100 updated_at: '2019-01-01T10:10:10+00:00' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /stats.get: post: tags: - Stats summary: Retrieves network statistics description: | Retrieves transaction count, block count and average block interval, both for all time and the last 24 hours. operationId: stats_get responses: '200': description: Stats Successful Response content: application/json: schema: allOf: - description: The response schema for a successful list operation type: object properties: version: type: string success: type: boolean data: type: array items: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0+abcdefa success: true data: [] - type: object properties: data: type: object items: type: object properties: transaction_count: type: object properties: all_time: type: integer format: int64 last_24_hours: type: integer format: int64 block_count: type: object properties: all_time: type: integer format: int64 last_24_hours: type: integer format: int64 average_block_interval: type: object properties: all_time: type: number format: int64 last_24_hours: type: number format: int64 example: data: transaction_count: all_time: 4 last_24_hours: 2 block_count: all_time: 2 last_24_hours: 1 average_block_interval: all_time: 100 last_24_hours: null '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher_info version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/paths.yaml ================================================ account.get_exitable_utxos: post: tags: - Account summary: Gets all utxos belonging to the given address. description: > **Note:** this is a performance intensive call and should only be used if the chain is byzantine and the user needs to retrieve utxo information to be able to exit. Normally an application should use the Informational API's [Account - Get Utxos](http://TODO) instead. This version is provided in case the Informational API is not available. operationId: account_get_exitable_utxos requestBody: $ref: 'request_bodies.yaml#/AddressBodySchema' responses: 200: $ref: 'responses.yaml#/AccountUtxoResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/request_bodies.yaml ================================================ AddressBodySchema: description: HEX-encoded address of the account required: true content: application/json: schema: title: 'AddressBodySchema' type: object properties: address: type: string required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/response_schemas.yaml ================================================ AccountUtxoResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/AccountUtxoSchema' example: data: - blknum: 123000 txindex: 111 oindex: 0 otype: 1 utxo_pos: 123000001110000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 10 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/responses.yaml ================================================ AccountUtxoResponse: description: Account utxos succcessful response content: application/json: schema: $ref: 'response_schemas.yaml#/AccountUtxoResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/schemas.yaml ================================================ AccountUtxoSchema: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 otype: type: integer format: int16 oindex: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/alarm/alarms_schema.yml ================================================ components: schemas: ethereum_connection_error: type: object properties: ethereum_connection_error: type: object properties: node: type: string reporter: type: string ethereum_stalled_sync: type: object properties: ethereum_stalled_sync: type: object properties: ethereum_height: type: integer minimum: 0 synced_at: type: string format: date-time invalid_fee_source: type: object properties: invalid_fee_source: type: object properties: node: type: string reporter: type: string statsd_client_connection: type: object properties: statsd_client_connection: type: object properties: node: type: string reporter: type: string system_memory_high_watermark: type: object properties: statsd_client_connection: type: array items: type: string default: [] disk_almost_full: type: object properties: disk_almost_full: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/alarm/paths.yaml ================================================ alarm.get: get: tags: - Alarm summary: Provides alarms related to system memory, cpu and storage and application specific alarms. description: > **Note:** Service operator alarms. operationId: alarm_get responses: 200: $ref: 'responses.yaml#/AlarmResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/alarm/response_schemas.yaml ================================================ AlarmResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/AlarmSchema' example: data: - disk_almost_full: "/dev/null" ethereum_connection_error: {} ethereum_stalled_sync: {} system_memory_high_watermark: [] ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/alarm/responses.yaml ================================================ AlarmResponse: description: System alarms content: application/json: schema: $ref: 'response_schemas.yaml#/AlarmResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/alarm/schemas.yaml ================================================ AlarmSchema: type: array items: anyOf: - $ref: 'alarms_schema.yml#/components/schemas/ethereum_connection_error' - $ref: 'alarms_schema.yml#/components/schemas/ethereum_stalled_sync' - $ref: 'alarms_schema.yml#/components/schemas/invalid_fee_source' - $ref: 'alarms_schema.yml#/components/schemas/statsd_client_connection' - $ref: 'alarms_schema.yml#/components/schemas/system_memory_high_watermark' - $ref: 'alarms_schema.yml#/components/schemas/disk_almost_full' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/batch_transaction/paths.yaml ================================================ transaction.batch_submit: post: tags: - Transaction summary: This endpoint submits an array of signed transaction to the child chain. description: > Normally you should call the Watcher's Transaction - Submit instead of this. The Watcher's version performs various security and validation checks (TO DO) before submitting the transaction, so is much safer. However, if the Watcher is not available this version exists. operationId: batch_submit requestBody: $ref: 'request_bodies.yaml#/TransactionBatchSubmitBodySchema' responses: 200: $ref: 'responses.yaml#/TransactionBatchSubmitResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/batch_transaction/request_bodies.yaml ================================================ TransactionBatchSubmitBodySchema: description: Array of signed transactions, RLP-encoded to bytes, and HEX-encoded to string required: true content: application/json: schema: title: 'TransactionBatchSubmitBodySchema' type: object properties: transactions: type: array items: type: string required: - transactions example: transactions: ['0xf8d083015ba98080808080940000...', '0xf8d083a15ba98080808080920000...'] ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/batch_transaction/response_schemas.yaml ================================================ TransactionBatchSubmitResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: array $ref: 'schemas.yaml#/TransactionBatchSubmitSchema ' example: data: - blknum: 123000 txindex: 111 txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/batch_transaction/responses.yaml ================================================ TransactionBatchSubmitResponse: description: Transaction batch submission successful response content: application/json: schema: $ref: 'response_schemas.yaml#/TransactionBatchSubmitResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/batch_transaction/schemas.yaml ================================================ TransactionBatchSubmitSchema: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 txhash: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/paths.yaml ================================================ block.validate: post: tags: - Block summary: Verifies the stateless validity of a block. description: | - Verifies that given Merkle root matches reconstructed Merkle root. - Verifies that (payment and fee) transactions are correctly formed. - Verifies that there are no duplicate inputs at the block level. - Verifies that the number of transactions falls within the accepted range. - Verifies that fee transactions are correctly placed and unique per currency. operationId: validate requestBody: $ref: 'request_bodies.yaml#/BlockValidateBodySchema' responses: 200: $ref: 'responses.yaml#/BlockValidateResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/request_bodies.yaml ================================================ BlockValidateBodySchema: description: Block object with a hash, number and array of hexadecimal transaction bytes. required: true content: application/json: schema: title: 'BlockValidateBodySchema' type: object properties: hash: type: string transactions: type: array items: type: string number: type: integer required: - hash - transactions - number example: number: 1000 hash: '0xf8d083015ba98080808080940000...' transactions: ["0xf8c0f843b841fc6dbf49a4baa783ec576291f6083be5ea...", "0xf852c003eeed02eb94916f3753bd53e124d6d565ef1701..." ] ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/response_schemas.yaml ================================================ BlockValidateResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/BlockValidateSchema' example: data: valid: false ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/responses.yaml ================================================ BlockValidateResponse: description: Successful response to calling /block.validate content: application/json: schema: $ref: 'response_schemas.yaml#/BlockValidateResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/schemas.yaml ================================================ BlockValidateSchema: type: object properties: valid: type: boolean ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/configuration/configuration_schema.yml ================================================ components: schemas: deposit_finality_margin: type: integer contract_semver: type: string exit_processor_sla_margin: type: integer network: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/configuration/paths.yaml ================================================ configuration.get: get: tags: - Configuration summary: Provides configuration values description: > **Note:** Configuration values. operationId: configuration_get responses: 200: $ref: 'responses.yaml#/ConfigurationResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/configuration/response_schemas.yaml ================================================ ConfigurationResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: array items: $ref: 'schemas.yaml#/ConfigurationSchema' example: data: - deposit_finality_margin: 10 contract_semver: "1.0.0.1+a1s29s8" ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/configuration/responses.yaml ================================================ ConfigurationResponse: description: Configuration response content: application/json: schema: $ref: 'response_schemas.yaml#/ConfigurationResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/configuration/schemas.yaml ================================================ ConfigurationSchema: type: object properties: deposit_finality_margin: type: integer format: int256 contract_semver: type: string exit_processor_sla_margin: type: integer format: int256 network: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/in_flight_exit/paths.yaml ================================================ in_flight_exit.get_data: post: tags: - InFlightExit summary: Gets exit data for an in-flight exit. description: > Exit data are arguments to `startInFlightExit` root chain contract function. operationId: in_flight_exit_get_data requestBody: $ref: 'request_bodies.yaml#/InFlightExitTxBytesBodySchema' responses: 200: $ref: 'responses.yaml#/GetInFlightExitDataResponse' 500: $ref: '../responses.yaml#/InternalServerError' in_flight_exit.get_competitor: post: tags: - InFlightExit summary: Returns a competitor to an in-flight exit. description: Note that if the competing transaction has not been put into a block `competing_tx_pos` and `competing_proof` will not be returned. operationId: in_flight_exit_get_competitor requestBody: $ref: 'request_bodies.yaml#/InFlightExitTxBytesBodySchema' responses: 200: $ref: 'responses.yaml#/GetCompetitorResponse' 500: $ref: '../responses.yaml#/InternalServerError' in_flight_exit.prove_canonical: post: tags: - InFlightExit summary: Proves transaction is canonical. description: To respond to a challenge to an in-flight exit, this proves that the transaction has been put into a block (and therefore is canonical). operationId: in_flight_exit_prove_canonical requestBody: $ref: 'request_bodies.yaml#/InFlightExitTxBytesBodySchema' responses: 200: $ref: 'responses.yaml#/ProveCanonicalResponse' 500: $ref: '../responses.yaml#/InternalServerError' in_flight_exit.get_input_challenge_data: post: tags: - InFlightExit summary: Gets the data to challenge an invalid input piggybacked on an in-flight exit. description: To respond to invalid piggybacked input in non-canonical in-flight transaction provides data needed to challenge it, e.g. transaction that spent this input and signature. operationId: in_flight_exit_get_input_challenge_data requestBody: $ref: 'request_bodies.yaml#/InFlightExitInputChallengeDataBodySchema' responses: 200: $ref: 'responses.yaml#/InputChallengeDataResponse' 500: $ref: '../responses.yaml#/InternalServerError' in_flight_exit.get_output_challenge_data: post: tags: - InFlightExit summary: Gets the data to challenge an invalid output piggybacked on an in-flight exit. description: To respond to invalid piggybacked output in canonical in-flight transaction provides data needed to challenge it, e.g. in-flight transaction inclusion proof, transaction that spent this output and signature. operationId: in_flight_exit_get_output_challenge_data requestBody: $ref: 'request_bodies.yaml#/InFlightExitOutputChallengeDataBodySchema' responses: 200: $ref: 'responses.yaml#/OutputChallengeDataResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/in_flight_exit/request_bodies.yaml ================================================ InFlightExitTxBytesBodySchema: description: In-flight transaction bytes body required: true content: application/json: schema: title: 'InFlightExitTxBytesBodySchema' type: object properties: txbytes: type: string required: - txbytes example: txbytes: '0xf3170101c0940000...' InFlightExitInputChallengeDataBodySchema: description: In-flight transaction bytes and invalid input index required: true content: application/json: schema: title: 'InFlightExitInputChallengeDataBodySchema' type: object properties: txbytes: type: string input_index: type: integer format: int8 required: - txbytes - input_index example: txbytes: '0xf3170101c0940000...' input_index: 1 InFlightExitOutputChallengeDataBodySchema: description: In-flight transaction bytes and invalid output index required: true content: application/json: schema: title: 'InFlightExitOutputChallengeDataBodySchema' type: object properties: txbytes: type: string output_index: type: integer format: int8 required: - txbytes - output_index example: txbytes: '0xf3170101c0940000...' output_index: 0 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/in_flight_exit/response_schemas.yaml ================================================ GetInFlightExitDataResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/InFlightExitDataSchema' example: data: in_flight_tx: '0xf3170101c0940000...' input_txs: - '0xa3470101c0940000...' input_txs_inclusion_proofs: - '0xcedb8b31d1e4...' in_flight_tx_sigs: - '0x6bfb9b2dbe32...' input_utxos_pos: - 300010002001 GetCompetitorResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/CompetitorSchema' example: data: in_flight_txbytes: '0xf3170101c0940000...' in_flight_input_index: 1 competing_txbytes: '0x5df13a6bee20000...' competing_input_index: 1 competing_sig: '0xa3470101c0940000...' competing_tx_pos: 26000003920000 competing_proof: '0xcedb8b31d1e4...' input_tx: '0xaaa70101c0940000...' input_utxo_pos: 300010002001 ProveCanonicalResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/ProveCanonicalSchema' example: data: in_flight_txbytes: '0xf3170101c0940000...' in_flight_tx_pos: 26000003920000 in_flight_proof: '0xcedb8b31d1e4...' InputChallengeDataResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/InputChallengeDataSchema' example: data: in_flight_txbytes: '0xf3170101c0940000...' in_flight_input_index: 1 spending_txbytes: '0x5df13a6bee20000...' spending_input_index: 1 spending_sig: '0xa3470101c0940000...' input_tx: '0xaaa70101c0940000...' input_utxo_pos: 300010002001 OutputChallengeDataResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/OutputChallengeDataSchema' example: data: in_flight_txbytes: '0xf3170101c0940000...' in_flight_output_pos: 21000634002 in_flight_proof: '0xcedb8b31d1e4...' spending_txbytes: '0x5df13a6bee20000...' spending_input_index: 1 spending_sig: '0xa3470101c0940000...' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/in_flight_exit/responses.yaml ================================================ GetInFlightExitDataResponse: description: Get in-flight exit successful response content: application/json: schema: $ref: 'response_schemas.yaml#/GetInFlightExitDataResponseSchema' GetCompetitorResponse: description: Get competitor successful response content: application/json: schema: $ref: 'response_schemas.yaml#/GetCompetitorResponseSchema' ProveCanonicalResponse: description: Prove canonical successful response content: application/json: schema: $ref: 'response_schemas.yaml#/ProveCanonicalResponseSchema' InputChallengeDataResponse: description: Get input challenge successful response content: application/json: schema: $ref: 'response_schemas.yaml#/InputChallengeDataResponseSchema' OutputChallengeDataResponse: description: Get output challenge successful response content: application/json: schema: $ref: 'response_schemas.yaml#/OutputChallengeDataResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/in_flight_exit/schemas.yaml ================================================ InFlightExitDataSchema: description: The object schema for an in flight exit properties: in_flight_tx: type: string input_txs: type: array items: type: string input_txs_inclusion_proofs: type: array items: type: string in_flight_tx_sigs: type: array items: type: string input_utxos_pos: type: array items: type: integer format: int256 CompetitorSchema: description: The object schema for a competitor properties: in_flight_txbytes: type: string in_flight_input_index: type: integer format: int8 competing_txbytes: type: string competing_input_index: type: integer format: int8 competing_sig: type: string competing_tx_pos: type: integer format: int256 competing_proof: type: string input_tx: type: string input_utxo_pos: type: integer format: int256 ProveCanonicalSchema: description: The object schema for a canonical proof properties: in_flight_txbytes: type: string in_flight_tx_pos: type: integer format: int256 in_flight_proof: type: string InputChallengeDataSchema: description: The object schema for an input challenge data properties: in_flight_txbytes: type: string in_flight_input_index: type: integer format: int8 spending_txbytes: type: string spending_input_index: type: integer format: int8 spending_sig: type: string input_tx: type: string input_utxo_pos: type: integer format: int256 OutputChallengeDataSchema: description: The object schema for an output challenge data properties: in_flight_txbytes: type: string in_flight_output_pos: type: integer format: int256 in_flight_proof: type: string spending_txbytes: type: string spending_input_index: type: integer format: int8 spending_sig: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/response_schemas.yaml ================================================ WatcherBaseResponseSchema: description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: '1.0.0+abcdefa' success: true data: {} WatcherBaseListResponseSchema: description: The response schema for a successful list operation type: object properties: version: type: string success: type: boolean data: type: array items: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: '1.0+abcdefa' success: true data: [] WatcherErrorResponseSchema: description: The response schema for an error allOf: - $ref: 'response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: $ref: '../shared/schemas.yaml#/ErrorSchema' required: - data example: success: false data: object: error code: server:internal_server_error description: Something went wrong on the server messages: {error_key: error_reason} ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/responses.yaml ================================================ InternalServerError: description: Returns an internal server error content: application/json: schema: $ref: 'response_schemas.yaml#/WatcherErrorResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/status/byzantine_events_schema.yml ================================================ components: schemas: invalid_exit: type: object properties: event: type: string enum: [invalid_exit] details: type: object properties: eth_height: type: integer utxo_pos: type: integer owner: type: string currency: type: string amount: type: integer root_chain_txhash: type: string spending_txhash: type: string scheduled_finalization_time: type: integer unchallenged_exit: type: object properties: event: type: string enum: [unchallenged_exit] details: type: object properties: eth_height: type: integer utxo_pos: type: integer owner: type: string currency: type: string amount: type: integer root_chain_txhash: type: string spending_txhash: type: string scheduled_finalization_time: type: integer invalid_block: type: object properties: event: type: string enum: [invalid_block] details: type: object properties: blknum: type: integer blockhash: type: string error_type: type: string enum: [tx_execution, incorrect_hash] block_withholding: type: object properties: event: type: string enum: [block_withholding] details: type: object properties: hash: type: string blknum: type: string noncanonical_ife: type: object properties: event: type: string enum: [noncanonical_ife] details: type: object properties: txbytes: type: string invalid_ife_challenge: type: object properties: event: type: string enum: [invalid_ife_challenge] details: type: object properties: txbytes: type: string piggyback_available: type: object properties: event: type: string enum: [piggyback_available] details: type: object properties: txbytes: type: string available_outputs: type: array items: type: object properties: index: type: integer address: type: string available_inputs: type: array items: type: object properties: index: type: integer address: type: string invalid_piggyback: type: object properties: event: type: string enum: [invalid_piggyback] details: type: object properties: txbytes: type: string inputs: type: array items: type: integer outputs: type: array items: type: integer ethereum_stalled_sync: type: object properties: ethereum_stalled_sync: type: object properties: ethereum_height: type: integer minimum: 0 synced_at: type: string format: date-time ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/status/paths.yaml ================================================ status.get: post: tags: - Status summary: Returns information about the current state of the child chain and the watcher. description: > The most critical function of the Watcher is to monitor the ChildChain and report dishonest activity. The user must call the `/status.get` endpoint periodically to check. Any situation that requires the user to either exit or challenge an invalid exit will be included in the `byzantine_events` field. operationId: status_get responses: 200: $ref: 'responses.yaml#/StatusResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/status/request_bodies.yaml ================================================ ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/status/response_schemas.yaml ================================================ StatusResponseSchema: description: The response schema for a status allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/StatusSchema' example: data: last_validated_child_block_timestamp: 1558535130 last_validated_child_block_number: 10000 last_mined_child_block_timestamp: 1558535190 last_mined_child_block_number: 11000 last_seen_eth_block_timestamp: 1558535190 last_seen_eth_block_number: 4427041 contract_addr: plasma_framework: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' eth_syncing: true byzantine_events: - event: "invalid_exit" details: eth_height: 615440 utxo_pos: 10000000010000000 owner: "0xb3256026863eb6ae5b06fa396ab09069784ea8ea" currency: "0x0000000000000000000000000000000000000000" amount: 100 root_chain_txhash: "0xde8210dd179e4a067c5649ebeee8871e0f258fecbd1eb02e11db88121bb8de01" spending_txhash: "0x21aee8dcc74d6b309f6e98a967a6aa6002432f98a5bc13c75529dbe228e04451" scheduled_finalization_time: 1588144725 - event: "unchallenged_exit" details: eth_height: 615440 utxo_pos: 10000000010000000 owner: "0xb3256026863eb6ae5b06fa396ab09069784ea8ea" currency: "0x0000000000000000000000000000000000000000" amount: 100 root_chain_txhash: "0xde8210dd179e4a067c5649ebeee8871e0f258fecbd1eb02e11db88121bb8de01" spending_txhash: "0x21aee8dcc74d6b309f6e98a967a6aa6002432f98a5bc13c75529dbe228e04451" scheduled_finalization_time: 1588144725 - event: "invalid_block" details: blockhash: "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec" blknum: 10000 error_type: "tx_execution" - event: "block_withholding" details: hash: "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec" blknum: 10000 - event: "noncanonical_ife" details: txbytes: "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec" - event: "invalid_ife_challenge" details: txbytes: "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec" - event: "piggyback_available" details: txbytes: "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec" available_outputs: - index: 0 address: "0xb3256026863eb6ae5b06fa396ab09069784ea8ea" - index: 1, address: "0x488f85743ef16cfb1f8d4dd1dfc74c51dc496434" available_inputs: - index: 0 address: "0xb3256026863eb6ae5b06fa396ab09069784ea8ea" - event: "invalid_piggyback" details: txbytes: "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec" inputs: [1] outputs: [0] - event: "ethereum_stalled_sync" details: eth_height: 615440 synced_at: "2020-02-07T10:10:10+00:00" in_flight_txs: - txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' txbytes: '0x3eb6ae5b06f3...' input_addresses: - '0x1234...' ouput_addresses: - '0x1234...' - '0x7890...' in_flight_exits: - txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txbytes: '0xf3170101c094...' eth_height: 615441 piggybacked_inputs: - 1 piggybacked_outputs: - 0 - 1 services_synced_heights: - service: "block_getter" height: 4427041 - service: "challenges_responds_processor" height: 4427029 - service: "competitor_processor" height: 4427029 - service: "depositor" height: 4427031 - service: "exit_challenger" height: 4427029 - service: "exit_finalizer" height: 4427029 - service: "exit_processor" height: 4427029 - service: "ife_exit_finalizer" height: 4427029 - service: "in_flight_exit_processor" height: 4427029 - service: "piggyback_challenges_processor" height: 4427029 - service: "piggyback_processor" height: 4427029 - service: "root_chain_height" height: 4427041 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/status/responses.yaml ================================================ StatusResponse: description: Returns the status of the watcher content: application/json: schema: $ref: 'response_schemas.yaml#/StatusResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/status/schemas.yaml ================================================ StatusSchema: description: The object schema for a status type: object properties: last_validated_child_block_timestamp: type: integer format: int64 last_validated_child_block_number: type: integer format: int64 last_mined_child_block_timestamp: type: integer format: int64 last_mined_child_block_number: type: integer format: int64 last_seen_eth_block_timestamp: type: integer format: int64 last_seen_eth_block_number: type: integer format: int64 contract_addr: type: object additionalProperties: true properties: plasma_framework: type: string eth_syncing: type: boolean byzantine_events: type: array items: anyOf: - $ref: 'byzantine_events_schema.yml#/components/schemas/invalid_exit' - $ref: 'byzantine_events_schema.yml#/components/schemas/unchallenged_exit' - $ref: 'byzantine_events_schema.yml#/components/schemas/invalid_block' - $ref: 'byzantine_events_schema.yml#/components/schemas/block_withholding' - $ref: 'byzantine_events_schema.yml#/components/schemas/noncanonical_ife' - $ref: 'byzantine_events_schema.yml#/components/schemas/invalid_ife_challenge' - $ref: 'byzantine_events_schema.yml#/components/schemas/piggyback_available' - $ref: 'byzantine_events_schema.yml#/components/schemas/invalid_piggyback' - $ref: 'byzantine_events_schema.yml#/components/schemas/ethereum_stalled_sync' in_flight_txs: type: array items: type: object properties: txhash: type: string txbytes: type: string input_addresses: type: array items: type: string ouput_addresses: type: array items: type: string in_flight_exits: type: array items: type: object properties: txhash: type: string txbytes: type: string eth_height: type: integer piggybacked_inputs: type: array items: type: integer piggybacked_outputs: type: array items: type: integer services_synced_heights: type: array items: type: object properties: service: type: string height: type: integer format: int256 required: - last_validated_child_block_timestamp - last_validated_child_block_number - last_mined_child_block_timestamp - last_mined_child_block_number - last_seen_eth_block_timestamp - last_seen_eth_block_number - contract_addr - eth_syncing - byzantine_events - in_flight_txs - in_flight_exits ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/swagger.yaml ================================================ openapi: 3.0.0 info: version: '1.0.0' title: Watcher security-critical API description: > API specification of the Watcher's security-critical Service Error codes are available in [html](https://github.com/omgnetwork/elixir-omg/blob/master/docs/api_specs/errors.md#error-codes-description) format. contact: name: OMG Network email: engineering@omg.network license: name: 'Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0' url: 'https://omg.network/' servers: - url: https://watcher.ropsten.v1.omg.network/ - url: http://localhost:7434/ tags: - name: Status description: Status of the child chain. externalDocs: description: "Byzantine events description" url: "https://github.com/omgnetwork/elixir-omg/blob/master/docs/api_specs/status_events_specs.md#byzantine-events" - name: Account description: Account related API. - name: Block description: Block-related API - name: UTXO description: UTXO related API. - name: Transaction description: Transaction related API. - name: InFlightExit description: InFlightExit related API. paths: /alarm.get: $ref: 'alarm/paths.yaml#/alarm.get' /configuration.get: $ref: 'configuration/paths.yaml#/configuration.get' /status.get: $ref: 'status/paths.yaml#/status.get' /account.get_exitable_utxos: $ref: 'account/paths.yaml#/account.get_exitable_utxos' /block.validate: $ref: 'block/paths.yaml#/block.validate' /utxo.get_challenge_data: $ref: 'utxo/paths.yaml#/utxo.get_challenge_data' /utxo.get_exit_data: $ref: 'utxo/paths.yaml#/utxo.get_exit_data' /transaction.submit: $ref: 'transaction/paths.yaml#/transaction.submit' /transaction.batch_submit: $ref: 'batch_transaction/paths.yaml#/transaction.batch_submit' /in_flight_exit.get_data: $ref: 'in_flight_exit/paths.yaml#/in_flight_exit.get_data' /in_flight_exit.get_competitor: $ref: 'in_flight_exit/paths.yaml#/in_flight_exit.get_competitor' /in_flight_exit.prove_canonical: $ref: 'in_flight_exit/paths.yaml#/in_flight_exit.prove_canonical' /in_flight_exit.get_input_challenge_data: $ref: 'in_flight_exit/paths.yaml#/in_flight_exit.get_input_challenge_data' /in_flight_exit.get_output_challenge_data: $ref: 'in_flight_exit/paths.yaml#/in_flight_exit.get_output_challenge_data' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/transaction/paths.yaml ================================================ transaction.submit: post: tags: - Transaction summary: Sends transaction to Child chain. description: Watcher passes signed transaction to the child chain only if it's secure, e.g. Watcher is fully synced, all operator blocks have been verified, transaction doesn't spend funds not yet mined... operationId: submit requestBody: $ref: 'request_bodies.yaml#/TransactionSubmitBodySchema' responses: 200: $ref: 'responses.yaml#/TransactionSubmitResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/transaction/request_bodies.yaml ================================================ TransactionSubmitBodySchema: description: Signed transaction RLP-encoded to bytes and HEX-encoded to string required: true content: application/json: schema: title: 'TransactionSubmitBodySchema' type: object properties: transaction: type: string required: - transaction example: transaction: '0xf8d083015ba98080808080940000...' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/transaction/response_schemas.yaml ================================================ TransactionSubmitResponseSchema: allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/TransactionSubmitSchema' example: data: blknum: 123000 txindex: 111 txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/transaction/responses.yaml ================================================ TransactionSubmitResponse: description: Transaction submission successful response content: application/json: schema: $ref: 'response_schemas.yaml#/TransactionSubmitResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/transaction/schemas.yaml ================================================ TransactionSubmitSchema: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 txhash: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/utxo/paths.yaml ================================================ utxo.get_challenge_data: post: tags: - UTXO summary: Gets challenge data for a given utxo exit. description: Gets challenge data for a given utxo exit. operationId: utxo_get_challenge_data requestBody: $ref: 'request_bodies.yaml#/UtxoPositionBodySchema' responses: 200: $ref: 'responses.yaml#/GetUtxoChallengeResponse' 500: $ref: '../responses.yaml#/InternalServerError' utxo.get_exit_data: post: tags: - UTXO summary: Gets exit data for a given utxo. description: Gets exit data for a given utxo. operationId: utxo_get_exit_data requestBody: $ref: 'request_bodies.yaml#/UtxoPositionBodySchema' responses: 200: $ref: 'responses.yaml#/GetUtxoExitResponse' 500: $ref: '../responses.yaml#/InternalServerError' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/utxo/request_bodies.yaml ================================================ UtxoPositionBodySchema: description: Utxo position (encoded as single integer, the way contract represents them) required: true content: application/json: schema: title: 'UtxoPositionBodySchema' type: object properties: utxo_pos: type: integer format: int256 required: - utxo_pos example: utxo_pos: 10000000010000000 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/utxo/response_schemas.yaml ================================================ GetUtxoChallengeResponseSchema: description: The response schema for utxo challenge data allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/GetUtxoChallengeSchema' example: data: exit_id: 1717611893014159315373779059565546411346446754 input_index: 0 sig: '0x6bfb9b2dbe32...' txbytes: '0x3eb6ae5b06f3...' exiting_tx: '0x6d6bda6bd6d6...' GetUtxoExitResponseSchema: description: The response schema for utxo exit data allOf: - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' - type: object properties: data: type: object $ref: 'schemas.yaml#/GetUtxoExitSchema' example: data: proof: '0xcedb8b31d1e4...' txbytes: '0x3eb6ae5b06f3...' utxo_pos: 10000000010000000 ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/utxo/responses.yaml ================================================ GetUtxoChallengeResponse: description: Utxo challenge successful response content: application/json: schema: $ref: 'response_schemas.yaml#/GetUtxoChallengeResponseSchema' GetUtxoExitResponse: description: Utxo exit successful response content: application/json: schema: $ref: 'response_schemas.yaml#/GetUtxoExitResponseSchema' ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/utxo/schemas.yaml ================================================ GetUtxoChallengeSchema: type: object properties: exit_id: type: integer format: int192 input_index: type: integer format: int8 sig: type: string txbytes: type: string exiting_tx: type: string GetUtxoExitSchema: type: object properties: utxo_pos: type: integer format: int256 txbytes: type: string proof: type: string ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml ================================================ openapi: 3.0.0 info: version: 1.0.0 title: Watcher security-critical API description: | API specification of the Watcher's security-critical Service Error codes are available in [html](https://github.com/omgnetwork/elixir-omg/blob/master/docs/api_specs/errors.md#error-codes-description) format. contact: name: OMG Network email: engineering@omg.network license: name: 'Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0' url: 'https://omg.network/' servers: - url: 'https://watcher.ropsten.v1.omg.network/' - url: 'http://localhost:7434/' tags: - name: Status description: Status of the child chain. externalDocs: description: Byzantine events description url: 'https://github.com/omgnetwork/elixir-omg/blob/master/docs/api_specs/status_events_specs.md#byzantine-events' - name: Account description: Account related API. - name: Block description: Block-related API - name: UTXO description: UTXO related API. - name: Transaction description: Transaction related API. - name: InFlightExit description: InFlightExit related API. paths: /alarm.get: get: tags: - Alarm summary: 'Provides alarms related to system memory, cpu and storage and application specific alarms.' description: | **Note:** Service operator alarms. operationId: alarm_get responses: '200': description: System alarms content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: array items: anyOf: - type: object properties: ethereum_connection_error: type: object properties: node: type: string reporter: type: string - type: object properties: ethereum_stalled_sync: type: object properties: ethereum_height: type: integer minimum: 0 synced_at: type: string format: date-time - type: object properties: invalid_fee_source: type: object properties: node: type: string reporter: type: string - type: object properties: statsd_client_connection: type: object properties: node: type: string reporter: type: string - type: object properties: statsd_client_connection: type: array items: type: string default: [] - type: object properties: disk_almost_full: type: string example: data: - disk_almost_full: /dev/null ethereum_connection_error: {} ethereum_stalled_sync: {} system_memory_high_watermark: [] '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /configuration.get: get: tags: - Configuration summary: Provides configuration values description: | **Note:** Configuration values. operationId: configuration_get responses: '200': description: Configuration response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: object properties: deposit_finality_margin: type: integer format: int256 contract_semver: type: string exit_processor_sla_margin: type: integer format: int256 network: type: string example: data: - deposit_finality_margin: 10 contract_semver: 1.0.0.1+a1s29s8 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /status.get: post: tags: - Status summary: Returns information about the current state of the child chain and the watcher. description: | The most critical function of the Watcher is to monitor the ChildChain and report dishonest activity. The user must call the `/status.get` endpoint periodically to check. Any situation that requires the user to either exit or challenge an invalid exit will be included in the `byzantine_events` field. operationId: status_get responses: '200': description: Returns the status of the watcher content: application/json: schema: description: The response schema for a status allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object description: The object schema for a status properties: last_validated_child_block_timestamp: type: integer format: int64 last_validated_child_block_number: type: integer format: int64 last_mined_child_block_timestamp: type: integer format: int64 last_mined_child_block_number: type: integer format: int64 last_seen_eth_block_timestamp: type: integer format: int64 last_seen_eth_block_number: type: integer format: int64 contract_addr: type: object additionalProperties: true properties: plasma_framework: type: string eth_syncing: type: boolean byzantine_events: type: array items: anyOf: - type: object properties: event: type: string enum: - invalid_exit details: type: object properties: eth_height: type: integer utxo_pos: type: integer owner: type: string currency: type: string amount: type: integer root_chain_txhash: type: string spending_txhash: type: string scheduled_finalization_time: type: integer - type: object properties: event: type: string enum: - unchallenged_exit details: type: object properties: eth_height: type: integer utxo_pos: type: integer owner: type: string currency: type: string amount: type: integer root_chain_txhash: type: string spending_txhash: type: string scheduled_finalization_time: type: integer - type: object properties: event: type: string enum: - invalid_block details: type: object properties: blknum: type: integer blockhash: type: string error_type: type: string enum: - tx_execution - incorrect_hash - type: object properties: event: type: string enum: - block_withholding details: type: object properties: hash: type: string blknum: type: string - type: object properties: event: type: string enum: - noncanonical_ife details: type: object properties: txbytes: type: string - type: object properties: event: type: string enum: - invalid_ife_challenge details: type: object properties: txbytes: type: string - type: object properties: event: type: string enum: - piggyback_available details: type: object properties: txbytes: type: string available_outputs: type: array items: type: object properties: index: type: integer address: type: string available_inputs: type: array items: type: object properties: index: type: integer address: type: string - type: object properties: event: type: string enum: - invalid_piggyback details: type: object properties: txbytes: type: string inputs: type: array items: type: integer outputs: type: array items: type: integer - type: object properties: ethereum_stalled_sync: type: object properties: ethereum_height: type: integer minimum: 0 synced_at: type: string format: date-time in_flight_txs: type: array items: type: object properties: txhash: type: string txbytes: type: string input_addresses: type: array items: type: string ouput_addresses: type: array items: type: string in_flight_exits: type: array items: type: object properties: txhash: type: string txbytes: type: string eth_height: type: integer piggybacked_inputs: type: array items: type: integer piggybacked_outputs: type: array items: type: integer services_synced_heights: type: array items: type: object properties: service: type: string height: type: integer format: int256 required: - last_validated_child_block_timestamp - last_validated_child_block_number - last_mined_child_block_timestamp - last_mined_child_block_number - last_seen_eth_block_timestamp - last_seen_eth_block_number - contract_addr - eth_syncing - byzantine_events - in_flight_txs - in_flight_exits example: data: last_validated_child_block_timestamp: 1558535130 last_validated_child_block_number: 10000 last_mined_child_block_timestamp: 1558535190 last_mined_child_block_number: 11000 last_seen_eth_block_timestamp: 1558535190 last_seen_eth_block_number: 4427041 contract_addr: plasma_framework: '0x44de0ec539b8c4a4b530c78620fe8320167f2f74' eth_syncing: true byzantine_events: - event: invalid_exit details: eth_height: 615440 utxo_pos: 10000000010000000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 100 root_chain_txhash: '0xde8210dd179e4a067c5649ebeee8871e0f258fecbd1eb02e11db88121bb8de01' spending_txhash: '0x21aee8dcc74d6b309f6e98a967a6aa6002432f98a5bc13c75529dbe228e04451' scheduled_finalization_time: 1588144725 - event: unchallenged_exit details: eth_height: 615440 utxo_pos: 10000000010000000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 100 root_chain_txhash: '0xde8210dd179e4a067c5649ebeee8871e0f258fecbd1eb02e11db88121bb8de01' spending_txhash: '0x21aee8dcc74d6b309f6e98a967a6aa6002432f98a5bc13c75529dbe228e04451' scheduled_finalization_time: 1588144725 - event: invalid_block details: blockhash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' blknum: 10000 error_type: tx_execution - event: block_withholding details: hash: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' blknum: 10000 - event: noncanonical_ife details: txbytes: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' - event: invalid_ife_challenge details: txbytes: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' - event: piggyback_available details: txbytes: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' available_outputs: - index: 0 address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' - index: '1,' address: '0x488f85743ef16cfb1f8d4dd1dfc74c51dc496434' available_inputs: - index: 0 address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' - event: invalid_piggyback details: txbytes: '0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec' inputs: - 1 outputs: - 0 - event: ethereum_stalled_sync details: eth_height: 615440 synced_at: '2020-02-07T10:10:10+00:00' in_flight_txs: - txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' txbytes: 0x3eb6ae5b06f3... input_addresses: - 0x1234... ouput_addresses: - 0x1234... - 0x7890... in_flight_exits: - txhash: '0x5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21' txbytes: 0xf3170101c094... eth_height: 615441 piggybacked_inputs: - 1 piggybacked_outputs: - 0 - 1 services_synced_heights: - service: block_getter height: 4427041 - service: challenges_responds_processor height: 4427029 - service: competitor_processor height: 4427029 - service: depositor height: 4427031 - service: exit_challenger height: 4427029 - service: exit_finalizer height: 4427029 - service: exit_processor height: 4427029 - service: ife_exit_finalizer height: 4427029 - service: in_flight_exit_processor height: 4427029 - service: piggyback_challenges_processor height: 4427029 - service: piggyback_processor height: 4427029 - service: root_chain_height height: 4427041 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /account.get_exitable_utxos: post: tags: - Account summary: Gets all utxos belonging to the given address. description: | **Note:** this is a performance intensive call and should only be used if the chain is byzantine and the user needs to retrieve utxo information to be able to exit. Normally an application should use the Informational API's [Account - Get Utxos](http://TODO) instead. This version is provided in case the Informational API is not available. operationId: account_get_exitable_utxos requestBody: description: HEX-encoded address of the account required: true content: application/json: schema: title: AddressBodySchema type: object properties: address: type: string required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' responses: '200': description: Account utxos succcessful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 otype: type: integer format: int16 oindex: type: integer format: int8 utxo_pos: type: integer format: int256 owner: type: string currency: type: string amount: type: integer format: int256 example: data: - blknum: 123000 txindex: 111 oindex: 0 otype: 1 utxo_pos: 123000001110000 owner: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' currency: '0x0000000000000000000000000000000000000000' amount: 10 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /block.validate: post: tags: - Block summary: Verifies the stateless validity of a block. description: | - Verifies that given Merkle root matches reconstructed Merkle root. - Verifies that (payment and fee) transactions are correctly formed. - Verifies that there are no duplicate inputs at the block level. - Verifies that the number of transactions falls within the accepted range. - Verifies that fee transactions are correctly placed and unique per currency. operationId: validate requestBody: description: 'Block object with a hash, number and array of hexadecimal transaction bytes.' required: true content: application/json: schema: title: BlockValidateBodySchema type: object properties: hash: type: string transactions: type: array items: type: string number: type: integer required: - hash - transactions - number example: number: 1000 hash: 0xf8d083015ba98080808080940000... transactions: - 0xf8c0f843b841fc6dbf49a4baa783ec576291f6083be5ea... - 0xf852c003eeed02eb94916f3753bd53e124d6d565ef1701... responses: '200': description: Successful response to calling /block.validate content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: valid: type: boolean example: data: valid: false '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /utxo.get_challenge_data: post: tags: - UTXO summary: Gets challenge data for a given utxo exit. description: Gets challenge data for a given utxo exit. operationId: utxo_get_challenge_data requestBody: description: 'Utxo position (encoded as single integer, the way contract represents them)' required: true content: application/json: schema: title: UtxoPositionBodySchema type: object properties: utxo_pos: type: integer format: int256 required: - utxo_pos example: utxo_pos: 10000000010000000 responses: '200': description: Utxo challenge successful response content: application/json: schema: description: The response schema for utxo challenge data allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: exit_id: type: integer format: int192 input_index: type: integer format: int8 sig: type: string txbytes: type: string exiting_tx: type: string example: data: exit_id: 1.7176118930141594e+45 input_index: 0 sig: 0x6bfb9b2dbe32... txbytes: 0x3eb6ae5b06f3... exiting_tx: 0x6d6bda6bd6d6... '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /utxo.get_exit_data: post: tags: - UTXO summary: Gets exit data for a given utxo. description: Gets exit data for a given utxo. operationId: utxo_get_exit_data requestBody: description: 'Utxo position (encoded as single integer, the way contract represents them)' required: true content: application/json: schema: title: UtxoPositionBodySchema type: object properties: utxo_pos: type: integer format: int256 required: - utxo_pos example: utxo_pos: 10000000010000000 responses: '200': description: Utxo exit successful response content: application/json: schema: description: The response schema for utxo exit data allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: utxo_pos: type: integer format: int256 txbytes: type: string proof: type: string example: data: proof: 0xcedb8b31d1e4... txbytes: 0x3eb6ae5b06f3... utxo_pos: 10000000010000000 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /transaction.submit: post: tags: - Transaction summary: Sends transaction to Child chain. description: 'Watcher passes signed transaction to the child chain only if it''s secure, e.g. Watcher is fully synced, all operator blocks have been verified, transaction doesn''t spend funds not yet mined...' operationId: submit requestBody: description: Signed transaction RLP-encoded to bytes and HEX-encoded to string required: true content: application/json: schema: title: TransactionSubmitBodySchema type: object properties: transaction: type: string required: - transaction example: transaction: 0xf8d083015ba98080808080940000... responses: '200': description: Transaction submission successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 txhash: type: string example: data: blknum: 123000 txindex: 111 txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /transaction.batch_submit: post: tags: - Transaction summary: This endpoint submits an array of signed transaction to the child chain. description: | Normally you should call the Watcher's Transaction - Submit instead of this. The Watcher's version performs various security and validation checks (TO DO) before submitting the transaction, so is much safer. However, if the Watcher is not available this version exists. operationId: batch_submit requestBody: description: 'Array of signed transactions, RLP-encoded to bytes, and HEX-encoded to string' required: true content: application/json: schema: title: TransactionBatchSubmitBodySchema type: object properties: transactions: type: array items: type: string required: - transactions example: transactions: - 0xf8d083015ba98080808080940000... - 0xf8d083a15ba98080808080920000... responses: '200': description: Transaction batch submission successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: array items: type: object properties: blknum: type: integer format: int64 txindex: type: integer format: int16 txhash: type: string example: data: - blknum: 123000 txindex: 111 txhash: '0xbdf562c24ace032176e27621073df58ce1c6f65de3b5932343b70ba03c72132d' '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /in_flight_exit.get_data: post: tags: - InFlightExit summary: Gets exit data for an in-flight exit. description: | Exit data are arguments to `startInFlightExit` root chain contract function. operationId: in_flight_exit_get_data requestBody: description: In-flight transaction bytes body required: true content: application/json: schema: title: InFlightExitTxBytesBodySchema type: object properties: txbytes: type: string required: - txbytes example: txbytes: 0xf3170101c0940000... responses: '200': description: Get in-flight exit successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object description: The object schema for an in flight exit properties: in_flight_tx: type: string input_txs: type: array items: type: string input_txs_inclusion_proofs: type: array items: type: string in_flight_tx_sigs: type: array items: type: string input_utxos_pos: type: array items: type: integer format: int256 example: data: in_flight_tx: 0xf3170101c0940000... input_txs: - 0xa3470101c0940000... input_txs_inclusion_proofs: - 0xcedb8b31d1e4... in_flight_tx_sigs: - 0x6bfb9b2dbe32... input_utxos_pos: - 300010002001 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /in_flight_exit.get_competitor: post: tags: - InFlightExit summary: Returns a competitor to an in-flight exit. description: Note that if the competing transaction has not been put into a block `competing_tx_pos` and `competing_proof` will not be returned. operationId: in_flight_exit_get_competitor requestBody: description: In-flight transaction bytes body required: true content: application/json: schema: title: InFlightExitTxBytesBodySchema type: object properties: txbytes: type: string required: - txbytes example: txbytes: 0xf3170101c0940000... responses: '200': description: Get competitor successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object description: The object schema for a competitor properties: in_flight_txbytes: type: string in_flight_input_index: type: integer format: int8 competing_txbytes: type: string competing_input_index: type: integer format: int8 competing_sig: type: string competing_tx_pos: type: integer format: int256 competing_proof: type: string input_tx: type: string input_utxo_pos: type: integer format: int256 example: data: in_flight_txbytes: 0xf3170101c0940000... in_flight_input_index: 1 competing_txbytes: 0x5df13a6bee20000... competing_input_index: 1 competing_sig: 0xa3470101c0940000... competing_tx_pos: 26000003920000 competing_proof: 0xcedb8b31d1e4... input_tx: 0xaaa70101c0940000... input_utxo_pos: 300010002001 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /in_flight_exit.prove_canonical: post: tags: - InFlightExit summary: Proves transaction is canonical. description: 'To respond to a challenge to an in-flight exit, this proves that the transaction has been put into a block (and therefore is canonical).' operationId: in_flight_exit_prove_canonical requestBody: description: In-flight transaction bytes body required: true content: application/json: schema: title: InFlightExitTxBytesBodySchema type: object properties: txbytes: type: string required: - txbytes example: txbytes: 0xf3170101c0940000... responses: '200': description: Prove canonical successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object description: The object schema for a canonical proof properties: in_flight_txbytes: type: string in_flight_tx_pos: type: integer format: int256 in_flight_proof: type: string example: data: in_flight_txbytes: 0xf3170101c0940000... in_flight_tx_pos: 26000003920000 in_flight_proof: 0xcedb8b31d1e4... '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /in_flight_exit.get_input_challenge_data: post: tags: - InFlightExit summary: Gets the data to challenge an invalid input piggybacked on an in-flight exit. description: 'To respond to invalid piggybacked input in non-canonical in-flight transaction provides data needed to challenge it, e.g. transaction that spent this input and signature.' operationId: in_flight_exit_get_input_challenge_data requestBody: description: In-flight transaction bytes and invalid input index required: true content: application/json: schema: title: InFlightExitInputChallengeDataBodySchema type: object properties: txbytes: type: string input_index: type: integer format: int8 required: - txbytes - input_index example: txbytes: 0xf3170101c0940000... input_index: 1 responses: '200': description: Get input challenge successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object description: The object schema for an input challenge data properties: in_flight_txbytes: type: string in_flight_input_index: type: integer format: int8 spending_txbytes: type: string spending_input_index: type: integer format: int8 spending_sig: type: string input_tx: type: string input_utxo_pos: type: integer format: int256 example: data: in_flight_txbytes: 0xf3170101c0940000... in_flight_input_index: 1 spending_txbytes: 0x5df13a6bee20000... spending_input_index: 1 spending_sig: 0xa3470101c0940000... input_tx: 0xaaa70101c0940000... input_utxo_pos: 300010002001 '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason /in_flight_exit.get_output_challenge_data: post: tags: - InFlightExit summary: Gets the data to challenge an invalid output piggybacked on an in-flight exit. description: 'To respond to invalid piggybacked output in canonical in-flight transaction provides data needed to challenge it, e.g. in-flight transaction inclusion proof, transaction that spent this output and signature.' operationId: in_flight_exit_get_output_challenge_data requestBody: description: In-flight transaction bytes and invalid output index required: true content: application/json: schema: title: InFlightExitOutputChallengeDataBodySchema type: object properties: txbytes: type: string output_index: type: integer format: int8 required: - txbytes - output_index example: txbytes: 0xf3170101c0940000... output_index: 0 responses: '200': description: Get output challenge successful response content: application/json: schema: allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: type: object description: The object schema for an output challenge data properties: in_flight_txbytes: type: string in_flight_output_pos: type: integer format: int256 in_flight_proof: type: string spending_txbytes: type: string spending_input_index: type: integer format: int8 spending_sig: type: string example: data: in_flight_txbytes: 0xf3170101c0940000... in_flight_output_pos: 21000634002 in_flight_proof: 0xcedb8b31d1e4... spending_txbytes: 0x5df13a6bee20000... spending_input_index: 1 spending_sig: 0xa3470101c0940000... '500': description: Returns an internal server error content: application/json: schema: description: The response schema for an error allOf: - description: The response schema for a successful operation type: object properties: version: type: string success: type: boolean data: type: object service_name: type: string required: - service_name - version - success - data example: service_name: watcher version: 1.0.0+abcdefa success: true data: {} - type: object properties: data: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages required: - data example: success: false data: object: error code: 'server:internal_server_error' description: Something went wrong on the server messages: error_key: error_reason ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/shared/paths.yaml ================================================ ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/shared/request_bodies.yaml ================================================ ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/shared/schemas.yaml ================================================ ErrorSchema: description: The object schema for an error type: object properties: object: type: string code: type: string description: type: string messages: type: object required: - object - code - description - messages ================================================ FILE: apps/omg_watcher_rpc/priv/swagger/swagger.md ================================================ # Designing API specifications OpenAPI definitions, allow devs to specify the operations and metadata of their APIs in machine-readable form. This enables them to automate various processes around the API lifecycle. ### Development specs In order to facilitate the development and maintenance of the API documentation, the open api spec is splat into multiple files. These files are grouped under a resource and each resource has 5 spec files. The basic structure is as follow: ``` /info_api_specs /resource1 (account for example) paths.yaml request_bodies.yaml response_schemas.yaml responses.yaml schemas.yaml /resource2 paths.yaml request_bodies.yaml ... ... ``` Each of these file contain different part of the API definition. When developing you should modify these files, under the `info_api_specs/` and `security_critical_api_specs/` folders and NOT directly the `info_api_specs.yaml` or `security_critical_api_specs.yaml` which are automatically generated. ### Generating the final spec file When you are done editing the different spec files, you need to generate the final file which group all specifications together into one `"big"` file. In order to do this you need to have the following installed and available: - [node.js](https://nodejs.org/en/download/package-manager/) - [swagger-cli](https://www.npmjs.com/package/swagger-cli). Install using: `npm install -g swagger-cli` - [openapi-generator](https://github.com/OpenAPITools/openapi-generator). Install using: `https://github.com/OpenAPITools/openapi-generator` Then you need to run the following commands to generate the final spec. **Watcher Security-Critical API:** ``` swagger-cli bundle -r -t yaml -o apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/swagger.yaml openapi-generator-cli validate -i apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml ``` **Watcher Info API:** ``` swagger-cli bundle -r -t yaml -o apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml apps/omg_watcher_rpc/priv/swagger/info_api_specs/swagger.yaml openapi-generator-cli validate -i apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml ``` ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/release_tasks/set_endpoint_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.ReleaseTasks.SetEndpointTest do use ExUnit.Case, async: true alias OMG.WatcherRPC.ReleaseTasks.SetEndpoint alias OMG.WatcherRPC.Web.Endpoint @app :omg_watcher_rpc test "if environment variables get applied in the configuration" do :ok = System.put_env("PORT", "1") :ok = System.put_env("HOSTNAME", "host") config = SetEndpoint.load([], []) port = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Endpoint) |> Keyword.fetch!(:http) |> Keyword.fetch!(:port) host = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Endpoint) |> Keyword.fetch!(:url) |> Keyword.fetch!(:host) assert port == 1 assert host == "host" end test "if default configuration is used when there's no environment variables" do :ok = System.delete_env("PORT") :ok = System.delete_env("HOSTNAME") config = SetEndpoint.load([], []) port = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Endpoint) |> Keyword.fetch!(:http) |> Keyword.fetch!(:port) host = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Endpoint) |> Keyword.fetch!(:url) |> Keyword.fetch!(:host) config_port = Application.get_env(@app, Endpoint)[:http][:port] config_host = Application.get_env(@app, Endpoint)[:url][:host] assert port == config_port assert host == config_host end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/release_tasks/set_tracer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.ReleaseTasks.SetTracerTest do use ExUnit.Case, async: true import ExUnit.CaptureLog, only: [capture_log: 1] alias OMG.WatcherRPC.ReleaseTasks.SetTracer alias OMG.WatcherRPC.Tracer @app :omg_watcher_rpc setup do {:ok, pid} = __MODULE__.System.start_link([]) nil = Process.put(__MODULE__.System, pid) :ok end test "if environment variables get applied in the configuration" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUE") :ok = __MODULE__.System.put_env("APP_ENV", "YOLO") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) disabled = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:disabled?) env = config |> Keyword.fetch!(@app) |> Keyword.fetch!(Tracer) |> Keyword.fetch!(:env) assert disabled == true # if it's disabled, env doesn't matter, so we set it to an empty string assert env == "" end) end test "if default configuration is used when there's no environment variables" do :ok = __MODULE__.System.put_env("HOSTNAME", "this is my tracer test 3") assert capture_log(fn -> config = SetTracer.load([], system_adapter: __MODULE__.System) # we set env to an empty string because disabled? is set to true! configuration = @app |> Application.get_env(Tracer) |> Keyword.put(:env, "") |> Enum.sort() tracer_config = config |> Keyword.get(@app) |> Keyword.get(Tracer) |> Enum.sort() assert configuration == tracer_config end) end test "if exit is thrown when faulty configuration is used" do :ok = __MODULE__.System.put_env("DD_DISABLED", "TRUEeee") catch_exit(SetTracer.load([], system_adapter: __MODULE__.System)) end defmodule System do def start_link(args), do: GenServer.start_link(__MODULE__, args, []) def get_env(key), do: __MODULE__ |> Process.get() |> GenServer.call({:get_env, key}) def put_env(key, value), do: __MODULE__ |> Process.get() |> GenServer.call({:put_env, key, value}) def init(_), do: {:ok, %{}} def handle_call({:get_env, key}, _, state) do {:reply, state[key], state} end def handle_call({:put_env, key, value}, _, state) do {:reply, :ok, Map.put(state, key, value)} end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/tracer_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.TracerTest do @app :omg_watcher_rpc use ExUnit.Case import Plug.Conn alias OMG.WatcherRPC.Configuration alias OMG.WatcherRPC.Tracer setup do original_mode = Application.get_env(:omg_watcher_rpc, :api_mode) _ = on_exit(fn -> Application.put_env(:omg_watcher_rpc, :api_mode, original_mode) end) :ok end test "api responses without errors get traced with metadata" do :ok = Application.put_env(@app, :api_mode, :watcher) version = Configuration.version() resp_body = """ { "data": [], "service_name": "watcher", "success": true, "version": "#{version}" } """ conn = :get |> Phoenix.ConnTest.build_conn("/alerts.get") |> Plug.Conn.resp(200, resp_body) trace_metadata = Tracer.add_trace_metadata(conn) expected = Keyword.new([ {:tags, [version: version]}, {:service, :watcher}, {:http, [method: "GET", query_string: "", status_code: 200, url: "/alerts.get", user_agent: nil]}, {:resource, "GET /alerts.get"}, {:type, :web} ]) assert trace_metadata == expected end test "if api responses with errors get traced with metadata" do :ok = Application.put_env(@app, :api_mode, :watcher_info) version = Configuration.version() resp_body = """ { "data": { "code": "operation:not_found", "description": "Operation cannot be found. Check request URL.", "object": "error" }, "service_name": "watcher_info", "success": false, "version": "#{version}" } """ conn = :post |> Phoenix.ConnTest.build_conn("/") |> Plug.Conn.resp(200, resp_body) |> assign(:error_type, "operation:not_found") |> assign(:error_msg, "Operation cannot be found. Check request URL.") trace_metadata = Tracer.add_trace_metadata(conn) expected = Keyword.new([ { :tags, [ {:version, version}, {:"error.type", "operation:not_found"}, {:"error.msg", "Operation cannot be found. Check request URL."} ] }, {:error, [error: true]}, {:service, :watcher_info}, {:http, [method: "POST", query_string: "", status_code: 200, url: "/", user_agent: nil]}, {:resource, "POST /"}, {:type, :web} ]) assert trace_metadata == expected end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/conn_case.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.ConnCase do @moduledoc """ This module defines the test case to be used by tests that require setting up a connection. Such tests rely on `Phoenix.ConnTest` and also import other functionality to make it easier to build common datastructures and query the data layer. Finally, if the test case interacts with the database, it cannot be async. For this reason, every test runs inside a transaction which is reset at the beginning of the test unless the test case is marked as async. """ alias Ecto.Adapters.SQL alias OMG.WatcherInfo use ExUnit.CaseTemplate using do quote do # Import conveniences for testing with connections import Plug.Conn import Phoenix.ConnTest import OMG.WatcherRPC.Web.Router.Helpers # The default endpoint for testing @endpoint OMG.WatcherRPC.Web.Endpoint end end setup tags do :ok = SQL.Sandbox.checkout(OMG.WatcherInfo.DB.Repo) unless tags[:async] do SQL.Sandbox.mode(WatcherInfo.DB.Repo, {:shared, self()}) end {:ok, conn: Phoenix.ConnTest.build_conn()} end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/account_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.AccountTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.Crypto alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias Support.WatcherHelper require Utxo @eth <<0::160>> @payment_output_type OMG.Watcher.WireFormatTypes.output_type_for(:output_payment_v1) @eth_hex @eth |> Encoding.to_hex() @other_token <<127::160>> @other_token_hex @other_token |> Encoding.to_hex() @tag fixtures: [:alice, :bob, :blocks_inserter, :initial_blocks] test "Account balance groups account tokens and provide sum of available funds", %{ blocks_inserter: blocks_inserter, alice: alice, bob: bob } do assert [%{"currency" => @eth_hex, "amount" => 349}] == WatcherHelper.success?("account.get_balance", body_for(bob)) # adds other token funds for alice to make more interesting blocks_inserter.([ {11_000, [OMG.Watcher.TestHelper.create_recovered([], @other_token, [{alice, 121}, {alice, 256}])]} ]) data = WatcherHelper.success?("account.get_balance", body_for(alice)) assert [ %{"currency" => @eth_hex, "amount" => 201}, %{"currency" => @other_token_hex, "amount" => 377} ] == data |> Enum.sort(&(Map.get(&1, "currency") <= Map.get(&2, "currency"))) end @tag fixtures: [:phoenix_ecto_sandbox] test "Account balance for non-existing account responds with empty array" do no_account = %{addr: <<0::160>>} assert [] == WatcherHelper.success?("account.get_balance", body_for(no_account)) end defp body_for(%{addr: address}) do %{"address" => Encoding.to_hex(address)} end @tag fixtures: [:initial_blocks, :alice] test "returns last transactions that involve given address", %{ alice: alice } do # refer to `/transaction.all` tests for more thorough cases, this is the same alice_addr = Encoding.to_hex(alice.addr) assert [_] = WatcherHelper.success?("account.get_transactions", %{"address" => alice_addr, "limit" => 1}) end @tag fixtures: [:phoenix_ecto_sandbox] test "account.get_balance handles improper type of parameter" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "address", "validator" => ":hex" } } } == WatcherHelper.no_success?("account.get_balance", %{"address" => 1_234_567_890}) end @tag fixtures: [:alice, :phoenix_ecto_sandbox] test "account.get_balance returns bad request error if address is passed as a query parameter", %{ alice: alice } do %{"address" => address} = body_for(alice) assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "address", "validator" => ":hex" } } } == WatcherHelper.no_success?("account.get_balance?address=#{address}") end describe "standard_exitable" do @tag fixtures: [:phoenix_ecto_sandbox, :db_initialized, :carol] test "no utxos are returned for non-existing addresses", %{carol: carol} do assert [] == WatcherHelper.get_exitable_utxos(carol.addr) end @tag fixtures: [:phoenix_ecto_sandbox, :db_initialized, :alice, :bob] test "get_utxos and get_exitable_utxos have the same return values", %{alice: alice, bob: bob} do DB.EthEvent.insert_deposits!([ %{ root_chain_txhash: Crypto.hash(<<1000::256>>), log_index: 0, eth_height: 1, owner: alice.addr, currency: @eth, amount: 333, blknum: 1 } ]) # TODO: this test is brittle because of the way the DB entries are hardcoded OMG.DB.multi_update([ {:put, :utxo, { {1, 0, 0}, %{ output: %{amount: 333, currency: @eth, owner: alice.addr, output_type: @payment_output_type}, creating_txhash: nil } }}, {:put, :utxo, { {2, 0, 0}, %{ output: %{amount: 100, currency: @eth, owner: bob.addr, output_type: @payment_output_type}, creating_txhash: nil } }} ]) # utxos contain extra fields such as `spending_txhash` so we compare only the fields we expect from both. fields = ["blknum", "txindex", "oindex", "utxo_pos", "amount", "currency", "owner"] exitable_utxos = alice.addr |> WatcherHelper.get_exitable_utxos() |> Enum.map(fn utxo -> Map.take(utxo, fields) end) utxos = alice.addr |> WatcherHelper.get_utxos() |> Enum.map(fn utxo -> Map.take(utxo, fields) end) assert utxos == exitable_utxos end @tag fixtures: [:phoenix_ecto_sandbox] test "account.get_exitable_utxos handles improper type of parameter" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "address", "validator" => ":hex" } } } == WatcherHelper.no_success?("account.get_exitable_utxos", %{"address" => 1_234_567_890}) end end @tag fixtures: [:initial_blocks, :carol] test "no utxos are returned for non-existing addresses", %{carol: carol} do assert [] == WatcherHelper.get_utxos(carol.addr) end @tag fixtures: [:initial_blocks, :alice] test "utxo from initial blocks are available", %{alice: alice} do alice_enc = alice.addr |> Encoding.to_hex() assert [ %{ "amount" => 1, "currency" => @eth_hex, "blknum" => 2000, "txindex" => 0, "oindex" => 1, "owner" => ^alice_enc }, %{ "amount" => 150, "currency" => @eth_hex, "blknum" => 3000, "txindex" => 0, "oindex" => 0, "owner" => ^alice_enc }, %{ "amount" => 50, "currency" => @eth_hex, "blknum" => 3000, "txindex" => 1, "oindex" => 1, "owner" => ^alice_enc } ] = WatcherHelper.get_utxos(alice.addr) end @tag fixtures: [:initial_blocks, :alice] test "encoded utxo positions are delivered", %{alice: alice} do [%{"utxo_pos" => utxo_pos, "blknum" => blknum, "txindex" => txindex, "oindex" => oindex} | _] = WatcherHelper.get_utxos(alice.addr) assert Utxo.position(^blknum, ^txindex, ^oindex) = utxo_pos |> Utxo.Position.decode!() end @tag fixtures: [:initial_blocks, :bob, :carol] test "spent utxos are moved to new owner", %{bob: bob, carol: carol} do [] = WatcherHelper.get_utxos(carol.addr) # bob spends his utxo to carol block_application = %{ transactions: [OMG.Watcher.TestHelper.create_recovered([{2000, 0, 0, bob}], @eth, [{bob, 49}, {carol, 50}])], number: 11_000, hash: <>, timestamp: :os.system_time(:second), eth_height: 10 } {:ok, _} = DB.Block.insert_from_block_application(block_application) assert [ %{ "amount" => 50, "blknum" => 11_000, "txindex" => 0, "oindex" => 1, "currency" => @eth_hex } ] = WatcherHelper.get_utxos(carol.addr) end @tag fixtures: [:initial_blocks, :bob] test "unspent deposits are a part of utxo set", %{bob: bob} do bob_enc = bob.addr |> Encoding.to_hex() deposited_utxo = bob.addr |> WatcherHelper.get_utxos() |> Enum.find(&(&1["blknum"] < 1000)) assert %{ "amount" => 100, "currency" => @eth_hex, "blknum" => 2, "txindex" => 0, "oindex" => 0, "owner" => ^bob_enc } = deposited_utxo end @tag fixtures: [:initial_blocks, :alice] test "spent deposits are not a part of utxo set", %{alice: alice} do assert utxos = WatcherHelper.get_utxos(alice.addr) assert [] = utxos |> Enum.filter(&(&1["blknum"] < 1000)) end @tag fixtures: [:initial_blocks, :carol, :bob] test "deposits are spent", %{carol: carol, bob: bob} do assert [] = WatcherHelper.get_utxos(carol.addr) assert utxos = WatcherHelper.get_utxos(bob.addr) # bob has 1 unspent deposit assert %{ "amount" => 100, "currency" => @eth_hex, "blknum" => blknum, "txindex" => 0, "oindex" => 0 } = utxos |> Enum.find(&(&1["blknum"] < 1000)) block_application = %{ transactions: [OMG.Watcher.TestHelper.create_recovered([{blknum, 0, 0, bob}], @eth, [{carol, 100}])], number: 11_000, hash: <>, timestamp: :os.system_time(:second), eth_height: 10 } {:ok, _} = DB.Block.insert_from_block_application(block_application) utxos = WatcherHelper.get_utxos(bob.addr) # bob has spent his deposit assert [] == utxos |> Enum.filter(&(&1["blknum"] < 1000)) carol_enc = carol.addr |> Encoding.to_hex() # carol has new utxo from above tx assert [ %{ "amount" => 100, "currency" => @eth_hex, "blknum" => 11_000, "txindex" => 0, "oindex" => 0, "owner" => ^carol_enc } ] = WatcherHelper.get_utxos(carol.addr) end @tag fixtures: [:phoenix_ecto_sandbox] test "account.get_utxos handles improper type of parameter" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "address", "validator" => ":hex" } } } == WatcherHelper.no_success?("account.get_utxos", %{"address" => 1_234_567_890}) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/alarm_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.AlarmTest do use ExUnitFixtures use ExUnit.Case, async: true use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures import Plug.Conn import Phoenix.ConnTest @endpoint OMG.WatcherRPC.Web.Endpoint setup do {:ok, apps} = Application.ensure_all_started(:omg_status) Enum.each( :gen_event.call(:alarm_handler, OMG.Status.Alert.AlarmHandler, :get_alarms), fn alarm -> :alarm_handler.clear_alarm(alarm) end ) on_exit(fn -> Enum.each(Enum.reverse(apps), fn app -> :ok = Application.stop(app) end) end) end ### a very basic test of empty alarms should be sufficient, alarms encoding is ### covered in OMG.Utils.HttpRPC.ResponseTest @tag fixtures: [:phoenix_ecto_sandbox, :db_initialized] test "if the controller returns the correct result when there's no alarms raised", _ do assert [] == get("alarm.get") end @tag fixtures: [:phoenix_ecto_sandbox, :db_initialized] test "sets remote ip from cf-connecting-ip header", _ do response = build_conn() |> put_req_header("content-type", "application/json") |> put_req_header("cf-connecting-ip", "99.99.99.99") |> get("alarm.get") assert response.remote_ip == {99, 99, 99, 99} end defp get(path) do response_body = rpc_call_get(path, 200) version = Map.get(response_body, "version") %{"version" => ^version, "success" => true, "data" => data} = response_body data end defp rpc_call_get(path, expected_resp_status) do response = get(put_req_header(build_conn(), "content-type", "application/json"), path) # CORS check assert ["*"] == get_resp_header(response, "access-control-allow-origin") required_headers = [ "access-control-allow-origin", "access-control-expose-headers", "access-control-allow-credentials" ] for header <- required_headers do assert header in Enum.map(response.resp_headers, &elem(&1, 0)) end # CORS check assert response.status == expected_resp_status Jason.decode!(response.resp_body) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/block_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.BlockTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures use OMG.WatcherRPC.Web, :controller import OMG.WatcherInfo.Factory alias OMG.Eth.Encoding alias OMG.Watcher.Merkle alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.WireFormatTypes alias Support.WatcherHelper @valid_block %{ hash: "0x" <> String.duplicate("00", 32), number: 1000, transactions: ["0x00"] } @eth <<0::160>> @alice OMG.Watcher.TestHelper.generate_entity() @bob OMG.Watcher.TestHelper.generate_entity() @payment_tx_type WireFormatTypes.tx_type_for(:tx_payment_v1) describe "get_block/2" do @tag fixtures: [:initial_blocks] test "/block.get returns correct block if existent" do existent_blknum = 1000 %{"success" => success, "data" => data} = WatcherHelper.rpc_call("block.get", %{blknum: existent_blknum}, 200) assert data["blknum"] == existent_blknum assert success == true end @tag fixtures: [:initial_blocks] test "/block.get rejects parameter of wrong type" do string_blknum = "1000" %{"data" => data} = WatcherHelper.rpc_call("block.get", %{blknum: string_blknum}, 200) expected = %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{"parameter" => "blknum", "validator" => ":integer"} }, "object" => "error" } assert data == expected end @tag fixtures: [:initial_blocks] test "/block.get endpoint rejects request without parameters" do missing_param = %{} %{"data" => data} = WatcherHelper.rpc_call("block.get", missing_param, 200) expected = %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{"parameter" => "blknum", "validator" => ":integer"} }, "object" => "error" } assert data == expected end @tag fixtures: [:initial_blocks] test "/block.get returns expected error if block not found" do non_existent_block = 5000 %{"data" => data} = WatcherHelper.rpc_call("block.get", %{blknum: non_existent_block}, 200) expected = %{ "code" => "get_block:block_not_found", "description" => nil, "object" => "error" } assert data == expected end end describe "get_blocks/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns the API response with the blocks" do _ = insert(:block, blknum: 1000, hash: <<1>>, eth_height: 1, timestamp: 100) _ = insert(:block, blknum: 2000, hash: <<2>>, eth_height: 2, timestamp: 200) request_data = %{"limit" => 200, "page" => 1} response = WatcherHelper.rpc_call("block.all", request_data, 200) assert %{ "success" => true, "data" => [ %{ "blknum" => 2000, "eth_height" => 2, "hash" => "0x02", "timestamp" => 200, "tx_count" => 0 }, %{ "blknum" => 1000, "eth_height" => 1, "hash" => "0x01", "timestamp" => 100, "tx_count" => 0 } ], "data_paging" => %{ "limit" => 100, "page" => 1 }, "service_name" => _, "version" => _ } = response end @tag fixtures: [:phoenix_ecto_sandbox] test "returns the error API response when an error occurs" do request_data = %{"limit" => "this should error", "page" => 1} response = WatcherHelper.rpc_call("block.all", request_data, 200) assert %{ "success" => false, "data" => %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => _ }, "service_name" => _, "version" => _ } = response end end describe "validate_block/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns the error API response if a parameter is incorectly formed" do invalid_hash = "0x1234" invalid_params = Map.replace!(@valid_block, :hash, invalid_hash) %{"data" => data} = WatcherHelper.rpc_call("block.validate", invalid_params, 200) expected = %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "hash", "validator" => "{:length, 32}" } }, "object" => "error" } assert expected == data end @tag fixtures: [:phoenix_ecto_sandbox] test "returns the expected error if the block hash does not match the reconstructed Merkle root" do recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) signed_txbytes = [recovered_tx_1, recovered_tx_2] |> Enum.map(fn tx -> tx.signed_tx_bytes end) |> Enum.map(&Encoding.to_hex/1) invalid_merkle_root = "0x" <> String.duplicate("00", 32) params = %{ hash: invalid_merkle_root, number: 1000, transactions: signed_txbytes } %{"data" => data} = WatcherHelper.rpc_call("block.validate", params, 200) assert data == %{"valid" => false} end @tag fixtures: [:phoenix_ecto_sandbox] test "returns the expected error if the transactions are incorrectly formed" do input_1 = {1, 0, 0, @alice} input_2 = {2, 0, 0, @alice} input_3 = {3, 0, 0, @alice} signed_valid_tx = TestHelper.create_signed([input_1, input_2], @eth, [{@bob, 10}]) signed_invalid_tx = TestHelper.create_signed([input_3, input_3], @eth, [{@bob, 10}]) %{sigs: sigs_valid} = signed_valid_tx %{sigs: sigs_invalid} = signed_invalid_tx txbytes_valid = Transaction.raw_txbytes(signed_valid_tx) txbytes_invalid = Transaction.raw_txbytes(signed_invalid_tx) [_, inputs_valid, outputs_valid, _, _] = ExRLP.decode(txbytes_valid) [_, inputs_invalid, outputs_invalid, _, _] = ExRLP.decode(txbytes_invalid) hash_valid = [sigs_valid, @payment_tx_type, inputs_valid, outputs_valid, 0, <<0::256>>] |> ExRLP.encode() |> Encoding.to_hex() hash_invalid = [sigs_invalid, @payment_tx_type, inputs_invalid, outputs_invalid, 0, <<0::256>>] |> ExRLP.encode() |> Encoding.to_hex() merkle_root = [txbytes_invalid, txbytes_valid] |> Merkle.hash() |> Encoding.to_hex() params = %{ hash: merkle_root, transactions: [hash_invalid, hash_valid], number: 1000 } # Sanity check assert {:ok, Encoding.from_hex(merkle_root)} == expect(%{hash: merkle_root}, :hash, :hash) %{"data" => data} = WatcherHelper.rpc_call("block.validate", params, 200) assert data == %{"valid" => false} end @tag fixtures: [:phoenix_ecto_sandbox] test "returns the block if it is valid" do recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) signed_txbytes = [recovered_tx_1, recovered_tx_2] |> Enum.map(fn tx -> tx.signed_tx_bytes end) |> Enum.map(&Encoding.to_hex/1) valid_merkle_root = [recovered_tx_1, recovered_tx_2] |> Enum.map(&Transaction.raw_txbytes/1) |> Merkle.hash() |> Encoding.to_hex() # Sanity check assert {:ok, Encoding.from_hex(valid_merkle_root)} == expect(%{hash: valid_merkle_root}, :hash, :hash) params = %{ hash: valid_merkle_root, number: 1000, transactions: signed_txbytes } %{"data" => data} = WatcherHelper.rpc_call("block.validate", params, 200) assert data == %{"valid" => true} end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/challenge_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.ChallengeTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias Support.WatcherHelper require Utxo @eth <<0::160>> @tag skip: true @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "challenge data is properly formatted", %{alice: alice} do DB.EthEvent.insert_deposits!([%{owner: alice.addr, currency: @eth, amount: 100, blknum: 1, eth_height: 1}]) block_application = %{ transactions: [OMG.Watcher.TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 100}])], number: 1000, hash: <>, timestamp: :os.system_time(:second), eth_height: 1 } {:ok, _} = DB.Block.insert_from_block_application(block_application) utxo_pos = Utxo.position(1, 0, 0) |> Utxo.Position.encode() %{ "input_index" => _input_index, "utxo_pos" => _utxo_pos, "sig" => _sig, "txbytes" => _txbytes } = WatcherHelper.success?("utxo.get_challenge_data", %{"utxo_pos" => utxo_pos}) end @tag skip: true @tag fixtures: [:phoenix_ecto_sandbox] test "challenging non-existent utxo returns error" do utxo_pos = Utxo.position(1, 1, 0) |> Utxo.Position.encode() %{ "code" => "challenge:invalid", "description" => "The challenge of particular exit is invalid because provided utxo is not spent" } = WatcherHelper.no_success?("utxo.get_challenge_data", %{"utxo_pos" => utxo_pos}) end @tag fixtures: [:phoenix_ecto_sandbox] test "utxo.get_challenge_data handles improper type of parameter" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "utxo_pos", "validator" => ":integer" } } } == WatcherHelper.no_success?("utxo.get_challenge_data", %{"utxo_pos" => "1200000120000"}) end @tag fixtures: [:phoenix_ecto_sandbox] test "utxo.get_exit_data handles too low utxo position inputs" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "utxo_pos", "validator" => "{:greater, 0}" } } } = WatcherHelper.no_success?("utxo.get_challenge_data", %{"utxo_pos" => 0}) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/deposit_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.DepositTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures import OMG.WatcherInfo.Factory alias OMG.Utils.HttpRPC.Encoding alias Support.WatcherHelper describe "get_deposits/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected API response format with listed deposits" do owner_1 = <<1::160>> _ = insert(:ethevent, txoutputs: [build(:txoutput, %{owner: owner_1})]) address = Encoding.to_hex(owner_1) request_body = %{"limit" => 10, "page" => 1, "address" => address} WatcherHelper.rpc_call("deposit.all", request_body, 200) assert %{ "success" => true, "data" => [ %{ "event_type" => "deposit", "eth_height" => _, "log_index" => _, "root_chain_txhash" => _, "inserted_at" => _, "updated_at" => _, "txoutputs" => [ %{ "amount" => _, "blknum" => _, "creating_txhash" => _, "oindex" => _, "otype" => _, "owner" => ^address, "spending_txhash" => _, "txindex" => _ } ] } ], "data_paging" => %{ "limit" => 10, "page" => 1 }, "service_name" => "watcher_info", "version" => _ } = WatcherHelper.rpc_call("deposit.all", request_body, 200) end @tag fixtures: [:phoenix_ecto_sandbox] test "filters events by address" do owner_1 = <<1::160>> owner_2 = <<2::160>> txo_1 = build(:txoutput, %{owner: owner_1}) txo_2 = build(:txoutput, %{owner: owner_2}) _ = insert(:ethevent, event_type: :deposit, txoutputs: [txo_1]) _ = insert(:ethevent, event_type: :deposit, txoutputs: [txo_2]) address = Encoding.to_hex(owner_1) request_body = %{"address" => address} %{ "data" => [ %{"txoutputs" => [deposit_txo]} ] } = WatcherHelper.rpc_call("deposit.all", request_body, 200) assert deposit_txo["owner"] == address end @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected error if invalid parameters are given" do incorrect_address = 0 request_body = %{"address" => incorrect_address} assert %{ "data" => %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{"parameter" => "address", "validator" => ":hex"} }, "object" => "error" }, "service_name" => "watcher_info", "success" => false, "version" => _ } = WatcherHelper.rpc_call("deposit.all", request_body, 200) end end @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected error if no address parameter is given" do assert %{ "data" => %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{"parameter" => "address", "validator" => ":hex"} }, "object" => "error" }, "service_name" => "watcher_info", "success" => false, "version" => _ } = WatcherHelper.rpc_call("deposit.all", %{}, 200) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/enforce_content_plug_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.EnforceContentPlugTest do @moduledoc """ This test module tested header enforcing which we decided to remove in #759. Instead of removing it, it was reversed to show no header is required. We can remove this test as it basically shows HTTP protocol behavior. """ use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures use Plug.Test alias OMG.Utils.HttpRPC.Encoding @tag fixtures: [:phoenix_ecto_sandbox] test "Content type header is no longer required" do no_account = Encoding.to_hex(<<0::160>>) post = conn(:post, "account.get_balance", %{"address" => no_account}) response = OMG.WatcherRPC.Web.Endpoint.call(post, []) assert response.status == 200 assert %{"success" => true} = Jason.decode!(response.resp_body) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/fallback_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.FallbackTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures alias Support.WatcherHelper @tag fixtures: [:phoenix_ecto_sandbox] test "returns error for non existing method" do assert %{ "object" => "error", "code" => "operation:not_found", "description" => "Operation cannot be found. Check request URL." } == WatcherHelper.no_success?("no_such.endpoint", %{}) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/fee_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.FeeTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.WireFormatTypes alias OMG.WatcherInfo.TestServer alias Support.WatcherHelper @eth <<0::160>> @tx_type WireFormatTypes.tx_type_for(:tx_payment_v1) @str_tx_type Integer.to_string(@tx_type) setup do context = TestServer.start() on_exit(fn -> TestServer.stop(context) end) context end describe "fees_all/2" do @tag fixtures: [:phoenix_ecto_sandbox] test "forward a successful childchain response", context do childchain_response = %{ @str_tx_type => [ %{ "currency" => Encoding.to_hex(@eth), "amount" => 2, "subunit_to_unit" => 1_000_000_000_000_000_000, "pegged_amount" => 4, "pegged_currency" => "USD", "pegged_subunit_to_unit" => 100, "updated_at" => "2019-01-01T10:10:00+00:00" } ] } prepare_test_server(context, childchain_response) ^childchain_response = WatcherHelper.success?("/fees.all") end @tag fixtures: [:phoenix_ecto_sandbox] test "raises an error gracefully when childchain is unreachable" do assert %{ "code" => "connection:childchain_unreachable", "description" => "Cannot communicate with the childchain.", "object" => "error" } = WatcherHelper.no_success?("/fees.all") end @tag fixtures: [:phoenix_ecto_sandbox] test "fees.all endpoint rejects request with non list currencies" do assert %{ "object" => "error", "code" => "operation:bad_request", "messages" => %{ "validation_error" => %{ "parameter" => "currencies", "validator" => ":list" } } } = WatcherHelper.no_success?("/fees.all", %{currencies: "0x0000000000000000000000000000000000000000"}) end @tag fixtures: [:phoenix_ecto_sandbox] test "fees.all endpoint rejects request with non hex currencies" do assert %{ "object" => "error", "code" => "operation:bad_request", "messages" => %{ "validation_error" => %{ "parameter" => "currencies.currency", "validator" => ":hex" } } } = WatcherHelper.no_success?("/fees.all", %{currencies: ["invalid"]}) end @tag fixtures: [:phoenix_ecto_sandbox] test "fees.all endpoint rejects request with non list tx_types" do assert %{ "object" => "error", "code" => "operation:bad_request", "messages" => %{ "validation_error" => %{ "parameter" => "tx_types", "validator" => ":list" } } } = WatcherHelper.no_success?("/fees.all", %{tx_types: 1}) end @tag fixtures: [:phoenix_ecto_sandbox] test "fees.all endpoint rejects request with negative tx_types" do assert %{ "object" => "error", "code" => "operation:bad_request", "messages" => %{ "validation_error" => %{ "parameter" => "tx_types.tx_type", "validator" => "{:greater, -1}" } } } = WatcherHelper.no_success?("/fees.all", %{tx_types: [-5]}) end end defp prepare_test_server(context, response) do response |> TestServer.make_response() |> TestServer.with_response(context, "/fees.all") end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/in_flight_exit_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.InFlightExitTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias Support.WatcherHelper require Utxo @eth <<0::160>> describe "getting in-flight exits" do @tag fixtures: [:web_endpoint, :db_initialized, :bob, :alice] test "returns properly formatted in-flight exit data", %{bob: bob, alice: alice} do test_in_flight_exit_data = fn inputs, expected_input_txs -> in_flight_signed_txbytes = TestHelper.create_encoded(inputs, @eth, [{bob, 100}]) # `2 + ` for prepending `0x` in HEX encoded binaries in_flight_raw_txbytes = in_flight_signed_txbytes |> Transaction.Signed.decode!() |> Transaction.raw_txbytes() # checking just lengths in majority as we prepare verify correctness in the contract in integration tests assert %{ "in_flight_tx" => ^in_flight_raw_txbytes, "input_txs" => input_txs, "input_utxos_pos" => input_utxos_pos, "input_txs_inclusion_proofs" => proofs, "in_flight_tx_sigs" => sigs } = WatcherHelper.get_in_flight_exit(in_flight_signed_txbytes) input_txs = Enum.map(input_txs, &Transaction.decode!/1) assert Enum.count(input_txs) == Enum.count(inputs) assert Enum.count(input_utxos_pos) == Enum.count(inputs) assert Enum.count(proofs) == Enum.count(inputs) assert Enum.count(sigs) == Enum.count(inputs) input_utxos_pos |> Enum.map(&Utxo.Position.decode!/1) |> Enum.zip(inputs) # assert true because we just want to pattern match both positions against each other |> Enum.each(fn {Utxo.position(blknum, txindex, oindex), {blknum, txindex, oindex, _}} -> assert true end) Enum.each(proofs, fn proof -> assert byte_size(proof) == 16 * 32 end) Enum.each(sigs, fn sig -> assert byte_size(sig) == 65 end) assert input_txs == expected_input_txs end OMG.DB.multi_update( [ [ TestHelper.create_encoded([{1, 0, 0, alice}], @eth, [{bob, 300}]), TestHelper.create_encoded([{1000, 0, 0, bob}], @eth, [{alice, 100}, {bob, 200}]) ], [TestHelper.create_encoded([{1000, 1, 0, alice}], @eth, [{bob, 99}, {alice, 1}], <<1322::256>>)], [ TestHelper.create_encoded([], @eth, [{alice, 150}]), TestHelper.create_encoded([{1000, 1, 1, bob}], @eth, [{bob, 150}, {alice, 50}]) ] ] |> Enum.with_index(1) |> Enum.map(fn {transactions, index} -> {:put, :block, %{hash: <>, number: index * 1000, transactions: transactions}} end) ) test_in_flight_exit_data.([{3000, 1, 0, alice}], [ Transaction.Payment.new([{1000, 1, 1}], [{bob.addr, @eth, 150}, {alice.addr, @eth, 50}]) ]) test_in_flight_exit_data.([{3000, 1, 0, alice}, {2000, 0, 1, alice}], [ Transaction.Payment.new([{1000, 1, 1}], [{bob.addr, @eth, 150}, {alice.addr, @eth, 50}]), Transaction.Payment.new([{1000, 1, 0}], [{bob.addr, @eth, 99}, {alice.addr, @eth, 1}], <<1322::256>>) ]) end @tag fixtures: [:web_endpoint, :db_initialized, :bob] test "behaves well if input is not found", %{bob: bob} do in_flight_txbytes = [{3000, 1, 0, bob}] |> TestHelper.create_encoded(@eth, [{bob, 150}]) |> Encoding.to_hex() assert %{ "code" => "in_flight_exit:tx_for_input_not_found", "description" => "No transaction that created input." } = WatcherHelper.no_success?("/in_flight_exit.get_data", %{"txbytes" => in_flight_txbytes}) end @tag fixtures: [:web_endpoint, :db_initialized, :bob] test "Provides a report on unsupported start IFE case if input is a spent deposit", %{bob: bob} do in_flight_txbytes = [{1, 0, 0, bob}] |> TestHelper.create_encoded(@eth, [{bob, 150}]) |> Encoding.to_hex() assert %{ "code" => "in_flight_exit:deposit_input_spent_ife_unsupported", "description" => "Retrieving IFE data of a transaction with a spent deposit is unsupported." } = WatcherHelper.no_success?("/in_flight_exit.get_data", %{"txbytes" => in_flight_txbytes}) end @tag fixtures: [:web_endpoint] test "behaves well if input malformed" do assert %{"code" => "get_in_flight_exit:malformed_transaction"} = WatcherHelper.no_success?("/in_flight_exit.get_data", %{"txbytes" => "0x00"}) end @tag fixtures: [:web_endpoint] test "responds with error for malformed in-flight transaction bytes" do assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "txbytes", "validator" => ":hex" } } } = WatcherHelper.no_success?("/in_flight_exit.get_data", %{"txbytes" => "tx"}) assert %{ "code" => "get_in_flight_exit:malformed_transaction_rlp", "object" => "error" } = WatcherHelper.no_success?("/in_flight_exit.get_data", %{"txbytes" => "0x1234"}) end end describe "get_competitor/1" do @tag fixtures: [:web_endpoint] test "responds with validation error if given non-hex parameter" do non_hex_parameter = "tx" assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "txbytes", "validator" => ":hex" } } } = WatcherHelper.no_success?("/in_flight_exit.get_competitor", %{"txbytes" => non_hex_parameter}) end end describe "prove_canonical/1" do @tag fixtures: [:web_endpoint] test "responds with validation error if given non-hex parameter" do non_hex_parameter = "tx" assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "txbytes", "validator" => ":hex" } } } = WatcherHelper.no_success?("/in_flight_exit.prove_canonical", %{"txbytes" => non_hex_parameter}) end end describe "get_input_challenge_data/1" do @tag fixtures: [:web_endpoint] test "responds with validation error if given non-hex txbytes parameter" do non_hex_parameter = "tx" assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "txbytes", "validator" => ":hex" } } } = WatcherHelper.no_success?("/in_flight_exit.get_input_challenge_data", %{ "txbytes" => non_hex_parameter, "input_index" => 1 }) end @tag fixtures: [:web_endpoint] test "responds with validation error if given invalid input_index parameter" do invalid_input_index = "0" assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "input_index", "validator" => ":integer" } } } = WatcherHelper.no_success?("/in_flight_exit.get_input_challenge_data", %{ "txbytes" => "0x1234", "input_index" => invalid_input_index }) end end describe "get_output_challenge_data/1" do @tag fixtures: [:web_endpoint] test "responds with validation error if given non-hex txbytes parameter" do non_hex_parameter = "tx" assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "txbytes", "validator" => ":hex" } } } = WatcherHelper.no_success?("/in_flight_exit.get_output_challenge_data", %{ "txbytes" => non_hex_parameter, "output_index" => 1 }) end @tag fixtures: [:web_endpoint] test "responds with validation error if given invalid output_index parameter" do invalid_output_index = "0" assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "output_index", "validator" => ":integer" } } } = WatcherHelper.no_success?("/in_flight_exit.get_output_challenge_data", %{ "txbytes" => "0x1234", "output_index" => invalid_output_index }) end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/stats_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.StatsTet do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures alias Support.WatcherHelper import OMG.WatcherInfo.Factory @seconds_in_twenty_four_hours 86_400 describe "get/0" do @tag fixtures: [:phoenix_ecto_sandbox] test "retrieves expected statistics" do now = DateTime.to_unix(DateTime.utc_now()) within_today = now - @seconds_in_twenty_four_hours + 100 before_today = now - @seconds_in_twenty_four_hours - 100 block_1 = insert(:block, blknum: 1000, timestamp: within_today) _ = insert(:transaction, block: block_1, txindex: 0) _ = insert(:transaction, block: block_1, txindex: 1) block_2 = insert(:block, blknum: 2000, timestamp: before_today) _ = insert(:transaction, block: block_2, txindex: 0) _ = insert(:transaction, block: block_2, txindex: 1) %{"data" => data} = WatcherHelper.rpc_call("stats.get", %{}, 200) expected = %{ "block_count" => %{"all_time" => 2, "last_24_hours" => 1}, "transaction_count" => %{"all_time" => 4, "last_24_hours" => 2}, "average_block_interval_seconds" => %{"all_time" => 200.0, "last_24_hours" => nil} } assert data == expected end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/status_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.StatusTest do use ExUnitFixtures use ExUnit.Case, async: false @moduletag :integration @moduletag :watcher # a test in OMG.WatcherInfo.Integration.StatusTest fully tests the controller, # but it needs whole system setup so it's declared as integration test end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/transaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.TransactionTest do use ExUnitFixtures use ExUnit.Case, async: true use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures use OMG.Watcher.Fixtures import OMG.WatcherInfo.Factory, only: [build: 1, with_deposit: 1, insert: 1, insert: 2, with_inputs: 2, with_outputs: 2] alias OMG.Utils.HttpRPC.Encoding alias OMG.Utils.HttpRPC.Response alias OMG.Watcher.DevCrypto alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper, as: Test alias OMG.Watcher.TypedDataHash alias OMG.Watcher.Utxo alias OMG.Watcher.Utxo.Position alias OMG.Watcher.WireFormatTypes alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.TestServer alias Support.WatcherHelper require OMG.Watcher.State.Transaction.Payment require Utxo @eth <<0::160>> @other_token <<127::160>> @eth_hex Encoding.to_hex(@eth) @other_token_hex Encoding.to_hex(@other_token) @default_data_paging %{"limit" => 200, "page" => 1} @tx_type WireFormatTypes.tx_type_for(:tx_payment_v1) @str_tx_type Integer.to_string(@tx_type) describe "/transaction.get" do @tag fixtures: [:initial_blocks] test "verifies all inserted transactions available to get", %{initial_blocks: initial_blocks} do Enum.each(initial_blocks, fn {blknum, txindex, txhash, _recovered_tx} -> txhash_enc = Encoding.to_hex(txhash) assert %{"block" => %{"blknum" => ^blknum}, "txhash" => ^txhash_enc, "txindex" => ^txindex} = WatcherHelper.success?("transaction.get", %{id: txhash_enc}) end) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns transaction in expected format" do deposit_1 = with_deposit(build(:txoutput)) deposit_2 = with_deposit(build(:txoutput)) input_1 = build(:txoutput) input_2 = build(:txoutput) output_1 = build(:txoutput) output_2 = build(:txoutput) creating_transaction = insert(:transaction) |> with_inputs([deposit_1, deposit_2]) |> with_outputs([input_1, input_2]) spending_transaction = insert(:transaction) |> with_inputs(creating_transaction.outputs) |> with_outputs([output_1, output_2]) expected_response = %{ "block" => %{ "blknum" => spending_transaction.block.blknum, "eth_height" => spending_transaction.block.eth_height, "hash" => Encoding.to_hex(spending_transaction.block.hash), "timestamp" => spending_transaction.block.timestamp, "tx_count" => spending_transaction.block.tx_count, "inserted_at" => Response.serialize(spending_transaction.block.inserted_at).data, "updated_at" => Response.serialize(spending_transaction.block.updated_at).data }, "inputs" => Enum.map(spending_transaction.inputs, fn input -> %{ "amount" => input.amount, "blknum" => input.blknum, "currency" => Encoding.to_hex(input.currency), "oindex" => input.oindex, "owner" => Encoding.to_hex(input.owner), "txindex" => input.txindex, "otype" => input.otype, "utxo_pos" => Utxo.Position.encode({:utxo_position, input.blknum, input.txindex, input.oindex}), "creating_txhash" => to_hex_or_nil(input.creating_txhash), "spending_txhash" => to_hex_or_nil(input.spending_txhash), "inserted_at" => Response.serialize(input.inserted_at).data, "updated_at" => Response.serialize(input.updated_at).data } end), "outputs" => Enum.map(spending_transaction.outputs, fn output -> %{ "amount" => output.amount, "blknum" => output.blknum, "currency" => Encoding.to_hex(output.currency), "oindex" => output.oindex, "owner" => Encoding.to_hex(output.owner), "txindex" => output.txindex, "otype" => output.otype, "utxo_pos" => Utxo.Position.encode({:utxo_position, output.blknum, output.txindex, output.oindex}), "creating_txhash" => to_hex_or_nil(output.creating_txhash), "spending_txhash" => to_hex_or_nil(output.spending_txhash), "inserted_at" => Response.serialize(output.inserted_at).data, "updated_at" => Response.serialize(output.updated_at).data } end), "txhash" => Encoding.to_hex(spending_transaction.txhash), "txbytes" => Encoding.to_hex(spending_transaction.txbytes), "txindex" => spending_transaction.txindex, "txtype" => spending_transaction.txtype, "metadata" => Encoding.to_hex(spending_transaction.metadata), "inserted_at" => Response.serialize(spending_transaction.inserted_at).data, "updated_at" => Response.serialize(spending_transaction.updated_at).data } response = WatcherHelper.success?("transaction.get", %{"id" => Encoding.to_hex(spending_transaction.txhash)}) assert response == expected_response end @tag fixtures: [:blocks_inserter, :initial_deposits, :alice, :bob] test "returns up to 4 inputs / 4 outputs", %{ blocks_inserter: blocks_inserter, alice: alice } do [_, {_, _, txhash, _recovered_tx}] = blocks_inserter.([ {1000, [ Test.create_recovered( [{1, 0, 0, alice}], @eth, [{alice, 10}, {alice, 20}, {alice, 30}, {alice, 40}] ), Test.create_recovered( [{1000, 0, 0, alice}, {1000, 0, 1, alice}, {1000, 0, 2, alice}, {1000, 0, 3, alice}], @eth, [{alice, 1}, {alice, 2}, {alice, 3}, {alice, 4}] ) ]} ]) txhash = Encoding.to_hex(txhash) assert %{ "inputs" => [%{"amount" => 10}, %{"amount" => 20}, %{"amount" => 30}, %{"amount" => 40}], "outputs" => [%{"amount" => 1}, %{"amount" => 2}, %{"amount" => 3}, %{"amount" => 4}], "txhash" => ^txhash, "txindex" => 1 } = WatcherHelper.success?("transaction.get", %{"id" => txhash}) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns error for non existing transaction" do txhash = Encoding.to_hex(<<0::256>>) assert %{ "object" => "error", "code" => "transaction:not_found", "description" => "Transaction doesn't exist for provided search criteria" } == WatcherHelper.no_success?("transaction.get", %{"id" => txhash}) end @tag fixtures: [:phoenix_ecto_sandbox] test "handles improper length of id parameter" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "id", "validator" => "{:length, 32}" } } } == WatcherHelper.no_success?("transaction.get", %{"id" => "0x50e901b98fe3389e32d56166a13a88208b03ea75"}) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns bad request error if transaction hash is passed as query parameter" do txhash = insert(:transaction) |> Map.get(:txhash) |> Encoding.to_hex() assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "id", "validator" => ":hex" } } } == WatcherHelper.no_success?("transaction.get?id=#{txhash}") end end describe "/transaction.all" do @tag fixtures: [:initial_blocks] test "returns multiple transactions in expected format", %{initial_blocks: initial_blocks} do {blknum, txindex, txhash, _recovered_tx} = initial_blocks |> Enum.reverse() |> hd() %DB.Block{timestamp: timestamp, eth_height: eth_height, hash: block_hash} = get_block(blknum) txhash = Encoding.to_hex(txhash) block_hash = Encoding.to_hex(block_hash) assert [ %{ "block" => %{ "blknum" => ^blknum, "eth_height" => ^eth_height, "hash" => ^block_hash, "timestamp" => ^timestamp }, "inputs" => [ %{ "amount" => _, "blknum" => _, "currency" => _, "oindex" => _, "owner" => _, "txindex" => _, "utxo_pos" => _, "creating_txhash" => _, "spending_txhash" => _ } | _ ], "outputs" => [ %{ "amount" => _, "blknum" => _, "currency" => _, "oindex" => _, "owner" => _, "txindex" => _, "utxo_pos" => _, "creating_txhash" => _, "spending_txhash" => _ } | _ ], "txhash" => ^txhash, "txindex" => ^txindex } | _ ] = transaction_all_result() end @tag fixtures: [:blocks_inserter, :alice] test "returns tx from a particular block", %{ blocks_inserter: blocks_inserter, alice: alice } do blocks_inserter.([ {1000, [Test.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 300}])]}, {2000, [ Test.create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 300}]), Test.create_recovered([{2000, 1, 0, alice}], @eth, [{alice, 300}]) ]} ]) assert [%{"block" => %{"blknum" => 2000}, "txindex" => 1}, %{"block" => %{"blknum" => 2000}, "txindex" => 0}] = transaction_all_result(%{"blknum" => 2000}) assert [] = transaction_all_result(%{"blknum" => 3000}) end @tag fixtures: [:blocks_inserter, :alice, :bob] test "returns tx from a particular block that contains requested address as the sender", %{ blocks_inserter: blocks_inserter, alice: alice, bob: bob } do blocks_inserter.([ {1000, [Test.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 300}])]}, {2000, [ Test.create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 300}]), Test.create_recovered([{2, 0, 0, bob}], @eth, [{bob, 300}]) ]} ]) address = Encoding.to_hex(bob.addr) assert [%{"block" => %{"blknum" => 2000}, "txindex" => 1}] = transaction_all_result(%{"address" => address, "blknum" => 2000}) end @tag fixtures: [:blocks_inserter, :initial_deposits, :alice, :bob] test "returns tx that contains requested address as the sender and not recipient", %{ blocks_inserter: blocks_inserter, alice: alice, bob: bob } do blocks_inserter.([ {1000, [ Test.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 300}]) ]} ]) address = Encoding.to_hex(alice.addr) assert [%{"block" => %{"blknum" => 1000}, "txindex" => 0}] = transaction_all_result(%{"address" => address}) end @tag fixtures: [:blocks_inserter, :initial_deposits, :alice, :bob, :carol] test "returns only and all txs that match the address filtered", %{ blocks_inserter: blocks_inserter, alice: alice, bob: bob, carol: carol } do blocks_inserter.([ {1000, [ Test.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 300}]), Test.create_recovered([{2, 0, 0, bob}], @eth, [{bob, 300}]), Test.create_recovered([{1000, 1, 0, bob}], @eth, [{alice, 300}]) ]} ]) alice_addr = Encoding.to_hex(alice.addr) carol_addr = Encoding.to_hex(carol.addr) assert [%{"block" => %{"blknum" => 1000}, "txindex" => 2}, %{"block" => %{"blknum" => 1000}, "txindex" => 0}] = transaction_all_result(%{"address" => alice_addr}) assert [] = transaction_all_result(%{"address" => carol_addr}) end @tag fixtures: [:blocks_inserter, :alice, :bob] test "returns tx that contains requested address as the recipient and not sender", %{ blocks_inserter: blocks_inserter, alice: alice, bob: bob } do blocks_inserter.([ {1000, [ Test.create_recovered([{2, 0, 0, bob}], @eth, [{alice, 100}]) ]} ]) address = Encoding.to_hex(alice.addr) assert [%{"block" => %{"blknum" => 1000}, "txindex" => 0}] = transaction_all_result(%{"address" => address}) end @tag fixtures: [:blocks_inserter, :alice] test "returns tx that contains requested address as both sender & recipient is listed once", %{ blocks_inserter: blocks_inserter, alice: alice } do blocks_inserter.([ {1000, [ Test.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 100}]) ]} ]) address = Encoding.to_hex(alice.addr) assert [%{"block" => %{"blknum" => 1000}, "txindex" => 0}] = transaction_all_result(%{"address" => address}) end @tag fixtures: [:blocks_inserter, :alice] test "returns tx without inputs and contains requested address as recipient", %{ blocks_inserter: blocks_inserter, alice: alice } do blocks_inserter.([ {1000, [ Test.create_recovered([], @eth, [{alice, 10}]) ]} ]) address = Encoding.to_hex(alice.addr) assert [%{"block" => %{"blknum" => 1000}, "txindex" => 0}] = transaction_all_result(%{"address" => address}) end @tag fixtures: [:initial_blocks] test "returns transactions containing metadata", %{initial_blocks: initial_blocks} do {blknum, txindex, txhash, recovered_tx} = Enum.find(initial_blocks, &match?({2000, 0, _, _}, &1)) expected_metadata = Encoding.to_hex(recovered_tx.signed_tx.raw_tx.metadata) expected_txhash = Encoding.to_hex(txhash) assert [ %{ "block" => %{"blknum" => ^blknum}, "metadata" => ^expected_metadata, "txhash" => ^expected_txhash, "txindex" => ^txindex } ] = transaction_all_result(%{"metadata" => expected_metadata}) end @tag fixtures: [:blocks_inserter, :initial_deposits, :alice] test "returns transactions with matching txtype", %{ blocks_inserter: blocks_inserter, alice: alice } do blocks_inserter.([ {1000, [ Test.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 300}]), Test.create_recovered([{2, 0, 0, alice}], @eth, [{alice, 300}]), Test.create_recovered([{1000, 1, 0, alice}], @eth, [{alice, 300}]), Test.create_recovered_fee_tx(1000, alice.addr, @eth, 5) ]} ]) assert [%{"txindex" => 2}, %{"txindex" => 1}, %{"txindex" => 0}] = transaction_all_result(%{"txtypes" => [1]}) assert [%{"txindex" => 3}] = transaction_all_result(%{"txtypes" => [3]}) assert [%{"txindex" => 3}, %{"txindex" => 2}, %{"txindex" => 1}, %{"txindex" => 0}] = transaction_all_result(%{"txtypes" => [1, 3]}) assert [%{"txindex" => 3}, %{"txindex" => 2}, %{"txindex" => 1}, %{"txindex" => 0}] = transaction_all_result(%{"txtypes" => []}) end end describe "/transaction.all pagination" do @tag fixtures: [:alice, :bob, :initial_deposits, :blocks_inserter] test "returns list of transactions limited by address", %{ blocks_inserter: blocks_inserter, alice: alice, bob: bob } do blocks_inserter.([ {1000, [ Test.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 3}]), Test.create_recovered([{1_000, 0, 0, bob}], @eth, [{bob, 2}]) ]}, {2000, [ Test.create_recovered([{1_000, 1, 0, bob}], @eth, [{alice, 1}]) ]} ]) alice_addr = Encoding.to_hex(alice.addr) assert { [%{"block" => %{"blknum" => 2000}, "txindex" => 0}, %{"block" => %{"blknum" => 1000}, "txindex" => 1}], %{"limit" => 2, "page" => 1} } = transaction_all_with_paging(%{limit: 2}) assert {[%{"block" => %{"blknum" => 2000}, "txindex" => 0}, %{"block" => %{"blknum" => 1000}, "txindex" => 0}], %{"limit" => 2, "page" => 1}} = transaction_all_with_paging(%{address: alice_addr, limit: 2}) bob_addr = Encoding.to_hex(bob.addr) assert {[%{"block" => %{"blknum" => 1000}, "txindex" => 0}], %{"limit" => 2, "page" => 2}} = transaction_all_with_paging(%{address: bob_addr, limit: 2, page: 2}) end @tag fixtures: [:initial_blocks] test "returns list of transactions limited by block number" do assert {[%{"block" => %{"blknum" => 1000}, "txindex" => 1}], %{"limit" => 1, "page" => 1}} = transaction_all_with_paging(%{blknum: 1000, limit: 1, page: 1}) assert {[%{"block" => %{"blknum" => 1000}, "txindex" => 0}], %{"limit" => 1, "page" => 2}} = transaction_all_with_paging(%{blknum: 1000, limit: 1, page: 2}) assert {[], %{"limit" => 1, "page" => 3}} = transaction_all_with_paging(%{blknum: 1000, limit: 1, page: 3}) end @tag fixtures: [:initial_blocks] test "limiting all transactions without address filter" do assert {[ %{"block" => %{"blknum" => 3000}, "txindex" => 1} = tx1, %{"block" => %{"blknum" => 3000}, "txindex" => 0} = tx2 ], %{"limit" => 2, "page" => 1}} = transaction_all_with_paging(%{limit: 2}) assert {[^tx1, ^tx2], %{"limit" => 2, "page" => 1}} = transaction_all_with_paging(%{limit: 2, page: 1}) assert {[%{"block" => %{"blknum" => 2000}, "txindex" => 0}, %{"block" => %{"blknum" => 1000}, "txindex" => 1}], %{"limit" => 2, "page" => 2}} = transaction_all_with_paging(%{limit: 2, page: 2}) assert {[%{"block" => %{"blknum" => 1000}, "txindex" => 0}], %{"limit" => 2, "page" => 3}} = transaction_all_with_paging(%{limit: 2, page: 3}) end @tag fixtures: [:alice, :bob, :initial_deposits, :blocks_inserter] test "pagination is unstable - client libs needs to remove duplicates", %{ blocks_inserter: blocks_inserter, alice: alice, bob: bob } do blocks_inserter.([ {1000, [ Test.create_recovered([{1, 0, 0, alice}], @eth, [{bob, 3}]), Test.create_recovered([{1_000, 0, 0, bob}], @eth, [{bob, 2}]) ]} ]) assert {[ %{"block" => %{"blknum" => 1000}, "txindex" => 1} = tx1, %{"block" => %{"blknum" => 1000}, "txindex" => 0} = tx2 ], %{"limit" => 2, "page" => 1}} = transaction_all_with_paging(%{limit: 2}) # After 2 txs were requested 2 more was added, so then asking for the next page, the same # already seen transaction will be returned. This test shows the limitation of current implementation. blocks_inserter.([ {2000, [ Test.create_recovered([{5, 0, 0, alice}], @eth, [{bob, 10}]), Test.create_recovered([{1_002, 0, 0, bob}], @eth, [{alice, 5}]) ]} ]) assert {[^tx1, ^tx2], %{"limit" => 2, "page" => 2}} = transaction_all_with_paging(%{limit: 2, page: 2}) end @tag fixtures: [:phoenix_ecto_sandbox] test "handles improper limit parameter" do invalid_limit = "50" assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "limit", "validator" => ":integer" } } } == WatcherHelper.no_success?("transaction.all", %{"limit" => invalid_limit}) end @tag fixtures: [:phoenix_ecto_sandbox] test "handles improper address parameter" do too_short_address = "0x" <> String.duplicate("00", 19) assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "address", "validator" => "{:length, 20}" } } } == WatcherHelper.no_success?("transaction.all", %{"address" => too_short_address}) end end describe "/transaction.submit with binary-encoded transaction" do @tag fixtures: [:phoenix_ecto_sandbox] test "handles incorrectly encoded parameter" do hex_without_0x = "5df13a6bf96dbcf6e66d8babd6b55bd40d64d4320c3b115364c6588fc18c2a21" assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "transaction", "validator" => ":hex" } } } == WatcherHelper.no_success?("transaction.submit", %{"transaction" => hex_without_0x}) end @tag fixtures: [:alice, :phoenix_ecto_sandbox] test "provides stateless validation", %{alice: alice} do signed_bytes = Test.create_encoded([{1, 0, 0, alice}, {1, 0, 0, alice}], @eth, [{alice, 100}]) assert %{ "code" => "submit:duplicate_inputs", "description" => nil, "object" => "error" } == WatcherHelper.no_success?("transaction.submit", %{"transaction" => Encoding.to_hex(signed_bytes)}) end @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "does not accept fee transactions", %{alice: alice} do fee_tx = Transaction.Fee.new(1000, {alice.addr, @eth, 1551}) |> Test.sign_encode([]) |> Encoding.to_hex() assert %{ "code" => "submit:transaction_not_supported", "description" => _, "object" => "error" } = WatcherHelper.no_success?("transaction.submit", %{ "transaction" => fee_tx }) end end describe "/transaction.submit with structural transaction" do deffixture typed_data_request(alice, bob) do contract_addr = Application.fetch_env!(:omg_eth, :contract_addr) alice_addr = Encoding.to_hex(alice.addr) bob_addr = Encoding.to_hex(bob.addr) %{ # these values should match configuration :omg_watcher, :eip_712_domain "domain" => %{ "name" => "OMG Network", "version" => "1", "salt" => "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", "verifyingContract" => contract_addr.plasma_framework }, "message" => %{ "input0" => %{"blknum" => 1000, "txindex" => 0, "oindex" => 1}, "input1" => %{"blknum" => 3001, "txindex" => 0, "oindex" => 0}, "input2" => %{"blknum" => 0, "txindex" => 0, "oindex" => 0}, "input3" => %{"blknum" => 0, "txindex" => 0, "oindex" => 0}, "output0" => %{"owner" => alice_addr, "currency" => @eth_hex, "amount" => 10}, "output1" => %{"owner" => alice_addr, "currency" => @other_token_hex, "amount" => 300}, "output2" => %{"owner" => bob_addr, "currency" => @other_token_hex, "amount" => 100}, "output3" => %{"owner" => @eth_hex, "currency" => @eth_hex, "amount" => 0}, "metadata" => Encoding.to_hex(<<0::256>>) }, "signatures" => <<127::520>> |> List.duplicate(2) |> Enum.map(&Encoding.to_hex/1) } end @tag fixtures: [:phoenix_ecto_sandbox, :typed_data_request] test "ensures all required fields are passed", %{typed_data_request: typed_data_request} do req_without_domain = Map.drop(typed_data_request, ["domain"]) assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "object" => "error", "messages" => %{ "validation_error" => %{ "parameter" => "domain", "validator" => ":map" } } } == WatcherHelper.no_success?("transaction.submit_typed", req_without_domain) req_without_message = Map.drop(typed_data_request, ["message"]) assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "object" => "error", "messages" => %{ "validation_error" => %{ "parameter" => "message", "validator" => ":map" } } } == WatcherHelper.no_success?("transaction.submit_typed", req_without_message) req_without_sigs = Map.drop(typed_data_request, ["signatures"]) assert %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "object" => "error", "messages" => %{ "validation_error" => %{ "parameter" => "signatures", "validator" => ":list" } } } == WatcherHelper.no_success?("transaction.submit_typed", req_without_sigs) end @tag fixtures: [:phoenix_ecto_sandbox, :typed_data_request] test "input & sigs count should match", %{typed_data_request: typed_data_request} do # Providing 2 non-zero inputs & 1 signature too_little_sigs = Map.update!(typed_data_request, "signatures", fn sigs -> Enum.take(sigs, 1) end) assert %{ "code" => "submit_typed:missing_signature", "description" => "Signatures should correspond to inputs owner. When all non-empty inputs has the same owner, " <> "signatures should be duplicated.", "object" => "error" } == WatcherHelper.no_success?("transaction.submit_typed", too_little_sigs) # Providing 2 non-zero inputs & 4 signatures too_many_sigs = Map.update!(typed_data_request, "signatures", fn sigs -> sigs ++ sigs end) assert %{ "code" => "submit_typed:superfluous_signature", "description" => "Number of non-empty inputs should match signatures count. Remove redundant signatures.", "object" => "error" } == WatcherHelper.no_success?("transaction.submit_typed", too_many_sigs) end end describe "/transaction.create" do setup tags do context = TestServer.start() on_exit(fn -> TestServer.stop(context) end) Map.put(tags, :test_server, context) end @default_fee_amount 5 @default_fee_currency @eth_hex @fee_response %{ @str_tx_type => [ %{ "currency" => @default_fee_currency, "amount" => @default_fee_amount, "subunit_to_unit" => 1_000_000_000_000_000_000, "pegged_amount" => 4, "pegged_currency" => "USD", "pegged_subunit_to_unit" => 100, "updated_at" => "2019-01-01T10:10:00+00:00" } ] } deffixture more_utxos(alice, blocks_inserter) do blocks_inserter.([ {5000, [ Test.create_recovered([], @eth, [{alice, 40}, {alice, 42}, {alice, 43}, {alice, 44}]), Test.create_recovered([], @eth, [{alice, 41}, {alice, 45}]), Test.create_recovered([], @other_token, [{alice, 5}, {alice, 110}, {alice, 15}]), Test.create_recovered([], @other_token, [{alice, 105}, {alice, 10}, {alice, 115}]) ]} ]) end @tag fixtures: [:alice, :bob, :more_utxos] test "returns appropriate schema", %{alice: alice, bob: bob, more_utxos: inserted_txs, test_server: context} do alias OMG.Watcher.Utxo require Utxo prepare_test_server(context, @fee_response) alice_to_bob = 100 metadata = (alice.addr <> bob.addr) |> OMG.Watcher.Crypto.hash() |> Encoding.to_hex() alice_addr = Encoding.to_hex(alice.addr) bob_addr = Encoding.to_hex(bob.addr) blknum = 5000 creating_txhash = inserted_txs |> Enum.at(0) |> elem(2) |> Encoding.to_hex() assert %{ "result" => "complete", "transactions" => [ %{ "inputs" => [ %{ "owner" => ^alice_addr, "currency" => @eth_hex, "blknum" => ^blknum, "txindex" => txindex, "oindex" => oindex, "utxo_pos" => utxo_pos, "creating_txhash" => ^creating_txhash, "spending_txhash" => nil } | _ ], "outputs" => [ %{"amount" => ^alice_to_bob, "currency" => @eth_hex, "owner" => ^bob_addr}, %{"currency" => @eth_hex, "owner" => ^alice_addr, "amount" => _rest} ], "metadata" => ^metadata, "fee" => %{"amount" => @default_fee_amount, "currency" => @default_fee_currency}, "txbytes" => "0x" <> _txbytes, "sign_hash" => "0x" <> _hash } ] } = WatcherHelper.success?( "transaction.create", %{ "owner" => alice_addr, "payments" => [ %{"amount" => alice_to_bob, "currency" => @eth_hex, "owner" => bob_addr} ], "fee" => %{"currency" => @default_fee_currency}, "metadata" => metadata } ) assert Utxo.Position.encode(Utxo.position(blknum, txindex, oindex)) == utxo_pos end @tag fixtures: [:alice, :bob, :more_utxos] test "returns correctly formed transaction, identical with the verbose form", %{ alice: alice, bob: bob, test_server: context } do alias OMG.Watcher.State.Transaction prepare_test_server(context, @fee_response) assert %{ "result" => "complete", "transactions" => [ %{ "inputs" => verbose_inputs, "outputs" => verbose_outputs, "metadata" => verbose_metadata, "txbytes" => tx_hex, "sign_hash" => sign_hash_hex } ] } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [%{"amount" => 100, "currency" => @eth_hex, "owner" => Encoding.to_hex(bob.addr)}], "fee" => %{"currency" => @default_fee_currency}, "metadata" => Encoding.to_hex(<<123::256>>) } ) verbose_tx = Transaction.Payment.new( Enum.map(verbose_inputs, &{&1["blknum"], &1["txindex"], &1["oindex"]}), Enum.map(verbose_outputs, &{from_hex!(&1["owner"]), from_hex!(&1["currency"]), &1["amount"]}), from_hex!(verbose_metadata) ) assert tx_hex == verbose_tx |> Transaction.raw_txbytes() |> Encoding.to_hex() assert sign_hash_hex == verbose_tx |> TypedDataHash.hash_struct() |> Encoding.to_hex() end @tag fixtures: [:alice, :bob, :more_utxos] test "returns typed data in the form of request of typedDataSign", %{alice: alice, bob: bob, test_server: context} do metadata_hex = Encoding.to_hex(<<123::256>>) prepare_test_server(context, @fee_response) assert %{ "result" => "complete", "transactions" => [ %{ "typed_data" => %{ "primaryType" => "Transaction", "types" => %{ "EIP712Domain" => [%{"name" => "name"} | _], "Transaction" => [_ | _], "Input" => [_ | _], "Output" => [_ | _] }, "domain" => %{ "name" => "OMG Network", "verifyingContract" => "0x" <> _contract }, "message" => %{ "input0" => %{"blknum" => _, "txindex" => _, "oindex" => _}, "output0" => %{"owner" => "0x" <> _, "currency" => @eth_hex, "amount" => _}, "metadata" => ^metadata_hex } } } ] } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [%{"amount" => 100, "currency" => @eth_hex, "owner" => Encoding.to_hex(bob.addr)}], "fee" => %{"currency" => @default_fee_currency}, "metadata" => metadata_hex } ) end @tag fixtures: [:alice, :bob, :more_utxos, :blocks_inserter] test "allows to pay single token tx", %{ alice: alice, bob: bob, blocks_inserter: blocks_inserter, test_server: context } do alice_balance = balance_in_token(alice.addr, @eth) bob_balance = balance_in_token(bob.addr, @eth) payment = 100 prepare_test_server(context, @fee_response) assert %{ "result" => "complete", "transactions" => [%{"txbytes" => tx_hex}] } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => payment, "currency" => @eth_hex, "owner" => Encoding.to_hex(bob.addr)} ], "fee" => %{"currency" => @default_fee_currency} } ) make_payments(7000, alice, [tx_hex], blocks_inserter) assert alice_balance - (payment + @default_fee_amount) == balance_in_token(alice.addr, @eth) assert bob_balance + payment == balance_in_token(bob.addr, @eth) end @tag fixtures: [:alice, :bob, :more_utxos, :blocks_inserter] test "allows to pay multi token tx", %{ alice: alice, bob: bob, blocks_inserter: blocks_inserter, test_server: context } do alice_eth = balance_in_token(alice.addr, @eth) alice_token = balance_in_token(alice.addr, @other_token) bob_eth = balance_in_token(bob.addr, @eth) bob_token = balance_in_token(bob.addr, @other_token) payment_eth = 100 payment_token = 110 prepare_test_server(context, @fee_response) assert %{ "result" => "complete", "transactions" => [%{"txbytes" => tx_hex}] } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => payment_eth, "currency" => @eth_hex, "owner" => Encoding.to_hex(bob.addr)}, %{"amount" => payment_token, "currency" => @other_token_hex, "owner" => Encoding.to_hex(bob.addr)} ], "fee" => %{"currency" => @default_fee_currency} } ) make_payments(7000, alice, [tx_hex], blocks_inserter) assert alice_eth - (payment_eth + @default_fee_amount) == balance_in_token(alice.addr, @eth) assert alice_token - payment_token == balance_in_token(alice.addr, @other_token) assert bob_eth + payment_eth == balance_in_token(bob.addr, @eth) assert bob_token + payment_token == balance_in_token(bob.addr, @other_token) end @tag fixtures: [:alice, :bob, :more_utxos, :blocks_inserter] test "advice on merge single token tx", %{ alice: alice, bob: bob, blocks_inserter: blocks_inserter, test_server: context } do alice_balance = balance_in_token(alice.addr, @eth) max_spendable = max_amount_spendable_in_single_tx(alice.addr, @eth) payment = max_spendable + 10 prepare_test_server(context, @fee_response) assert %{ "result" => "intermediate", "transactions" => transactions } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => payment, "currency" => @eth_hex, "owner" => Encoding.to_hex(bob.addr)} ], "fee" => %{"currency" => @default_fee_currency} } ) make_payments(7000, alice, Enum.map(transactions, & &1["txbytes"]), blocks_inserter) assert alice_balance == balance_in_token(alice.addr, @eth) assert max_amount_spendable_in_single_tx(alice.addr, @eth) >= payment end @tag fixtures: [:alice, :bob, :more_utxos] test "advice on merge does not merge single utxo", %{alice: alice, bob: bob, test_server: context} do max_spendable = max_amount_spendable_in_single_tx(alice.addr, @eth) payment = max_spendable + 1 prepare_test_server(context, @fee_response) assert %{ "result" => "intermediate", "transactions" => [transaction] } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => payment, "currency" => @eth_hex, "owner" => Encoding.to_hex(bob.addr)} ], "fee" => %{"currency" => @default_fee_currency} } ) assert OMG.Watcher.State.Transaction.Payment.max_inputs() == length(transaction["inputs"]) end @tag fixtures: [:alice, :bob, :more_utxos, :blocks_inserter] test "allows to pay other token tx with fee in different currency", %{alice: alice, bob: bob, blocks_inserter: blocks_inserter, test_server: context} do alice_eth = balance_in_token(alice.addr, @eth) alice_token = balance_in_token(alice.addr, @other_token) bob_token = balance_in_token(bob.addr, @other_token) payment_token = 110 prepare_test_server(context, @fee_response) assert %{ "transactions" => [%{"txbytes" => tx_hex}] } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => payment_token, "currency" => @other_token_hex, "owner" => Encoding.to_hex(bob.addr)} ], "fee" => %{"currency" => @default_fee_currency} } ) make_payments(7000, alice, [tx_hex], blocks_inserter) assert alice_eth - @default_fee_amount == balance_in_token(alice.addr, @eth) assert alice_token - payment_token == balance_in_token(alice.addr, @other_token) assert bob_token + payment_token == balance_in_token(bob.addr, @other_token) end @tag fixtures: [:alice, :bob, :more_utxos] test "insufficient funds returns custom error", %{alice: alice, bob: bob, test_server: context} do balance = balance_in_token(alice.addr, @eth) payment = balance + 10 prepare_test_server(context, @fee_response) assert %{ "object" => "error", "code" => "transaction.create:insufficient_funds", "description" => "Account balance is too low to satisfy the payment.", "messages" => [%{"token" => @eth_hex, "missing" => payment + @default_fee_amount - balance}] } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => payment, "currency" => @eth_hex, "owner" => Encoding.to_hex(bob.addr)} ], "fee" => %{"currency" => @default_fee_currency} } ) end @tag fixtures: [:alice, :bob, :more_utxos] test "unknown owner returns insufficient funds error", %{alice: alice, bob: bob, test_server: context} do assert 0 == balance_in_token(bob.addr, @eth) payment = 25 prepare_test_server(context, @fee_response) assert %{ "object" => "error", "code" => "transaction.create:insufficient_funds", "description" => "Account balance is too low to satisfy the payment.", "messages" => [%{"token" => @eth_hex, "missing" => payment + @default_fee_amount}] } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => Encoding.to_hex(bob.addr), "payments" => [ %{"amount" => payment, "currency" => @eth_hex, "owner" => Encoding.to_hex(alice.addr)} ], "fee" => %{"currency" => @default_fee_currency} } ) end @tag fixtures: [:alice, :bob, :more_utxos] test "total number of outputs exceeds allowed outputs returns custom error", %{ alice: alice, bob: bob, test_server: context } do bob_addr = Encoding.to_hex(bob.addr) prepare_test_server(context, @fee_response) assert %{ "object" => "error", "code" => "transaction.create:too_many_outputs", "description" => "Total number of payments + change + fees exceed maximum allowed outputs." } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => 1, "currency" => @other_token_hex, "owner" => bob_addr}, %{"amount" => 2, "currency" => @other_token_hex, "owner" => bob_addr}, %{"amount" => 3, "currency" => @other_token_hex, "owner" => bob_addr} ], "fee" => %{"currency" => @default_fee_currency} } ) end @tag fixtures: [:alice, :more_utxos] test "transaction without payments that burns funds in fees is created correctly and incorrect on decoding", %{alice: alice, test_server: context} do prepare_test_server(context, %{ @str_tx_type => [ %{ "currency" => @other_token_hex, "amount" => @default_fee_amount, "subunit_to_unit" => 1_000_000_000_000_000_000, "pegged_amount" => 4, "pegged_currency" => "USD", "pegged_subunit_to_unit" => 100, "updated_at" => "2019-01-01T10:10:00+00:00" } ] }) assert %{ "transactions" => [%{"txbytes" => tx_hex}] } = WatcherHelper.success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [], "fee" => %{"currency" => @other_token_hex} } ) assert {:error, :empty_outputs} = tx_hex |> from_hex!() |> Transaction.decode() end @tag fixtures: [:alice, :more_utxos] test "empty transaction without payments list is not allowed", %{alice: alice, test_server: context} do alice_addr = Encoding.to_hex(alice.addr) prepare_test_server(context, %{ @str_tx_type => [ %{ "currency" => @default_fee_currency, "amount" => 0, "subunit_to_unit" => 1_000_000_000_000_000_000, "pegged_amount" => 4, "pegged_currency" => "USD", "pegged_subunit_to_unit" => 100, "updated_at" => "2019-01-01T10:10:00+00:00" } ] }) assert %{ "object" => "error", "code" => "transaction.create:empty_transaction", "description" => "Requested payment transfers no funds." } == WatcherHelper.no_success?( "transaction.create", %{"owner" => alice_addr, "payments" => [], "fee" => %{"currency" => @default_fee_currency}} ) end @tag fixtures: [:alice, :more_utxos] test "returns an error when requester is equal to all the outputs owner", %{alice: alice} do params = %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [ %{"amount" => 1, "currency" => @eth_hex, "owner" => Encoding.to_hex(alice.addr)}, %{"amount" => 1, "currency" => @eth_hex, "owner" => Encoding.to_hex(alice.addr)} ], "fee" => %{"currency" => @default_fee_currency} } assert %{ "object" => "error", "code" => "transaction.create:self_transaction_not_supported", "description" => "This endpoint cannot be used to create merge or split transactions." } == WatcherHelper.no_success?("transaction.create", params) end end describe "/transaction.create stealth add inputs" do setup tags do context = TestServer.start() on_exit(fn -> TestServer.stop(context) end) Map.put(tags, :test_server, context) end @tag fixtures: [:phoenix_ecto_sandbox] test "does not stealth add inputs where the number of inputs reached maximum", %{ test_server: context } do alice = OMG.Watcher.TestHelper.generate_entity() bob = OMG.Watcher.TestHelper.generate_entity() prepare_test_server(context, @fee_response) _payment_1 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) _payment_2 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) _payment_3 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) _payment_and_fee = insert(:txoutput, amount: 15, currency: @eth, owner: alice.addr) alice_addr_hex = Encoding.to_hex(alice.addr) bob_addr_hex = Encoding.to_hex(bob.addr) params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 40, "currency" => @eth_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => 15, "currency" => @eth_hex} ], "outputs" => [ %{"amount" => 40, "currency" => @eth_hex, "owner" => ^bob_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) end @tag fixtures: [:phoenix_ecto_sandbox] test "stealth add 1 more input when 3 inputs with single currency covers payment and fee", %{test_server: context} do alice = OMG.Watcher.TestHelper.generate_entity() bob = OMG.Watcher.TestHelper.generate_entity() prepare_test_server(context, @fee_response) _payment_1 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) _payment_2 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) _fee_1 = insert(:txoutput, amount: @default_fee_amount, currency: @eth, owner: alice.addr) _stealth_merge_1 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) alice_addr_hex = Encoding.to_hex(alice.addr) bob_addr_hex = Encoding.to_hex(bob.addr) # Assert when payment amount exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 20, "currency" => @eth_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex} ], "outputs" => [ %{"amount" => 20, "currency" => @eth_hex, "owner" => ^bob_addr_hex}, %{"amount" => 10, "currency" => @eth_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) # Assert when payment amount doesn't exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 18, "currency" => @eth_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex} ], "outputs" => [ %{"amount" => 18, "currency" => @eth_hex, "owner" => ^bob_addr_hex}, %{"amount" => 12, "currency" => @eth_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) end @tag fixtures: [:phoenix_ecto_sandbox] test "stealth add 1 more input when 3 inputs with multiple currency covers payment and fee", %{ test_server: context } do alice = OMG.Watcher.TestHelper.generate_entity() bob = OMG.Watcher.TestHelper.generate_entity() prepare_test_server(context, @fee_response) _payment_1 = insert(:txoutput, amount: 5, currency: @other_token, owner: alice.addr) _payment_2 = insert(:txoutput, amount: 5, currency: @other_token, owner: alice.addr) _fee = insert(:txoutput, amount: @default_fee_amount, currency: @eth, owner: alice.addr) _stealth_add_1 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) alice_addr_hex = Encoding.to_hex(alice.addr) bob_addr_hex = Encoding.to_hex(bob.addr) # Assert when payment amount exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 10, "currency" => @other_token_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 5, "currency" => @other_token_hex}, %{"amount" => 5, "currency" => @other_token_hex} ], "outputs" => [ %{"amount" => 10, "currency" => @other_token_hex, "owner" => ^bob_addr_hex}, %{"amount" => 10, "currency" => @eth_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) # Assert when payment amount doesn't exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 8, "currency" => @other_token_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 5, "currency" => @other_token_hex}, %{"amount" => 5, "currency" => @other_token_hex} ], "outputs" => [ %{"amount" => 8, "currency" => @other_token_hex, "owner" => ^bob_addr_hex}, %{"amount" => 10, "currency" => @eth_hex, "owner" => ^alice_addr_hex}, %{"amount" => 2, "currency" => @other_token_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) end @tag fixtures: [:phoenix_ecto_sandbox] test "stealth add 2 more inputs when 2 inputs with single currency covers payments and fee", %{test_server: context} do alice = OMG.Watcher.TestHelper.generate_entity() bob = OMG.Watcher.TestHelper.generate_entity() prepare_test_server(context, @fee_response) _payment_1 = insert(:txoutput, amount: 30, currency: @eth, owner: alice.addr) _fee = insert(:txoutput, amount: @default_fee_amount, currency: @eth, owner: alice.addr) _stealth_add_1 = insert(:txoutput, amount: 20, currency: @eth, owner: alice.addr) _stealth_add_2 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) alice_addr_hex = Encoding.to_hex(alice.addr) bob_addr_hex = Encoding.to_hex(bob.addr) # Assert when payment amount exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 30, "currency" => @eth_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 20, "currency" => @eth_hex}, %{"amount" => 30, "currency" => @eth_hex} ], "outputs" => [ %{"amount" => 30, "currency" => @eth_hex, "owner" => ^bob_addr_hex}, %{"amount" => 30, "currency" => @eth_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) # Assert when payment amount doesn't exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 28, "currency" => @eth_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 20, "currency" => @eth_hex}, %{"amount" => 30, "currency" => @eth_hex} ], "outputs" => [ %{"amount" => 28, "currency" => @eth_hex, "owner" => ^bob_addr_hex}, %{"amount" => 32, "currency" => @eth_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) end @tag fixtures: [:phoenix_ecto_sandbox] test "stealth add 2 more inputs when 2 inputs with multiple currency covers payments and fee", %{ test_server: context } do alice = OMG.Watcher.TestHelper.generate_entity() bob = OMG.Watcher.TestHelper.generate_entity() prepare_test_server(context, @fee_response) _payment_1 = insert(:txoutput, amount: 30, currency: @other_token, owner: alice.addr) _fee = insert(:txoutput, amount: @default_fee_amount, currency: @eth, owner: alice.addr) _stealth_add_1 = insert(:txoutput, amount: 20, currency: @eth, owner: alice.addr) _stealth_add_2 = insert(:txoutput, amount: 10, currency: @eth, owner: alice.addr) alice_addr_hex = Encoding.to_hex(alice.addr) bob_addr_hex = Encoding.to_hex(bob.addr) # Assert when payment amount exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 30, "currency" => @other_token_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 20, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 30, "currency" => @other_token_hex} ], "outputs" => [ %{"amount" => 30, "currency" => @other_token_hex, "owner" => ^bob_addr_hex}, %{"amount" => 30, "currency" => @eth_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) # Assert when payment amount doesn't exactly matched with input amounts params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 28, "currency" => @other_token_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 20, "currency" => @eth_hex}, %{"amount" => 10, "currency" => @eth_hex}, %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 30, "currency" => @other_token_hex} ], "outputs" => [ %{"amount" => 28, "currency" => @other_token_hex, "owner" => ^bob_addr_hex}, %{"amount" => 30, "currency" => @eth_hex, "owner" => ^alice_addr_hex}, %{"amount" => 2, "currency" => @other_token_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) end @tag fixtures: [:phoenix_ecto_sandbox] test "stealth add 1 more input when there're currently 2 inputs and have only 1 utxo left", %{test_server: context} do alice = OMG.Watcher.TestHelper.generate_entity() bob = OMG.Watcher.TestHelper.generate_entity() prepare_test_server(context, @fee_response) _fee = insert(:txoutput, amount: @default_fee_amount, currency: @eth, owner: alice.addr) _payment = insert(:txoutput, amount: 20, currency: @eth, owner: alice.addr) _merge_1 = insert(:txoutput, amount: 30, currency: @eth, owner: alice.addr) alice_addr_hex = Encoding.to_hex(alice.addr) bob_addr_hex = Encoding.to_hex(bob.addr) params = %{ "owner" => alice_addr_hex, "payments" => [ %{"amount" => 20, "currency" => @eth_hex, "owner" => bob_addr_hex} ], "fee" => %{"currency" => @eth_hex} } assert %{ "transactions" => [ %{ "fee" => %{ "amount" => @default_fee_amount, "currency" => @eth_hex }, "inputs" => [ %{"amount" => 20, "currency" => @eth_hex}, %{"amount" => @default_fee_amount, "currency" => @eth_hex}, %{"amount" => 30, "currency" => @eth_hex} ], "outputs" => [ %{"amount" => 20, "currency" => @eth_hex, "owner" => ^bob_addr_hex}, %{"amount" => 30, "currency" => @eth_hex, "owner" => ^alice_addr_hex} ] } ] } = WatcherHelper.success?("transaction.create", params) end end describe "/transaction.create validation" do @tag fixtures: [:alice, :more_utxos] test "incorrect payment in payment list", %{alice: alice} do alice_addr = Encoding.to_hex(alice.addr) assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "payments.amount", "validator" => ":integer" } } } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => alice_addr, "payments" => [%{"amount" => "zonk", "currency" => @other_token_hex, "owner" => alice_addr}], "fee" => %{"currency" => @eth_hex} } ) end @tag fixtures: [:alice, :more_utxos] test "too many payments attempted", %{alice: alice} do alice_addr = Encoding.to_hex(alice.addr) too_many_payments = List.duplicate(%{"amount" => 1, "currency" => @other_token_hex, "owner" => alice_addr}, 5) assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{"parameter" => "payments", "validator" => "{:too_many_payments, 4}"} } } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => alice_addr, "payments" => too_many_payments, "fee" => %{"currency" => @eth_hex} } ) end @tag fixtures: [:phoenix_ecto_sandbox] test "owner should be hex-encoded address" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "owner", "validator" => ":hex" } } } == WatcherHelper.no_success?( "transaction.create", %{"owner" => "not-a-hex"} ) end @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "metadata should be hex-encoded hash", %{alice: alice} do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "metadata", "validator" => ":hex" } } } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [], "fee" => %{"currency" => @eth_hex}, "metadata" => "no-a-hex" } ) end @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "payment should have valid fields", %{alice: alice} do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "payments", "validator" => ":list" } } } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => "not-a-list", "fee" => %{"currency" => @eth_hex} } ) end @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "fee should have valid fields", %{alice: alice} do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "fee.currency", "validator" => ":hex" } } } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [], "fee" => %{"currency" => "123"} } ) end @tag fixtures: [:phoenix_ecto_sandbox, :alice] test "request's fee object is mandatory", %{alice: alice} do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "fee", "validator" => ":map" } } } == WatcherHelper.no_success?( "transaction.create", %{ "owner" => Encoding.to_hex(alice.addr), "payments" => [] } ) end end describe "/transaction.merge" do setup do alice = OMG.Watcher.TestHelper.generate_entity() bob = OMG.Watcher.TestHelper.generate_entity() state = %{ alice: Map.get(alice, :addr), bob: Map.get(bob, :addr) } {:ok, state} end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a merge transaction for the given currency", %{alice: alice} do alice_hex = Encoding.to_hex(alice) insert(:txoutput) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @other_token, owner: alice) _ = insert(:txoutput, amount: 1, currency: @other_token, owner: alice) _ = insert(:txoutput, amount: 1, currency: @other_token, owner: alice) assert %{ "transactions" => [ %{ "typed_data" => _, "txbytes" => _, "inputs" => [ %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex } ], "outputs" => [ %{ "amount" => 3, "currency" => @eth_hex, "owner" => ^alice_hex } ] } ] } = WatcherHelper.success?( "transaction.merge", %{ "address" => alice_hex, "currency" => @eth_hex } ) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns multiple valid merge transactions if more than four UTXO exist for the given currency", %{ alice: alice } do alice_hex = Encoding.to_hex(alice) insert(:txoutput) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) assert %{ "transactions" => [ %{ "typed_data" => _, "txbytes" => _, "inputs" => [ %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex } ], "outputs" => [ %{ "amount" => 4, "currency" => @eth_hex, "owner" => ^alice_hex } ] }, %{ "typed_data" => _, "txbytes" => _, "inputs" => [ %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex } ], "outputs" => [ %{ "amount" => 2, "currency" => @eth_hex, "owner" => ^alice_hex } ] } ] } = WatcherHelper.success?( "transaction.merge", %{ "address" => alice_hex, "currency" => @eth_hex } ) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected error if there is only one input for the given address and currency", %{alice: alice} do alice_hex = Encoding.to_hex(alice) insert(:txoutput) _ = insert(:txoutput, amount: 1, currency: @eth, owner: alice) assert %{ "code" => "merge:single_input", "description" => "Only one input found for the given address and currency." } = WatcherHelper.no_success?( "transaction.merge", %{ "address" => alice_hex, "currency" => @eth_hex } ) end @tag fixtures: [:phoenix_ecto_sandbox] test "request with valid UTXO positions returns correctly formed merge transaction", %{alice: alice} do alice_hex = Encoding.to_hex(alice) insert(:txoutput) position_1 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() position_2 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() position_3 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() assert %{ "transactions" => [ %{ "typed_data" => _, "txbytes" => _, "inputs" => [ %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex }, %{ "amount" => 1, "currency" => @eth_hex, "owner" => ^alice_hex } ], "outputs" => [ %{ "amount" => 3, "currency" => @eth_hex, "owner" => ^alice_hex } ] } ] } = WatcherHelper.success?( "transaction.merge", %{ "utxo_positions" => [position_1, position_2, position_3] } ) end @tag fixtures: [:phoenix_ecto_sandbox] test "does not accept a request with more than four UTXO positions", %{alice: alice} do insert(:txoutput) position_1 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() position_2 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() position_3 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() position_4 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() position_5 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() result = WatcherHelper.no_success?( "transaction.merge", %{ "utxo_positions" => [position_1, position_2, position_3, position_4, position_5] } ) assert result == %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "utxo_positions", "validator" => "{:max_length, 4}" } }, "object" => "error" } end @tag fixtures: [:phoenix_ecto_sandbox] test "does not accept a request with less than two UTXO positions", %{alice: alice} do insert(:txoutput) position_1 = :txoutput |> insert(owner: alice, currency: @eth, amount: 1) |> encoded_position_from_insert() result = WatcherHelper.no_success?( "transaction.merge", %{ "utxo_positions" => [position_1] } ) assert result == %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "utxo_positions", "validator" => "{:min_length, 2}" } }, "object" => "error" } end @tag fixtures: [:phoenix_ecto_sandbox] test "does not accept duplicate input positions", %{alice: alice} do insert(:txoutput) position_1 = :txoutput |> insert(owner: alice, currency: @eth) |> encoded_position_from_insert() assert %{"code" => "merge:duplicate_input_positions"} = WatcherHelper.no_success?( "transaction.merge", %{ "utxo_positions" => [position_1, position_1] } ) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected error if there is more than one owner for a currency", %{alice: alice, bob: bob} do insert(:txoutput) position_1 = :txoutput |> insert(owner: alice, currency: @eth) |> encoded_position_from_insert() position_2 = :txoutput |> insert(owner: bob, currency: @eth) |> encoded_position_from_insert() assert %{"code" => "merge:multiple_input_owners"} = WatcherHelper.no_success?( "transaction.merge", %{ "utxo_positions" => [position_1, position_2] } ) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns expected error if there is more than one currency", %{alice: alice} do insert(:txoutput) position_1 = :txoutput |> insert(owner: alice, currency: @eth) |> encoded_position_from_insert() position_2 = :txoutput |> insert(owner: alice, currency: @other_token) |> encoded_position_from_insert() assert %{"code" => "merge:multiple_currencies"} = WatcherHelper.no_success?( "transaction.merge", %{ "utxo_positions" => [position_1, position_2] } ) end end defp encoded_position_from_insert(%{oindex: oindex, txindex: txindex, blknum: blknum}) do Position.encode({:utxo_position, blknum, txindex, oindex}) end defp get_block(blknum), do: DB.Repo.get(DB.Block, blknum) defp from_hex!(hex) do {:ok, result} = Encoding.from_hex(hex) result end defp to_hex_or_nil(hash) do case hash do nil -> nil hash -> Encoding.to_hex(hash) end end defp transaction_all_with_paging(body) do %{ "success" => true, "data" => data, "data_paging" => paging } = WatcherHelper.rpc_call("transaction.all", body, 200) {data, paging} end defp transaction_all_result(body \\ nil) do {result, paging} = transaction_all_with_paging(body) assert @default_data_paging == paging result end defp balance_in_token(address, token) do currency = Encoding.to_hex(token) Enum.find_value(WatcherHelper.get_balance(address), 0, fn %{"currency" => ^currency, "amount" => amount} -> amount _ -> false end) end defp make_payments(blknum, spender, txs_bytes, blocks_inserter) when is_list(txs_bytes) do recovered_txs = Enum.map(txs_bytes, fn "0x" <> tx -> raw_tx = tx |> Base.decode16!(case: :lower) |> Transaction.decode!() n_inputs = raw_tx |> Transaction.get_inputs() |> length raw_tx |> DevCrypto.sign(List.duplicate(spender.priv, n_inputs)) |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from!() end) blocks_inserter.([{blknum, recovered_txs}]) end defp prepare_test_server(context, response) do response |> TestServer.make_response() |> TestServer.with_response(context, "/fees.all") end defp max_amount_spendable_in_single_tx(address, token) do currency = Encoding.to_hex(token) address |> WatcherHelper.get_utxos() |> Stream.filter(&(&1["currency"] == currency)) |> Enum.sort_by(& &1["amount"], &>=/2) |> Stream.take(Transaction.Payment.max_inputs()) |> Stream.map(& &1["amount"]) |> Enum.sum() end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/utxo_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Controller.UtxoTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.Watcher.Fixtures use OMG.WatcherInfo.Fixtures alias OMG.Watcher.State.Transaction alias OMG.Watcher.State.Transaction.Signed alias OMG.Watcher.Utxo alias OMG.Watcher.Utxo.Position alias Support.WatcherHelper require Utxo @eth <<0::160>> @tag fixtures: [:phoenix_ecto_sandbox, :db_initialized] test "get_exit_data should return error when there is no txs in specfic block" do assert %{ "code" => "exit:invalid", "description" => "Utxo was spent or does not exist.", "object" => "error" } == WatcherHelper.no_success?("utxo.get_exit_data", %{ "utxo_pos" => Position.encode(Utxo.position(7001, 1, 0)) }) end @tag fixtures: [:phoenix_ecto_sandbox, :db_initialized] test "get_exit_data should return error when there is no tx in specfic block" do assert %{ "code" => "exit:invalid", "description" => "Utxo was spent or does not exist.", "object" => "error" } == WatcherHelper.no_success?("utxo.get_exit_data", %{ "utxo_pos" => Position.encode(Utxo.position(7000, 1, 0)) }) end @tag fixtures: [:phoenix_ecto_sandbox, :db_initialized, :bob] test "getting exit data returns properly formatted response", %{bob: bob} do tx = OMG.Watcher.TestHelper.create_signed([{1, 0, 0, bob}], @eth, [{bob, 100}]) tx_encode = Signed.encode(tx) OMG.DB.multi_update([ {:put, :utxo, {{1000, 0, 0}, %{amount: 100, creating_txhash: Transaction.raw_txhash(tx), currency: @eth, owner: bob.addr}}}, {:put, :block, %{number: 1000, hash: <<>>, transactions: [tx_encode]}} ]) %{ "utxo_pos" => _utxo_pos, "txbytes" => _txbytes, "proof" => proof } = WatcherHelper.get_exit_data(1000, 0, 0) assert <<_proof::bytes-size(512)>> = proof end @tag fixtures: [:web_endpoint, :db_initialized] test "getting exit data returns error when there is no txs in specfic block" do utxo_pos = Position.encode(Utxo.position(7000, 1, 0)) assert %{ "object" => "error", "code" => "exit:invalid", "description" => "Utxo was spent or does not exist." } = WatcherHelper.no_success?("utxo.get_exit_data", %{"utxo_pos" => utxo_pos}) end @tag fixtures: [:phoenix_ecto_sandbox] test "utxo.get_exit_data handles improper type of parameter" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "utxo_pos", "validator" => ":integer" } } } == WatcherHelper.no_success?("utxo.get_exit_data", %{"utxo_pos" => "1200000120000"}) end @tag fixtures: [:phoenix_ecto_sandbox] test "utxo.get_exit_data handles too low utxo position inputs" do assert %{ "object" => "error", "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", "messages" => %{ "validation_error" => %{ "parameter" => "utxo_pos", "validator" => "{:greater, 0}" } } } = WatcherHelper.no_success?("utxo.get_exit_data", %{"utxo_pos" => 0}) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/data_case.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.DataCase do @moduledoc """ This module defines the setup for tests requiring access to the application's data layer. You may define functions here to be used as helpers in your tests. Finally, if the test case interacts with the database, it cannot be async. For this reason, every test runs inside a transaction which is reset at the beginning of the test unless the test case is marked as async. """ alias Ecto.Adapters.SQL use ExUnit.CaseTemplate using do quote do alias OMG.WatcherInfo.DB import Ecto import Ecto.Changeset import Ecto.Query import OMG.WatcherRPC.DataCase end end setup tags do :ok = SQL.Sandbox.checkout(OMG.WatcherInfo.DB.Repo) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(OMG.WatcherInfo.DB.Repo, {:shared, self()}) end :ok end @doc """ A helper that transform changeset errors to a map of messages. assert {:error, changeset} = Accounts.create_user(%{password: "short"}) assert "password is too short" in errors_on(changeset).password assert %{password: ["password is too short"]} = errors_on(changeset) """ def errors_on(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> Enum.reduce(opts, message, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", to_string(value)) end) end) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/plugs/method_param_filter_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Plugs.MethodParamFilterTest do use ExUnit.Case, async: true use Plug.Test alias OMG.WatcherRPC.Web.Plugs.MethodParamFilter # this test seems like it's reaching far to deep into internals of plugs test "filters query params for POST" do conn = :post |> conn("/some_endpoint?foo=bar", %{"foo_1" => "bar_1"}) |> Plug.Parsers.call({[:json], [], nil, false}) |> MethodParamFilter.call([]) assert conn.body_params == %{"foo_1" => "bar_1"} assert conn.query_params == %{} assert conn.params == %{"foo_1" => "bar_1"} end # this test seems like it's reaching far to deep into internals of plugs test "filters body params for GET" do conn = MethodParamFilter.call( %Plug.Conn{body_params: %{"foo_1" => "bar_1"}, query_params: %{"foo" => "bar"}, method: "GET"}, [] ) assert conn.body_params == %{} assert conn.query_params == %{"foo" => "bar"} assert conn.params == %{"foo" => "bar"} end test "returns original conn for other methods" do original_conn = conn(:put, "/some_endpoint?foo=bar", %{"foo_1" => "bar_1"}) assert MethodParamFilter.call(original_conn, []) == original_conn end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/plugs/supported_watcher_modes_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Plugs.SupportedWatcherModesTest do # async: false because it needs to manipulate the global :api_mode application env. use ExUnit.Case, async: false use Plug.Test alias OMG.WatcherRPC.Web.Plugs.SupportedWatcherModes @app :omg_watcher_rpc setup do original_mode = Application.get_env(@app, :api_mode) on_exit(fn -> _ = Application.put_env(@app, :api_mode, original_mode) end) conn = :post |> conn("/some_endpoint", %{}) |> Phoenix.Controller.accepts(["json"]) {:ok, %{conn: conn}} end test "returns the original conn if the API mode matches a supported modes", context do :ok = Application.put_env(@app, :api_mode, :watcher_info) conn = SupportedWatcherModes.call(context.conn, [:watcher, :watcher_info]) assert conn == context.conn end test "returns operation:not_found if the API mode does not match a supported modes", context do :ok = Application.put_env(@app, :api_mode, :watcher) conn = SupportedWatcherModes.call(context.conn, [:watcher_info]) assert conn.assigns[:code] == "operation:not_found" end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/response_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.ResponseTest do # async: false because it needs to manipulate the global :api_mode application env. use ExUnit.Case, async: false alias OMG.WatcherRPC.Web.Response @app :omg_watcher_rpc setup do original_mode = Application.get_env(@app, :api_mode) on_exit(fn -> _ = Application.put_env(@app, :api_mode, original_mode) end) end describe "add_app_infos/1" do test "appends the given map with a service_name and semver-compliant version" do :ok = Application.put_env(@app, :api_mode, :watcher) assert %{foo: "bar", service_name: "watcher", version: version} = Response.add_app_infos(%{foo: "bar"}) assert {:ok, _} = Version.parse(version) end test "appends the given map with the correct service_name" do :ok = Application.put_env(@app, :api_mode, :watcher) assert %{foo: "bar", service_name: "watcher"} = Response.add_app_infos(%{foo: "bar"}) :ok = Application.put_env(@app, :api_mode, :watcher_info) assert %{foo: "bar", service_name: "watcher_info"} = Response.add_app_infos(%{foo: "bar"}) end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/router_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.RouterTest do # async: false as we need to change :api_mode application env. use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures alias Support.WatcherHelper setup do original_mode = Application.get_env(:omg_watcher_rpc, :api_mode) _ = on_exit(fn -> Application.put_env(:omg_watcher_rpc, :api_mode, original_mode) end) :ok end @tag fixtures: [:phoenix_ecto_sandbox] test "returns a successful response when calling an :info_api endpoint from :watcher_info mode" do :ok = Application.put_env(:omg_watcher_rpc, :api_mode, :watcher_info) assert WatcherHelper.success?("transaction.all", %{}) end @tag fixtures: [:phoenix_ecto_sandbox] test "returns an error response when calling an :info_api endpoint from :watcher mode" do :ok = Application.put_env(:omg_watcher_rpc, :api_mode, :watcher) assert %{ "object" => "error", "code" => "operation:not_found", "description" => "Operation cannot be found. Check request URL." } == WatcherHelper.no_success?("transaction.all", %{}) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/account_contraints_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.AccountConstraintsTest do @moduledoc """ Account constraints validate test """ use ExUnit.Case, async: true alias OMG.Eth.Encoding alias OMG.WatcherRPC.Web.Validator.AccountConstraints @fake_address_hex_string "0x7977fe798feef376b74b6c1c5ebce8a2ccf02afd" describe("parse/1") do test "returns page, limit and adress constraints when given page, limit and adress" do request_data = %{ "page" => 1, "limit" => 100, "address" => @fake_address_hex_string } {:ok, constraints} = AccountConstraints.parse(request_data) assert constraints == [ address: Encoding.from_hex(@fake_address_hex_string), page: 1, limit: 100 ] end test "return error if does not provide address" do request_data = %{ "page" => 1, "limit" => 100 } assert AccountConstraints.parse(request_data) == {:error, {:validation_error, "address", :hex}} end test "return error if limit exceed 1000" do request_data = %{ "address" => @fake_address_hex_string, "page" => 1, "limit" => 2200 } assert AccountConstraints.parse(request_data) == {:error, {:validation_error, "limit", {:lesser, 1000}}} end test "return address if only address is provided" do request_data = %{ "address" => @fake_address_hex_string } {:ok, constraints} = AccountConstraints.parse(request_data) assert constraints == [ address: Encoding.from_hex(@fake_address_hex_string) ] end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/block_constraints_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.BlockConstraintsTest do use ExUnit.Case, async: true alias OMG.WatcherRPC.Web.Validator.BlockConstraints @valid_block %{ "hash" => "0x" <> String.duplicate("00", 32), "number" => 1000, "transactions" => ["0x00"] } describe "parse/1" do test "returns page and limit constraints when given page and limit params" do request_data = %{"page" => 1, "limit" => 100} {:ok, constraints} = BlockConstraints.parse(request_data) assert constraints == [page: 1, limit: 100] end test "returns empty constraints when given no params" do request_data = %{} {:ok, constraints} = BlockConstraints.parse(request_data) assert constraints == [] end test "returns a :validation_error when the given page == 0" do assert BlockConstraints.parse(%{"page" => 0}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given page < 0" do assert BlockConstraints.parse(%{"page" => -1}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given page is not an integer" do assert BlockConstraints.parse(%{"page" => 3.14}) == {:error, {:validation_error, "page", :integer}} assert BlockConstraints.parse(%{"page" => "abcd"}) == {:error, {:validation_error, "page", :integer}} end test "returns a :validation_error when the given limit == 0" do assert BlockConstraints.parse(%{"page" => 0}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given limit < 0" do assert BlockConstraints.parse(%{"page" => -1}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given limit is not an integer" do assert BlockConstraints.parse(%{"page" => 3.14}) == {:error, {:validation_error, "page", :integer}} assert BlockConstraints.parse(%{"page" => "abcd"}) == {:error, {:validation_error, "page", :integer}} end end describe "parse_to_validate/1" do test "rejects invalid Merkle root hash" do invalid_hash = "0x1234" invalid_block = Map.replace!(@valid_block, "hash", invalid_hash) assert {:error, {:validation_error, "hash", {:length, 32}}} == BlockConstraints.parse_to_validate(invalid_block) end test "rejects non-list transactions parameter" do invalid_transactions_param = "0x1234" invalid_block = Map.replace!(@valid_block, "transactions", invalid_transactions_param) assert {:error, {:validation_error, "transactions", :list}} == BlockConstraints.parse_to_validate(invalid_block) end test "rejects non-hex elements in transactions list" do invalid_tx_rlp = "0xZ" invalid_block = Map.replace!(@valid_block, "transactions", [invalid_tx_rlp]) assert {:error, {:validation_error, "transactions.hash", :hex}} == BlockConstraints.parse_to_validate(invalid_block) end test "rejects invalid block number parameter" do invalid_blknum = "ONE THOUSAND" invalid_block = Map.replace!(@valid_block, "number", invalid_blknum) assert {:error, {:validation_error, "number", :integer}} == BlockConstraints.parse_to_validate(invalid_block) end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/merge_constraints_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.MergeConstraintsTest do use ExUnit.Case, async: true alias OMG.Eth.Encoding alias OMG.WatcherRPC.Web.Validator.MergeConstraints @eth Encoding.to_hex(<<0::160>>) @alice Encoding.to_hex(<<1::160>>) describe "parse/1" do test "fails if unrecognized parameters are passed in" do request_data = %{ "foo" => "bar" } assert MergeConstraints.parse(request_data) == {:error, :operation_bad_request} end test "returns address and currency when given valid address and currency params" do request_data = %{ "address" => @alice, "currency" => @eth } {:ok, constraints} = MergeConstraints.parse(request_data) assert constraints == [{:currency, Encoding.from_hex(@eth)}, {:address, Encoding.from_hex(@alice)}] end test "fails address/currency constraints when address is not in the right format" do request_data = %{ "address" => "0xFake", "currency" => @eth } assert MergeConstraints.parse(request_data) == {:error, {:validation_error, "address", :hex}} end test "fails address/currency constraints when currency is not in the right format" do request_data = %{ "address" => @alice, "currency" => "0xFake" } assert MergeConstraints.parse(request_data) == {:error, {:validation_error, "currency", :hex}} end test "returns `utxo_positions` when given parameter is valid" do request_data = %{ "utxo_positions" => [1, 2] } {:ok, constraints} = MergeConstraints.parse(request_data) assert constraints == [{:utxo_positions, [1, 2]}] end test "fails utxo_positions constraints when given less than two positions" do request_data = %{ "utxo_positions" => [1] } assert MergeConstraints.parse(request_data) == {:error, {:validation_error, "utxo_positions", {:min_length, 2}}} end test "fails utxo_positions constraints when given more than four positions" do request_data = %{ "utxo_positions" => [1, 2, 3, 4, 5] } assert MergeConstraints.parse(request_data) == {:error, {:validation_error, "utxo_positions", {:max_length, 4}}} end test "fails utxo_positions constraints when given a random string" do request_data = %{ "utxo_positions" => [1, 2, "foo"] } assert MergeConstraints.parse(request_data) == {:error, {:validation_error, "utxo_positions.utxo_pos", :integer}} end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/transaction_constraints_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validator.TransactionConstraintsTest do use ExUnit.Case, async: true alias OMG.Eth.Encoding alias OMG.WatcherRPC.Web.Validator.TransactionConstraints @eth <<0::160>> @zero_metadata <<0::256>> describe "parse/1" do test "returns page and limit constraints when given page and limit params" do request_data = %{"page" => 1, "limit" => 100} {:ok, constraints} = TransactionConstraints.parse(request_data) assert constraints == [page: 1, limit: 100] end test "returns supported constraints when given" do request_data = %{ "address" => Encoding.to_hex(@eth), "blknum" => 1000, "metadata" => Encoding.to_hex(@zero_metadata), "txtypes" => [1, 3], "end_datetime" => 12_345_678 } {:ok, constraints} = TransactionConstraints.parse(request_data) assert constraints == [ end_datetime: 12_345_678, txtypes: [1, 3], metadata: @zero_metadata, blknum: 1000, address: @eth ] end test "filters unsupported constraints" do request_data = %{ "something" => "123" } {:ok, constraints} = TransactionConstraints.parse(request_data) assert constraints == [] end test "returns empty constraints when given no params" do request_data = %{} {:ok, constraints} = TransactionConstraints.parse(request_data) assert constraints == [] end test "returns validation errors when given invalid tx_types" do assert TransactionConstraints.parse(%{"txtypes" => 1}) == {:error, {:validation_error, "txtypes", :list}} assert TransactionConstraints.parse(%{"txtypes" => Enum.to_list(1..17)}) == {:error, {:validation_error, "txtypes", {:max_length, 16}}} assert TransactionConstraints.parse(%{"txtypes" => ["1"]}) == {:error, {:validation_error, "txtypes.txtype", :integer}} end test "returns a :validation_error when the given page == 0" do assert TransactionConstraints.parse(%{"page" => 0}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given page < 0" do assert TransactionConstraints.parse(%{"page" => -1}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given page is not an integer" do assert TransactionConstraints.parse(%{"page" => 3.14}) == {:error, {:validation_error, "page", :integer}} assert TransactionConstraints.parse(%{"page" => "abcd"}) == {:error, {:validation_error, "page", :integer}} end test "returns a :validation_error when the given limit == 0" do assert TransactionConstraints.parse(%{"page" => 0}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given limit < 0" do assert TransactionConstraints.parse(%{"page" => -1}) == {:error, {:validation_error, "page", {:greater, 0}}} end test "returns a :validation_error when the given limit is not an integer" do assert TransactionConstraints.parse(%{"page" => 3.14}) == {:error, {:validation_error, "page", :integer}} assert TransactionConstraints.parse(%{"page" => "abcd"}) == {:error, {:validation_error, "page", :integer}} end test "returns a :validation_error when the given end_datetime is not an integer" do assert TransactionConstraints.parse(%{"end_datetime" => 3.14}) == {:error, {:validation_error, "end_datetime", :integer}} assert TransactionConstraints.parse(%{"end_datetime" => "abcd"}) == {:error, {:validation_error, "end_datetime", :integer}} end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/typed_data_signed_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.Validators.TypedDataSignedTest do use ExUnitFixtures use ExUnit.Case, async: true alias OMG.Utils.HttpRPC.Encoding alias OMG.Watcher.State.Transaction alias OMG.Watcher.TestHelper alias OMG.Watcher.Utxo alias OMG.WatcherRPC.Web.Validator.TypedDataSigned require Utxo @eth <<0::160>> @other_token <<127::160>> @eth_hex Encoding.to_hex(@eth) @token_hex Encoding.to_hex(@other_token) @alice TestHelper.generate_entity() @bob TestHelper.generate_entity() @ari_network_address "44DE0EC539B8C4A4B530C78620FE8320167F2F74" |> Base.decode16!() @eip_domain %{ name: "OMG Network", version: "1", salt: <<0::256>>, verifyingContract: @ari_network_address } defp get_message() do alice_addr = Encoding.to_hex(@alice.addr) bob_addr = Encoding.to_hex(@bob.addr) %{ "input0" => %{"blknum" => 1000, "txindex" => 0, "oindex" => 1}, "input1" => %{"blknum" => 3001, "txindex" => 0, "oindex" => 0}, "input2" => %{"blknum" => 0, "txindex" => 0, "oindex" => 0}, "input3" => %{"blknum" => 0, "txindex" => 0, "oindex" => 0}, "output0" => %{"owner" => alice_addr, "currency" => @eth_hex, "amount" => 10}, "output1" => %{"owner" => alice_addr, "currency" => @token_hex, "amount" => 300}, "output2" => %{"owner" => bob_addr, "currency" => @token_hex, "amount" => 100}, "output3" => %{"owner" => @eth_hex, "currency" => @eth_hex, "amount" => 0}, "metadata" => Encoding.to_hex(<<0::256>>) } end defp get_domain(network) do %{ "name" => network, "version" => "1", "salt" => Encoding.to_hex(<<0::256>>), "verifyingContract" => @ari_network_address |> Encoding.to_hex() } end test "parses transaction from message data" do params = %{"message" => get_message()} assert {:ok, tx} = TypedDataSigned.parse_transaction(params) assert [Utxo.position(1000, 0, 1), Utxo.position(3001, 0, 0)] == Transaction.get_inputs(tx) alice_addr = @alice.addr bob_addr = @bob.addr assert [ %{owner: ^alice_addr, currency: @eth, amount: 10}, %{owner: ^alice_addr, currency: @other_token, amount: 300}, %{owner: ^bob_addr, currency: @other_token, amount: 100} ] = Transaction.get_outputs(tx) assert <<0::256>> == tx.metadata end test "parses transaction with metadata from message data" do metadata = (@alice.addr <> @bob.addr) |> OMG.Watcher.Crypto.hash() params = %{"message" => %{get_message() | "metadata" => Encoding.to_hex(metadata)}} assert {:ok, tx} = TypedDataSigned.parse_transaction(params) assert tx.metadata == metadata end test "validates message correctness" do invalid_input_blknum = Map.put(get_message(), "input2", %{"blknum" => -1, "txindex" => 0, "oindex" => 1}) assert {:error, {:validation_error, "input2.blknum", {:greater, -1}}} == TypedDataSigned.parse_transaction(%{"message" => invalid_input_blknum}) invalid_owner_addr = Map.put(get_message(), "output1", %{"owner" => "0x", "currency" => @eth_hex, "amount" => 10}) assert {:error, {:validation_error, "output1.owner", {:length, 20}}} == TypedDataSigned.parse_transaction(%{"message" => invalid_owner_addr}) end test "parses eip712 domain" do assert {:ok, @eip_domain} == "OMG Network" |> get_domain() |> TypedDataSigned.parse_domain() end test "ensures network domains match" do correct_domain = @eip_domain incorrect_domain = %{@eip_domain | name: "Z0nk"} assert TypedDataSigned.ensure_network_match(correct_domain, @eip_domain) assert {:error, {:validation_error, "domain", :domain_separator_mismatch}} == TypedDataSigned.ensure_network_match(incorrect_domain, @eip_domain) end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/view_case.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.ViewCase do @moduledoc """ This module defines common behaviors shared between view tests. """ use ExUnit.CaseTemplate using do quote do use ExUnit.Case import Phoenix.View end end end ================================================ FILE: apps/omg_watcher_rpc/test/omg_watcher_rpc/web/views/transaction_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule OMG.WatcherRPC.Web.View.TransactionTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures alias OMG.Utils.Paginator alias OMG.Watcher.Utxo alias OMG.WatcherInfo.DB alias OMG.WatcherRPC.Web.View require Utxo describe "render/2 with transaction.json" do @tag fixtures: [:initial_blocks] test "renders the transaction's inputs and outputs" do transaction = 1000 |> DB.Transaction.get_by_position(1) |> DB.Repo.preload([:inputs, :outputs]) rendered = View.Transaction.render("transaction.json", %{response: transaction}) # Asserts all transaction inputs get rendered assert Map.has_key?(rendered.data, :inputs) assert utxos_match_all?(rendered.data.inputs, transaction.inputs) # Asserts all transaction outputs get rendered assert Map.has_key?(rendered.data, :outputs) assert utxos_match_all?(rendered.data.outputs, transaction.outputs) end end describe "render/2 with transactions.json" do @tag fixtures: [:initial_blocks] test "renders the transactions' inputs and outputs" do tx_1 = DB.Transaction.get_by_position(1000, 0) |> DB.Repo.preload([:inputs, :outputs]) tx_2 = DB.Transaction.get_by_position(1000, 1) |> DB.Repo.preload([:inputs, :outputs]) paginator = %Paginator{ data: [tx_1, tx_2], data_paging: %{ limit: 10, page: 1 } } rendered = View.Transaction.render("transactions.json", %{response: paginator}) [rendered_1, rendered_2] = rendered.data assert utxos_match_all?(rendered_1.inputs, tx_1.inputs) assert utxos_match_all?(rendered_1.outputs, tx_1.outputs) assert utxos_match_all?(rendered_2.inputs, tx_2.inputs) assert utxos_match_all?(rendered_2.outputs, tx_2.outputs) end end defp utxos_match_all?(renders, originals) when length(renders) != length(originals), do: false defp utxos_match_all?(renders, originals) do original_utxo_positions = Enum.map(originals, fn utxo -> Utxo.position(utxo.blknum, utxo.txindex, utxo.oindex) |> Utxo.Position.encode() end) Enum.all?(renders, fn rendered -> rendered.utxo_pos in original_utxo_positions end) end end ================================================ FILE: apps/omg_watcher_rpc/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ExUnit.configure(exclude: [mix_based_child_chain: true, integration: true, property: true, wrappers: true]) ExUnitFixtures.start() ExUnit.start() {:ok, _} = Application.ensure_all_started(:httpoison) {:ok, _} = Application.ensure_all_started(:fake_server) {:ok, _} = Application.ensure_all_started(:ex_machina) Mix.Task.run("ecto.create", ~w(--quiet)) Mix.Task.run("ecto.migrate", ~w(--quiet)) ================================================ FILE: apps/xomg_tasks/lib/mix/tasks/watcher.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Mix.Tasks.Xomg.Watcher.Start do @moduledoc """ Contains mix.task to run the watcher in security-critical only modes. See the README.md file. """ use Mix.Task import XomgTasks.Utils @shortdoc "Starts the security-critical watcher. See Mix.Tasks.Watcher." def run(args) do args |> config_db("--db") |> generic_prepare_args() |> generic_run([:omg_watcher, :omg_watcher_rpc]) end end ================================================ FILE: apps/xomg_tasks/lib/mix/tasks/watcher_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Mix.Tasks.Xomg.WatcherInfo.Start do @moduledoc """ Contains mix.task to run the watcher in security-critical + informational mode. See the README.md file. """ use Mix.Task import XomgTasks.Utils @shortdoc "Starts the security-critical + informational watcher. See Mix.Tasks.Xomg.WatcherInfo.Start." def run(args) do args |> generic_prepare_args() |> generic_run([:omg_watcher_info, :omg_watcher_rpc]) end end ================================================ FILE: apps/xomg_tasks/lib/utils.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule XomgTasks.Utils do @moduledoc """ Common convenience code used to run Mix.Tasks goes here """ @doc """ Runs a specific app for some arguments. Will handle IEx, if one's running """ def generic_run(_args, apps) when is_list(apps) do _ = Enum.each(apps, fn app -> {:ok, _} = Application.ensure_all_started(app) end) iex_running?() || Process.sleep(:infinity) end @doc """ Will do all the generic preparations on the arguments required """ def generic_prepare_args(args) do args |> ensure_contains("--no-start") |> ensure_doesnt_contain("--no-halt") end def config_db(args, arg) do index = Enum.find_index(args, fn x -> x == arg end) Application.put_env(:omg_db, :path, Enum.at(args, index + 1), persistent: true) args end def config_logger_level(args, arg) do index = Enum.find_index(args, fn x -> x == arg end) :ok = Logger.configure(level: String.to_atom(Enum.at(args, index + 1))) args end defp iex_running?() do Code.ensure_loaded?(IEx) and IEx.started?() end defp ensure_contains(args, arg) do if Enum.member?(args, arg) do args else [arg | args] end end defp ensure_doesnt_contain(args, arg) do List.delete(args, arg) end end ================================================ FILE: apps/xomg_tasks/mix.exs ================================================ defmodule OMG.XomgTasks.MixProject do @moduledoc """ This is just a proxy app to hold and use all the code related to running `xomg` Mix.Tasks. NOTE: this is not a proper mix app, just some Mix.Tasks which call into other mix apps """ use Mix.Project def project() do [ app: :xomg_tasks, version: version(), build_path: "../../_build", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: ["lib"], start_permanent: false, deps: [] ] end def application() do [extra_applications: [:iex, :logger]] end defp version() do "git" |> System.cmd(["describe", "--tags", "--abbrev=0"]) |> elem(0) |> String.replace("v", "") |> String.replace("\n", "") end end ================================================ FILE: apps/xomg_tasks/test/test_helper.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: bin/generate-localchain-env ================================================ #!/bin/bash # this script takes in arguments (in key value pairs) and template file (localchain_contract_addresses.env) # looks for keys and replaces them with valuesß for ARGUMENT in "$@" do KEY=$(echo $ARGUMENT | cut -f1 -d=) VALUE=$(echo $ARGUMENT | cut -f2 -d=) case "$KEY" in AUTHORITY_ADDRESS) AUTHORITY_ADDRESS=${VALUE} ;; ETH_VAULT) ETH_VAULT=${VALUE} ;; ERC20_VAULT) ERC20_VAULT=${VALUE} ;; PAYMENT_EXIT_GAME) PAYMENT_EXIT_GAME=${VALUE} ;; CONTRACT_ADDRESS_PAYMENT_EXIT_GAME) CONTRACT_ADDRESS_PAYMENT_EXIT_GAME=${VALUE} ;; PLASMA_FRAMEWORK_TX_HASH) PLASMA_FRAMEWORK_TX_HASH=${VALUE} ;; PLASMA_FRAMEWORK) PLASMA_FRAMEWORK=${VALUE} ;; PAYMENT_EIP712_LIBMOCK) PAYMENT_EIP712_LIBMOCK=${VALUE} ;; MERKLE_WRAPPER) MERKLE_WRAPPER=${VALUE} ;; ERC20_MINTABLE) ERC20_MINTABLE=${VALUE} ;; *) esac done sed 's/{AUTHORITY_ADDRESS}/'$AUTHORITY_ADDRESS'/g' ../contract_addresses_template.env | \ sed 's/{CONTRACT_ADDRESS_ETH_VAULT}/'$ETH_VAULT'/g' | \ sed 's/{CONTRACT_ADDRESS_ERC20_VAULT}/'$ERC20_VAULT'/g' | \ sed 's/{CONTRACT_ADDRESS_PAYMENT_EXIT_GAME}/'$PAYMENT_EXIT_GAME'/g' | \ sed 's/{TXHASH_CONTRACT}/'$PLASMA_FRAMEWORK_TX_HASH'/g' | \ sed 's/{CONTRACT_ADDRESS_PLASMA_FRAMEWORK}/'$PLASMA_FRAMEWORK'/g' | \ sed 's/{CONTRACT_ADDRESS_PAYMENT_EIP_712_LIB_MOCK}/'$PAYMENT_EIP712_LIBMOCK'/g' | \ sed 's/{CONTRACT_ADDRESS_MERKLE_WRAPPER}/'$MERKLE_WRAPPER'/g' | \ sed 's/{CONTRACT_ERC20_MINTABLE}/'$ERC20_MINTABLE'/g' \ > ../localchain_contract_addresses.env ================================================ FILE: bin/revert ================================================ #!/bin/bash # This is for geth # https://gist.github.com/gluk64/fdea559472d957f1138ed93bcbc6f78a # Fetching revert reason -- https://ethereum.stackexchange.com/questions/48383/how-to-receive-revert-reason-for-past-transactions if [ -z "$1" ] then echo "Usage: revert-reason " exit fi TX=$1 SCRIPT=" tx = eth.getTransaction( \"$TX\" ); tx.data = tx.input; eth.call(tx, tx.blockNumber)" geth --exec "$SCRIPT" attach http://localhost:8545 | cut -d '"' -f 2 | cut -c139- | xxd -r -p echo ================================================ FILE: bin/rocksdb ================================================ #!/usr/bin/env sh fancy_echo() { local fmt="$1"; shift # shellcheck disable=SC2059 printf "\\n$fmt\\n" "$@" } install_linux_rocksdb(){ echo "Installing Rocksdb v6.14.5" mkdir tmp_rocksdb \ && cd tmp_rocksdb \ && git clone https://github.com/facebook/rocksdb.git \ && cd rocksdb \ && git checkout v6.14.5 \ && make shared_lib \ && sudo mkdir -p /usr/local/rocksdb/lib \ && sudo mkdir -p /usr/local/rocksdb/include \ && sudo cp -P librocksdb.so* /usr/local/rocksdb/lib \ && sudo cp -P /usr/local/rocksdb/lib/librocksdb.so* /usr/lib/ \ && sudo cp -rP include /usr/local/rocksdb/ \ && sudo cp -rP include/* /usr/include/ \ && cd ../../ \ && rm -rf tmp_rocksdb } install_rocksdb_source(){ git clone https://github.com/facebook/rocksdb.git cd rocksdb/ git checkout tags/v6.14.5 make clean make static_lib make shared_lib make install INSTALL_PATH=/usr/local/Cellar/rocksdb/6.14.5 } install_deps() { local sys=`uname -s` echo $sys case $sys in Linux*) install_linux_rocksdb ;; Darwin*) install_rocksdb_source ;; *) fancy_echo "Unknown system" exit 1 ;; esac } # Exit if any subcommand fails set -e install_deps ================================================ FILE: bin/setup ================================================ #!/usr/bin/env sh fancy_echo() { local fmt="$1"; shift # shellcheck disable=SC2059 printf "\\n$fmt\\n" "$@" } install_linux_deps() { local LINUX_DEPS="automake autoconf autogen libtool gcc curl cmake gnupg liblz4-dev libsnappy-dev librocksdb-dev" local flavor=`grep "^ID=" /etc/os-release | cut -d"=" -f 2` echo $flavor case $flavor in debian|ubuntu) sudo apt-get install -y $LINUX_DEPS libgmp-dev erlang-dev build-essential libssl-dev libncurses5-dev unzip lsb-release software-properties-common ;; fedora|centos) sudo yum install $LINUX_DEPS gmp-devel ;; *) fancy_echo "Unrecognized distribution $flavor" exit 1 ;; esac } install_linux_postgres(){ echo "Installing Postgres 9.6." echo "Adding public key 7FCC7D46ACCC4CF8 to repo." sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7FCC7D46ACCC4CF8 sudo add-apt-repository "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -sc)-pgdg main" wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt-get update sudo apt-get install postgresql-9.6 -y sudo apt-get install postgresql-client-9.6 echo "Start PG." sudo service postgresql start echo "Create user." sudo -i -u postgres psql -c "CREATE USER omisego_dev WITH PASSWORD 'omisego_dev';" echo "Grant permission." sudo -i -u postgres psql -c "ALTER USER omisego_dev CREATEDB;" echo "Done with PG." } install_linux_geth(){ echo "Installing Geth 1.9.12" mkdir gethDownload \ && wget -qO- "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.12-b6f1c8dc.tar.gz" | tar xvz -C gethDownload --strip-components 1 \ && cd gethDownload \ && mkdir -p /usr/local/bin \ && sudo mv geth /usr/local/bin/ \ && cd .. \ && rm -r gethDownload } install_darwin_deps(){ fancy_echo "Installing dependencies via brew" brew bundle --file=- <> ~/.bashrc #echo -e '\n. $HOME/.asdf/completions/asdf.bash' >> ~/.bashrc #source ~/.bashrc #MacOS #echo -e '\n. $HOME/.asdf/asdf.sh' >> ~/.bash_profile #echo -e '\n. $HOME/.asdf/completions/asdf.bash' >> ~/.bash_profile #source ~/.bash_profile #Linux and MacOS #asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git #asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git #asdf install if ! command -v mix > /dev/null; then fancy_echo "It looks like you don't have Elixir installed." echo "See https://asdf-vm.com/#/core-manage-asdf-vm for instructions." echo "See https://github.com/asdf-vm/asdf-elixir for instructions." echo "See https://github.com/asdf-vm/asdf-erlang for instructions." exit 1 fi mix local.hex --force if ! command -v mix > /dev/null; then fancy_echo "\`mix\`: command not found" fancy_echo "Please add \`~/.mix\` to your \$PATH environment variable" exit 1 fi fancy_echo "Installing elixir dependencies." mix local.hex --force mix local.rebar --force fancy_echo "You're all set!" ================================================ FILE: bin/variables ================================================ #!/usr/bin/env sh export APP_ENV=local-development export HOSTNAME=http://localhost/ export DB_PATH=~/plasma-data/ export ETHEREUM_RPC_URL=http://127.0.0.1:8545 export ETH_NODE=geth export ETHEREUM_NETWORK=LOCALCHAIN export DATABASE_URL=postgresql://omisego_dev:omisego_dev@127.0.0.1:5432/omisego_dev export CHILD_CHAIN_URL=http://127.0.0.1:9656 export ETHEREUM_HEIGHT_CHECK_INTERVAL_MS=800 export ETHEREUM_EVENTS_CHECK_INTERVAL_MS=800 export ETHEREUM_STALLED_SYNC_THRESHOLD_MS=20000 export EXIT_PROCESSOR_SLA_MARGIN=5520 export FEE_CLAIMER_ADDRESS=0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837 export FEE_ADAPTER=feed export FEE_FEED_URL=http://127.0.0.1:4000/api/v1 export STORED_FEE_UPDATE_INTERVAL_MINUTES=1 export FEE_CHANGE_TOLERANCE_PERCENT=1 export FEE_SPECS_FILE_PATH=./priv/dev-artifacts/fee_specs.dev.json export DD_HOSTNAME=localhost export DD_DISABLED=true export LOGGER_BACKEND=console export RELEASE_COOKIE=development export NODE_HOST=127.0.0.1 # expects it's executed from the root of the project FILE='./localchain_contract_addresses.env' while IFS= read -r line; do DATA_TO_EXPORT='export '$line eval $DATA_TO_EXPORT done < ${FILE} ================================================ FILE: bin/variables_test_barebone ================================================ #!/usr/bin/env sh # this is a version of the bin/variables script which enables cabbage integration tests to run against a test-friendly # setup of our services # test_barebone_release taylored values. Compare also with priv/cabbage/docker-compose-cabbage.yml export EXIT_PROCESSOR_SLA_MARGIN=30 # the rest stays same as bin/variables export APP_ENV=local-development export HOSTNAME=http://localhost/ export DB_PATH=~/plasma-data/ export ETHEREUM_RPC_URL=http://127.0.0.1:8545 export ETH_NODE=geth export ETHEREUM_NETWORK=LOCALCHAIN export DATABASE_URL=postgresql://omisego_dev:omisego_dev@127.0.0.1:5432/omisego_dev export CHILD_CHAIN_URL=http://127.0.0.1:9656 export ETHEREUM_HEIGHT_CHECK_INTERVAL_MS=800 export ETHEREUM_EVENTS_CHECK_INTERVAL_MS=800 export ETHEREUM_STALLED_SYNC_THRESHOLD_MS=20000 export FEE_CLAIMER_ADDRESS=0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837 export FEE_ADAPTER=feed export FEE_FEED_URL=http://127.0.0.1:4000/api/v1 export STORED_FEE_UPDATE_INTERVAL_MINUTES=1 export FEE_CHANGE_TOLERANCE_PERCENT=1 # Fee specs file path needs to be an absolute path as the childchain will start deep in the _build subdirectory export FEE_SPECS_FILE_PATH=$(pwd)/priv/dev-artifacts/fee_specs.dev.json export DD_HOSTNAME=localhost export DD_DISABLED=true export LOGGER_BACKEND=console export RELEASE_COOKIE=development export NODE_HOST=127.0.0.1 # expects it's executed from the root of the project FILE='./localchain_contract_addresses.env' while IFS= read -r line; do DATA_TO_EXPORT='export '$line eval $DATA_TO_EXPORT done < ${FILE} ================================================ FILE: config/.credo.exs ================================================ # This file contains the configuration for Credo and you are probably reading # this after creating it with `mix credo.gen.config`. # # If you find anything wrong or unclear in this file, please report an # issue on GitHub: https://github.com/rrrene/credo/issues # %{ # # You can have as many configs as you like in the `configs:` field. configs: [ %{ # # Run any exec using `mix credo -C `. If no exec name is given # "default" is used. # name: "default", # # These are the files included in the analysis: files: %{ # # You can give explicit globs or simply directories. # In the latter case `**/*.{ex,exs}` will be used. # included: ["lib/", "src/", "test/", "web/", "apps/", "config/", "mix.exs"], excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] }, # # If you create your own checks, you must specify the source files for # them here, so they can be loaded by Credo before running the analysis. # requires: [ "config/credo/license_header.ex", "config/credo/require_parentheses_on_zero_arity_defs.ex" ], # # If you want to enforce a style guide and need a more traditional linting # experience, you can change `strict` to `true` below: # strict: true, # # If you want to use uncolored output by default, you can change `color` # to `false` below: # color: true, # # You can customize the parameters of any check by adding a second element # to the tuple. # # To disable a check put `false` as second element: # # {Credo.Check.Design.DuplicatedCode, false} # checks: [ {Credo.Check.Refactor.MapInto, false}, # custom checks {Credo.Check.Warning.LicenseHeader}, {Credo.Check.Readability.RequireParenthesesOnZeroArityDefs}, # ## Consistency Checks # {Credo.Check.Consistency.ExceptionNames, []}, {Credo.Check.Consistency.LineEndings, []}, {Credo.Check.Consistency.ParameterPatternMatching, []}, {Credo.Check.Consistency.SpaceAroundOperators, []}, {Credo.Check.Consistency.SpaceInParentheses, []}, {Credo.Check.Consistency.TabsOrSpaces, []}, # ## Design Checks # # You can customize the priority of any check # Priority values are: `low, normal, high, higher` # {Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 3, if_called_more_often_than: 1]}, # You can also customize the exit_status of each check. # If you don't want TODO comments to cause `mix credo` to fail, just # set this value to 0 (zero). # {Credo.Check.Design.TagTODO, [exit_status: 0]}, {Credo.Check.Design.TagFIXME, []}, # ## Readability Checks # {Credo.Check.Readability.AliasOrder, []}, {Credo.Check.Readability.FunctionNames, []}, {Credo.Check.Readability.LargeNumbers, []}, {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, {Credo.Check.Readability.ModuleAttributeNames, []}, {Credo.Check.Readability.ModuleDoc, []}, {Credo.Check.Readability.ModuleNames, []}, {Credo.Check.Readability.ParenthesesInCondition, []}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false}, {Credo.Check.Readability.PredicateFunctionNames, []}, {Credo.Check.Readability.PreferImplicitTry, []}, {Credo.Check.Readability.RedundantBlankLines, []}, {Credo.Check.Readability.Semicolons, []}, {Credo.Check.Readability.SpaceAfterCommas, []}, {Credo.Check.Readability.StringSigils, []}, {Credo.Check.Readability.TrailingBlankLine, []}, {Credo.Check.Readability.TrailingWhiteSpace, []}, {Credo.Check.Readability.VariableNames, []}, # ## Refactoring Opportunities # {Credo.Check.Refactor.CondStatements, []}, {Credo.Check.Refactor.CyclomaticComplexity, []}, {Credo.Check.Refactor.FunctionArity, []}, {Credo.Check.Refactor.LongQuoteBlocks, []}, {Credo.Check.Refactor.MatchInCondition, []}, {Credo.Check.Refactor.NegatedConditionsInUnless, []}, {Credo.Check.Refactor.NegatedConditionsWithElse, []}, {Credo.Check.Refactor.Nesting, []}, {Credo.Check.Refactor.PipeChainStart, false}, {Credo.Check.Refactor.UnlessWithElse, []}, # ## Warnings # {Credo.Check.Warning.BoolOperationOnSameValues, []}, {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, {Credo.Check.Warning.IExPry, []}, {Credo.Check.Warning.IoInspect, []}, {Credo.Check.Warning.LazyLogging, false}, {Credo.Check.Warning.OperationOnSameValues, []}, {Credo.Check.Warning.OperationWithConstantResult, []}, {Credo.Check.Warning.RaiseInsideRescue, []}, {Credo.Check.Warning.UnusedEnumOperation, []}, {Credo.Check.Warning.UnusedFileOperation, []}, {Credo.Check.Warning.UnusedKeywordOperation, []}, {Credo.Check.Warning.UnusedListOperation, []}, {Credo.Check.Warning.UnusedPathOperation, []}, {Credo.Check.Warning.UnusedRegexOperation, []}, {Credo.Check.Warning.UnusedStringOperation, []}, {Credo.Check.Warning.UnusedTupleOperation, []}, # # Controversial and experimental checks (opt-in, just remove `, false`) # {Credo.Check.Readability.SinglePipe, []}, {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, {Credo.Check.Design.DuplicatedCode, false}, {Credo.Check.Readability.Specs, false}, {Credo.Check.Refactor.ABCSize, false}, {Credo.Check.Refactor.AppendSingleItem, false}, {Credo.Check.Refactor.DoubleBooleanNegation, false}, {Credo.Check.Refactor.VariableRebinding, false}, {Credo.Check.Warning.MapGetUnsafePass, false}, {Credo.Check.Warning.UnsafeToAtom, false} # # Custom checks can be created using `mix credo.gen.check`. # ] } ] } ================================================ FILE: config/config.exs ================================================ import Config ethereum_events_check_interval_ms = 8_000 config :logger, level: :info config :logger, :console, format: "$date $time [$level] $metadata⋅$message⋅\n", discard_threshold: 2000, metadata: [:module, :function, :request_id, :trace_id, :span_id] config :logger, backends: [Sentry.LoggerBackend, Ink] config :logger, Ink, name: "elixir-omg", exclude_hostname: true, log_encoding_error: true config :logger, Sentry.LoggerBackend, include_logger_metadata: true, ignore_plug: true config :sentry, filter: OMG.Status.SentryFilter, dsn: nil, environment_name: nil, included_environments: [], server_name: 'localhost', tags: %{ application: nil, eth_network: nil, eth_node: :geth } config :omg_watcher, deposit_finality_margin: 10, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, coordinator_eth_height_check_interval_ms: 6_000 config :omg_watcher, :eip_712_domain, name: "OMG Network", version: "1", salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83" # Configures the endpoint # https://ninenines.eu/docs/en/cowboy/2.4/manual/cowboy_http/ # defaults are: # protocol_options:[max_header_name_length: 64, # max_header_value_length: 4096, # max_headers: 100, # max_request_line_length: 8096 # ] # Use Poison for JSON parsing in Phoenix config :phoenix, json_library: Jason, serve_endpoints: true, persistent: true config :omg_db, metrics_collection_interval: 60_000 ethereum_client_timeout_ms = 20_000 config :ethereumex, url: "http://localhost:8545", http_options: [recv_timeout: ethereum_client_timeout_ms] config :omg_eth, contract_addr: nil, authority_address: nil, txhash_contract: nil, eth_node: :geth, child_block_interval: 1000, min_exit_period_seconds: nil, ethereum_block_time_seconds: 15, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, ethereum_stalled_sync_threshold_ms: 20_000, node_logging_in_debug: false config :omg_status, statsd_reconnect_backoff_ms: 10_000, system_memory_check_interval_ms: 10_000, system_memory_high_threshold: 0.8 config :omg_status, OMG.Status.Metric.Tracer, service: :omg_status, adapter: SpandexDatadog.Adapter, disabled?: true, type: :backend config :spandex, :decorators, tracer: OMG.Status.Metric.Tracer config :statix, host: "datadog", port: 8125 config :spandex_datadog, host: "datadog", port: 8126, batch_size: 10, sync_threshold: 100, http: HTTPoison config :vmstats, sink: OMG.Status.Metric.VmstatsSink, interval: 15_000, base_key: 'vmstats', key_separator: '$.', sched_time: true, memory_metrics: [ total: :total, processes_used: :procs_used, atom_used: :atom_used, binary: :binary, ets: :ets ] # Disable :os_mon's system_memory_high_watermark in favor of our own OMG.Status.Monitor.SystemMemory # See http://erlang.org/pipermail/erlang-questions/2006-September/023144.html config :os_mon, system_memory_high_watermark: 1.00, process_memory_high_watermark: 1.00 config :omg_watcher, child_chain_url: "http://localhost:9656" config :omg_watcher, # 23 hours worth of blocks - this is how long the child chain server has to block spends from exiting utxos exit_processor_sla_margin: 23 * 60 * 4, # this means we don't want the `sla_margin` above be larger than the `min_exit_period` exit_processor_sla_margin_forced: false, maximum_block_withholding_time_ms: 15 * 60 * 60 * 1000, maximum_number_of_unapplied_blocks: 50, exit_finality_margin: 12, block_getter_reorg_margin: 200, metrics_collection_interval: 60_000 config :omg_watcher, OMG.Watcher.Tracer, service: :omg_watcher, adapter: SpandexDatadog.Adapter, disabled?: true, type: :omg_watcher config :omg_watcher_info, child_chain_url: "http://localhost:9656", namespace: OMG.WatcherInfo, ecto_repos: [OMG.WatcherInfo.DB.Repo], metrics_collection_interval: 60_000 # Configures the endpoint config :omg_watcher_info, OMG.WatcherInfo.DB.Repo, adapter: Ecto.Adapters.Postgres, # NOTE: not sure if appropriate, but this allows reasonable blocks to be written to unoptimized Postgres setup timeout: 180_000, connect_timeout: 180_000, url: "postgres://omisego_dev:omisego_dev@localhost/omisego_dev", migration_timestamps: [type: :timestamptz], telemetry_prefix: [:omg_watcher, :watcher_info, :db, :repo] config :omg_watcher_info, OMG.WatcherInfo.Tracer, service: :ecto, adapter: SpandexDatadog.Adapter, disabled?: true, type: :db # In mix environment, all modules are loaded, therefore it behaves like a watcher_info config :omg_watcher_rpc, api_mode: :watcher_info # Configures the endpoint # https://ninenines.eu/docs/en/cowboy/2.4/manual/cowboy_http/ # defaults are: # protocol_options:[max_header_name_length: 64, # max_header_value_length: 4096, # max_headers: 100, # max_request_line_length: 8096 # ] config :omg_watcher_rpc, OMG.WatcherRPC.Web.Endpoint, render_errors: [view: OMG.WatcherRPC.Web.Views.Error, accepts: ~w(json)], enable_cors: true, http: [:inet6, port: 7434, protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]], url: [host: "w.example.com", port: 80], code_reloader: false config :phoenix, json_library: Jason, serve_endpoints: true, persistent: true config :spandex_ecto, SpandexEcto.EctoLogger, tracer: OMG.WatcherInfo.Tracer config :omg_watcher_rpc, OMG.WatcherRPC.Tracer, service: :web, adapter: SpandexDatadog.Adapter, disabled?: true, type: :web config :spandex_phoenix, tracer: OMG.WatcherRPC.Tracer config :briefly, directory: ["/tmp/omisego"] import_config "#{Mix.env()}.exs" ================================================ FILE: config/credo/license_header.ex ================================================ defmodule Credo.Check.Warning.LicenseHeader do @moduledoc """ Checks whether license header has been included in every file, except those where it shouldn't be **Doesn't** check the correctness of the header, just that it exists, so it checks first line to say `# Copyright` """ @explanation [ check: @moduledoc ] # you can configure the basics of your check via the `use Credo.Check` call use Credo.Check, base_priority: :high, category: :custom @doc false def run(%SourceFile{filename: source_path} = source_file, params \\ []) do # we ignore config, mix.exs and migration files, so all of these return no issues, i.e. [] case Path.split(source_path) do ["apps", _, "config" | _] -> [] ["config" | _] -> [] ["mix.exs"] -> [] ["apps", _, "mix.exs" | _] -> [] ["apps", _, "priv" , "repo" | _] -> [] _ -> do_run(source_file, params) end end defp do_run(source_file, params) do lines = SourceFile.lines(source_file) {1, first_line} = hd(lines) # IssueMeta helps us pass down both the source_file and params of a check # run to the lower levels where issues are created, formatted and returned issue_meta = IssueMeta.for(source_file, params) if String.starts_with?(first_line, "# Copyright") do [] else trigger = first_line new_issue = issue_for(issue_meta, 1, trigger) [new_issue] end end defp issue_for(issue_meta, line_no, trigger) do # format_issue/2 is a function provided by Credo.Check to help us format the # found issue format_issue issue_meta, message: "File is missing a license header, make sure to include the license header as other files do", line_no: line_no, trigger: trigger end end ================================================ FILE: config/credo/require_parentheses_on_zero_arity_defs.ex ================================================ defmodule Credo.Check.Readability.RequireParenthesesOnZeroArityDefs do @moduledoc false @checkdoc """ Use parentheses even when defining a function which has no arguments. The code in this example ... def summer? do # ... end ... should be refactored to look like this: def summer?() do # ... end Like all `Readability` issues, this one is not a technical concern. But you can improve the odds of others reading and liking your code by making it easier to follow. This conforms with the style guide at https://github.com/lexmag/elixir-style-guide/blob/master/README.md#parentheses """ @explanation [check: @checkdoc] @def_ops [:def, :defp, :defmacro, :defmacrop] use Credo.Check, base_priority: :normal @doc false def run(source_file, params \\ []) do issue_meta = IssueMeta.for(source_file, params) Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) end for op <- @def_ops do # catch variables named e.g. `defp` defp traverse({unquote(op), _, nil} = ast, issues, _issue_meta) do {ast, issues} end defp traverse({unquote(op), _, body} = ast, issues, issue_meta) do function_head = Enum.at(body, 0) {ast, issues_for_definition(function_head, issues, issue_meta)} end end defp traverse(ast, issues, _issue_meta) do {ast, issues} end # skip the false positive for a metaprogrammed definition defp issues_for_definition({{:unquote, _, _}, _, _}, issues, _) do issues end defp issues_for_definition({_, _, args}, issues, _) when length(args) > 0 do issues end defp issues_for_definition({name, meta, _}, issues, issue_meta) do line_no = meta[:line] text = remaining_line_after(issue_meta, line_no, name) case String.match?(text, ~r/^\(([\w]*)\)(.)*/) do true -> issues false -> issues ++ [issue_for(issue_meta, line_no)] end end defp remaining_line_after(issue_meta, line_no, text) do source_file = IssueMeta.source_file(issue_meta) line = SourceFile.line_at(source_file, line_no) name_size = text |> to_string |> String.length() skip = (column(source_file, line_no, text) || -1) + name_size - 1 String.slice(line, skip..-1) end defp issue_for(issue_meta, line_no) do format_issue( issue_meta, message: "Use parentheses even when defining a function which has no arguments.", line_no: line_no ) end # A modified version of https://github.com/rrrene/credo/blob/master/lib/credo/source_file.ex#L140-L161 # that removes `Regex.escape()` as it's breaking question-mark ending functions. defp column(source_file, line_no, trigger) defp column(source_file, line_no, trigger) when is_binary(trigger) or is_atom(trigger) do line = SourceFile.line_at(source_file, line_no) regexed = to_string(trigger) case Regex.run(~r/\b#{regexed}\b/, line, return: :index) do nil -> nil result -> {col, _} = List.first(result) col + 1 end end defp column(_, _, _), do: nil end ================================================ FILE: config/dev.exs ================================================ import Config config :logger, backends: [:console, Sentry.LoggerBackend] config :omg_watcher, ethereum_events_check_interval_ms: 500, coordinator_eth_height_check_interval_ms: 1_000 config :omg_db, path: Path.join([System.get_env("HOME"), ".omg/data"]) config :ethereumex, http_options: [recv_timeout: 60_000] config :omg_eth, min_exit_period_seconds: 10 * 60, ethereum_block_time_seconds: 1, node_logging_in_debug: true config :omg_watcher_rpc, environment: :dev config :phoenix, :stacktrace_depth, 20 config :omg_watcher_rpc, OMG.WatcherRPC.Tracer, disabled?: true, env: "development" config :omg_watcher_info, environment: :dev config :omg_watcher_info, OMG.WatcherInfo.Tracer, disabled?: true, env: "development" config :omg_watcher, environment: :dev config :omg_watcher, # 1 hour of Ethereum blocks exit_processor_sla_margin: 60 * 4, # this means we allow the `sla_margin` above be larger than the `min_exit_period` exit_processor_sla_margin_forced: true config :omg_watcher, OMG.Watcher.Tracer, disabled?: true, env: "development" config :omg_status, OMG.Status.Metric.Tracer, env: "development", disabled?: true ================================================ FILE: config/prod.exs ================================================ use Mix.Config ================================================ FILE: config/releases.exs ================================================ import Config # This `releases.exs` config file gets evaluated at RUNTIME, unlike other config files that are # evaluated at compile-time. # # See https://hexdocs.pm/mix/1.9.0/Mix.Tasks.Release.html#module-runtime-configuration # with this helper anon. function you can # load and validate specific watcher or watcher info # configuration # env_var_name - gets passed into System.get_env/1 # exception - is the string that gets thrown so that we prevent release boot # third argument is if this is a watcher info resolver # fourth argument is whether this is a watcher info specific configuration mandatory = fn env_var_name, _exception, false, true -> # this case covers a watcher info setting # under watcher security application # it's ok if the env var is missing case System.get_env(env_var_name) do nil -> "WATCHER_INFO_SETTING" data -> data end env_var_name, exception, true, true -> case System.get_env(env_var_name) do nil -> throw(exception) data -> data end env_var_name, exception, _, false -> case System.get_env(env_var_name) do nil -> throw(exception) data -> data end end watcher_info? = fn -> Code.ensure_loaded?(OMG.WatcherInfo) end config :omg_watcher_info, OMG.WatcherInfo.DB.Repo, url: mandatory.("DATABASE_URL", "DATABASE_URL needs to be set.", watcher_info?.(), true), # Have at most `:pool_size` DB connections on standby and serving DB queries. pool_size: String.to_integer(System.get_env("WATCHER_INFO_DB_POOL_SIZE") || "10"), # Wait at most `:queue_target` for a connection. If all connections checked out during # a `:queue_interval` takes more than `:queue_target`, then we double the `:queue_target`. # If checking out connections take longer than the new target, a DBConnection.ConnectionError is raised. # See: https://hexdocs.pm/db_connection/DBConnection.html#start_link/2-queue-config queue_target: String.to_integer(System.get_env("WATCHER_INFO_DB_POOL_QUEUE_TARGET_MS") || "50"), queue_interval: String.to_integer(System.get_env("WATCHER_INFO_DB_POOL_QUEUE_INTERVAL_MS") || "1000") config :omg_watcher, child_chain_url: mandatory.("CHILD_CHAIN_URL", "CHILD_CHAIN_URL needs to be set.", watcher_info?.(), false) config :omg_watcher_info, child_chain_url: mandatory.("CHILD_CHAIN_URL", "CHILD_CHAIN_URL needs to be set.", watcher_info?.(), true) ================================================ FILE: config/test.exs ================================================ use Mix.Config ethereum_events_check_interval_ms = 400 parse_contracts = fn -> local_umbrella_path = Path.join([File.cwd!(), "../../", "localchain_contract_addresses.env"]) contract_addreses_path = case File.exists?(local_umbrella_path) do true -> local_umbrella_path _ -> # CI/CD Path.join([File.cwd!(), "localchain_contract_addresses.env"]) end contract_addreses_path |> File.read!() |> String.split("\n", trim: true) |> List.flatten() |> Enum.reduce(%{}, fn line, acc -> [key, value] = String.split(line, "=") Map.put(acc, key, value) end) end contracts = parse_contracts.() config :logger, level: :warn config :logger, backends: [:console, Sentry.LoggerBackend] config :sentry, dsn: nil, environment_name: nil, included_environments: [], server_name: nil, tags: %{ application: nil, eth_network: nil, eth_node: :geth } config :omg_utils, environment: :test config :omg_watcher, deposit_finality_margin: 1, ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, coordinator_eth_height_check_interval_ms: 10, environment: :test, fee_claimer_address: Base.decode16!("DEAD000000000000000000000000000000000000") # config :omg_db, # path: Path.join([System.get_env("HOME"), ".omg/data"]) # bumping these timeouts into infinity - let's rely on test timeouts rather than these config :ethereumex, url: System.get_env("ETHEREUM_RPC_URL", "http://localhost:8545"), http_options: [recv_timeout: :infinity], id_reset: true config :omg_eth, # Needed for test only to have some value of address when `:contract_address` is not set explicitly # required by the EIP-712 struct hash code txhash_contract: contracts["TXHASH_CONTRACT"], authority_address: contracts["AUTHORITY_ADDRESS"], contract_addr: %{ erc20_vault: contracts["CONTRACT_ADDRESS_ERC20_VAULT"], eth_vault: contracts["CONTRACT_ADDRESS_ETH_VAULT"], payment_exit_game: contracts["CONTRACT_ADDRESS_PAYMENT_EXIT_GAME"], plasma_framework: contracts["CONTRACT_ADDRESS_PLASMA_FRAMEWORK"] }, node_logging_in_debug: true, # Lower the event check interval too low and geth will die ethereum_events_check_interval_ms: ethereum_events_check_interval_ms, min_exit_period_seconds: 22, ethereum_block_time_seconds: 1, eth_node: :geth, run_test_eth_dev_node: true config :omg_status, metrics: false, environment: :test, statsd_reconnect_backoff_ms: 10 config :omg_status, OMG.Status.Metric.Tracer, env: "test", disabled?: true config :statix, host: "datadog", port: 8125 config :spandex_datadog, host: "datadog", port: 8126, batch_size: 10, sync_threshold: 10, http: HTTPoison config :os_mon, memsup_helper_timeout: 120, memory_check_interval: 5, system_memory_high_watermark: 0.99, disk_almost_full_threshold: 0.99, disk_space_check_interval: 120 config :omg_watcher, child_chain_url: System.get_env("CHILD_CHAIN_URL", "http://localhost:9656/") config :omg_watcher, # NOTE `exit_processor_sla_margin` can't be made shorter. At 8 it sometimes # causes unchallenged exits events because `geth --dev` is too fast # Chaning this value for dockerized geth in OMG.Watcher.Fixtures!!! exit_processor_sla_margin: 10, # this means we allow the `sla_margin` above be larger than the `min_exit_period` exit_processor_sla_margin_forced: true, # NOTE: `maximum_block_withholding_time_ms` must be here - one of our integration tests # actually fakes block withholding to test something maximum_block_withholding_time_ms: 1_000, exit_finality_margin: 1 config :omg_watcher, OMG.Watcher.Tracer, disabled?: true, env: "test" config :omg_watcher_info, child_chain_url: System.get_env("CHILD_CHAIN_URL", "http://localhost:9656/") config :omg_watcher_info, OMG.WatcherInfo.DB.Repo, ownership_timeout: 500_000, pool: Ecto.Adapters.SQL.Sandbox, # DATABASE_URL format is following `postgres://{user_name}:{password}@{host:port}/{database_name}` url: System.get_env("TEST_DATABASE_URL", "postgres://omisego_dev:omisego_dev@localhost:5432/omisego_test") config :omg_watcher_info, OMG.WatcherInfo.Tracer, disabled?: true, env: "test" config :omg_watcher_info, environment: :test config :omg_watcher_rpc, OMG.WatcherRPC.Web.Endpoint, http: [port: 7435], server: true config :omg_watcher_rpc, OMG.WatcherRPC.Tracer, service: :omg_watcher_rpc, adapter: SpandexDatadog.Adapter, disabled?: true, env: "test", type: :web ================================================ FILE: contract_addresses_template.env ================================================ AUTHORITY_ADDRESS={AUTHORITY_ADDRESS} CONTRACT_ADDRESS_ETH_VAULT={CONTRACT_ADDRESS_ETH_VAULT} CONTRACT_ADDRESS_ERC20_VAULT={CONTRACT_ADDRESS_ERC20_VAULT} CONTRACT_ADDRESS_PAYMENT_EXIT_GAME={CONTRACT_ADDRESS_PAYMENT_EXIT_GAME} CONTRACT_ADDRESS_PLASMA_FRAMEWORK={CONTRACT_ADDRESS_PLASMA_FRAMEWORK} TXHASH_CONTRACT={TXHASH_CONTRACT} CONTRACT_ADDRESS_PAYMENT_EIP_712_LIB_MOCK={CONTRACT_ADDRESS_PAYMENT_EIP_712_LIB_MOCK} CONTRACT_ADDRESS_MERKLE_WRAPPER={CONTRACT_ADDRESS_MERKLE_WRAPPER} CONTRACT_ERC20_MINTABLE={CONTRACT_ERC20_MINTABLE} ================================================ FILE: coveralls.json ================================================ { "skip_files": [ "apps/omg/test/support", "apps/omg_bus/test/support", "apps/omg_db/test/support", "apps/omg_eth/test/support", "apps/omg_status/test/support", "apps/omg_utils/test/support", "apps/omg_watcher/test/support", "apps/omg_watcher_info/test/support", "apps/omg_watcher_info/lib/omg_watcher_info/release_tasks/init_postgresql_db.ex", "apps/omg_watcher_rpc/test/support", "apps/xomg_tasks" ] } ================================================ FILE: dialyzer.ignore-warnings ================================================ test/support/ apps/omg/lib/omg/utxo/position.ex:46: Type specification 'Elixir.OMG.Utxo.Position':'decode!'(number()) -> t() is a supertype of the success typing: 'Elixir.OMG.Utxo.Position':'decode!'(number()) -> {'utxo_position',non_neg_integer(),non_neg_integer(),char()} apps/omg/lib/omg/utxo/position.ex:52: Type specification 'Elixir.OMG.Utxo.Position':decode(number()) -> {'ok',t()} | {'error','encoded_utxo_position_too_low'} is a supertype of the success typing: 'Elixir.OMG.Utxo.Position':decode(number()) -> {'error','encoded_utxo_position_too_low'} | {'ok',{'utxo_position',non_neg_integer(),non_neg_integer(),char()}} apps/omg/lib/omg/utxo/position.ex:76: Type specification 'Elixir.OMG.Utxo.Position':get_position(pos_integer()) -> {non_neg_integer(),non_neg_integer(),non_neg_integer()} is a supertype of the success typing: 'Elixir.OMG.Utxo.Position':get_position(pos_integer()) -> {non_neg_integer(),non_neg_integer(),char()} apps/omg_watcher_rpc/lib/web/endpoint.ex:15: Expression produces a value of type 'error' | 'excluded' | 'ignored' | 'unsampled' | {'ok',binary() | pid() | #{'__struct__':='Elixir.Task', 'owner':='nil' | pid(), 'pid':='nil' | pid(), 'ref':='nil' | reference()}}, but this value is unmatched lib/phoenix/router.ex:316: The pattern 'error' can never match the type {#{'conn':='nil', 'log':='debug', 'path_params':=map(), 'pipe_through':=[any(),...], 'plug':='Elixir.OMG.ChildChainRPC.Web.Controller.Block' | 'Elixir.OMG.ChildChainRPC.Web.Controller.Fallback' | 'Elixir.OMG.ChildChainRPC.Web.Controller.Transaction', 'plug_opts':='Elixir.Route.NotFound' | 'get_block' | 'submit', 'route':=<<_:48,_:_*8>>},fun((_,map()) -> any()),fun((_) -> map()),{'Elixir.OMG.ChildChainRPC.Web.Controller.Block','get_block'} | {'Elixir.OMG.ChildChainRPC.Web.Controller.Fallback','Elixir.Route.NotFound'} | {'Elixir.OMG.ChildChainRPC.Web.Controller.Transaction','submit'}} lib/phoenix/router.ex:316: The pattern 'error' can never match the type {#{'conn':='nil', 'log':='debug', 'path_params':=map(), 'pipe_through':=[any(),...], 'plug':='Elixir.OMG.WatcherRPC.Web.Controller.Account' | 'Elixir.OMG.WatcherRPC.Web.Controller.Alarm' | 'Elixir.OMG.WatcherRPC.Web.Controller.Challenge' | 'Elixir.OMG.WatcherRPC.Web.Controller.Fallback' | 'Elixir.OMG.WatcherRPC.Web.Controller.InFlightExit' | 'Elixir.OMG.WatcherRPC.Web.Controller.Status' | 'Elixir.OMG.WatcherRPC.Web.Controller.Transaction' | 'Elixir.OMG.WatcherRPC.Web.Controller.Utxo', 'plug_opts':=atom(), 'route':=<<_:48,_:_*8>>},fun((_,map()) -> any()),fun((_) -> map()),{'Elixir.OMG.WatcherRPC.Web.Controller.Account' | 'Elixir.OMG.WatcherRPC.Web.Controller.Alarm' | 'Elixir.OMG.WatcherRPC.Web.Controller.Challenge' | 'Elixir.OMG.WatcherRPC.Web.Controller.Fallback' | 'Elixir.OMG.WatcherRPC.Web.Controller.InFlightExit' | 'Elixir.OMG.WatcherRPC.Web.Controller.Status' | 'Elixir.OMG.WatcherRPC.Web.Controller.Transaction' | 'Elixir.OMG.WatcherRPC.Web.Controller.Utxo',atom()}} test/support/integration/test_server.ex:47: The pattern {'error', _} can never match the type {'ok','false' | 'nil' | 'true' | binary() | [any()] | number() | map()} test/support/test_server.ex:84: The pattern {'error', _} can never match the type {'ok','false' | 'nil' | 'true' | binary() | [any()] | number() | map()} #### # # Protocol-related problems, these ignores workaround the problem reported # here: https://github.com/elixir-lang/elixir/issues/7708 and here https://github.com/jeremyjh/dialyxir/issues/221 # fixed in https://github.com/jeremyjh/dialyxir/commit/3d0a13f17a46649bca2413d57cef45bb278d1474, not yet released in `dialyxir` # undo once we use the `dialyxir` release that includes this (presumably >1.0.0-rc.6) :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Atom':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.BitString':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Float':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Function':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Integer':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.List':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Map':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.PID':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Port':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Reference':'__impl__'/1 :0: Unknown function 'Elixir.OMG.State.Transaction.Protocol.Tuple':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Atom':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.BitString':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Float':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Function':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Integer':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.List':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Map':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.PID':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Port':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Reference':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Tuple':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Atom':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.BitString':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Float':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Function':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Integer':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.List':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Map':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.PID':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Port':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Reference':'__impl__'/1 :0: Unknown function 'Elixir.OMG.Output.Protocol.Tuple':'__impl__'/1 # #### ================================================ FILE: docker/create_databases.sql ================================================ CREATE USER feefeed; ALTER USER feefeed CREATEDB; ALTER USER feefeed WITH PASSWORD 'feefeed'; CREATE DATABASE feefeed; GRANT ALL PRIVILEGES ON DATABASE feefeed TO feefeed; CREATE USER engine_repo; ALTER USER engine_repo CREATEDB; ALTER USER engine_repo WITH PASSWORD 'engine_repo'; CREATE DATABASE engine_repo; GRANT ALL PRIVILEGES ON DATABASE engine_repo TO engine_repo; CREATE USER omisego_dev; ALTER USER omisego_dev CREATEDB; ALTER USER omisego_dev WITH PASSWORD 'omisego_dev'; CREATE DATABASE omisego_dev; GRANT ALL PRIVILEGES ON DATABASE omisego_dev TO omisego_dev; CREATE DATABASE omisego_test; GRANT ALL PRIVILEGES ON DATABASE omisego_test TO omisego_dev; ================================================ FILE: docker/geth/command ================================================ # Configures geth with the deployer and authority accounts. This includes: # 1. Configuring the deployer's keystore # 2. Configuring the authority's keystore # 3. Configuring the keystores' password # 4. Unlocking the accounts by their indexes # CAREFUL with --allow-insecure-unlock! # Starts geth # Websocket is not used by the applications but enabled for debugging/testing convenience geth \ --verbosity 0 \ --miner.gastarget 7500000 \ --nousb \ --miner.gasprice "10" \ --nodiscover \ --maxpeers 0 \ --datadir data/ \ --syncmode 'fast' \ --networkid 1337 \ --gasprice '1' \ --keystore ./data/geth/keystore/ \ --password /data/geth-blank-password \ --unlock "0,1" \ --rpc \ --rpcapi personal,web3,eth,net \ --rpcaddr 0.0.0.0 \ --rpcvhosts=* \ --rpcport=${RPC_PORT} \ --ws \ --wsaddr 0.0.0.0 \ --wsorigins='*' \ --mine \ --allow-insecure-unlock ================================================ FILE: docker/geth/geth-blank-password ================================================ ================================================ FILE: docker/nginx/geth_nginx.conf ================================================ server { listen 80; access_log off; location / { proxy_pass http://172.27.0.101:8545; } } server { listen 81; access_log off; location / { proxy_pass http://172.27.0.101:8546; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; } } ================================================ FILE: docker/nginx/nginx.conf ================================================ user nginx; events { worker_connections 1000; } http { upstream childchain { server 172.27.0.103:9656; } server { listen 9656; access_log off; location / { proxy_pass http://childchain; proxy_next_upstream non_idempotent invalid_header error timeout http_500 http_502 http_504 http_403 http_404; fastcgi_read_timeout 10; proxy_read_timeout 10; error_page 504 502 =503 @empty; } location @empty { internal; return 200 ""; } } include "/etc/nginx/server_config/*.conf"; } include "/etc/nginx/main_config/*.conf"; ================================================ FILE: docker/nginx/nginx.reorg.conf ================================================ events {} http { upstream geth { server 172.27.0.201:8545; server 172.27.0.202:8545; } upstream websocket { server 172.27.0.201:8546; server 172.27.0.202:8546; } server { listen 80; location / { proxy_pass http://geth; proxy_next_upstream non_idempotent invalid_header error timeout http_500 http_502 http_504 http_403 http_404; proxy_next_upstream_tries 4; fastcgi_read_timeout 10; proxy_read_timeout 10; } } server { listen 81; location / { proxy_pass http://websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_next_upstream non_idempotent invalid_header error timeout http_500 http_502 http_504 http_403 http_404; proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; } } } ================================================ FILE: docker/static_feefeed/file.json ================================================ { "data":{ "1":{ "0x0000000000000000000000000000000000000000":{ "amount":1, "contract_address":"0x0000000000000000000000000000000000000000", "pair":"eth_eth", "pair_reversed":false, "pegged_amount":null, "pegged_currency":null, "pegged_subunit_to_unit":null, "subunit_to_unit":1000000000000000000, "symbol":"ETH", "type":"fixed", "updated_at":"2021-01-18T18:56:59.939198Z" }, "0xb1952c4e36d153a49eccc01a4d93b9ecbabb7208":{ "amount":414023614895550, "contract_address":"0xd26114cd6EE289AccF82350c8d8487fedB8A0C07", "pair":"omg_eth", "pair_reversed":false, "pegged_amount":0.33, "pegged_currency":"eth_gas", "pegged_subunit_to_unit":1000000000000000000, "subunit_to_unit":1000000000000000000, "symbol":"OMG", "type":"pegged", "updated_at":"2021-01-18T18:56:59.939198Z" } } }, "success":true, "version":"1" } ================================================ FILE: docker-compose-infura.yml ================================================ version: "2.3" services: plasma-contracts: environment: # The private keys are likely different from the main docker-compose.yml: # 1. DEPLOYER_PRIVATEKEY needs to have enough ETH on the REMOTE_URL network # 2. AUTHORITY_PRIVATEKEY needs to have nonce=0 on the REMOTE_URL network - REMOTE_URL=https://rinkeby.infura.io/v3/${INFURA_API_KEY} - DEPLOYER_PRIVATEKEY=${DEPLOYER_PRIVATEKEY} - MAINTAINER_PRIVATEKEY=${MAINTAINER_PRIVATEKEY} - AUTHORITY_PRIVATEKEY=${AUTHORITY_PRIVATEKEY} childchain: environment: - ETHEREUM_RPC_URL=https://rinkeby.infura.io/v3/${INFURA_API_KEY} - PRIVATE_KEY=${AUTHORITY_PRIVATEKEY} watcher: environment: - ETHEREUM_RPC_URL=https://rinkeby.infura.io/v3/${INFURA_API_KEY} geth: # We don't need geth but docker-compose doesn't support overrides to remove or disable a service # So here we set `--dev.period 0` to minimize resource utilization. entrypoint: /bin/sh -c "apk add curl && geth --dev --dev.period 0 --rpc --rpcaddr 0.0.0.0 --rpcvhosts=* --rpcport=8545" ================================================ FILE: docker-compose-watcher.yml ================================================ version: "2.3" services: postgres: image: postgres:9.6.13-alpine restart: always ports: - "5432:5432" environment: POSTGRES_USER: omisego_dev POSTGRES_PASSWORD: omisego_dev POSTGRES_DB: omisego_dev healthcheck: test: pg_isready -U omisego_dev interval: 5s timeout: 3s retries: 5 watcher: #last stable integration watcher image: omisego/watcher:1.0.1 command: "full_local" environment: - ETHEREUM_RPC_URL=https://ropsten.infura.io/v3/${INFURA_API_KEY} - CHILD_CHAIN_URL=https://childchain.ropsten.v1.omg.network - ETHEREUM_NETWORK=ROPSTEN - AUTHORITY_ADDRESS=0x3272b97b7f1b74b338cb0fdda167cf76bc4da3b6 - TXHASH_CONTRACT=0x25e445594f425a7a94141a20b8831580953b92ddd0d12e9c775c571e4f3da08c - CONTRACT_ADDRESS_PLASMA_FRAMEWORK=0xa72c9dceeef26c9d103d55c53d411c36f5cdf7ec - CONTRACT_ADDRESS_ETH_VAULT=0x2c7533f76567241341d1c27f0f239a20b6115714 - CONTRACT_ADDRESS_ERC20_VAULT=0x2bed2ff4ee93a208edbf4185c7813103d8c4ab7f - CONTRACT_ADDRESS_PAYMENT_EXIT_GAME=0x960ca6b9faa85118ba6badbe0097b1afd8827fac - DATABASE_URL=postgres://omisego_dev:omisego_dev@postgres:5432/omisego_dev - PORT=7434 - DD_DISABLED=true - DB_PATH=/app/.omg/data - ETHEREUM_EVENTS_CHECK_INTERVAL_MS=8000 - ETHEREUM_STALLED_SYNC_THRESHOLD_MS=300000 - ETHEREUM_BLOCK_TIME_SECONDS=15 - EXIT_PROCESSOR_SLA_MARGIN=5520 - EXIT_PROCESSOR_SLA_MARGIN_FORCED=TRUE - LOGGER_BACKEND=console - DD_HOSTNAME=datadog - APP_ENV=local-development ports: - "7434:7434" healthcheck: test: curl watcher:7434 interval: 5s timeout: 3s retries: 5 watcher_info: image: omisego/watcher_info:1.0.1 command: "full_local" environment: - ETHEREUM_RPC_URL=https://ropsten.infura.io/v3/${INFURA_API_KEY} - CHILD_CHAIN_URL=https://childchain.ropsten.v1.omg.network - ETHEREUM_NETWORK=ROPSTEN - AUTHORITY_ADDRESS=0x3272b97b7f1b74b338cb0fdda167cf76bc4da3b6 - TXHASH_CONTRACT=0x25e445594f425a7a94141a20b8831580953b92ddd0d12e9c775c571e4f3da08c - CONTRACT_ADDRESS_PLASMA_FRAMEWORK=0xa72c9dceeef26c9d103d55c53d411c36f5cdf7ec - CONTRACT_ADDRESS_ETH_VAULT=0x2c7533f76567241341d1c27f0f239a20b6115714 - CONTRACT_ADDRESS_ERC20_VAULT=0x2bed2ff4ee93a208edbf4185c7813103d8c4ab7f - CONTRACT_ADDRESS_PAYMENT_EXIT_GAME=0x960ca6b9faa85118ba6badbe0097b1afd8827fac - DATABASE_URL=postgres://omisego_dev:omisego_dev@postgres:5432/omisego_dev - PORT=7534 - DD_DISABLED=true - DB_PATH=/app/.omg/data - ETHEREUM_EVENTS_CHECK_INTERVAL_MS=8000 - ETHEREUM_STALLED_SYNC_THRESHOLD_MS=300000 - ETHEREUM_BLOCK_TIME_SECONDS=15 - EXIT_PROCESSOR_SLA_MARGIN=5520 - EXIT_PROCESSOR_SLA_MARGIN_FORCED=TRUE - LOGGER_BACKEND=console - DD_HOSTNAME=datadog - APP_ENV=local-development restart: always ports: - "7534:7534" healthcheck: test: curl watcher_info:7534 interval: 5s timeout: 3s retries: 5 depends_on: postgres: condition: service_healthy ================================================ FILE: docker-compose.datadog.yml ================================================ version: "2.3" services: datadog: image: datadog/agent:latest restart: always environment: - DD_API_KEY=${DD_API_KEY} - DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true - DD_LOG_LEVEL=debug - DOCKER_CONTENT_TRUST=1 - DD_APM_ENABLED=true volumes: - /var/run/docker.sock:/var/run/docker.sock - /proc/:/host/proc/:ro - /sys/fs/cgroup:/host/sys/fs/cgroup:ro ports: - "2003-2004:2003-2004" - "2023-2024:2023-2024" - "8125:8125/udp" - "8126:8126/tcp" ================================================ FILE: docker-compose.dev.yml ================================================ version: "2.3" services: elixir-omg: image: omisegoimages/elixir-omg-builder:stable-20201207 environment: DATABASE_URL: postgres://omisegodev:omisegodev@postgres:5432/omisego_dev TEST_DATABASE_URL: postgres://omisegodev:omisegodev@postgres:5432/omisego_test SHELL: /bin/bash volumes: - .:/app:rw depends_on: postgres: condition: service_healthy networks: chain_net: ipv4_address: 172.27.0.119 watcher: environment: - DD_DISABLED=false depends_on: datadog: condition: service_healthy watcher_info: environment: - DD_DISABLED=false depends_on: datadog: condition: service_healthy childchain: environment: - DD_DISABLED=false depends_on: datadog: condition: service_healthy ================================================ FILE: docker-compose.feefeed.yml ================================================ version: "2.3" services: childchain: environment: - FEE_ADAPTER=feed #- FEE_FEED_URL=http://172.27.0.110:4000/api/v1 - FEE_FEED_URL=http://172.27.0.110/file.json depends_on: feefeed: condition: service_healthy feefeed: image: omisego/feefeed_mock:latest volumes: - ./docker/static_feefeed/:/www-data/ ports: - "4000:80" expose: - "4000" networks: chain_net: ipv4_address: 172.27.0.110 # feefeed: # image: "gcr.io/omisego-development/feefeed:latest" # command: "start" # container_name: feefeed # environment: # - GITHUB_TOKEN="" # - GITHUB_ORGANISATION=omgnetwork # - GITHUB_REPO=fee-rules-public # - SENTRY_DSN="" # - GITHUB_BRANCH=master # - RULES_FETCH_INTERVAL=20 # - RATES_FETCH_INTERVAL=20 # - GITHUB_FILENAME=fee_rules # - DATABASE_URL=postgresql://feefeed:feefeed@172.27.0.107:5432/feefeed # - SECRET_KEY_BASE="Y8naENMR8b+vbPHILjwNtEfWFrnbGi2k+UYWm75VnKHfsavmyGLtTmmeJxAGK+zJ" # - DATADOG_DISABLED=true # - DATADOG_HOST="localhost" # - ETHEREUM_NODE_URL=http://172.27.0.102:80 # ports: # - "4000:4000" # expose: # - "4000" # depends_on: # postgres: # condition: service_healthy # nginx: # condition: service_healthy # restart: always # healthcheck: # test: curl -v --silent http://localhost:4000/api/v1/fees 2>&1 | grep contract_address # interval: 4s # timeout: 2s # retries: 30 # start_period: 60s # networks: # chain_net: # ipv4_address: 172.27.0.110 ================================================ FILE: docker-compose.reorg.yml ================================================ version: "2.3" services: geth: entrypoint: ["echo", "clique geth is disabled for reorgs"] geth-1: image: ethereum/client-go:v1.9.15 container_name: geth-1 environment: - ACCOUNT=0x6de4b3b9c28e9c3e84c2b2d3a875c947a84de68d - BOOTNODES=enode://b655cc3e5b72ab9beb8a8536a3c3ae92fbeb79feb1ebd7f95d72be72554ca586428bd48a54eb9c2bcaae455cc674299b6dd3df3c6556a493dfd50070f1a448aa@172.27.0.202:30303 - INIT=false entrypoint: /bin/sh -c ". data/geth/command" expose: - 8545 - 8546 - 30303 ports: - 9000:8545 volumes: - ./data1:/data - ./data/ethash:/root/.ethash healthcheck: test: curl geth-1:8545 interval: 5s timeout: 3s retries: 5 networks: chain_net: ipv4_address: 172.27.0.201 geth-2: image: ethereum/client-go:v1.9.15 container_name: geth-2 depends_on: - geth-1 environment: - ACCOUNT=0xc0f780dfc35075979b0def588d999225b7ecc56f - BOOTNODES=enode://4574f825d67bf570b9216e704a5b761d05d5015c458e2c9dd4b30abb2fe8c881400c2074a126df94690c4c9fb72ee046e6e3ac2bb73dede42fce66cb0a963b36@172.27.0.201:30303 - INIT=false entrypoint: /bin/sh -c ". data/geth/command" expose: - 8546 - 8545 - 30303 ports: - 9001:8545 volumes: - ./data2:/data - ./data/ethash:/root/.ethash healthcheck: test: curl geth-2:8545 interval: 5s timeout: 3s retries: 5 networks: chain_net: ipv4_address: 172.27.0.202 nginx: depends_on: geth-1: condition: service_healthy geth-2: condition: service_healthy volumes: - ./docker/nginx/nginx.reorg.conf:/etc/nginx/nginx.conf ================================================ FILE: docker-compose.specs.yml ================================================ # this is an override to our usual docker-compose.yml which enables cabbage integration tests to run against a # test-friendly setup of our services version: "2.3" services: watcher: environment: - EXIT_PROCESSOR_SLA_MARGIN=30 watcher_info: environment: - EXIT_PROCESSOR_SLA_MARGIN=30 ================================================ FILE: docker-compose.yml ================================================ version: "2.3" services: nginx: image: nginx:latest container_name: nginx volumes: - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./docker/nginx/geth_nginx.conf:/etc/nginx/server_config/geth.conf:ro ports: - 9656:9656 - 8545:80 - 8546:81 - 443:443 healthcheck: test: curl geth:80 interval: 5s timeout: 3s retries: 5 networks: chain_net: ipv4_address: 172.27.0.108 postgres: image: postgres:9.6.13-alpine restart: always ports: - "5432:5432" environment: POSTGRES_USER: omisegodev POSTGRES_PASSWORD: omisegodev volumes: - ./docker/create_databases.sql:/docker-entrypoint-initdb.d/create_databases.sql healthcheck: test: pg_isready -U omisego_dev interval: 5s timeout: 3s retries: 5 networks: chain_net: ipv4_address: 172.27.0.107 feefeed: image: omisego/feefeed_mock:latest volumes: - ./docker/static_feefeed/:/www-data/ ports: - "4000:80" expose: - "4000" networks: chain_net: ipv4_address: 172.27.0.110 # feefeed: # image: gcr.io/omisego-development/feefeed:latest # command: "start" # container_name: feefeed # environment: # - GITHUB_TOKEN="" # - GITHUB_ORGANISATION=omgnetwork # - GITHUB_REPO=fee-rules-public # - SENTRY_DSN="" # - GITHUB_BRANCH=master # - RULES_FETCH_INTERVAL=200 # - RATES_FETCH_INTERVAL=200 # - GITHUB_FILENAME=fee_rules # - DATABASE_URL=postgresql://feefeed:feefeed@172.27.0.107:5432/feefeed # - SECRET_KEY_BASE="Y8naENMR8b+vbPHILjwNtEfWFrnbGi2k+UYWm75VnKHfsavmyGLtTmmeJxAGK+zJ" # - DATADOG_DISABLED=true # - DATADOG_HOST="localhost" # - ETHEREUM_NODE_URL=http://172.27.0.108:80 # ports: # - "4000:4000" # expose: # - "4000" # depends_on: # - postgres # restart: always # networks: # chain_net: # ipv4_address: 172.27.0.110 geth: image: ethereum/client-go:v1.9.15 entrypoint: /bin/sh -c ". data/command" environment: RPC_PORT: 8545 ports: - "8555:8545" - "8556:8546" expose: - "8546" - "8545" volumes: - ./data:/data - ./docker/geth/command:/data/command - ./docker/geth/geth-blank-password:/data/geth-blank-password healthcheck: test: curl localhost:8545 interval: 5s timeout: 3s retries: 5 networks: chain_net: ipv4_address: 172.27.0.101 childchain: image: omisego/child_chain:latest command: "full_local" container_name: childchain env_file: - ./localchain_contract_addresses.env - ./fees_setup.env environment: - ETHEREUM_NETWORK=LOCALCHAIN - ETHEREUM_RPC_URL=http://172.27.0.108:80 - APP_ENV=local_docker_development - DD_HOSTNAME=datadog - DD_DISABLED=true - DB_PATH=/data - ETHEREUM_EVENTS_CHECK_INTERVAL_MS=800 - ETHEREUM_STALLED_SYNC_THRESHOLD_MS=20000 - LOGGER_BACKEND=console - RELEASE_COOKIE=development - NODE_HOST=127.0.0.1 - PULSE_API_KEY=${PULSE_API_KEY} - FEE_CLAIMER_ADDRESS=0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837 restart: always volumes: - ./data:/data - ./priv/dev-artifacts:/dev-artifacts healthcheck: test: curl localhost:9656 interval: 30s timeout: 10s retries: 5 start_period: 60s depends_on: - nginx - geth networks: chain_net: ipv4_address: 172.27.0.103 watcher: image: omisego/watcher:latest command: "full_local" container_name: watcher env_file: - ./localchain_contract_addresses.env environment: - ETHEREUM_NETWORK=LOCALCHAIN - ETHEREUM_RPC_URL=http://172.27.0.108:80 - CHILD_CHAIN_URL=http://172.27.0.108:9656 - PORT=7434 - APP_ENV=local_docker_development - DD_HOSTNAME=datadog - DD_DISABLED=true - DB_PATH=/data - ETHEREUM_EVENTS_CHECK_INTERVAL_MS=800 - ETHEREUM_STALLED_SYNC_THRESHOLD_MS=20000 - ETHEREUM_BLOCK_TIME_SECONDS=1 - EXIT_PROCESSOR_SLA_MARGIN=5520 - EXIT_PROCESSOR_SLA_MARGIN_FORCED=TRUE - LOGGER_BACKEND=console - RELEASE_COOKIE=development - NODE_HOST=127.0.0.1 restart: always ports: - "7434:7434" expose: - "7434" volumes: - ./data:/data healthcheck: test: curl localhost:7434 interval: 30s timeout: 1s retries: 5 start_period: 30s depends_on: childchain: condition: service_healthy networks: chain_net: ipv4_address: 172.27.0.104 watcher_info: image: omisego/watcher_info:latest command: "full_local" container_name: watcher_info env_file: - ./localchain_contract_addresses.env environment: - ETHEREUM_NETWORK=LOCALCHAIN - ETHEREUM_RPC_URL=http://172.27.0.108:80 - CHILD_CHAIN_URL=http://172.27.0.108:9656 - DATABASE_URL=postgresql://omisego_dev:omisego_dev@172.27.0.107:5432/omisego_dev - PORT=7534 - APP_ENV=local_docker_development - DD_HOSTNAME=datadog - DD_DISABLED=true - DB_PATH=/data - ETHEREUM_EVENTS_CHECK_INTERVAL_MS=800 - ETHEREUM_BLOCK_TIME_SECONDS=1 - EXIT_PROCESSOR_SLA_MARGIN=5520 - EXIT_PROCESSOR_SLA_MARGIN_FORCED=TRUE - LOGGER_BACKEND=console - RELEASE_COOKIE=development - NODE_HOST=127.0.0.1 restart: always ports: - "7534:7534" expose: - "7534" volumes: - ./data:/data healthcheck: test: curl localhost:7534 interval: 30s timeout: 1s retries: 5 start_period: 30s depends_on: childchain: condition: service_healthy postgres: condition: service_healthy networks: chain_net: ipv4_address: 172.27.0.105 networks: chain_net: name: chain_net driver: bridge ipam: config: - subnet: 172.27.0.0/24 ================================================ FILE: docs/api_specs/errors.md ================================================ # Errors Note that HTTP calls will almost always return `200`, even if the result is an error. One exception to this is if an internal server error occurs - in this case it will return `500` When an error occurs, `success` will be set to `false` and `data` will contain more information about the error ```json { "version": "1", "success": false, "data": { "code": "account:not_found", "description": "Account not found" } } ``` # Error codes description Code | Description ---- | ----------- server:internal_server_error | Something went wrong on the server. Try again soon. operation:bad_request | Parameters required by this operation are missing or incorrect. More information about error in response object `data/messages` property. operation:not_found | Operation cannot be found. Check request URL. challenge:exit_not_found | The challenge of particular exit is impossible because exit is inactive or missing challenge:utxo_not_spent | The challenge of particular exit is impossible because provided utxo is not spent exit:invalid | Utxo was spent or does not exist. get_status:econnrefused | Cannot connect to the Ethereum node. in_flight_exit:tx_for_input_not_found | No transaction that created input. transaction:not_found | Transaction doesn't exist for provided search criteria transaction.create:insufficient_funds | Account balance is too low to satisfy the payment. transaction.create:too_many_outputs | Total number of payments + change + fees exceed maximum allowed outputs in transaction. We need to reserve one output per payment and one output per change for each currency used in the transaction. transaction.create:empty_transaction | Requested payment resulted in empty transaction that transfers no funds. submit_typed:missing_signature | Signatures should correspond to inputs owner. When all non-empty inputs has the same owner, signatures should be duplicated. submit_typed:superfluous_signature | Number of non-empty inputs should match signatures count. Remove redundant signatures. Refer to `...web/controllers/fallback.ex` family of files for a comprehensive list of error codes and descriptions. ================================================ FILE: docs/api_specs/index.html.md ================================================ --- title: OMG Network APIs Reference language_tabs: # must be one of https://git.io/vQNgJ - shell - elixir - javascript toc_footers: - Documentation Powered by Slate includes: - operator_api_specs - watcher_api_specs - info_api_specs - errors search: true --- # Introduction This is the HTTP-RPC API for the Child Chain Server and Watcher. All calls use HTTP POST and pass options in the request body in JSON format. Errors will usually return with HTTP response code 200, and the details of the error in the response body. See [Errors](#errors). ================================================ FILE: docs/api_specs/status_events_specs.md ================================================ ### Byzantine events All of the following events indicate byzantine behaviour and that the user should either exit or challenge. #### `invalid_exit` > An invalid_exit event ```json { "event": "invalid_exit", "details": { "eth_height" : 3521678, "utxo_pos" : 10000000010000000, "owner" : "0xb3256026863eb6ae5b06fa396ab09069784ea8ea", "currency" : "0x0000000000000000000000000000000000000000", "amount" : 100 } } ``` Indicates that an invalid exit is occurring. It should be challenged. #### `unchallenged_exit` > An unchallenged_exit event ```json { "event": "unchallenged_exit", "details": { "eth_height" : 3521678, "utxo_pos" : 10000000010000000, "owner" : "0xb3256026863eb6ae5b06fa396ab09069784ea8ea", "currency" : "0x0000000000000000000000000000000000000000", "amount" : 100 } } ``` Indicates that an invalid exit is dangerously close to finalization and hasn't been challenged. User should exit. See docs on [`unchallenged_exit` condition](../exit_validation.md#unchallenged-exit-condition) for more details. #### `invalid_block` > An invalid_block event ```json { "event": "invalid_block", "details": { "blockhash" : "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec", "blknum" : 10000, "error_type": "tx_execution" } } ``` An invalid block has been added to the chain. User should exit. #### `block_withholding` > A block_withholding event ```json { "event": "block_withholding", "details": { "hash" : "0x0017372421f9a92bedb7163310918e623557ab5310befc14e67212b660c33bec", "blknum" : 10000 } } ``` The ChildChain is withholding a block whose hash has been published on the root chain. User should exit. #### `non_canonical_ife` > A noncanonical_ife event ```json { "event": "non_canonical_ife", "details": { "txbytes": "0xf3170101c0940000..." } } ``` An in-flight exit of a non-canonical transaction has been started. It should be challenged. Event details: Attribute | Type | Description --------- | ------- | ----------- txbytes | Hex encoded string | The in-flight transaction that the event relates to #### `unchallenged_non_canonical_ife` ```json { "event": "unchallenged_non_canonical_ife", "details": { "txbytes": "0xf3170101c0940000..." } } ``` Indicates that there is unchallenged non canonical in-flight exit that is dangerously close to finalization and hasn't been challenged. User should exit. See docs on [`unchallenged_exit` condition](../exit_validation.md#unchallenged-exit-condition) for more details. Event details: Attribute | Type | Description --------- | ------- | ----------- txbytes | Hex encoded string | The in-flight transaction that the event relates to #### `invalid_ife_challenge` > A invalid_ife_challenge event ```json { "event": "invalid_ife_challenge", "details": { "txbytes": "0xf3170101c0940000..." } } ``` A canonical in-flight exit has been challenged. The challenge should be responded to. Event details: Attribute | Type | Description --------- | ------- | ----------- txbytes | Hex encoded string | The in-flight transaction that the event relates to #### `piggyback_available` > A piggyback_available event ```json { "event": "piggyback_available", "details": { "txbytes": "0xf3170101c0940000...", "available_outputs" : [ {"index": 0, "address": "0xb3256026863eb6ae5b06fa396ab09069784ea8ea"}, {"index": 1, "address": "0x488f85743ef16cfb1f8d4dd1dfc74c51dc496434"}, ], "available_inputs" : [ {"index": 0, "address": "0xb3256026863eb6ae5b06fa396ab09069784ea8ea"} ], } } ``` An in-flight exit has been started and can be piggybacked. If all inputs are owned by the same address, then `available_inputs` will not be present. This event is reported only for in-flight exits from transactions that have not been included in a block. If input or output of exiting transaction is piggybacked it does not show up as available for piggybacking. When in-flight exit is finalized, transaction's inputs and outputs are not available for piggybacking. Event details: Attribute | Type | Description --------- | ------- | ----------- txbytes | Hex encoded string | The in-flight transaction that the event relates to available_outputs | Object array | The outputs (index and address) available to be piggybacked available_inputs | Object array | The inputs (index and address) available to be piggybacked #### `invalid_piggyback` > A invalid_piggyback event ```json { "event": "invalid_piggyback", "details": { "txbytes": "0xf3170101c0940000...", "inputs": [1], "outputs": [0] } } ``` An invalid piggyback is in process. Should be challenged. Event details: Attribute | Type | Description --------- | ------- | ----------- txbytes | Hex encoded string | The in-flight transaction that the event relates to inputs | Integer array | A list of invalid piggybacked inputs outputs | Integer array | A list of invalid piggybacked outputs #### `unchallenged_piggyback` > A invalid_piggyback event ```json { "event": "unchallenged_piggyback", "details": { "txbytes": "0xf3170101c0940000...", "inputs": [1], "outputs": [0] } } ``` An invalid piggyback is dangerously close to finalization and hasn't been challenged. User should exit. Event details: Attribute | Type | Description --------- | ------- | ----------- txbytes | Hex encoded string | The in-flight transaction that the event relates to inputs | Integer array | A list of invalid piggybacked inputs outputs | Integer array | A list of invalid piggybacked outputs ================================================ FILE: docs/architecture.md ================================================ # Architecture This is a high-level rundown of the architecture of the `elixir-omg` apps. The below diagram demonstrates the various pieces and where this umbrella app fits in. ![high level architecture overview diagram](assets/architecture_overview.jpg) **NOTE** only use the high-level diagram to get a vague idea, meaning of boxes/arrows may be imprecise. ## Interactions **[Diagram](https://docs.google.com/drawings/d/11ugr_VQzqh0afU6NPpHW893jww182POaGE3sYhgm9Gw/edit?usp=sharing)** illustrates the interactions described below. This lists only interactions between the different processes that build up both the Child Chain Server and Watcher. For responsibilities of the processes/modules look into respective docs in `.ex` files. **NOTE** The hexagonal shape hints towards component being a wrapper (port/adapter) to something external, versus rectangular shape being an internal component. ### `OMG.State` - writes blocks and UTXO set to `OMG.DB` ### `OMG.ChildChain` - accepts child chain transactions, decodes, stateless-validates and executes on `OMG.State` ### `OMG.Watcher.RootChainCoordinator` - reads Ethereum block height from `OMG.Eth` - synchronizes view of Ethereum block height of all enrolled processes (see other processes descriptions) ### `:exiter`'s Actually `OMG.EthereumEventListener` setup with `:exiter`. **NOTE** there's a multitude of exiter-related processes, which work along these lines, we're not listing them here - pushes exit-related events to `OMG.State` on child chain server's side - pushes exit-related events to `OMG.Watcher.ExitProcessor` on watcher's side - pushes exit-related events to `WatcherDB` ### `:depositor` Actually `OMG.EthereumEventListener` setup with `:depositor`. - pushes deposits to `OMG.State` - pushes deposits to `WatcherDB` ### `OMG.ChildChain.BlockQueue` - requests `form_block` on `OMG.State` and takes block hashes in return - tracks Ethereum height and child chain block submission mining via `OMG.Eth` and `OMG.Watcher.RootChainCoordinator` ### `OMG.ChildChain.FeeServer` - `OMG.ChildChain` calls it to get required fee amounts to validate transactions ### `OMG.Watcher.BlockGetter` - tracks child chain blocks via `OMG.Watcher.RootChainCoordinator` - manages concurrent `Task`'s to pull blocks from child chain server API (JSON-RPC) - pushes decoded and statelessly valid blocks to `OMG.State` - pushes statefully valid blocks and transactions (acknowledged by `OMG.State` above) to `WatcherDB` - stops if `OMG.Watcher.ExitProcessor` reports a dangerous byzantine condition related to exits ### `OMG.Watcher.ExitProcessor` - get various Ethereum events from `OMG.EthereumEventListener` - used only in Watcher - validates exits - spends finalizing exits in `OMG.State` ### `OMG.WatcherRPC` - uses `OMG.Watcher` to server user's requests ### `OMG.Performance` - executes requests to `OMG.WatcherRPC` - executes requests to `OMG.ChildChainRPC` - forces block forming by talking directly to `OMG.State` ## Databases - The Child Chain talks to its local RocksDB - The Watcher talks to its local RocksDB - The Watcher Info talks to its local RocksDB and PostgreSQL ### `OMG.DB` An "intimate" database for `OMG.State` that holds the UTXO set and blocks. May be seen and read by other processes to sync on the persisted state of `OMG.State` and UTXO set by consequence. Non-relational data, so we're having a simple KV for this. Each instance of either Child Chain Server or Watcher should have it's own instance. Database necessary to properly ensure validity and availability of blocks and transactions - it is read by `OMG.State` on restart to discover where it left off, whole UTXO set is not loaded. - it is read by many other processes to discover where they left off, on restart - it is used for the Watcher's security critical features to access exits info and blocks ### Watcher Info DB A database running used by the Watcher in convenience API mode **only**. Holds all information necessary to conveniently manage the funds held: - UTXOs owned by user's particular address(es) - transaction history Relational data, to be able to navigate through the transactions and UTXOs. Implemented with PostgreSQL. ================================================ FILE: docs/branching.md ================================================ # Branching and deployments model This document aims to discuss and document the relations between branches and deployments of `elixir-omg`, with respect to branches and deployments of `plasma-contracts`. It is a refinement of the [OIP4 branching model](https://github.com/omgnetwork/OIP/blob/master/0004-ewallet-release-and-versioning.md), applicable to `elixir-omg` and `plasma-contracts` versioning. Rationale: - the history and the relations between the versions are readable and simple to understand - we can predictably sync our respective watchers/run child chain servers against deployed contracts - we can move with the `master` branch quickly ## Dependency Rules for elixir-omg ### For mix.exs - `elixir-omg/master` will point to the `plasma-contracts/master` - A release branch in `elixir-omg` will point to the corresponding release branch in `plasma-contracts`. For example: - `elixir-omg/v0.1` -> `plasma-contracts/v0.1` ### For mix.lock - Always points to a specific SHA that in the history of the `plasma-contracts` branch referenced in `mix.exs` ## Deployment Scenarios ### 1 - Single production deployment, ongoing development This is the active scenario most of the time. Branches and environments: - `master` is automatically deployed to **development** environment - `v0.1` is automatically deployed to **staging-v0-1** environment - changes to the release branch will be merged into `master`, as needed - `v0.1` is manually deployed to **production-v0-1** environment Deploying new contracts in `master`: - make a PR to `elixir-omg/master` bumping the contract version in `mix.lock` - CI checks on the new integration - merge the PR - redeploy contracts - redeploy child chain and watcher - NB – contract deployment is currently a manual process, so we may be in a state where `mix.lock` points to a newer SHA than deployed on **development**. _We will correct this disparity as quickly as possible. This may mean rolling back `mix.lock`, if needed._ - TODO – Automate contract deployments in **development** Deploying new contracts in the release branch: - :stop_sign: - _NOPE_ - We cannot deploy any `elixir-omg` code that is incompatible with the currently deployed contracts in **staging** and **production** ### 2 - Production deployment, validating a new version for network upgrade This is a _feature freeze_ for the new version (`v0.2` branch). Try to minimize merging changes from `master` to any of the release branches. Branches and environments: - `master` is automatically deployed to **development** environment - `v0.2` is automatically deployed to **staging-v0-2** environment - This assumes that during the process of validating a network upgrade, all work merged onto `master` will get deployed to for the upgrade. - `v0.1` is automatically deployed to **staging-v0-1** environment - Keep this environment around for hotfixes - `v0.1` is manually deployed to **production-v0-1** environment Deploying new contracts in `master`: - Same as Scenario 1 Deploying new contracts in `v0.1`: - :stop_sign: _NOPE_ Deploying new contracts to `v0.2` - Manually deploy to **staging-v0-2** ### 3 - Production deployment, ready to deploy network upgrade to production, ongoing development When we're confident of the stability on **staging-v0-2** and ready to go to Private Alpha, create the `v0.2` branch from `master` for both `elixir-omg` and `plasma-contracts` repos. Most importantly, we're confident about the contracts. A contract redeployment in this phase would have the most impact. Branches and environments: - `master` is automatically deployed to **development** environment - `v0.2` is automatically deployed to **staging-v0-2** environment - changes to this release branch will be merged into `master`, as needed - `v0.2` is manually deployed to **production-v0-2** environment - `v0.1` is automatically deployed to **staging-v0-1** environment - changes to the release branch will be merged into `master`, as needed - `v0.1` is manually deployed to **production-v0-1** environment Deploying new contracts to `master`: - Same as Scenario 1 Deploying new contracts to `v0.1` - :stop_sign: _NOPE_ Deploying new contracts to `v0.2` - Manually deploy to **staging-v0-2** and **production-v0-2** environments, _if absolutely necessary_ ### 4 - Two production deployments, ongoing development We will have two production environments during the network upgrade, so that users have the opportunity to exit the old environment and deposit into the new environment. Everything the same as Scenario 3 except - Deploying new contracts to `v0.2` - :stop_sign: _NOPE_ Once this phase ends, we take down the older `production-v0-1` and `staging-v0-1` and return to Scenario 1. We may want to consider continuing to run a watcher for an old version a longer period of time. ================================================ FILE: docs/deployment_configuration.md ================================================ # Configuration via environment variables for deployment of Child Chain, Watcher and Watcher Info releases ***Child Chain, Watcher and Watcher Info*** - "PORT" - Child Chain or Watcher API port. Defaults to 9656 for Child Chain and 7434 for Watcher. - "HOSTNAME" - server domain name of Child Chain or Watcher. *mandatory* - "DD_DISABLED" - boolean that allows you to turn on or of Datadog metrics. Defaults to true. - "APP_ENV" - environment name in which the the application was deployed. *mandatory* - "DB_PATH" - directory of the KV db. *mandatory* - "ETHEREUM_RPC_URL" - address of Geth or Parity instance. *mandatory* - "ETH_NODE" - Geth, Parity or Infura. *mandatory* - "SENTRY_DSN" - if not set, Sentry is disabled. - "DD_HOSTNAME" - Datadog hostname. - "DD_PORT" - Datadog agent UDP port for metrics. - "DD_APM_PORT" - Datadog TCP port for APM. - "BATCH_SIZE" - Datadog batch size for APM. - "SYNC_THRESHOLD" - Datadog sync threshold for APM. - "ETHEREUM_BLOCK_TIME_SECONDS" - Should mirror Ethereum network's setting, defaults to 15 seconds. - "ETHEREUM_EVENTS_CHECK_INTERVAL_MS" - the frequency of HTTP requests towards the Ethereum clients and scanning for interested events. Should be less then average block time (10 to 20 seconds) on Ethereum mainnet. - "ETHEREUM_STALLED_SYNC_THRESHOLD_MS" - the threshold before considering an unchanging Ethereum block height to be considered a stalled sync. Should be slightly larger than the expected block time. - "LOGGER_BACKEND" - Ink or console. Ink will encode logs as json (useful for Datadog). Console will use the default elixir Logger backend. Default is Ink. ***Child Chain only*** - "BLOCK_SUBMIT_MAX_GAS_PRICE" - The maximum gas price to use for block submission. The first block submission after application boot will use the max price. The gas price gradually adjusts on subsequent blocks to reach the current optimum price. Defaults to `20000000000` (20 Gwei). - "BLOCK_SUBMIT_STALL_THRESHOLD_BLOCKS" - The number of root chain blocks passed until a child chain block pending submission is considered stalled. Defaults to `4` root chain blocks. - "FEE_ADAPTER" - The adapter to use to populate the fee specs. Either `file` or `feed` (case-insensitive). Defaults to `file` with an empty fee specs. - "FEE_CLAIMER_ADDRESS" - 20-bytes HEX-encoded string of Ethereum address of Fee Claimer. - "FEE_BUFFER_DURATION_MS" - Buffer period during which a fee is still valid after being updated. - "FEE_SPECS_FILE_PATH" - The path to the fee specs file including the file name. Only applicable when `FEE_ADAPTER=file`. - "FEE_FEED_URL" - URL to fee feed service. Only applicable when `FEE_ADAPTER=feed`. - "FEE_CHANGE_TOLERANCE_PERCENT" - Positive integer describes significance of price change. When price in new reading changes above tolerance level, prices are updated immediately. Otherwise update interval is preserved. Only applicable when `FEE_ADAPTER=feed`. - "STORED_FEE_UPDATE_INTERVAL_MINUTES" - Positive integer describes time interval in minutes. The updates of token prices are carried out in update intervals as long as the changes are within tolerance. Only applicable when `FEE_ADAPTER=feed`. ***Watcher and Watcher Info only*** - "CHILD_CHAIN_URL" - Location of the Child Chain API. *mandatory* - "EXIT_PROCESSOR_SLA_MARGIN" - Number of Ethereum blocks since start of an invalid exit, before `unchallenged_exit` is reported to prompt to mass exit. Must be smaller than "MIN_EXIT_PERIOD_SECONDS", unless "EXIT_PROCESSOR_SLA_MARGIN_FORCED=TRUE". ***Watcher Info only*** - "DATABASE_URL" - Postgres address *mandatory* - "WATCHER_INFO_DB_POOL_SIZE" - The size of the database connection pool. Defaults to `10`. - "WATCHER_INFO_DB_POOL_QUEUE_TARGET_MS" - The maximum time to wait for a DB connection in milliseconds. Defaults to `50`. - "WATCHER_INFO_DB_POOL_QUEUE_INTERVAL_MS" - The interval in milliseconds to determine whether the queue target period above has been exceeded. Defaults to `1000`. ***Erlang VM configuration*** - "NODE_HOST" - The fully qualified host name of the current host. - "ERLANG_COOKIE" - Magic cookie of the node. - "REPLACE_OS_VARS" - An environment variable you export at runtime which instructed the tool to replace occurances of ${VAR} with the value from the system environment in the vm.args. ***Contract address configuration*** We allow a static configuration or a dynamic one, served as a http endpoint (one of them is mandatory). - "ETHEREUM_NETWORK" - "RINKEBY" or "LOCALCHAIN". - "CONTRACT_EXCHANGER_URL" - a server that can serve JSON in form of ``` { "plasma_framework_tx_hash":"", "plasma_framework":"", "eth_vault":"", "erc20_vault":"", "payment_exit_game":"", "authority_address":"" } ``` Static configuration - "ETHEREUM_NETWORK" - RINKEBY, ROPSTEN, MAINNET, or LOCALCHAIN - "TXHASH_CONTRACT" - "AUTHORITY_ADDRESS" - "CONTRACT_ADDRESS_PLASMA_FRAMEWORK" - "CONTRACT_ADDRESS_ETH_VAULT - "CONTRACT_ADDRESS_ERC20_VAULT - "CONTRACT_ADDRESS_PAYMENT_EXIT_GAME" ***Required contract addresses*** The contract addresses that are required to be included in the `contract_addr` field (or `_CONTRACT_ADDRESS` JSON) are: ``` { "plasma_framework": "...", "eth_vault": "...", "erc20_vault": "...", "payment_exit_game": "..." } ``` ================================================ FILE: docs/details.md ================================================ **Table of Contents** * [elixir-omg applications](#elixir-omg-applications) * [Child chain server](#child-chain-server) * [Using the child chain server's API](#using-the-child-chain-servers-api) * [HTTP-RPC](#http-rpc) * [Ethereum private key management](#ethereum-private-key-management) * [geth](#geth) * [Managing the operator address](#managing-the-operator-address) * [Nonces restriction](#nonces-restriction) * [Funding the operator address](#funding-the-operator-address) * [Watcher and Watcher Info](#watcher-and-Watcher-info) * [Modes of the watcher](#modes-of-the-watcher) * [Using the watcher](#using-the-watcher) * [Endpoints](#endpoints) ## `elixir-omg` applications `elixir-omg` is an umbrella app comprising of several Elixir applications: The general idea of the apps responsibilities is: - `omg` - common application logic used by both the child chain server and watcher - `omg_bus` - an internal event bus to tie services together - `omg_child_chain` - child chain server - tracks Ethereum for things happening in the root chain contract (deposits/exits) - gathers transactions, decides on validity, forms blocks, persists - submits blocks to the root chain contract - see `apps/omg_child_chain/lib/omg_child_chain/application.ex` for a rundown of children processes involved - `omg_child_chain_rpc` - an HTTP-RPC server being the gateway to `omg_child_chain` - `omg_db` - wrapper around the child chain server's database to store the UTXO set and blocks necessary for state persistence - `omg_eth` - wrapper around the [Ethereum RPC client](https://github.com/exthereum/ethereumex) - `omg_status` - application monitoring facilities - `omg_utils` - various non-omg-specific shared code - `omg_watcher` - the [Watcher](#watcher-and-watcher-info) - `omg_watcher_info` - the [Watcher Info](#watcher-and-watcher-info) - `omg_watcher_rpc` - an HTTP-RPC server being the gateway to `omg_watcher` See [application architecture](architecture.md) for more details. ## Child chain server `:omg_child_chain` is the Elixir app which runs the child chain server, whose API is exposed by `:omg_child_chain_rpc`. For the responsibilities and design of the child chain server see [Plasma Blockchain Design document](tesuji_blockchain_design.md). ## Using the child chain server's API The child chain server is listening on port `9656` by default. ### HTTP-RPC HTTP-RPC requests are served up on the port specified in `omg_child_chain_rpc`'s `config` (`:omg_child_chain_rpc, OMG.RPC.Web.Endpoint, http: [port: ...]`). The available RPC calls are defined by `omg_child_chain` in `api.ex` - paths follow RPC convention e.g. `block.get`, `transaction.submit`. All requests shall be POST with parameters provided in the request body in JSON object. Object's properties names correspond to the names of parameters. Binary values shall be hex-encoded strings. For API documentation see: https://docs.omg.network/. ## Ethereum private key management ### `geth` Currently, the child chain server assumes that the authority account is unlocked or otherwise available on the Ethereum node. This might change in the future. ## Managing the operator address (a.k.a `authority address`) The Ethereum address which the operator uses to submit blocks to the root chain is a special address which must be managed accordingly to ensure liveness and security. ## Nonces restriction The [reorg protection mechanism](tesuji_blockchain_design.md#reorgs) enforces there to be a strict relation between the `submitBlock` transactions and block numbers. Child block number `1000` uses Ethereum nonce `1`, child block number `2000` uses Ethereum nonce `2`, **always**. This provides a simple mechanism to avoid submitted blocks getting reordered in the root chain. This restriction is respected by the child chain server, whereby the Ethereum nonce is simply derived from the child block number. As a consequence, the operator address must never send any other transactions, if it intends to continue submitting blocks. (Workarounds to this limitation are available, if there's such requirement.) **NOTE** Ethereum nonce `0` is necessary to call the `RootChain.init` function, which must be called by the operator address. This means that the operator address must be a fresh address for every child chain brought to life. ## Funding the operator address The address that is running the child chain server and submitting blocks needs to be funded with Ether. At the current stage this is designed as a manual process, i.e. we assume that every **gas reserve checkpoint interval**, someone will ensure that **gas reserve** worth of Ether is available for transactions. Gas reserve must be enough to cover the gas reserve checkpoint interval of submitting blocks, assuming the most pessimistic scenario of gas price. Calculate the gas reserve as follows: ``` gas_reserve = child_blocks_per_day * days_in_interval * gas_per_submission * highest_gas_price ``` where ``` child_blocks_per_day = ethereum_blocks_per_day / submit_period ``` **Submit period** is the number of Ethereum blocks per a single child block submission) - configured in `:omg_child_chain, :child_block_submit_period` **Highest gas price** is the maximum gas price which the operator allows for when trying to have the block submission mined (operator always tries to pay less than that maximum, but has to adapt to Ethereum traffic) - configured in (**TODO** when doing OMG-47 task) **Example** Assuming: - submission of a child block every Ethereum block - 15 second block interval on Ethereum, on average - weekly cadence of funding, i.e. `days_in_interval == 7` - allowing gas price up to 40 Gwei - `gas_per_submission == 71505` (checked for `RootChain.sol` [at this revision](https://github.com/omgnetwork/plasma-contracts/commit/50653d52169a01a7d7d0b9e2e4e3c4a4b904f128). C.f. [here](https://rinkeby.etherscan.io/tx/0x1a79fdfa310f91625d93e25139e15299b4ab272ae504c56b5798a018f6f4dc7b)) we get ``` gas_reserve ~= (4 * 60 * 24 / 1) * 7 * 71505 * (40 / 10**9) ~= 115 ETH ``` **NOTE** that the above calculation doesn't imply this is what is going to be used within a week, just a pessimistic scenario to calculate an adequate reserve. If one assumes an _average_ gas price of 4 Gwei, the amount is immediately reduced to ~11.5 ETH weekly. ## Watcher and Watcher Info The Watcher is an observing node that connects to Ethereum and the child chain server's API. It ensures that the child chain is valid and notifies otherwise. It exposes the information it gathers via an HTTP-RPC interface (driven by Phoenix). It provides a secure proxy to the child chain server's API and to Ethereum, ensuring that sensitive requests are only sent to a valid chain. For more on the responsibilities and design of the Watcher see [Plasma Blockchain Design document](tesuji_blockchain_design.md). ### Modes of the watcher The watcher can be run in one of two modes: - **security-critical only** - intended to provide light-weight Watcher just to ensure security of funds deposited into the child chain - this mode will store all of the data required for security-critical operations (exiting, challenging, etc.) - it will not store data required for current and performant interacting with the child chain (spending, receiving tokens, etc.) - it will not expose some endpoints related to current and performant interacting with the child chain (`account.get_utxos`, `transaction.*`, etc.) - it will only require the `OMG.DB` key-value store database - this mode will prune all security-related data not necessary anymore for security reasons (from `OMG.DB`) - some requests to the API might be slow but must always work (called rarely in unhappy paths only, like mass exits) - **security-critical and informational API** - intended to provide convenient and performant API to the child chain data, on top of the security-related one - this mode will provide/store everything the **security-critical** mode does - this mode will store easily accessible register of all transactions _for a subset of addresses_ (currently, all addresses) - this mode will leverage the PostgreSQL - based `WatcherDB` database In releases, `watcher` refers to the security-critical mode, while `watcher_info` refers to the security-critical and informational API mode. ### Using the watcher The watcher is listening on port `7434` by default. And watcher info listens on port `7534`. ### Endpoints For API documentation see: https://docs.omg.network/ ### Ethereum private key management Watcher doesn't hold or manage user's keys. All signatures are assumed to be done outside. # Configuration parameters For docker deployments, and release deployments please refer to [Deployment Configuration](deployment_configuration.md). **NOTE**: all margins are denominated in Ethereum blocks ## Child chain server configuration - `:omg_child_chain` app * **`submission_finality_margin`** - the margin waited before mined block submissions are purged from `BlockQueue`'s memory * **`block_queue_eth_height_check_interval_ms`** - polling interval for checking whether the root chain had progressed for the `BlockQueue` exclusively * **`fee_adapter_check_interval_ms`** - interval for checking fees updates from the fee adapter. * * **`fee_buffer_duration_ms`** - duration for which a fee is still valid after beeing updated. * **`block_submit_every_nth`** - how many new Ethereum blocks must be mined, since previous submission **attempt**, before another block is going to be formed and submitted. * **`block_submit_max_gas_price`** - the maximum gas price to use for block submission. The first block submission after application boot will use the max price, and gradually adjusts to the current optimum price for subsequent blocks. * **`fee_specs_file_path`** - path to the file which defines fee requirements * **`fee_adapter`** - is a tuple, where first element is a module name implementing `FeeAdapter` behaviour, e.g. `OMG.ChildChain.Fees.FileAdapter` and the second element is a Keyword `[opts: fee_adapter_opts]` Options of the fee adapter, depends on adapter - **`specs_file_path`** - [FileAdaper only] path to file (including the file name) which defines fee requirements, see [fee_specs.json](fee_specs.json) for an example. - **`fee_feed_url`** - [FeedAdapter only] url to the fee service, that privides actual fees prices. Response should follow the file specs format. - **`fee_change_tolerance_percent`** - [FeedAdapter only!] positive integer describes significance of price change. When price in new reading changes above tolerance level, prices are updated immediately. Otherwise update interval is preserved. - **`stored_fee_update_interval_minutes`** - [FeedAdapter only!] positive integer describes time interval in minutes. The updates of token prices are carried out in update intervals as long as the changes are within tolerance. ## Watcher configuration - `:omg_watcher` app * **`deposit_finality_margin`** - the margin that is waited after a `DepositCreated` event in the root chain contract. Only after this margin had passed: - the child chain will allow spending the deposit - the watcher and watcher info will consider a transaction spending this deposit a valid transaction It is important that for a given child chain, the child chain server and watchers use the same value of this margin. **NOTE**: This entry is defined in `omg`, despite not being accessed there, only in `omg_child_chain` and `omg_watcher`. The reason here is to minimize risk of Child Chain server's and Watcher's configuration entries diverging. * **`ethereum_events_check_interval_ms`** - polling interval for pulling Ethereum events (logs) from the Ethereum client. * **`coordinator_eth_height_check_interval_ms`** - polling interval for checking whether the root chain had progressed for the `RootChainCoordinator`. Affects how quick the services reading Ethereum events realize there's a new block. * **`exit_processor_sla_margin`** - the margin to define the notion of a "late", invalid exit. After this margin passes, every invalid exit is deemed a critical failure of the child chain (`unchallenged_exit`). Such event will prompt a mass exit and stop processing new blocks. See [exit validation documentation](docs/exit_validation.md) for details. Cannot be larger than `min_exit_period_seconds` because otherwise it leads to a dangerous setup of the Watcher (in particular - muting the reports of unchallenged_exits). Override using the `EXIT_PROCESSOR_SLA_MARGIN` system environment variable. * **`exit_processor_sla_margin_forced`** - if set to `true`, will allow one to set a `exit_processor_sla_margin` that is larger than the `min_exit_period_seconds` of the child chain we're running for. Set to `true` only when you know what you are doing. Defaults to `false`, override using the `EXIT_PROCESSOR_SLA_MARGIN_FORCED` system environment variable. * **`maximum_block_withholding_time_ms`** - for how long the Watcher will tolerate failures to get a submitted child chain block, before reporting a block withholding attack and stopping * **`maximum_number_of_unapplied_blocks`** - the maximum number of downloaded and statelessly validated child chain blocks to hold in queue for applying * **`exit_finality_margin`** - the margin waited before an exit-related event is considered final enough to pull and process * **`block_getter_reorg_margin`** - the margin considered by `OMG.Watcher.BlockGetter` when searching for recent child chain block submission events. This is driving the process of determining the height and particular event related to the submission of a particular child chain block ## `OMG.DB` configuration - `:omg_db` app * **`path`** - path to the directory holding the LevelDB data store * **`server_module`** - the module to use when talking to the `OMG.DB` * **`server_name`** - the named process to refer to when talking to the `OMG.DB` ## `OMG.Eth` configuration - `:omg_eth` app All binary entries are expected in hex-encoded, `0x`-prefixed. * **`contract_addr`** - the address of the root chain contract * **`authority_address`** - the address used by the operator to submit blocks * **`txhash_contract`** - the Ethereum-transaction hash holding the deployment of the root chain contract * **`eth_node`** - the Ethereum client which is used: `"geth" | "infura"`. * **`node_logging_in_debug`** - whether the output of the Ethereum node being run in integration test should be printed to `:debug` level logs. If you set this to false, remember to set the logging level to `:debug` to see the logs * **`child_block_interval`** - mirror of contract configuration `uint256 constant public CHILD_BLOCK_INTERVAL` from `RootChain.sol` * **`min_exit_period_seconds`** - mirror of contract configuration `uint256 public minExitPeriod` * **`ethereum_block_time_seconds`** - mirror the block time of the underlying root chain. Defaults to 15 seconds, suitable for public networks (`mainnet` and testnets). Override using the `ETHEREUM_BLOCK_TIME_SECONDS` system environment variable. ================================================ FILE: docs/dex_design.md ================================================ # The OMG decentralized Exchange (ODEX) The term decentralized is a very broad term and typically is a catch-all for a number of characteristics or dimensions. For example, we may use the term decentralized to refer to the lack of a single controlling entity, and as a result bring the benefit of censorship resistance. Similarly the term “DEX” (decentralized exchange) also sees broad use with no widespread accepted definition. In order to provide clarity and consistency of understanding when talking about decentralization, or more specifically a DEX, we've decided to decompose our use of the term into a non exhaustive list of dimensions. We can then use these dimensions to describe the OMG DEX itself, its benefits and detail how we will prioritize its development. For the remainder of this document we will refer to the OMG DEX as "ODEX" for brevity. ## A Taxonomy for Decentralization To help establish a reusable taxonomy of decentralization using our identified dimensions we have grouped the them into three categories. Each category represents some aspect, or potential benefit, that may arise as a result of decentralization. These categories are: * **Intrinsic Dimensions** — these dimensions arise directly as a result of decentralization. * **Value Dimensions** — these dimensions are values we wish to support by leveraging the intrinsic and structural properties decentralization can bring. Specifically these are value dimensions that facilitate the delivery of fair and transparent markets. * **Structural Dimensions** — structural dimensions are properties that arise from the specific choices made when designing our decentralized exchange. ## Intrinsic Dimensions of Decentralization |Dimension|Benefit to the End User| | - | - | |Secure / Trustless|User funds are safe from being hacked as users retain custodial control of their funds| |Uncensorable|No single venue can stop you from being a participant| ## Value Dimensions Enabled by Decentralization |Dimension|Benefit to the End User| | - | - | |Private|Post-trade Anonymity — no adverse market reaction, by the time the market sees a trade the market reaction has already been considered in the visible market| |Private|Pre-trade Anonymity — no adverse market selection| |Transparent|Pre-trade Transparency — The ability to offer an accurate view of market liquidity| |Fair|Quality liquidity — the ability to access provable transparency information resulting in increased trust in the market brings better price discovery and a fairer market overall| |Liquid|Reduced fragmentation of liquidity by providing a single network on which all order flow can be connected| |Cost control|Visibility of cost structure| ## Structural Dimensions Resulting from OMG Network Implementation |Dimension|Benefit to the End User| | - | - | |Transparent|Post-trade Transparency — The ability to offer a provably accurate view of post-trade prices| |Fast settlement|Trades settle quickly and you choose when your trades settle| |Mobility of funds|Easily move your funds between different venues on the OMG Network to achieve 'best execution'| |Interoperable|Trade with other blockchains that are compatible with the OMG Network: Bitcoin, Litecoin, etc| |Upgradeable|Transparency in the upgrade process| |Responsive|Trade when you want to trade (reduced probability of network congestion due to the higher throughput of the OMG Network)| |Private|Identifying yourself may not be required in all exchanges| It isn’t possible to solve for *all* of the dimensions listed above, and each of the dimensions must be treated as design decisions on a spectrum, however using the taxonomy allows us to make informed choices around the impacts of our design choices and aids prioritization. In the following sections we will offer overviews of a number of alternative, evolving models, each driven by various tradeoffs for the dimensions above. In each of the models our primary goal is to solve for security and specifically the minimisation of funds loss. Thereafter, there exists a number of options in the design space balancing other key dimensions such as fairness, price transparency, user privacy, and speed. # ODEX Features ## Introduction This section will review the key features of ODEX. The following diagram is a high level view of what we think the future state of the ODEX may look like. This diagram will be described in detail throughout the remaining sections. A key observation should be that the ODEX is more than just a single market. It is an infrastructure upon which any participant can engage in using, making or delivering a market. This means many market models can be supported, all with the same underlying benefits and guarantees of the ODEX, while simultaneously offering tailored trading experiences as appropriate to the target users. This means that ODEX is more than just a single market, it is a network supporting many markets with both direct users of the ODEX co-existing beside indirect users of the ODEX such as venues themselves. ![ODEX Overview](assets/dex_design/01_ODEX%20Features.png) ## Restricted Custody OMG Network proposes a solution whereby user funds are secured by the child chain consensus mechanism. The exchange of value occurs in a secure manner, which vastly reduces the risk exposure for both a venue *and* for the user. We can provide this custodial safety using a model which utilizes the safety of the child chain consensus mechanism. Restricted Custody allows custody transfer to an off-chain venue to facilitate matching but only allows the venue to perform the required, fundamental actions, such as partial matching, canceling and initiation of settlement. The constraints that are placed upon funds in custody of a venue are: * The venue can neither deposit nor exit from the child chain * Transactions to move funds must be represented as trade settlements. The venue must prove that the beneficiaries of the settlements wanted to trade (by their signed orders). In other words, a venue cannot spend or exit user funds. Therefore, whilst users may transfer custody of their funds to a venue, the users will rely on the safety of the child chain consensus to enforce how a users' funds may be used. Also, importantly, this facilitates, in an efficient and fundamental way, firm orders. Firm orders are an essential component of any fair market. Note that work on Restricted Custody is continuing and changes can be expected to this design. ## Multiple Venues As stated in the introduction the ODEX will support multiple venues, removing single points of failure and reducing the possibility that a user will be refused exchange services. Multiple venues increases censorship resistance. The possible addition of an on-chain exchange in the future would add much greater censorship resistance. ## Off-Chain Markets Off-chain markets (typically orderbook driven) are important, not only to move computationally intensive tasks away from the operator/validators, but also to give venues the flexibility to change their market microstructure (such as minimum tick size, fee structure and so on) to ensure a competitive and differentiated market for an exchange's services, and markets themselves. It also has material implications on the speed and efficiency of the matching and trading process as we will see later. ## On-Chain Markets (Parallel Phase) In conjunction with off-chain markets, an on-chain market that is maintained by the child chain consensus system may be added. Further research is underway in this area to identify how an on-chain market, which could be offered using an orderbook structure, fits into the ODEX market model. It is also possible that multiple on-chain markets may be supported to differentiate between the specific needs of particular markets. The research and the introduction of an on-chain market may occur in parallel to the development of off-chain market support. Initially, it is thought that an on-chain market would have the following characteristics: * Call market, rather than continuous market * Auction based mechanism built on an orderbook Whilst an on-chain market may not offer the best price to participants (for example, because of lower liquidity concentration or slowness in reacting to fundamental changes in value), the on-chain market can offer guaranteed access to an exchange mechanism for participants that cannot, or choose not to, gain access to other venues on the ODEX. ## Batch Settlement Without batch settlement, it would be expected for venues to immediately settle any order execution. In a low transaction environment that can work effectively however it doesn't scale particularly well. By supporting batch settlement we can reduce the number of transactions required for settlement, and it is therefore useful for: * Auctions * Efficiently settling in highly liquid markets * Atomic settlement where multiple orders are resolved simultaneously, such as options markets * Settlement of implied orders. Implied orders are necessary for the OMG Network core user story. Implied orders enable the exchange of two assets that do not directly trade against one another. For example, * A user has Burger tokens and a cafe accepts Coffee tokens * There is no market for BURGER/COFFEE tokens * However, there is a market for BURGER/ETH and ETH/COFFEE * Therefore an implied price and an implied order can be derived ## Proveable Trade Settlement A venue will not be allowed to ‘spend’ or move funds in an unconstrained manner. Instead funds may only be ‘settled’ and a proof must be produced by the venue with the orders that constitute the resultant settlement. Proveable trade settlement increases the safety of the exchange for users and provides valuable post-trade transparency (see next section). ## Pre-trade and Post-trade Transparency Post-trade transparency provides trade information after a trade has been executed. Whereas pre-trade transparency indicates prices at which participants are willing to pay. High quality, and trusted transparency is an essential requirement for fair markets and good price discovery. Transparency is also important because the data can be used to predict price changes and to validate the proofs generated by venues on the ODEX. Whilst there may be multiple venues operating independently on the ODEX, information about how the market is operating may be consolidated. A ‘global’ ticker tape could be derived for: * Prices for all trade executions for a particular venue, or the network as a whole * Last price for any instrument (currency pair such as ETH/OMG) * Trusted pricing metrics and price movements for any instrument could be offered as an extension to basic ticker information ## Order Privacy In traditional markets, order details are private to a trader. This is a very important as it allows traders to take positions which reflect what they see as the fundamental value of the instrument being traded, without the knowledge that *they* are taking a position or *how* they are taking the position. If the order details were public other traders could use it to infer information about the instrument's value or other traders intent and strategies. For example that a specific trader was hedging one instrument against another. It is also a key aspect of fair markets in general, and so it is highly undesirable for this information to be made public. # ODEX Phases ## Introduction In this section we present our current phasing of development for ODEX to illustrate how we can incrementally prove out the features of ODEX that underpin the value proposition captured by the previous dimensions of decentralization. As this phasing is incremental (though not necessarily serial) any functionality added in each phase is *cumulative*, unless otherwise specified. ## Phase 1 - Technology Proof of Concept ![phase 1 diagram](assets/dex_design/02_Phase%201%20-%20Technology%20Proof%20of%20Concept.png) Phase 1 implements all the technical components and the basic child chain consensus changes that are required to prove out the feasibility of the ODEX. Orders are matched off-chain and trades are immediately settled on-chain with a proof. ## Phase 2 - MVP ![phase 2 diagram](assets/dex_design/03_Phase%202%20-%20MVP.png) Batch settlements are introduced in Phase 2. Batch settlements most importantly will enable implied orders, which are required to fulfill one of the primary OMG Network use cases (see Appendix). Batch settlement optimizes the settlement process because settlement is the net outcome of all of the trades in a batch. Matching must be performed in a deterministic way such that proofs may be independently verified. Phase 1 and phase 2 have an interesting safety property. Since the private key for a venue is only used to sign proofs for settlements, loss of the private key will not result in user funds being lost. However, if an attacker was able to gain access to a venue's private key, the attacker may be able to spam the network. Caution: Care must be taken to ensure that computation complexity of validating all of the settlement proofs can be accommodated by both a single operator (in PoA) and by the target validator sizing (in PoS). ## Phase 3 - Bonded Exchanges ![phase 3 diagram](assets/dex_design/04_Phase%203%20-%20Bonded%20Exchages.png) Phase 3 introduces an explicit economic disincentive for a venue to perform bad behavior. Upon proof of bad behaviour, such as an invalid settlement, a venue would lose some or all of their bond. The size of the bond that needs to be posted is yet to be decided. However, it may be possible for the bond to be sized proportionally to the amount of risk on the order books of an exchange. Without a considered approach to bond sizing venues would be disincentivized from providing matching facilities and therefore users would ultimately suffer as the cost of transacting would become very high. This would incentivize users to seek alternative, less secure and more centralized venue offerings. ## Phase 4 - Order Privacy ![phase 4 diagram](assets/dex_design/05_Phase%204%20-%20Order%20Privacy.png) Phase 4 introduces order privacy, most likely utilizing zero knowledge proofs. Note that this phase is a research topic and is subject to change. Zero knowledge proofs protect traders from revealing order details, but maintains provable trade settlement and post-trade transparency. In other words the basic economic details of a trade is known (the price and quantity), but not the specific details of the orders that were executed to complete that trade. This details could include information such as who traded, any limit price, original quantity and so on. Were this information available it would be very difficult for any trader to achieve a fair price with risk of adverse selection and therefore transitory volatility would increase in the market, damaging price discovery and increasing the cost of trading overall. Some flexibility may be possible with order privacy, whereby some order details are public and some order details are not public. This may be on a per venue or a per order basis. This is an active area of our research, both from a technical and markets perspective. ## OMG On-Chain Markets ![on chain venue diagram](assets/dex_design/06_OMG%20On-chain%20Venue.png) In conjunction with off-chain markets, on-chain markets that are maintained by the child chain consensus system is planned. Further research will be performed in this area to identify how on-chain markets fit into the ODEX market model. The research may occur in parallel to the development of off-chain markets, but development will initially commence with Restricted Custody (phase 1 and 2). As was stated previously it is thought that viable and useful on-chain markets would be: * Periodic Call markets, rather than Continuous markets, and be, * Auction based, executed using an orderbook On-chain markets have the attraction of providing guaranteed access (censorship resistance) to matching services for any participant. However, on-chain markets in and of themselves my not offer the best available execution because of a slower speed of market adaption (slower movement towards the fundamental value of an instrument), lower access to counterparty interest at the desired price (lower concentration for liquidity of an instrument) and potential a higher cost of trading (it may not be feasible to reduce the cost of trading in on-chain markets as batching may not be as efficient). Having said that, on-chain markets can provide several key features beyond simply universal access. On-chain markets can provide uniform access to all participants on ODEX, venues and users alike and therefore on-chain markets can provide a conduit for liquidity access between all participants and a baseline view of pricing for any trading instrument. ## Comparison of Phases ||Phase 1|Phase 2|Phase 3|Phase 4|On-Chain| |--- |--- |--- |--- |--- |--- | |Settlement|Per-Order|Batch|Batch|Batch|Batch + Auction| |Partial fills|Y|Y|Y|Y|Y| |Implied orders, auctions|N|Y|Y|Y|Y| |Direct disincentive against incorrect venue behaviour?|N|N|Y|Y|Y| |Orders private?|N|N|N|Y|Y| |On-chain order book|N|N|N|N|Y| # Functional Market Structure ## Introduction For the ODEX to be successful, the DEX must address the basic market model principles of a well-functioning market. A well functioning market has many characteristics and the features of ODEX previously discussed has already called out many of these. The purpose of this section is to call out, in a practical sense, the requirements that need to be met to support the OMG Network vision. ## Market Microstructure In order to appreciate the detail in this section we need to introduce how we define the concept of market duration. Any market can exist for an arbitrary period of time, and in fact while someone, somewhere is making a market in a particular instrument we can say there exists a market for that instrument. In practical terms however we must assume that for any venue there will be a period over which they explicitly make a market for an instrument. In a traditional venue these market durations are usually determined by wider practical limitations. For example most market durations are a single day, with the market being reset each day, or perhaps over a week with the market being reset, or paused weekly. These limitations can be things like people sleep at night and therefore in a localized market there will only be thin trading overnight so the markets simply close. It could be aligned to a feature of the instrument and so on. In the crypto markets we tend to have global 24/7 markets and so concepts like a trading "day" are less valuable. However these concepts are still critical in operating an effective and orderly market as they facilitate things like the ability to determine orders are stale, or provide an opportunity to perform market resets, or venue maintenance. ### Market Duration To help us identify and understand the market structures that the ODEX must support we have adopted a taxonomy, really a vocabulary, for describing market duration so as to allow us to anchor the requirements we will place on ODEX to support any overlaid market model. This vocabulary is as follows: * Any market can be divided into Trading Cycles (a cycle could for example last a day) * A Trading Cycle may have one or more Trading Phases (within a cycle you might have a pre-open, open, after-hours and close phase) * Each Trading Phases may be decomposed into Trading Sessions (a session might be an auction session, or a continuous trading session) With this macro view of market duration, as delivered by any one specific venue on the ODEX, we can now be more specific about duration requirements as they relate to the ODEX itself. Any venue connected to the ODEX can of course adopt any duration model they like but the vocabulary identified will allow those models to be mapped as needed to the features that ODEX offers. ### Required Order Types This section details the minimal characteristics of orders that must be supported. Typically these characteristics are captured as "order types" in a venue. Most other order types can be composed from these fundamental features. **Fill Constraints** * Partial fills of orders must be supported **Continuous Market - Immediacy** * Market order (only a quantity is specified) * Limit order (quantity, price) * Quote limit order (two-sided limit order that is required for market-makers) **Continuous Market - Time in Force** * Good for cycle (order is good until the end of the current cycle) * Immediate or cancel (order must fill immediately, otherwise the order is cancelled) **Auctions** * Market order (quantity only) * Limit order (quantity, price) **Implied orders** * Exchange of two assets that do not have directly trade against one another ## Minimal Required Participants In order for ODEX to be minimally viable we will need to have at least the following participants: * At least one exchange (aka venue) * Users * Market maker * Child Chain Operator (at PoA), Validators (at PoS) # Safety Considerations The following diagram illustrates the security at differing layers for the OMG Network. ![safety considerations diagram](assets/dex_design/07_safety_considerations.png) The OMG Network relies on the safety of Ethereum, and applications that run on the OMG Network will rely on the consensus mechanism of the OMG Network. Further research will be performed to consider the safety of the OMG Network and the core goal for what the OMG Network should be providing safety for. The prior descriptions about the ODEX assume that the safety of venues are tightly coupled in the consensus of the OMG Network. That is, venues are considered as part of layer 2. It may not be desirable to couple the consensus of the OMG Network into venues because of the potentially unbounded amount of computation that would be required for validators. In that event, venues could be moved to the ‘App’ layer, and define their own safety and security guarantees. An alternative final state of the ODEX would then look as follows: ![ODEX Alternative state](assets/dex_design/08_ODEX%20Alternative%20State.png) Whilst this model may relieve validators of computational load, we should take note the following points: 1. The value proposition to venues to integrate into the ODEX is greatly reduced because the operational risk for the funds in a venue is borne by the venue. 2. It is generally understood that a continuous market, not a call market (which a batch auction is) has greater liquidity. This is due to the limited pre-trade transparency and lack of immediacy of call markets. Further research may uncover mechanisms to support more functional on-chain markets and how those markets would fit into the overall ODEX market model. # Value Proposition ## Introduction This section describes the proposed value proposition of the ODEX to each of the different users and stakeholders in the OMG ecosystem. These value propositions still need to be validated for each of the target users and stakeholders. ## End Users of Venues * A list of benefits can be found at the start of this document. ## Existing Crypto and Traditional Venues Existing crypto exchanges can be hacked, leading to loss of user funds, loss of capital and loss of user confidence in the crypto ecosystem. Traditional venues (typically regulated) would like to participate in the crypto markets and offer services to their users but face all the same issues as crypto venues and therefore expose themselves to significant reputational damage and fail to offer a secure enough market to be attractive to their existing members. Both venue types could benefit significantly from: * Restricted custody of user funds * Reduced regulatory exposure (such as not taking user deposits) * Simplified operations due to lack of requiring a hot/cold wallet system * Existing business models are generally compatible with the ODEX ## eWallet and Wallet Users eWallet and Wallet users suffer from security risk and immediacy of fund transfers. The ODEX will offer the following key benefits: * Access to exchange functions that can support cross-currency payments * Faster access to liquid markets to trade in * The same benefits as end users of venues ## OMG Network The OMG network provides value by incentivizing a growing network of participants all of whom can benefit from the existence of the network. ODEX can help support network growth and evolution from: * Fees derived from transaction volume that is generated from settlements # Appendix ## Supporting User Story During all discussions surrounding the ODEX design, it important to remember that the DEX must support the following fundamental user story: >As Alice I can cheaply use Burger tokens to Bob to make a payment to Bob (who accepts Coffee tokens) Similarly, the DEX should support the above user story when "Burger tokens" and Coffee tokens” are substituted for any type of asset that may be traded on the OMG Network. This may include fiat or asset backed tokens, stablecoins and cryptocurrencies. ## Prior DEX Designs We investigated multiple types of designs, of which many have acted as inputs to the designs outlined within this document. A matrix comparing each of the current and prior designs against the dimensions that were described earlier in this document can be found below. [https://docs.google.com/spreadsheets/d/1-i304AhhiddXOezouQVCJZzyCa2RlQ-TfrKiBjyoLZY/edit?usp=sharing](https://docs.google.com/spreadsheets/d/1-i304AhhiddXOezouQVCJZzyCa2RlQ-TfrKiBjyoLZY/edit?usp=sharing) ================================================ FILE: docs/exit_validation.md ================================================ # Exit validation This document describes the exit validation (processing) done by the Watcher in `ExitProcessor`. ## Definitions * **scheduled finalization time** - a point in time when an exit will be able to process, see [this section in the blockchain design document](docs/tesuji_blockchain_design.md#finalization-of-exits). * **`exit_finality_margin`** - margin of the exit processor (in Ethereum blocks) - how many blocks to wait for finality of exit-related events * **child chain exit recognition SLA** - a form of a Service Level Agreement - how fast will the child chain recognize newly started exits and prohibit spending of exiting UTXOs * **unchallenged exit tolerance** - a Watcher's tolerance to an invalid exit not having a challenge for a long time since its start. - **NOTE**: in practice, violation of the child chain exit recognition SLA and violation of the unchallenged exit tolerance are similar. The correct reaction to both is a prompt to mass exit and a challenge of the invalid exit. Because of this, only `sla_margin` is used as a configurable setting on the Watcher, covering for both conditions. * **`sla_margin`** - margin of the child chain exit recognition SLA (in Ethereum blocks). This is a number of blocks after the start of an exit (or piggyback), during which a Child Chain Server still might include a transaction invalidating a previously valid exit, without violating the child chain exit recognition SLA. Similarly, this is a number of blocks after the start of an exit (or piggyback), during which the Watcher will not report [unchallenged exits](#unchallenged_exit-condition). ## Notes on the Child Chain Server This document focuses on the Watcher, but for completeness we give a quick run-down of the rules followed by the Child Chain Server, in terms of processing exits events from the root chain contract. 1. The child chain operator's objective is to pro-actively minimize the risk of chain becoming insecure or, in worst case scenario, insolvent. The child chain becomes insolvent if any invalid exit gets finalized, which leads to loss of child chain funds. 2. To satisfy this objective, the Child Chain Server: - listens to every `ExitStarted` root chain event and _immediately_ marks as spent the exiting UTXO, - listens to every `InFlightExitStarted` root chain event and _immediately_ marks as spent the exiting transaction's **inputs**, - listens to every `InFlightExitOutputPiggybacked` root chain event and _immediately_ marks as spent the piggybacked outputs - as long as the IFEing transaction has been included in the chain and the output exists, prohibiting spending of the exiting UTXO, preventing exit invalidation. - **NOTE**: This immediacy is limited because the server must process deposits before exits and deposits _must_ wait for finality on the root chain. There are scenarios, when a race condition/reorg on the root chain might make the Child Chain Server prohibit spending of a particular UTXO **late**, regardless of the immediacy mentioned above. This is acceptable as long as the delay doesn't exceed the `sla_margin`. ## Watcher ### Choice of the `sla_margin` setting value `sla_margin` is a set on the Watcher (via [`exit_processor_sla_margin`/`EXIT_PROCESSOR_SLA_MARGIN`](./details.md#configuration-parameters)), which needs to be determined correctly for various deployments and environments. It should reflect the exit period and the intended usage patterns and security requirements of the environment. `sla margin` should be large enough: - for the Child Chain Server (that runs the child chain the Watcher validates), to recognize exiting UTXOs, to prevent an invalidating transaction going through - for anyone concerned with challenging to challenge invalid exits. `sla_margin` should be tight enough: - to allow a successful mass exit in case of an [`unchallenged_exit` condition](#unchallenged_exit-condition). **NOTE** The `sla_margin` is usually much larger and unrelated to any margins that the Child Chain Server may wait before recognizing exits. So, if everything is functioning correctly, the spending of exiting UTXOs is blocked _much_ earlier than the `sla_margin`. In other words, `sla_margin` is usually picked to be ample (to avoid spurious mass exit prompts), and this doesn't impact the immediacy of the Child Chain Server's reaction to exits. ### Standard Exits #### Actions that the Watcher should prompt 1. If an exit is known to be invalid it should be challenged. The Watcher prompts by an `:invalid_exit` event. 2. If an exit is invalidated with a transaction submitted *before* `start_eth_height + sla_margin` it must be challenged (`:invalid_exit` event) 3. If an exit is invalidated with a transaction submitted *after* `start_eth_height + sla_margin` it must be challenged **AND** the Watcher prompts to exit. The Watcher prompts by both `:invalid_exit` and `:unchallenged_exit` events. Users should not deposit or spend 4. If an exit is invalid and remains unchallenged *after* `start_eth_height + sla_margin` it must be challenged **AND** the Watcher prompts to exit. The Watcher prompts by both `:invalid_exit` and `:unchallenged_exit` events. Users should not deposit or spend. 5. The `unchallenged_exit` event also covers the case where the invalid exit finalizes, causing an insolvent chain until [issue #1318 is solved](https://github.com/omgnetwork/elixir-omg/issues/1318). More on the [`unchallenged_exit` condition](#unchallenged_exit-condition). The occurrence of the `unchallenged_exit` condition is checked for on every child chain block being synced. Assumptions: - the user's funds must be safe even if the user only syncs and validates the chain periodically (but not less frequently than required) - the user needs to have the ability to spend their UTXOs at any time, thereby requiring more stringent validity checking of exits #### Implementation 2. `ExitProcessor` pulls new start exit events from root chain contract logs, as soon as they're `exit_finality_margin` blocks old (~12 blocks) 3. For every open exit request run `OMG.State.utxo_exists?` method * if `true` -> noop, * if `false` -> emit `:invalid_exit` event prompts to challenge * if `false` and exit is older than `sla_margin` -> emit additionally an `:unchallenged_exit` event which promts mass exit 4. Spend UTXOs in `OMG.State` on exit finalization * if the spent UTXO is present at the moment, forget the exit from validation - this is a valid finalization * if the spent UTXO is missing, keep on emitting `:unchallenged_exit` (until [issue #1318 is solved](https://github.com/omgnetwork/elixir-omg/issues/1318)) - this is an invalid finalization. 5. `ExitProcessor` recognizes exits that are (as seen at the tip of the root chain) already gone, when pulled from old logs. This prevents spurious event raising during syncing. This is the current behavior ("inactive on recognition"), to be substituted by a more verbose one in [#1318](https://github.com/omgnetwork/elixir-omg/issues/1318) 6. Checking the validation of exits is user's responsibility. This is done by calling `/status.get` endpoint. #### `unchallenged_exit` condition This section treats this particular condition in-depth and explains the rationale. Unchallenged exit events are reported in the `byzantine_events` in `/status.get`'s response, whenever there is _any_ exit, which is invalid and old. "Old" means that its respective challenge required _might be_ approaching scheduled finalization time, or just has been unchallenged for an unjustified amount of time. Unchallenged exits are signaled by either: - `unchallenged_exit` event for unchallenged invalid standard exit - `unchallenged_piggyback` event for unchallenged invalid piggyback on in-flight exit input or output - `unchallenged_non_canononical_ife` event for in-flight exit, considered canonical that has a known competitor The action to take, when such condition is detected is to _exit all UTXOs_ held on the child chain. The rationale is that we suspect that the chain is imminent to become invalid, because some funds that shouldn't be exiting are being allowed to exit. We do not wait until it's "too late" and report _post factum_ - if we did, our mass exit could end up having too low a priority. Another thing to explain here is that the Watcher will **stop getting new child chain blocks** whenever it finds itself in an unchallenged exit condition. This stopping behavior is similar to as when an `invalid_block` condition is detected. The reason for this is to: - protect the user from relying on a possibly corrupt or insecure state of the system (e.g. accepting funds that won't be exitable) - make the byzantine report loud and make the warning logged more visible - prevent a possible corruption of the internal state (this is more of an implementation detail, but worth to keep in mind), which could result if the exit finalized. In short, if at any point when watcher realizes it's in the "unsafe world" it stops processing blocks. An important thing to remember though, is that challenges keep on processing. In particular, the root cause of the unchallenged exit condition, **might be gone** at one point, because the invalid exit got challenged. **In particular, it won't show up in the `byzantine_events` list, when queried from `/status.get`!**. However, by design, the Watcher won't resume getting new blocks without a manual restart; the process of "coming back to validity" is not supported. This behavior is driven by the notion that if things go this bad, it's game over, so yanking the watcher back into "safe world" automatically hasn't been considered, for simplicity's sake and to avoid resuming when one shouldn't by error. From the protocol point of view, the first moment `unchallenged_exit` is spotted, the user should have commenced their mass exit. This is another reason, why resuming syncing is not currently supported. ##### Notes on implementation 1. We don't want to have any type of exit-related flags in `OMG.State`'s UTXOs 2. The reason to wait `exit_finality_margin` is to not have a situation, where due to a reorg, an exit is tracked and then vanishes. If we didn't handle that, it could grow old and at some point raise prompts to mass exit (`:unchallenged_exit`). An alternative is to always check the current status of every exit, before taking action, but that might create excessive load on the Ethereum RPC and be quite complex ### In-flight exits All the above rules will apply analogically to in-flight exits. See [MoreVP](./morevp.md) for specs and introduction to in-flight exits. Invalid attempts to do an in-flight related action prompt challenges. Absence of challenges within the `sla_margin`, as well as invalid finalization, should result in client prompting to mass exit (to be implemented in [issue #1275](https://github.com/omgnetwork/elixir-omg/issues/1275)). `OMG.State` is modified on IFE finalization. ================================================ FILE: docs/fee_design.md ================================================ # Fee Exit Design This document describes the design for fee exits. It starts with the requirements, and then describes the basic fee mechanism within our Plasma M(ore)VP design. ## Requirement ### Functional Requirement 1. The operator can exit fees to an address the operator owns. 2. Able to support fee exit for multiple transaction types where each transaction type can have different fee rules. 3. Support the initial fee rule for *payment* transaction, which is a fixed amount in cents per transaction. 4. More fee rules could be added later. Here are some examples: 1. as a percentage of ETH gas. 2. as a percentage of notional (aka transaction dollar amount). 3. fixed token price, floating with USD or other fiat. ### Non-functional Requirement 1. A Fee exit does not take more time than a normal exit to process. 2. A Fee exit can batch-exit multiple fees collected in different transactions. ### Out of Scope The fee rules are enforced and implemented only on the child chain. While a POA Plasma network gives the operator the ability to censor transactions, users are protected by the Watcher which would report misbehaving operators that try to exit more fees than allowed in a recorded transaction. ## General fee mechanism design ### High level description Child chain operators implement the format and rules for fees on a transaction, not the contracts. This is the case as long as the Plasma network runs as a POA. Transaction fees are an implied behavior for a Payment Transaction. It is calculated by the difference between a transaction's sum of inputs and its sum of outputs. For instance: The sum of inputs is 10 ETH The sum of outputs is 9.9 ETH The transaction fee is 0.1 ETH To exit fees, an operator puts a special fee transaction into a plasma chain block. This allows the operator to spend the transaction fee output as a Payment transaction. By leveraging MoreVP, we only need a contract to represent the fee transaction type and can use the pre-existing exit game contract. A fee transaction is unique from a normal transaction in that: 1. It does not need to consume any inputs. As the fee is implied (at least for the *payment* transactions), there is no output that consumes an input. 2. Verification of transaction fees occur only on the Child Chain and Watcher. The Plasma MoreVP security relies on the Watcher, not the contracts, to verify the validity of a transaction fee. If an invalid transaction fee is mined, the Watcher will consider an operator as "rogue" and notify users to mass exit the network. This decoupling of fee rule from smart contract gives the operator a more fine-tuned control on updating the fee rules. ### Fee rules upgrade/change The Child Chain sets the fee rules, while the Watcher informs users about the fee rules. The fee rules are enforced by the Child Chain during the transaction processing. Any transaction not following the fee rules is rejected. Note that the fee transaction verification part is not bound to the logic of the fee rules. Find more details on transaction verification in the design section. As a result, fee rules updates are done by changing the logic of how the Child Chain service would accept/reject an incoming transaction and how Watcher/wallet update the logic to follow the new fee rule when generating transaction. ### Fee for new transaction type In order to allow fees for a new transaction type, one would need to define how the fee is collected in the transaction type. We would flavor the fee to be always collected in an implicit way as it has already been decided to collect fee implicitly for *payment* transactions. To collect fees explicitly, the exit game contract of such transaction type might need to make sure the fees are exited via the standard fee exit mechanism instead of directly exiting with the explicit fee transaction output. Once the way to collect fee is designed and defined, whenever there is a new transaction coming in, the Child Chain and Watcher should add the fee amount to the storage that records the sum of fee. ### Fee rule change within a transaction type The Child chain service (the operator) needs to provide the fee rules to its clients. Clients can pass in the essential information such as address, token, transaction type, etc., and then the Child Chain can accept transactions that pay sufficient fees. This gives the operator the ability to update fees in a traditional SaaS way. The operator can adjust fee rules at anytime with flexible control. It can be upgraded with a feature flag or via A/B testing. ## Chosen Design: Generate fee transactions to summarize the fees of each block The design proposes that the Child Chain service automatically generates fee transactions at the end of each block. The Child Chain and Watcher perform the fee transaction verifications at the block level. A block, apart from the existing transaction verification logic, is valid if: 1. All fee transactions are included after all other types of transactions in that block. 1. A fee transaction's output is the sum of the fees paid in a particular token by all transactions in that block. 1. There must be at most one fee transaction for a token per block. 1. There can be at most one fee transaction type per block block. (PS. as we extend the Plasma Framework, there can potentially be multiple transaction types that all count as "fee tx type") Since the transactions are automatically generated from the Child Chain service, there is no need to do an authentication check. Inclusion of the fee transactions are optional, however fees collected in omitted blocks are lost to claim. ### Transaction design The transaction would be of the following format: ``` { txType: xxx, inputs: [], outputs: [], nonce: xxx, metaData: xxx, } ``` The list of inputs for fee transactions is empty, and the list of outputs only contains one output. This output follows the fungible token output format, which can be spent as an input for a *payment* transaction. ``` { outputType: xxx, amount: xxx, outputGuard: xxx, token: xxx, } ``` To represent fees for multiple tokens, one would need to generate multiple fee transactions. The last field, `nonce`, would be computed via `hash(blockNum, token)`. `blockNum` is the block number that the fee transaction is mined to. `token` is the token that is claimed in the fee transaction output. This combination promises the `nonce` to be unique, and thus promises the fee transaction to be unique. Since this fee transaction would have a specific transaction type (and also its outputs would have unique output types), we don’t need to worry about other transaction types that use the same mechanism (block number + token) to ensure uniqueness. Even if another transaction type, for instance, collides with the same outputs in the same block, they would end up with different transaction hash due to the transaction type difference. Transaction uniqueness is granted. ### Fee transaction type extension The current Plasma Framework design is immutable on the ability of spending an output type in another transaction type once the contracts have been deployed. So let's say we have *payment* v1 that can spend the fee claiming output. When we extend the framework with *payment* v2, we would need another fee output type that is able to be spent in *payment* v2. As a result, we would need fee transaction type 2 as well during the extension. (As only new transaction type can create new output type) Given this, the block verification should limit the block to be existing with a singe fee transaction type per block for simplicity. No matter which fee transaction types (1 or 2), it should calculate the fee balance of all transaction within the block. The only difference is how the output can be spent. Child chain and Watcher could even further deprecate old fee transaction types afterward if not needed anymore. The logic change could be done by upgrading Child Chain service and Watcher together. ## Adding the fee exit feature to the Plasma Framework This section will discuss how we can add the fee exit feature to our Plasma Framework using the chosen design, assuming we are launching the network without fee exit feature at the beginning. Since the network would first be running with a *payment* transaction type which does not support having fee transaction type as input, to enable spending fee transaction into *payment* transaction, we would need a *payment* transaction v2 for that. As a result, a high level steps of adding fee exit feature would be: 1. Implement the contracts to enable spending Fee transaction in *payment* transaction V2 2. Implement the Child Chain and Watcher logic to support new transaction types: fee transaction type and *payment* V2. Need to be able to check the correctness of the transactions, including special logic for fee transaction and the changes of block verification logics. 3. Implement Child Chain to automatically generate fee transactions within each block. 4. Makes sure that clients update to new Watchers 5. Could turn on the auto fee generation feature once all clients update to the new Watcher 6. Deploy the *paymentExitGame* for *payment* transaction v2 that has the ability to spend the fee transaction 7. (Optional) update deposit verifier to use *payment* transaction v2 directly 8. Wait 3 weeks for the new ExitGame contract to take effect 9. Spend fee transaction in *payment* transaction v2 and exit. ## Reference * Tetsuji blockchain fee design: https://github.com/omgnetwork/elixir-omg/blob/master/docs/tesuji_blockchain_design.md#fees * Scope of Five Guys * Fee exit high level discussion: https://github.com/omgnetwork/plasma-contracts/issues/165 ## FAQ Q1: Is it possible for the operator to start collecting fees now, but defer adding the new fee-exit ALD to some time in the future? > Yes, it should be possible in the abandoned design. But be aware pre-collecting fee meanings we would need to put extra effort to migrate DB for fee feature during production. For instance, we would need to calculate the sum of fees while accepting new transactions, which might impact sum of fees too. > In the chosen design, we can defer the fee-exit feature to the future but we would lose all pre-collected fees. Q2: How often does a fee exit get called? > This is for the operator to decide. We probably want to do this based upon finance requirements and risk management. Q3: Since smart contracts do not check the fee logic, how do we handle in-flight exits that do not follow the fee rules? > To be clear, it is about in-flight exit on other transaction types that are using MoreVP protocol instead of MVP, as in-flight exits are not possible for fee transactions. For other in-flight exit transactions, our current implementation would flag the inputs as spent directly. Those transactions would be overpaying on Ethereuem (assuming the gas to start the in-flight exit is larger than the fee we charge). > In the future, we are planning to include the IFE transaction into the block if not already there. We might only include the transactions that follows the fee rules to a block. If an IFE occurs that does not follow the fee rules, it can still be a valid in-flight exit and be processed. > See: https://github.com/omgnetwork/elixir-omg/issues/994 Q4: Should we collect fees when spending the fee transaction's output to a *payment* transaction? > To keep the transaction processing code simple, and in order not to introduce artificial code, we can treat this *payment* transaction the same as any *payment* transaction which is spending regular inputs. Therefore, when *payment* transaction will consolidate several fee-outputs it will be fee-free as a merge-transaction. Otherwise, a fee needs to be paid, which will be collectable by another transaction. Q5: How should excessive fees paid be handled? > They should be collected/exited normally, thereby decoupling fee requirements from fee collection. We can add some sanity checks on client software to avoid excessive fees. Q6: Can we do 4 fee utxos as input (to *payment* transaction) and one *payment* utxo as output? > Yes. As each block would generate a new fee utxo, we, as the operator, would like to continuously merge all fee utxos to single *payment* UTXO. ================================================ FILE: docs/in_flight_exit_scenarios.md ================================================ # Simple In-flight Exit Alice sends tokens to Bob in transaction `tx` which has one input and 2 outputs, one to Bob and one back to herself as change. The block is withheld. Alice calls `watcher/status.get` and gets a response: ```json { "version": "1", "success": true, "data": { "last_validated_child_block_number": 10000, "last_mined_child_block_timestamp": 1535031020, "last_mined_child_block_number": 11000, "eth_syncing": true, "byzantine_events": [ { "event": "block_withholding", "details": { "blockhash" : "DB32876CC6F...", "blocknum" : 10000, } } ] } } ``` She notices that the chain is byzantine and the transaction she just submitted was not included in a block (therefore it's an in-flight transaction) TODO - How does Alice know that her transaction hasn't been included? Does she have to store the details of every transaction she submits until it is put into a block? Alice starts an in-flight exit. #### 1. Get the exit data `/in_flight_exit.get_data` ```json { "txbytes": "F3170101C0940000..." } ``` response: ```json { "data": { "txbytes": "F3170101C0940000...", "sigs": "7C29FB8327F60BBFC62...", "input_txs" : [ "F81891018080808...", "2A0341808602A01..." ], "input_proofs" : [ "CEDB8B31D1E4C...", "A67131D1E904C..." ] } } ``` #### 2. Start the IFE ``` RootChain.startInFlightExit( response.data.txbytes, response.data.input_txs, response.data.input_proofs, response.data.sigs, {"value": inFlightExitBond} ) ``` #### 3. Check status again ```json { "version": "1", "success": true, "data": { "last_validated_child_block_number": 10000, "last_mined_child_block_timestamp": 1535031020, "last_mined_child_block_number": 11000, "eth_syncing": true, "byzantine_events": [ { "event": "block_withholding", "details": { "blockhash" : "DB32876CC6F...", "blocknum" : 10000, } }, { "event": "piggyback_available", "details": { "txbytes": "F3170101C0940000...", "available_outputs" : [ {"index": 0, "address": "0x7890..."}, {"index": 1, "address": "0x1234..."}, ] } }, ], "in_flight_exits": [ { "txhash": "230C450180808080...", "txbytes": "F3170101C0940000...", "eth_height" : 615441, } ] } } ``` Alice sees that her in-flight exit is in progress and she can now piggyback her ouput. Note that as Alice is the sole owner of the inputs, she does not need to piggyback any input. #### 4. Piggyback the output The second argument is `5` because she is piggybacking the second output. ``` RootChain.piggybackInFlightExit( response.data.txbytes, 5, {"value": piggybackBond} ) ``` After finalization, if nobody challenges the exit, Alice will exit her output and get her `inFlightExitBond` and `piggybackBond` back. #### 5. Bob finds out that he can piggyback his output When Bob calls `watcher/status.get` he gets this response: ```json { "version": "1", "success": true, "data": { "last_validated_child_block_number": 10000, "last_mined_child_block_timestamp": 1535031020, "last_mined_child_block_number": 11000, "eth_syncing": true, "byzantine_events": [ { "event": "block_withholding", "details": { "blockhash" : "DB32876CC6F...", "blocknum" : 10000, } }, { "event": "piggyback_available", "details": { "txbytes": "F3170101C0940000...", "available_outputs" : [ {"index": 0, "address": "0x7890..."}, ] } }, ], "in_flight_exits": [ { "txhash": "230C450180808080...", "txbytes": "F3170101C0940000...", "eth_height" : 615441, "piggybacked_outputs" : [1] } ] } } ``` Because Alice has already started an exit for the transaction there is a `piggyback_available` event indicating that `output[0]` (Bob's address) can be piggybacked. #### 6. Bob Piggybacks his output on the IFE The second argument is `4` because Bob is piggybacking the first output. ``` RootChain.piggybackInFlightExit( in_flight_exits[0].txbytes, 4, {"value": piggybackBond} ) ``` After finalization (if nobody challenges the exit) Bob will exit his output and get his `piggybackBond` back. # Challenge an IFE To challenge an IFE we must attempt to prove that it is non-canonical by presenting a competing transaction that also spends one its inputs. If the competing transaction has already been included in a block, then we must present its inclusion proof. Imagine that Alice's transaction `tx1` in the previous example is a double spend - its `input0` was already spent as `input1` of another transaction `tx0` Request `watcher/status.get`: ```json { "version": "1", "success": true, "data": { "last_validated_child_block_number": 10000, "last_mined_child_block_timestamp": 1535031020, "last_mined_child_block_number": 11000, "eth_syncing": true, "byzantine_events": [ { "event": "block_withholding", "details": { "blockhash" : "DB32876CC6F...", "blocknum" : 10000, } }, { "event": "noncanonical_ife", "details": { "txbytes": "F3170101C0940000..." } }, ], "in_flight_exits": [ { "txhash": "230C450180808080...", "txbytes": "F3170101C0940000...", "eth_height" : 615441, "piggybacked_inputs" : [0], "piggybacked_outputs" : [0, 1], } ] } } ``` #### 1. Get the competing transaction and its inclusion proof (if available). `/in_flight_exit.get_competitor` ```json { "txbytes": "F3170101C0940000..." } ``` response: ```json { "version": "1", "success": true, "data": { "in_flight_txbytes": "F847010180808080940000...", "in_flight_input_index": 0, "competing_txbytes": "F317010180808080940000...", "competing_input_index": 1, "competing_sig": "9A23010180808080940000...", "competing_tx_pos": 26000003920000, "competing_proof": "004C010180808080940000..." } } ``` Note that if the competing transaction has _not_ been included in a block then its inclusion proof and tx position will not be available. In this case, you should pass "" and 0 to `RootChain.challengeInFlightExitNotCanonical()` #### 2. Challenge the IFE with an included competitor ``` tx0_data = response.data RootChain.challengeInFlightExitNotCanonical( in_flight_txbytes, in_flight_input_index, competing_txbytes, competing_input_index, competing_tx_pos, competing_proof, competing_sig ) ``` # Respond to an IFE challenge To respond to a challenge to an IFE, we need to show that the transaction _is_ included. This situation can arise if the user that started the exit did not see the transaction in a block, but subsequently he or another user _did_ see the transaction being put into a block. `/watcher/status.get` response will contain: ``` "byzantine_events": [ { "event": "invalid_ife_challenge", "details": { "txbytes": "F3170101C0940000..." } } ] ``` #### 1. Get the in-flight transaction's inclusion proof. `/in_flight_exit.prove_canonical` ```json { "txbytes": "F3170101C0940000..." } ``` response: ```json { "version": "1", "success": true, "data": { "in_flight_txbytes": "F847010180808080940000...", "in_flight_tx_pos": 26000003920000, "in_flight_proof": "004C010180808080940000..." } } ``` #### 2. Respond to an IFE challenge ``` RootChain.challengeInFlightExitNotCanonical( in_flight_txbytes, in_flight_tx_pos, in_flight_proof, ) ``` If this transaction is the oldest competitor then it is canonical and the IFE succeeds - Bob exits his output. # Challenging a Piggybacked input To challenge a piggybacked input we must present a different transaction that spends that input. `/watcher/status.get` response will contain: ``` "byzantine_events": [ { "event": "invalid_piggyback", "details": { "txbytes": "F3170101C0940000...", "inputs": [1] } } ] ``` #### 1. Get the transaction that challenges the input `/in_flight_exit.get_input_challenge_data` ```json { "txbytes": "F3170101C0940000...", "input_index": 1 } ``` response: ```json { "version": "1", "success": true, "data": { "in_flight_txbytes": "F3170101C0940000...", "in_flight_input_index": 1, "spending_txbytes": "F847010180808080940000...", "spending_input_index": 1, "spending_sig": "9A23010180808080940000..." } } ``` #### 2. Challenge the input ``` RootChain.challengeInFlightExitInputSpent( in_flight_tx.txbytes, in_flight_tx.input_index, spending_tx.txbytes, spending_tx.input_index, spending_tx.sigs ) ``` # Challenging a Piggybacked output To challenge a piggybacked output we must present a transaction that spends that output. The in-flight transaction must have been put into a block, but the spending transaction does _not_ need to be in a block. `/watcher/status.get` response will contain: ``` "byzantine_events": [ { "event": "invalid_piggyback", "details": { "txbytes": "F3170101C0940000...", "outputs": [0] } } ] ``` #### 1. Get the output's proof of inclusion `/in_flight_exit.get_output_challenge_data` ```json { "txbytes": "F3170101C0940000...", "output_index": 0 } ``` response: ```json { "version": "1", "success": true, "data": { "in_flight_txbytes": "F3170101C0940000...", "in_flight_output_pos": 21000634002, "in_flight_proof": "03F451067A805540000...", "spending_txbytes": "F847010180808080940000...", "spending_input_index": 1, "spending_sig": "9A23010180808080940000..." } } ``` #### 2. Challenge the output ``` RootChain.challengeInFlightExitOutputSpent( in_flight_tx.txbytes, in_flight_tx.output_pos, in_flight_tx.proof, spending_tx.txbytes, spending_tx.input_index, spending_tx.sigs ) ``` ================================================ FILE: docs/install.md ================================================ # Full Installation **NOTE**: Currently the child chain server and watcher are bundled within a single umbrella app. Only **Linux** and **OSX** platforms are supported now. These instructions have been tested on a fresh Linode 2048 instance with Ubuntu 16.04. ## Prerequisites * **Erlang OTP** `>=22` (check with `elixir --version`) * **Elixir** `=1.10.*` (check with `elixir --version`) ## Install prerequisite packages It will install common development tools, geth and postgres. ``` sh bin/setup ``` ## Install Erlang and Elixir Add the Erlang Solutions repo and install ``` wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb \ && sudo apt install ./erlang-solutions_2.0_all.deb \ && rm ./erlang-solutions_2.0_all.deb sudo apt-get update sudo apt-get install esl-erlang=1:22.3.1-1 elixir=1.10.2-1 sudo apt-get install -y erlang-os-mon erlang-parsetools erlang-tools ``` ## Install hex and rebar ``` mix do local.hex --force, local.rebar --force ``` ## Clone repo ``` git clone https://github.com/omgnetwork/elixir-omg ``` ## Build ``` cd elixir-omg mix deps.get mix compile ``` ## Check this works! For a quick test (with no integration tests) ``` make init_test mix test ``` To run integration tests (requires **not** having `geth` running in the background): ``` make init_test mix test --only integration ``` ================================================ FILE: docs/morevp.md ================================================ # More Viable Plasma This document is based on the original [More Viable Plasma post](https://ethresear.ch/t/more-viable-plasma/2160/49) on ethresearch. This document has been moved from the document created in [the research repo](https://github.com/omgnetwork/research/pull/44). See there for the original work and discussion done on this design. ## Introduction [Minimal Viable Plasma](https://ethresear.ch/t/minimal-viable-plasma/426) (“Plasma MVP”) describes a simple specification for a UTXO-based Plasma chain. A key component of the Plasma MVP design is a protocol for “exits,” whereby a user may withdraw back to the root chain any funds available to them on the Plasma chain. The protocol presented in the MVP specification requires users sign two signatures for every transaction. Concerns over the poor user experience presented by this requirement motivated the search for an alternative exit protocol. In this document, we describe More Viable Plasma (“MoreVP”), a modification to the Plasma MVP exit protocol that removes the need for a second signature and generally improves user experience. The MoreVP exit protocol ensures the security of assets for clients correctly following the protocol. We initially present the most basic form of the MoreVP protocol and provide intuition towards its correctness. We also formalize certain requirements for Plasma MVP exit protocols and provide a proof that this protocol satisfies these requirements in the appendix. An optimized version of the protocol is presented to account for restrictions of the Ethereum Virtual Machine. We further optimize on the observation that the MoreVP exit protocol is only necessary for transactions that are in-flight when a Plasma chain becomes byzantine. We note the existence of certain attack vectors in the protocol, but find that most of these vectors can be largely mitigated and isolated to a relatively small attack surface. These attack vectors and their mitigations are described in detail. Although we conclude that the design is safe under certain reasonable assumptions about user behavior, some points are highlighted and earmarked for future consideration. Overall, we find that the MoreVP exit protocol is a significant improvement over the original Plasma MVP exit protocol. We can further combine several optimizations to enhance user experience and reduce costs for users. Future work will focus on decreasing implementation complexity of the design and minimizing contract gas usage. ## Basic Mechanism In this section, we specify the basic MoreVP exit mechanism and give an intuitive argument toward its correctness. A formal treatment of the protocol is presented in the appendix. ### Definitions #### Deposit A deposit creates a new output on the Plasma chain. Although deposits are typically represented as transactions that spend some "special" input, we do not allow deposits to exit via the MoreVP exit protocol. Instead, deposits can be safely exited with the Plasma MVP exit protocol. #### Spend Transaction A spend transaction is any transaction that spends a UTXO already present on the Plasma chain. #### In-flight Transaction A transaction is considered to be “in-flight” if it has been broadcast but has not yet been included in the Plasma chain. A transaction may be in-flight from the perspective of an individual user if that user does not have access to the block in which the transaction is included. #### Competing Transaction, Competitors Two transactions are “competing” if they share at least one input. The “competitors” to a transaction is the set of all transactions that are competing with the transaction in question, including the transaction itself. #### Canonical Transaction A transaction is “canonical” if none of its inputs were previously spent in any other transaction, i.e. that the transaction is the oldest among all its competitors. The definition of “previously spent” depends on whether or not the transaction in question is included in the Plasma chain. The position of a transaction in the chain is determined by the tuple (block number, transaction index). If the transaction was included in the chain, an input to that transaction would be considered previously spent if another transaction also spending the input was included in the chain *before* the transaction in question, decided by transaction position. If the transaction was not included in the chain, an input to that transaction would be considered previously spent if another transaction also spending the input is *known to exist*. Note that in this second case it’s unimportant whether or not the other transaction is included in the chain. If the other transaction is included in the chain, then the other transaction is clearly included before the transaction in question. If the other transaction is not included in the chain, then we can’t tell which transaction “came first” and therefore simply say that neither is canonical. #### Exitable Transaction A spend transaction can be called “exitable” if the transaction is correctly formed (e.g. more input value than output value, inputs older than outputs, proper structure) and is properly signed by the owners of the transaction’s inputs. If a transaction is “exitable,” then a user may attempt to start an exit that references the transaction. #### Valid Transaction A spend transaction is “valid” if and only if it is exitable, canonical, and only stems from valid transactions (i.e. all transactions in the history are also valid transactions). Note that a transaction would therefore be considered invalid if even a single invalid transaction is present in its history. An exitable transaction is not necessarily a valid transaction, but all valid transactions are, by definition, exitable. Our exit mechanism ensures that all outputs created by valid transactions can process before any output created by an invalid transaction. ### Desired Exit Mechanism The MoreVP exit protocol allows the owners of both inputs and outputs to transactions to attempt an exit. We want to design a mechanism that allows inputs and outputs to be withdrawn under the following conditions. The owner of an input `in` to a transaction `tx` must prove that: 1. `tx` is exitable. 2. `tx` is non-canonical. 3. `in` is not spent in any transaction other than `tx`. The owner of an output `out` to a transaction `tx` must prove that: 1. `tx` is exitable. 2. `tx` is canonical. 3. `out` is not spent. Because a transaction either is or is not canonical, only the transaction's inputs or outputs, and not both, may exit. #### Priority The above game correctly selects the inputs or outputs that are eligible to exit. However, invalid transactions can still be exitable. We therefore need to enforce an ordering on exits to ensure that all outputs created by valid transactions will be paid out before any output created by an invalid transaction. We do this by ordering every exit by the position of the *youngest input* to the transaction referenced in each exit, regardless of whether an input or an output is being exited. ## Exit Protocol ### Motivation The basic exit mechanism described above guarantees that correctly behaving users will always be able to withdraw any funds they hold on the Plasma chain. However, we avoided describing how the users actually prove the statements they’re required to prove. This section presents a more detailed specification for the exit protocol. The MoreVP mechanism is designed to be deployed to Ethereum and, as a result, some particulars of this specification take into account limitations of the EVM. Additionally, it's important to note that the MoreVP exit protocol is not necessary in all cases. We can use the Plasma MVP exit protocol without confirmation signatures for any transaction included before an invalid (or, in the case of withheld blocks, potentially invalid) transaction. We therefore only need to make use of the MoreVP protocol for the set transactions that are in-flight when a Plasma chain becomes byzantine. The MoreVP protocol guarantees that if transaction is exitable then either the unspent inputs or unspent outputs can be withdrawn. Whether the inputs or outputs can be withdrawn depends on if the transaction is canonical. However, in the particular situation in which MoreVP exits are required, users may not be aware that an in-flight transaction is actually non-canonical. This can occur if the owner of an input to an in-flight transaction is malicious and has signed a second transaction spending the same input. To account for this problem, we allow exits to be ambiguous about canonicity. Users can start MoreVP exits with the *assumption* that the referenced transaction is canonical. Other owners of inputs or outputs to the transaction can then “piggyback" the exit. We add cryptoeconomic mechanisms that determine whether the transaction is canonical and which of the inputs or outputs are unspent. The end result is that we can correctly determine which inputs or outputs should be paid out. ### MoreVP Exit Protocol Specification #### Timeline The MoreVP exit protocol makes use of a “challenge-response” mechanism, whereby users can submit a challenge but are subject to a response that invalidates the challenge. To give users enough time to respond to a challenge, the exit process is split into two “periods.” When challenges are subject to a response, we require that the challenges be submitted before the end of the first exit period and that responses be submitted before the end of the second. We define each period to have a length of half the minimum finalization period (`MFP`). Currently, `MFP` is set to 7 days, so each period has a length of 3.5 days. Watchers must validate the chain at least once every period (`MFP/2`). #### Starting the Exit Any user may initiate an exit by presenting a spend transaction and proving that the transaction is exitable. The user must submit a bond, `exit bond`, for starting this action. This bond is later used to cover the cost for other users to publish statements about the canonicity of the transaction in question. We provide several possible mechanisms that allow a user to prove a transaction is exitable. Two ways in which spend transactions can be proven exitable are as follows: 1. The user may present `tx` along with each of the `input_tx1, input_tx2, ... , input_txn` that created the inputs to the transaction, a Merkle proof of inclusion for each `input_tx`, and a signature over `tx` from the `newowner` of each `input_tx`. The contract can then validate that these transactions are the correct ones, that they were included in the chain, that the signatures are correct, and that the exiting transaction is correctly formed. This proves the exitability of the transaction. 2. The user may present the transaction along signatures the user claims to be valid. The contract can validate that the exiting transaction is correctly formed. Another user can challenge one of these signatures by presenting some transaction that created an input such that the true input owner did not sign the signature. In this case, the exit would be blocked entirely and the challenging user would receive `exit bond`. Option (1) (chosen in the implementation) checks that a transaction is exitable when the exit is started. This has lower communication cost and complexity but higher up-front gas cost. This option also ensures that only a single exit on any given transaction can exist at any point in time. Option (2) allows a user to assert that a transaction is exitable, but leaves the proof to a challenge-response game. This is cheaper up-front but adds complexity. This option must permit multiple exits on the same transaction, as some exits may provide invalid signatures. These are not the only possible mechanisms that prove a transaction is exitable. There may be further ways to optimize these two options. We still need to provide a deterministic ordering of exits by some priority. MoreVP exits are given a priority based on the position in the Plasma chain of the most recently included (youngest) input to that transaction. Unlike the MVP protocol, we give each input and output to a transaction the same priority. This should be implemented by inserting a single “exit” object into a priority queue of exits and tracking a list of inputs or outputs to be paid out once the exit is processed. #### Proving Canonicity Whether unspent inputs or unspent outputs are paid out in an exit depends on the canonicity of the referenced transaction, independent of any piggybacking by other users. Unfortunately it’s too expensive to directly prove that a transaction is or is not canonical. Instead, we assume that the referenced transaction is canonical by default and allow a series of challenges and responses to determine the true canonicity of the transaction. The process of determining canonicity involves a challenge-response game. In the first period of the exit, any user may reveal a competing transaction that potentially makes the exiting transaction non-canonical. This competing transaction must be exitable and must share an input with the exiting transaction, but does not have to be included in the Plasma chain. Multiple competing transactions can be revealed during this period, but only the oldest presented transaction is considered for the purposes of a response. If any transactions have been presented during the first period, any other user can respond to the challenge by proving that the exiting transaction is actually included in the chain before the oldest presented competing transaction. If this response is given before the end of the second period, then the exiting transaction is determined to be canonical and the responder receives the `exit bond` placed by the user who started the exit. Otherwise, the exiting transaction is determined to be non-canonical and the challenger receives `exit bond`. Note that this challenge means it’s possible for an honest user to lose `exit bond` as they might not be aware their transaction is non-canonical. We address this attack vector and several mitigations in detail later. It might also be the case that in-flight exit is opened where some of the inputs where referenced in standard exit and those standard exits were finalized. In such case in-flight exit is flagged as non-canonical and further canonicity game can't change its status. #### Piggybacking an Exit As noted earlier, it’s possible that some participants in a transaction may not be aware that the transaction is non-canonical. Owners of both inputs and outputs to a transaction may want to start an exit in the case that they would receive the funds from the exit. However, we want to avoid the gas cost of repeatedly publishing and proving statements about the same transaction. We therefore allow owners of inputs or outputs to a transaction to piggyback an existing exit that references the transaction. Users must piggyback an exit within the first period. To piggyback an exit, an input or output owner places a bond, `piggyback bond`. This bond is used to cover the cost of challenges that show the input or output is spent. A successful challenge blocks the specified input or output from exiting. These challenges must be presented before the end of the second period. Note that it isn’t mandatory to piggyback an exit. Users who choose not to piggyback an exit are choosing not to attempt a withdrawal of their funds. If the chain is byzantine, not piggybacking could potentially mean loss of funds. #### Processing Exits An exit can be processed after the second period. If the referenced transaction was determined to be canonical, all piggybacked outputs still unchallenged are paid out. If the referenced transaction was determined to be non-canonical, all piggybacked inputs still unchallenged are paid out. Any inputs or outputs paid out should be saved in the contract so that any future exit referencing the same inputs or outputs can be challenged. #### Combining with Plasma MVP Exit Protocol The MoreVP protocol can be combined with the Plasma MVP protocol in a way that simultaneously preserves the integrity of exits and minimizes gas cost. Although the two protocols use different determinations for exit priority, total ordering on exits is still needed. Therefore, every exit, no matter the protocol used, must be included in the same priority queue for processing. Honest user which enjoys data availability should be able to ignore in-flight exits that involve their outputs. Owners of outputs on the Plasma chain should be able to start an exit via either mechanism, but not both. To guarantee that money can't be double-spend via those two mechanisms, two approaches are possible. ##### Chosen solution This approach minimizes complexity of interactive games while negatively affecting gas cost of a happy path. Contract needs to check if other type of exit exists for particular output when standard exit is being submitted and it checks if standard exit is in progress / was finalized when in-flight exit is being added. In first case new exit is blocked. In second case - in-flight exit is marked as one which can be exited only from inputs, and problematic inputs are marked as spent for piggybacking purposes. To make such checks possible, both types of exits need to use transaction hash as an exit id. No additional interactive games arise from the fact of coexistence of MVP and MoreVP protocols. ##### Alternative solution, to be implemented later To reduce gas costs for honest participants, new types of challenges needs to be introduced. Piggybacks on outputs should be challenged by standard exits and vice-versa. Standard exits on UTXO seen as the input of a in-flight tx exit can be challenged using tx body. Canonicity of in-flight exit can be removed by pointing contract to finalized standard exit from in-flight exit inputs, marking particular input as spent. For details, [see here](./standard_vs_in_flight_exits_interaction.md). ## Alice-Bob Scenarios ### Alice & Bob are honest and cooperating: 1. Alice spends `UTXO1` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is in-flight. 3. Operator begins withholding blocks while `TX1` is still in-flight. Neither Alice nor Bob know if the transaction has been included in a block. 4. Someone with access to `TX1` (Alice, Bob, or otherwise) starts an exit referencing `TX1` and places `exit bond`. 5. Bob piggybacks onto the exit and places `piggyback bond`. 6. Alice is honest, so she hasn’t spent `UTXO1` in any transaction other than `TX1`. 7. After period 2, Bob receives the value of `UTXO2`. All bonds are refunded. ### Mallory tries to exit a spent output: 1. Alice spends `UTXO1` in `TX1` to Mallory, creating `UTXO2`. 2. `TX1` is included in block `N`. 3. Mallory spends `UTXO2` in `TX2`. 4. Mallory starts an exit referencing `TX1` and places `exit bond`. 5. Mallory piggybacks onto the exit and places `piggyback bond`. 6. In period 2, someone reveals `TX2` spending `UTXO2`. This challenger receives Mallory’s `piggyback bond`. 7. Alice is honest, so she hasn’t spent `UTXO1` in any transaction other than `TX1`. 8. After period 2, Mallory’s `exit bond` is refunded, no one exits any UTXOs. ### Mallory double spends her input: 1. Mallory spends `UTXO1` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is in-flight. 3. Operator begins withholding blocks while `TX1` is still in-flight. Neither Mallory nor Bob know if the transaction has been included in a block. 4. Mallory spends `UTXO1` in `TX2`. 5. `TX2` is included in a withheld block. `TX1` is not included in a block. 6. Bob starts an exit referencing `TX1` and places `exit bond`. 7. Bob piggybacks onto the exit and places `piggyback bond`. 8. In period 1, someone challenges the canonicity of `TX1` by revealing `TX2`. 9. No one is able to respond to the challenge in period 2, so `TX1` is determined to be non-canonical. 10. After period 2, Bob’s `piggyback bond` is refunded, no one exits any UTXOs. The challenger receives Bob’s `exit bond`. ### Mallory spends her input again later: 1. Mallory spends `UTXO1` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is included in block `N`. 3. Mallory spends `UTXO1` in `TX2`. 4. `TX2` is included in block `N+M`. 5. Mallory starts an exit referencing `TX1` and places `exit bond`. 6. In period 1, someone challenges the canonicity of `TX1` by revealing `TX2`. 7. In period 2, someone responds to the challenge by proving that `TX1` was included before `TX2`. 8. After period 2, the user who responded to the challenge receives Mallory’s `exit bond`, no one exits any UTXOs. ### Mallory attempts to exit a spent input: 1. Mallory spends `UTXO1` and `UTXO2` in `TX1`. 2. Mallory spends `UTXO1` in `TX2`. 3. `TX1` and `TX2` are in-flight. 4. Mallory starts an exit referencing `TX1` and places `exit bond`. 5. Mallory starts an exit referencing `TX2` and places `exit bond`. 6. In period 1 of the exit for `TX1`, someone challenges the canonicity of `TX1` by revealing `TX2`. 7. In period 1 of the exit for `TX2`, someone challenges the canonicity of `TX2` by revealing `TX1`. 8. After period 2 of the exit for `TX1`, the challenger receives `exit bond`, no one exits any UTXOs. 9. After period 2 of the exit for `TX2`, the challenger receives `exit bond`, no one exits any UTXOs. ### Operator tries to steal funds from an included transaction 1. Alice spends `UTXO1` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is included in (valid) block `N`. 3. Operator creates invalid deposit, creating `UTXO3`. 4. Operator spends `UTXO3` in `TX3`, creating `UTXO4`. 5. Operator starts an exit referencing `TX3` and places `exit bond`. 6. Operator piggybacks onto the exit and places `piggyback bond`. 7. Bob starts a *standard* exit for `UTXO2`. 8. Operator’s exit will have priority of position of `UTXO3`. Bob’s exit will have priority of position of `UTXO2`. 9. Bob receives the value of `UTXO2` first, Operator receives the value of `UTXO4` second (ideally contract is empty by this point). All bonds are refunded. ### Operator tries to steal funds from an in-flight transaction. 1. Alice spends `UTXO1` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is in-flight. 3. Operator creates invalid deposit, creating `UTXO3`. 4. Operator spends `UTXO3` in `TX3`, creating `UTXO4`. 5. Operator starts an exit referencing `TX3` and places `exit bond`. 6. Operator piggybacks onto the exit and places `piggyback bond`. 7. Bob starts an exit referencing `TX1` and places `exit bond`. 8. Bob piggybacks onto the exit and places `piggyback bond`. 9. Alice is honest, so she hasn’t spent `UTXO1` in any transaction other than `TX1`. 10. Operator’s exit will have priority of position of `UTXO3`. Bob’s exit will have priority of position of `UTXO1`. 11. Bob receives the value of `UTXO2` first, Operator receives the value of `UTXO4` second (ideally contract is empty by this point). All bonds are refunded. ### Operator tries to steal funds from a multi-input in-flight transaction. 1. Alice spends `UTXO1a`, Malory spends `UTXO1m` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is in-flight. 3. Operator creates invalid deposit, creating `UTXO3`. 4. Operator spends `UTXO3` in `TX3`, creating `UTXO4`. 5. Operator starts an exit referencing `TX3` and places `exit bond`. 6. Operator piggybacks onto the exit and places `piggyback bond` 7. Malory starts an exit referencing `TX1` and places `exit bond`. 8. Bob piggybacks onto the exit and places `piggyback bond`. 9. Alice piggybacks onto the exit and places `piggyback bond`. 9. Mallory double-spends `UTXO1m` in `TX2` and broadcasts. 9. Operator includes `TX2` and submits as a competitor to `TX1` rendering it non-canonical 10. Operator's exit of `TX3` will have priority of position of `UTXO3`. Alice-Mallory exit will have priority of position of `UTXO1`. 11. Alice receives the value of `UTXO1a` first, Operator receives the value of `UTXO4` second (ideally contract is empty by this point). Bob receives nothing. Mallory's `exit bond` goes to the Operator. Mallory's `TX2` is canonical and owners of outputs can attempt to exit them. ### Honest receiver should not start in-flight exits An honest user obtaining knowledge about an in-flight transaction **crediting** them **should not** start an exit, otherwise risks having their exit bond slashed. The out-of-band process in such event should always put the burden of starting in-flight exits **on the sender**. The following scenario demonstrates an attack that is **possible if receivers are too eager to start in-flight exits**: 1. Mallory spends `UTXO1` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is in-flight. 3. Operator begins withholding blocks while `TX1` is still in-flight. 4. Bob **eaglerly** starts an exit referencing `TX1` and places `exit bond`. 5. Mallory spends `UTXO1` in `TX2`. 6. In period 1 of the exit for `TX1`, Mallory challenges the canonicity of `TX1` by revealing `TX2`. 7. No one is able to respond to the challenge in period 2, so `TX1` is determined to be non-canonical. 8. After period 2, Mallory receives Bob’s `exit bond`, no one exits any UTXOs. Mallory has therefore caused Bob to lose `exit bond`, even though Bob was acting honestly. ### Attack Vectors and Mitigations #### Honest Exit Bond Slashing It’s possible for an honest user to start an exit and have their exit bond slashed. This can occur if one of the inputs to a transaction is malicious and signs a second transaction spending the same input. The following scenario demonstrates this attack: 1. Mallory spends `UTXO1m` and Alice spends `UTXO1a` in `TX1` to Bob, creating `UTXO2`. 2. `TX1` is in-flight. 3. Operator begins withholding blocks while `TX1` is still in-flight. 4. Alice starts an exit referencing `TX1` and places `exit bond`. 4. Alice piggybacks onto the exit and places `piggyback bond`. 5. Mallory spends `UTXO1m` in `TX2`. 6. In period 1 of the exit for `TX1`, Mallory challenges the canonicity of `TX1` by revealing `TX2`. 7. No one is able to respond to the challenge in period 2, so `TX1` is determined to be non-canonical. 8. After period 2, Mallory receives Alice's `exit bond`, Alice receives `UTXO1a` and `piggyback bond`. Mallory has therefore caused Alice to lose `exit bond`, even though Alice was acting honestly. We want to mitigate the impact of this attack as much as possible so that this does not prevent users from receiving funds. **NOTE** in the scenarios where Mallory double-spends her input, she doesn't get to successfully piggyback that, unless the operator includes and makes canonical her double-spending transaction. As a result she might lose more than she's getting from stolen `exit bonds`. #### Honest transaction retries attack Retrying a transaction that has failed for a trivial reason is not safe under MoreVP. Scenario is: 1. Honest Alice creates/signs/submits a transaction `tx1` 2. This fails, either loudly (error response from child chain server) or quietly (no response) - `tx1` doesn't get included in a block 3. Alice is forced to in-flight exit, even if she just made a trivial mistake (e.g. incorrect fee) 4. If instead Alice retries with amended `tx2`, then she opens an attack on her funds: - if the child chain is nice, `tx2` will get included in a valid, non-withheld block, all is good - if the child chain decides to go rogue, Alice is left defenseless, because she double-spent her input, i.e. she can't in-flight exit neither `tx1` nor `tx2` anymore See [Timeouts section](#Timeouts) for discussion on one possible mitigation. However, due to uncertainty of timeouts in MoreVP, other mitigations for the retry problem might be necessary. ##### Mitigations for Honest Exit Bond Slashing ###### Bond Sharing One way to partially mitigate this attack is for each user who piggybacks to cover some portion of `exit bond`. This cuts the per-person value of `exit bond` proportionally to the number of users who have piggybacked. Note that this is a stronger mitigation the more users are piggybacking on the exit and would not have any impact if only a single user starts the exit/piggybacks. ###### Small Exit Bond The result of the above attack is that users may not exit from an in-flight transaction if the gas cost of exiting plus the value of `exit bond` is greater than the value of their input or output. We can reduce the impact of this attack by minimizing the gas cost of exiting and the value of `exit bond`. Gas cost should be highly optimized in any case, so the value of `exit bond` is of more importance. `exit bond` is necessary to incentivize challenges. However, we believe that challenges can be sufficiently incentivized if `exit bond` simply covers the gas cost of challenging. Observations from the Bitcoin and Ethereum ecosystems suggest that sufficiently many nodes will verify transactions without a direct in-protocol incentive to do so. Our system requires only a single node be properly incentivized to challenge, and it’s likely that many node operators will have strong external incentives. Modeling the “correct” size of the exit bond is an ongoing area of research. ##### Timeouts We can add timeouts to each transaction (“must be included in the chain by block X”) to - reduce number of transactions vulnerable to [**Honest Exit Bond Slashing**](#Honest-Exit-Bond-Slashing) point in time. - alleviate [**Honest transaction retries attack**](#Honest-transaction-retries-attack), allowing Alice to just wait the timeout and retry This will probably also be necessary from a user experience point of view, as we don’t want users to accidentally sign a double-spend simply because the first transaction hasn’t been processed yet. **TODO** At this point, it is uncertain how the timeouts scheme would modify MoreVP and whether it's feasible at all. ## Appendix ### Formalization of Definitions #### Transactions $TX$ is the transaction space, where each transaction has $inputs$ and $outputs. For simplicity, each input and output is an integer that represents the position of that input or output in the Plasma chain. $$ TX: ((I_1, I_2, … ,I_n), (O_1, O_2, … ,O_m)) $$ For every transaction $t$ in $TX$ we define the “inputs” and “outputs” functions: $$ I(t) = (I_1, I_2, …, I_n) O(t) = (O_1, O_2, …, O_m) $$ #### Chain A Plasma chain is composed of transactions. For each Plasma chain $T$, we define a mapping between each transaction position and the corresponding transaction at that position. $$ T_n: [1, n] \rightarrow TX $$ We also define a shortcut to point to a specific transaction in the chain. $$ t_i = T_n(i) $$ #### Competing Transaction, Competitors Two transactions are competing if they have at least one input in common. $$ competing(t, t’) = I(t) \cap I(t’) \neq \varnothing $$ The set of competitors to a transaction is therefore every other transaction competing with the transaction in question. $$ competitors(t) = \{ t_{i} : i \in (0, n], competing(t_{i}, t) \} $$ #### Canonical Transaction A transaction is called “canonical” if it’s oldest of all its competitors. We define a function $first$ that determines which of a set $T$ of transactions is the oldest transaction. $$ first(T) = t \in T : \forall t’ \in T, t \neq t’, min(O(t)) < min(O(t’)) $$ The set of canonical transactions is any transaction which is the first of all its competitors. $$ canonical(t) = (first(competitors(t)) \stackrel{?}{=} t) $$ For convenience, we define $reality$ as the set of all canonical transactions for a given chain. $$ reality(T_{n}) = \{ canonical(t_{i}) : i \in [1, n] \} $$ #### Unspent, Double Spent We define two helper functions $unspent$ and $double\_spent$. $unspent$ takes a set of transactions and returns the list of transaction outputs that haven't been spent. $double\_spent$ takes a list of transactions and returns any outputs that have been used as inputs to more than one transaction. First, we define a function $txo$ that takes a transaction and returns a list of its inputs and outputs. $$ txo(t) = O(t) \cup I(t) $$ Next, we define a function $TXO$ that lists all inputs and outputs for an entire set of transactions: $$ TXO(T_{n}) = \bigcup_{i = 1}^{n} txo(t_{i}) $$ Now we can define $unspent$ and $double\_spent$: $$ unspent(T) = \{ o \in TXO(T) : \forall t \in T, o \not\in I(t) \} $$ $$ double\_spent(T) = \{ o \in TXO(T) : \exists t,t' \in T, t \neq t', o \in I(t) \wedge o \in I(t') \} $$ ### Requirements #### Safety The safety rule, in English, says "if an output was exitable at some time and is not spent in a later transaction, then it must still be exitable". If we didn't have this condition, then it might be possible for a user to receive money but not be able to spend or exit from it later. Formally, if we say that $E(T_{n})$ represents the set of exitable outputs for some Plasma chain and $T_{n+1}$ is $T_{n}$ plus some new transaction $t_{n+1}$: $$ \forall o \in E(T_{n}) : o \not\in I(t_{n+1}) \implies o \in E(T_{n+1}) $$ #### Liveness The liveness rule states that "if an output was exitable at some time and *is* spent later, then immediately after that spend, either it's still exitable or all of the spend's outputs are exitable, but not both". The second part ensures that something is spent, then all the resulting outputs should be exitable. The first case is special - if the spend is invalid, then the outputs should not be exitable and the input should still be exitable. $$ \forall o \in E(T_{n}), o \in I(t_{n+1}) \implies o \in E(T_{n+1}) \oplus O(t_{n+1}) \subseteq E(T_{n+1}) $$ ### Basic Exit Protocol #### Formalization $$ E(T_{n}) = unspent(reality(T_{n})) \setminus double\_spent(T_{n}) $$ ##### Priority Exits are ordered by a given priority number. An exit with a lower priority number will process before an exit with a higher priority number. We define the priority of an exit from a transaction $t$, $p(t)$, as: $$ p(t) = \max(I(t)) $$ #### Proof of Requirements #### Safety Our safety property says: $$ \forall o \in E(T_{n}), o \not\in I(t_{n+1}) \implies o \in E(T_{n+1}) $$ So to prove this for our $E(T_{n})$, let's take some $o \in E(T_{n})$. From our definition, $o$ must be in $unspent(reality(T_{n}))$, and must not be in $double\_spent(T_{n})$. $o \not\in I(t_{n+1})$ means that $o$ will still be in $reality$, because only a transaction spending $o$ can impact its inclusion in $reality$. Also, $o$ can't be spent (or double spent) if it wasn't used as an input. So our function is safe! #### Liveness Our liveness property states: $$ \forall o \in E(T_{n}), o \in I(t_{n+1}) \implies o \in E(T_{n+1}) \oplus O(t_{n+1}) \subseteq E(T_{n+1}) $$ However, *we do have a case for which liveness does not hold* - namely that if the second transaction is a non-canonical double spend, then both the input and all of the outputs will not be exitable. This is a result of the $\setminus double\_spent(T_{n})$ clause. We think this is fine, because it means that only double spent inputs are at risk of being "lost". The updated property is therefore: $$ \forall o \in E(T_{n}), o \in I(t_{n+1}) \implies o \in E(T_{n+1}) \oplus O(t_{n+1}) \subseteq E(T_{n+1}) \oplus o \in double\_spent(T_{n+1}) $$ This is more annoying to prove, because we need to show each implication holds separately, but not together. Basically, given $\forall o \in E(T_{n}), o \in I(t_{n+1})$, we need: $$ o \in E(T_{n+1}) \implies O(t_{n+1}) \cap E(T_{n+1}) = \varnothing \wedge o \not\in double\_spent(T_{n+1}) $$ and $$ O(t_{n+1}) \subseteq E(T_{n+1}) \implies o \not\in E(T_{n+1}) \wedge o \not\in double\_spent(T_{n+1}) $$ and $$ o \in double\_spent(T_{n+1}) \implies O(t_{n+1}) \cap E(T_{n+1}) = \varnothing \wedge o \not\in E(T_{n+1}) $$ Let's show the first. $o \in I(t_{n+1})$ means $o$ was spent in $t_{n+1}$. However, $o \in E(T_{n+1})$ means that it's unspent in any canonical transaction. Therefore, $t_{n+1}$ cannot be a canonical transaction. $O(t_{n+1}) \cap E(T_{n+1})$ is empty if $t_{n+1}$ is not canonical, so we've shown the half. Our specification states that $o \in double\_spent(T_{n+1}) \implies o \not\in E(T_{n+1})$, so we can show the second half by looking at the contrapositive of that statement $o \in E(T_{n+1}) \implies o \not\in double\_spent(T_{n+1})$. Next, we'll show the second statement. $O(t_{n+1}) \subseteq E(T_{n+1})$ implies that $t_{n+1}$ is canonical. If $t_{n+1}$ is canonical, and $o$ is an input to $t_{n+1}$, then $o$ is no longer unspent, and therefore $o \not\in E(T_{n+1})$. If $t$ is canonical then there cannot exist another earlier spend of the input, so $o \not\in double\_spent(T_{n+1})$. Now the third statement. $o \in double\_spent(T_{n+1})$ means $t$ is necessarily not canonical, so we have $O(t_{n+1}) \cap E(T_{n+1}) = \varnothing$. It also means that $o \not\in E(T_{n+1})$ because of our $\setminus double\_spent(T_{n})$ clause. Finally, we'll show that at least one of these must be true. Let's do a proof by contradiction. Assume the following: $$ O(t_{n+1}) \cap E(T_{n+1}) = \varnothing \wedge o \not\in E(T_{n+1}) \wedge o \not\in double\_spent(T_{n+1}) $$ We've already shown that: $$ o \in E(T_{n+1}) \implies O(t_{n+1}) \cap E(T_{n+1}) = \varnothing \wedge o \not\in double\_spent(T_{n+1}) $$ We can negate this statement to find: $$ o \not\in E(T_{n+1}) \wedge (O(t_{n+1}) \subseteq E(T_{n+1}) \vee o \in double\_spent(T_{n+1})) $$ However, we assumed that: $$ O(t_{n+1}) \cap E(T_{n+1}) = \varnothing \wedge o \not\in double\_spent(T_{n+1}) $$ Therefore we've shown the statement by contradiction. #### Exit Ordering Let $t_{v}$ be some valid transaction and $t_{iv}$ be the first invalid, but still exitable and canonical, transaction in the chain. $t_{iv}$ must either be a deposit transaction or spend some input that didn’t exist when $t_{v}$ was created, otherwise $t_{iv}$ would be valid. Therefore: $$ \max(I(t_{v})) < \max(I(t_{iv})) $$ and therefore by our definition of $p(t)$: $$ p(t_{v}) < p(t_{iv}) $$ So $p(t_{v})$ will exit before $p(t_{iv})$. We now need to show that for any $t'$ that stems from $t_{iv}$, $p(t_{v}) < p(t')$ as well. Because $t'$ stems from $t_{iv}$, we know that: $$ (O(t_{iv}) \cap I(t') \neq \varnothing) \vee (\exists t : stems\_from(t_{iv}, t) \wedge stems\_from(t, t')) $$ If the first is true, then we can show $p(t_{iv}) < p(t')$: $$ p(t') = \max(I(t')) \geq \max(I(t') \cap O(t_{iv})) \geq \min(O(t_{iv})) > \max(I(t_{iv})) = p(t_{iv}) $$ Otherwise, there's a chain of transactions from $p_{iv}$ to $p'$ for which the first is true, and therefore the inequality holds by transitivity. Therefore, all exiting outputs created by valid transactions will exit before any output created by an invalid transaction. ================================================ FILE: docs/perf_test_result_dumps.md ================================================ # Dumps of results of perf test run over commits ## `7d219a9806cb566ede860e7a26d2b3057838ed4b`, 2018-07-05 Command: ``` mix run --no-start -e 'OMG.Performance.start_simple_perftest(8_000, 32, %{block_every_ms: 15_000})' ``` Performance statistics: ``` [ %{"blknum" => 1000, "span_ms" => 16904, "tps" => 3876.95, "txs" => 65536}, %{"blknum" => 2000, "span_ms" => 14920, "tps" => 4050.34, "txs" => 60431}, %{"blknum" => 3000, "span_ms" => 15428, "tps" => 3907.25, "txs" => 60281}, %{"blknum" => 4000, "span_ms" => 14706, "tps" => 4035.5, "txs" => 59346}, %{"blknum" => 5000, "span_ms" => 2054, "tps" => 5066.21, "txs" => 10406} ] ``` typical block forming log: ``` 15:30:00.267 [info] Calculations for forming block 1000 done in 1017 ms 15:30:01.083 [info] DB.multi_update done in 816 ms 15:30:01.148 [info] Done forming block in 1898 ms ``` run on ``` 4x version: Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz ``` ## `685b5f75b283ab64b56ae5b6ac046b99692d3fbd`, 2018-07-18 Command as above + observer: ``` mix run --no-start -e ':observer.start(); OMG.Performance.start_simple_perftest(8_000, 32, %{block_every_ms: 15_000})' ``` Observer tells us that peak memory usage (total) is ~600MB, oscillating around ~400MB most of the time. ## `62249098e852d52552616364cb1ca9184be43d02`, 2018-10-16 Command as above: ``` mix run --no-start -e 'OMG.Performance.start_simple_perftest(8_000, 32, %{block_every_ms: 15_000})' ``` ``` [ {"blknum":1000, "span_ms":16378, "tps":3937, "txs":64484}, {"blknum":2000, "span_ms":15115, "tps":4020, "txs":60761}, {"blknum":3000, "span_ms":14915, "tps":4040, "txs":60257}, {"blknum":4000, "span_ms":14726, "tps":4110, "txs":60530}, {"blknum":5000, "span_ms":1865, "tps":5345, "txs":9968} ] ``` typical block forming log: ``` 2018-10-16 17:30:05.815 [info] ... ⋅Calculations for forming block number 2000 done in 1036 ms⋅ 2018-10-16 17:30:06.312 [info] ... ⋅Forming block done in 1533 ms⋅ ``` run on: as above. ## `869c964df00c17a54b399c33c8e917d23ab05dd7`, 2018-12-07 Command as above (new syntax): ``` mix run --no-start -e 'OMG.Performance.start_simple_perftest(8_000, 32, %{block_every_ms: 15_000})' ``` ``` [ {"blknum":1000, "span_ms":16488, "tps":3157, "txs":52057}, {"blknum":2000, "span_ms":15219, "tps":2974, "txs":45254}, {"blknum":3000, "span_ms":14964, "tps":2789, "txs":41742}, {"blknum":4000, "span_ms":14740, "tps":2847, "txs":41965}, {"blknum":5000, "span_ms":14889, "tps":3005, "txs":44742}, {"blknum":6000, "span_ms":7210, "tps":4194, "txs":30240} ] ``` and ``` 2018-12-07 15:14:44.129 [info] ... ⋅Calculations for forming block number 3000 done in 1391 ms⋅ ``` Some drop in throughput since last dump, but still bottlenecks lie elsewhere. ## `17f73a0f90e0cec35d684da0104b97234425f787`, 2019-02-11 Command as above ``` mix run --no-start -e 'OMG.Performance.start_simple_perftest(8_000, 32, %{block_every_ms: 15_000})' ``` ``` [ {"txs": 65536, "tps": 3976, "span_ms": 16482, "blknum": 1000}, { "txs": 65536, "tps": 4397, "span_ms": 14904, "blknum": 2000}, { "txs": 65536, "tps": 4264, "span_ms": 15370, "blknum": 3000}, { "txs": 59392, "tps": 5942, "span_ms": 9995, "blknum": 4000} ] ``` and ``` 2019-02-11 17:16:23.392 [info] ... ⋅Calculations for forming block number 2000 done in 832 ms⋅ ``` ## `53dc46f80eca374c64983aacd37bd6851ec794f4`, 2019-06-28 (perf drop introduced as documented in [#731](https://github.com/omgnetwork/elixir-omg/issues/731)) Command as above ``` [ %{blknum: 1000, span_ms: 18449, tps: 3042, txs: 56124}, %{blknum: 2000, span_ms: 13970, tps: 3152, txs: 44029}, %{blknum: 3000, span_ms: 15340, tps: 3125, txs: 47933}, %{blknum: 4000, span_ms: 14598, tps: 3236, txs: 47233}, %{blknum: 5000, span_ms: 15039, tps: 3248, txs: 48840}, %{blknum: 6000, span_ms: 1199, tps: 9876, txs: 11841} ] ``` ## `f3828a0f0a658b32a48a74649561ac2c452f7277`, 2019-06-28 (fixed the perf drop) Command as above ``` [ %{blknum: 1000, span_ms: 16699, tps: 3889, txs: 64940}, %{blknum: 2000, span_ms: 14753, tps: 4007, txs: 59120}, %{blknum: 3000, span_ms: 15225, tps: 3950, txs: 60140}, %{blknum: 4000, span_ms: 11507, tps: 5206, txs: 59903}, %{blknum: 5000, span_ms: 4059, tps: 2931, txs: 11897} ] ``` ================================================ FILE: docs/run_local_watcher.md ================================================ # Running your own Watcher locally The `docker-compose` tooling in the root of `elixir-omg` allows users to run their own instance of the Watcher to connect to the OMG Network and validate transactions. ### Requirements - Docker - `docker-compose` - known to work with `docker-compose version 1.24.0, build 0aa59064`, version `1.17` has had problems - Ethereum connectivity: local Ethereum node or Infura ### Startup 1) Add an ENV variable `INFURA_API_KEY` to your environment or override the ETHEREUM_RPC_URL completely in the `docker-compose-watcher.yml` file with the RPC connection information. 2) From the root of the `elixir-omg` execute: - `docker-compose -f docker-compose-watcher.yml up` Modify the other environment variables for connecting to other networks. ================================================ FILE: docs/source_consumption_log.md ================================================ # Source Consumption Log ### About: Identifies each module of pre-existing source code used in developing source code, what license (if any) that source code was provided under, where the preexisting source code and the license can be obtained publicly (if so available), and identification of where that source is located. ## Redistributed already * Elixir/Erlang/BEAM/OTP * `Elixir`, Apache 2.0, https://github.com/elixir-lang/elixir * `Erlang`, Apache 2.0, https://www.erlang.org * `BEAM/OTP`, Apache 2.0, https://github.com/erlang/otp * MIX deps, as listed `mix licenses` ([licensir](https://github.com/unnawut/licensir/)), cleaned/completed manually. **NOTE**, unless otherwise noted, package is obtained from `hex.pm/packages/`: ``` abi 0.1.12 -> MIT binary 0.0.4 -> MIT blockchain 0.1.7 -> MIT bunt 0.2.0 -> MIT briefly 0.3.0 -> Apache 2.0 certifi 2.3.1 -> BSD cowboy 1.1.2 -> ISC cowlib 1.0.2 -> ISC credo 0.9.3 -> MIT db_connection 1.1.3 -> Apache 2.0 decimal 1.5.0 -> Apache 2.0 dialyxir 1.0.0-rc.6 -> Apache 2.0 ecto 3.1.5 -> Apache 2.0 erlang-rocksdb 1.2.0 -> Apache 2.0 erlexec 1.7.5 -> BSD ethereumex 0.5.4 -> MIT ex_rlp 0.5.3 -> MIT ex_unit_fixtures -> MIT exexec 0.1.0 -> Apache 2.0 exth_crypto 0.1.4 -> MIT fake_server 2.1.0 -> Apache 2.0 hackney 1.15.1 -> Apache 2 httpoison 1.6.0 -> MIT idna 5.1.1 -> BSD keccakf1600 2.0.0 -> MPL 2.0 (ok'd by legal, compliant with our Apache 2.0) libsecp256k1 0.1.9 -> MIT licensir 0.2.7 -> MIT merkle_patricia_tree 0.2.6-> MIT merkle_tree 1.5.0 -> MIT metrics 1.0.1 -> BSD mime 1.3.0 -> Apache 2 mimerl 1.0.2 -> MIT parse_trans 3.2.0 -> Apache 2.0 phoenix 1.3.2 -> MIT phoenix_ecto 3.3.0 -> Apache 2.0 phoenix_pubsub 1.0.2 -> MIT plug 1.5.0 -> Apache 2 poison 3.1.0 -> CC0-1.0 (ok'd by legal, compliant with our Apache 2.0) postgrex 0.13.5 -> Apache 2.0 ranch 1.3.2 -> ISC poolboy 1.5.1 -> Apache 2.0 socket 0.3.13 -> WTFPL ssl_verify_fun 1.1.1 -> MIT unicode_util_compat 0.3.1-> Apache 2.0 ``` ## Likely to be redistributed * `geth`, LGPL 3.0, https://github.com/ethereum/go-ethereum, (used via an interface, so ok) * `zeppelin-solidity`, MIT, https://github.com/OpenZeppelin/zeppelin-solidity ## Likely to be used, but not redistributed * `web3`, MIT, https://github.com/ethereum/web3.py * `ethereum`, MIT, https://github.com/ethereum/pyethereum * `rlp`, MIT, https://github.com/ethereum/pyrlp * `py-solc`, MIT, https://github.com/ethereum/py-solc * `solc`, GPL 3.0, https://github.com/ethereum/solidity * `postgresql`, PostgreSQL License, https://www.postgresql.org ================================================ FILE: docs/stack_architecture.md ================================================ # OMG Network Client Architecture This describes the client services stack that communicates with the child chain and root chain to secure the entire Plasma construction and ease application development. An application provider will run these client services on their own or use hosted versions. ## Foundations ### Root chain #### Purpose Trusted chain used by the Plasma construction to secure funds in the child chain. In our case, this is Ethereum. ### Child chain #### Purpose Blockchain of transactions for our application. Continually submits block hashes to the root chain, as required by the Plasma construction. Described in the [Tesuji design](tesuji_blockchain_design.md) and the [More Viable Plasma] documentation. ## Client Services ### Watcher #### Purpose The watcher first and foremost plays a critical security role in the system. The watcher monitors the child chain and root chain (Ethereum) for faulty activity. #### Design principles - Only include functionality that is critical to the operation of the Plasma security model - Strict focus on security role reduces complexity and attack surface area - Limited feature helps scalability - The more the watcher does, the slower it can verify - 3 primary security functions: - Tracking of the root chain submissions, pulling block contents (from somewhere) and validating, in order to ensure safety of funds passively in possession on the child chain. Watcher notifies in case of the funds are jeopardized. - Proxy API to the child chain API (whatever it may be - PoA server or a P2P PoS network) and the root chain, that ensures that these two are never talked to if the chain is invalid or in unknown state. Only proxy calls that require the chain is operational. - Storage of data critical to access of the funds - UTXO positions, `txbytes` or any other kinds of proofs #### Specifications - [Current API](https://docs.omg.network/elixir-omg/docs-ui/?url=master%2Foperator_api_specs.yaml&urls.primaryName=master%2Finfo_api_specs) - Events - [Byzantine Events](https://github.com/omgnetwork/elixir-omg/blob/master/docs/api_specs/status_events_specs.md#byzantine-events) ### Informational API Service #### Purpose Non-critical convenience API proxy and provide data about the chain. #### Design principles - Provide convenience APIs to proxy to the child chain/root chain/watcher to ease integration and reduce duplicate code in libraries - Storage of informational data about the chain - Support direct client requests (web browser, mobile, etc.) #### Specifications - [Current API](https://docs.omg.network/elixir-omg/docs-ui/?url=master%2Foperator_api_specs.yaml&urls.primaryName=master%2Finfo_api_specs) ### Integration libraries #### Purpose Native wrappers to the Watcher and Informational API Service for supported languages and frameworks. #### Design principles - Adopt all native conventions and standards - Encourage open source community development #### Requirements - Support all events and API calls of the Watcher - Support all events and API calls of the API Service #### Current implementations - [omg-js-lib](https://github.com/omgnetwork/omg-js) ### Application Layer #### Purpose Third party applications that use the OMG Network for value transfer and exchange. #### Design principles - Key management happens at this layer #### Requirements - Generate and secure keys - Sign transactions ================================================ FILE: docs/standard_vs_in_flight_exits_interaction.md ================================================ # Standard vs In-flight Exits Since both Standard exit (aka SE) and In-flight exit (aka IFE) provide an exit opportunity, it is possible to effectively double-spend. To prevent such possibility, whenever an output is finalized/withdrawn from the Plasma M(ore)VP framework, we flag the output as spent to the `PlasmaFramework`. For all exit game contract implementations, they should ignore those already-spent-outputs during `processExit`. In other words, though there can be multiple exits for the same output started concurrently, only the first that get processed can really exit the output. All the others would just not process and ignore the output if it is already flagged. #### Glossary * `IFE` - in-flight exit, active unless stated otherwise; * `SE` - standard exit, active unless stated otherwise; * `utxoPos` - output position inside Plasma chain. It is a combination of `blockNumber`, `txIndex` and `outputIndex` that shows where the output is located in the plasma chain; * `OutputId` - global output identifier used in Plasma Framework, see description below; #### OutputId To be able to flag all finalized outputs, we would need a global schema for all outputs in our Plasma Framework. In current implementation, the global identifier for output is called `OutputId`. The schema is as followed: 1. For normal transaction outputs: `OutputId = hash(txBytes, outputIndex)` 2. For deposit transaction outputs: `OutputId = hash(txBytes, outputIndex, utxoPos)` We add `utxoPos` as a salt for deposit transaction output because deposit transaction can potentially have same `txBytes` as another deposit transactions (see: [this issue](https://github.com/omgnetwork/plasma-contracts/issues/80)), a naive `hash(txBytes, outputIndex)` would collide when `txBytes` are not unique. Also, there was discussion to abstract the output identifier to be more flexible, see [this note](https://github.com/omgnetwork/plasma-contracts/issues/387). But as the first version of Plasma Framework, we decided to go forward with using `OutputId` as a global schema to flag all outputs. #### Standard Exit scenario A standard exit would only impact an output. Thus, in the case of SE, only one output need to be considered. The `processExit` function for SE would check whether that output has been flagged or not before withdrawing the fund. Also, once processed, it would flag that output as spent in `PlasmaFramework`. #### In-flight Exit scenario An in-flight exit would impact all inputs and outputs of the in-flight exit transaction. If the IFE is canonical, it would exit the unchallenged piggybacked outputs. On the other hand, if the IFE is non-canonical, the unchallenged piggybacked inputs would be exited. If any of the in-flight exit input is flagged during `processExit`, that exit would be considered as non-canonical (for more detail on the reasons, see: [this issue](https://github.com/omgnetwork/plasma-contracts/issues/470)). Otherwise, the canonicity would be decided by the canonicity challenge game of the in-flight exit. If the in-flight exit is considered non-canonical during processing, we flag only the exiting inputs as spent. In other words, if the input is piggybacked, unchallenged and not flagged as spent yet, it would be exited and then be flagged as spent to the `PlasmaFramework`. On the other hand, if the in-flight exit is considered canonical, _all_ inputs plus the exiting outputs would be flagged. So if the output is piggybacked, unchallenged, and not flagged as spent yet, it would be exited and then be flagged as spent. Also all inputs would be flagged as well. We flag all the inputs when canonical because the current interaction game would have some edge cases during data unavailability, operator can try to double spend via IFE. For more detail, see this issue: [here](https://github.com/omgnetwork/plasma-contracts/issues/102). In short, current IFE interactive game design can potentially decide the canonicity differently during data unavailability to the real canonicity when there is full data availability. We mitigate this by using Kelvin's solution of flagging all inputs (see: [this comment](https://github.com/omgnetwork/plasma-contracts/issues/102#issuecomment-495809967)). So even a mismatch canonicity happens, it cannot be double spent. #### Previous design on SE <> IFE interaction Previously we had a set of rules and action on the SE <> IFE interaction. It takes some time to evolve to the current one. See the original doc on `0.2` branch used for `RootChain.sol`: https://github.com/omgnetwork/elixir-omg/blob/v0.2.0/docs/standard_vs_in_flight_exits_interaction.md. We end up change the mechanism heavily for two reasons: 1. Simplicity on the rule 2. The best solution to mitigate the IFE canonicity issue is to flag all inputs. So we need to flag output anyway. Quite a lot of our current `id` schema comes from the previous doc, such as `exitId`. Note that in previous design, `startInFlightExit` would auto challenge an existing standard exit. We removed such feature from the contract. As a result, watcher should add monitoring on such event when a standard exit can be challenged by an in-flight exit tx. Also, see the discussion of changing the SE <> IFE interaction mechanism: https://github.com/omgnetwork/plasma-contracts/issues/110 #### Current Implementation Of Payment Exit Game For more details on our current implementation of Payment Exit Game: https://github.com/omgnetwork/plasma-contracts/blob/master/plasma_framework/docs/design/payment-game-implementation-v1.md ================================================ FILE: docs/tesuji_blockchain_design.md ================================================ # Tesuji Plasma Blockchain Design This document describes in detail the blockchain (consensus) design used by the first iteration of OMG Plasma-based implementation. The design is heavily based on [Minimal Viable Plasma design](https://ethresear.ch/t/minimal-viable-plasma/426), but incorporates several modifications. The reader is assumed to have prior knowledge of Ethereum and familiarity with general ideas behind [Plasma](http://plasma.io). ## Overview Tesuji Plasma's architecture allows users to take advantage of cheaper transactions with higher throughput without sacrificing security. This is accomplished by allowing users to make transactions on a child chain which derives its security from a root chain. By **child chain** we mean a blockchain that coalesces multiple transactions into a **child chain block** compacting them into a single, cheap transaction on a **root chain**. In our case the root chain is the Ethereum blockchain. ### Key Features These are the key features of the design, which might be seen as main deviations from the big picture Plasma, as outlined by the original Plasma paper: 1. Supports only transactions that transfer value between addresses (Multiple currencies: Eth + ERC20). See **Transactions** section. (The value transfer can take the form of an atomic swap - two currencies being exchanged in a single transaction.) 5. The network is a non-p2p, proof-of-authority network, i.e. child chain is centrally controlled by a designated, fixed Ethereum address (**child chain operator**), other participants (**users**) connect to the child chain server. See **Child chain server** section 6. The Plasma construction employed is a single-tiered one, i.e. the child chain doesn't serve as a parent of any chain 7. There aren't facilities that allow cheap, coordinated mass exits The essence of security and scalability features is the ability of users to perform the following scenario: 1. Deposit funds into a contract on the root chain 2. Cheaply make multiple transfers of funds deposited on the child chain 3. Exit any funds held on the child chain to reclaim them on the root chain, securely. That is, every exit of funds held on the child chain must come with an attestation that the exit is justified. The nature of that attestation will be clarified in following sections. Since exits can be done regardless of the state of the PoA child chain, the funds held on the child chain and root chain might be treated _as equivalent_. The condition here is that if anything goes wrong on the child chain, everyone must exit to the root chain. It's worth noting that the Plasma architecture presumes root chain availability. The consensus is driven by the following components: 1. **root chain contract** - responsible for securing the child chain: - holds funds deposited by other addresses (users) - tracks child chain block hashes submitted that account for the funds being moved on the child chain - manages secure exiting of funds, including exits of in-flight transactions 2. **child chain server** - responsible for creating and submitting blocks: - collects valid transactions that move funds on the child chain - submits child chain block hashes to the root chain contract - publishes child chain blocks' contents 3. **watcher** - responsible for validating the child chain and making sure the child chain consensus mechanism is working properly: - tracks the root chain contract, published blocks and transactions - reports any breach of consensus - (as additional service) collects and stores the account information required to use the child chain - (as additional service) provides a convenience API to access the child chain API and Ethereum. Such access is restricted only to times when the child chain is valid, in order to protect the user. **NOTE** all cryptographic primitives used (signatures, hashes) are understood to be ones compatible with whatever EVM uses. ## Root chain contract Note that the child chain and the root chain contract securing it manage funds using the UTXO model (see **Transactions** section). ### Deposits Any Ethereum address may deposit Eth or ERC20 token into the root chain contract. Such deposit increases the pool of funds held by the root chain contract and also signals to the child chain server, that the funds should be accessible on the child chain. Depositing causes the deposited funds to be recognized as a single UTXO on the child chain. Such UTXO can then be both spent on the child chain (provided that the child chain server follows consensus) or exited immediately on the root chain (regardless of whether child chain server follows consensus). The mechanism of depositing consists in forming of a "pseudo-block" of the child chain, that contains a single transaction with the deposited funds as a new UTXO. ### Exits w/ exit challenges Exits are the most important part of the root chain contract facilities, as they give the equivalence of funds sitting in the child chain vs funds on the root chain. In principle, the exits must satisfy the following conditions: - **E1**: only funds represented by UTXOs that were provably included in the child chain may be exited (see **Transactions** section). This means that only funds that provably _existed_ may be exited. - **E2**: attempts to exit funds, which have been provably spent on the child chain, must be thwarted and punished. - **E3**: there must be a priority given to earlier UTXOs, for the event when the attacking child chain operator submits a block creating UTXOs dishonestly and attempts to exit these UTXOs. It allows all UTXOs created before the dishonest UTXOs to exit first. - **E4**: funds that are in-flight, i.e. locked up in a transaction, that might have or might have not been included in the child chain, must be able to exit on par with funds whose inclusion is known. #### Submitting exit requests and challenging **E1** and **E2** are satisfied by the following mechanism, depending on the inclusion: **UTXOs, whose creating transaction is included in a known position in a child chain valid up to that point, use the _regular exit_:** Any Ethereum address that proves possession of funds (UTXO) on the child chain, can submit a request to exit. The proof consists in showing the transaction (containing the UTXO as output) and proving inclusion of the transaction in one of the submitted child chain blocks. However, this isn't the full attestation required to be able to withdraw funds from the root chain contract. The submitted (proven) exit request must still withstand a **challenge period** when it can be challenged by anyone who provides evidence that the exited UTXO has been spent. The evidence consists in a signed transaction spending the exiting UTXO, regardless of its inclusion. Exit's challenge period counts from exit request submission till that exit's scheduled finalization time (see below). A successful and timely exit challenge invalidates the exit. **Funds that are in-flight, i.e. where inclusion of a transaction manipulating them is not known or inclusion is in an invalid chain, use the _in-flight exit_:** Assuming that the in-flight transaction has inputs that had been outputs of a transaction included in a valid chain, such funds are recoverable using the [MoreVP protocol](morevp.md). #### Finalization of exits Finalizing an exit means releasing funds from the root chain contract to the exitor. **E3** is satisfied by exit scheduling and priorities. Exits finalize at their **Scheduled finalization time (`SFT`)**, which is: ``` SFT = max(exit_request_block.timestamp + MFP, utxo_submission_block.timestamp + MFP + REP) ``` for regular exits, and: ``` exitable_at = max(exit_request_block.timestamp + MFP, youngest_input_block.timestamp + MFP + REP) ``` for in-flight exits, see [MoreVP protocol](morevp.md) for details. Deposits are protected against malicious operator by elevating their exit priority: ``` SFT = max(exit_request_block.timestamp + MFP, utxo_submission_block.timestamp + MFP) ``` In the above formulae: - `exit_request_block` - root chain block, where the exit request is mined - `utxo_submission_block` - root chain block, where the exiting UTXO was created in a child chain block - `youngest_input_block` - root chain block, where the youngest input of the exiting transaction was created - all exits must wait at least the **Minimum finalization period (`MFP`)**, to always have the challenge period - fresh UTXOs exited must wait an additional **Required exit period (`REP`)**, counting from their submission to root chain contract. > Example values of `MFP` and `REP` are 1 week and 1 week respectively, as in Minimal Viable Plasma. Root chain contract allows to finalize exits which `SFT` had passed, always processing exits in ascending order of **exit priority**. Exit priority has two keys: - primary key is the `SFT` - secondary key is the UTXO position (see **Transactions**) #### Frequency of child chain validation There are maximum periods of time a user can spend offline, without validating a particular aspect of the chain and exposing themselves to risk of fund loss: - must validate child chain every `REP` to have enough time to submit an exit request in case chain invalid - must validate exits every `MFP` to challenge invalid regular exits - must validate in-flight exits every `MFP/2` to challenge invalid actions in the in-flight exit protocol Reassuming, to cover all the possible misbehavior of the network, the user must validate at rarest every `min(REP, MFP/2)`. #### Example exit scenarios The relation between `MFP` and `REP` is illustrated by the following: - **Example 1**: `MFP = 1 day`, `REP = 2 day` - day 1 operator creates, includes, and starts to exit an invalid UTXO - day 3 user checks chain after being offline for 2 days (`REP`) and sees the invalid transaction, exits his old UTXO - day 4 both operator and user can exit (after `MFP`), but user's exit takes precedence based on `utxoPos` ### Block submissions Only a designated address belonging to the child chain operator can submit blocks. Every block submitted to the root chain contract compacts multiple child chain transactions. Effectively, the block being submitted means that during exiting, ownership of funds (inclusion of transaction) can be now proven using a new child chain block hash. ### Network congestion (TODO: **Note: This is currently being researched and discussed**) The child chain will allow a maximum of N UTXOs at given time on the child chain. N is bound by root chain's bandwidth limitations and is the maximum amount of UTXOs that can safely requested to exit, if the child chain becomes invalid. Plasma assumes root chain network and block gas availability to start all users' exits in time. If the network becomes too congested, we'll freeze time on the root chain contract until it becomes safe to operate again. ### Reorgs Reorgs (block and transaction order changing) of the root chain can lead to spurious invalidity of the child chain. For instance, without any protection, a deposit can be placed and then spent quickly on the child chain. Everything is valid, if the submit block root chain transaction gets mined after the deposit (making the honest child chain to allow the spend). However, if the order of these transactions gets reversed due to a reorg, the spend will appear before the deposit, rendering the child chain invalid. We'll protect ourselves against reorgs by: 1. Only allowing deposits to be used on the child chain after N Ethereum Block confirmations (should be configurable). This makes invalidating of the child chain by miners as expensive as we want it to be. This rule will be built into the child chain itself, i.e. the root chain contract won't enforce this in any way. 2. Submitting blocks to the root chain contract is protected by account nonce mechanism. Miner attempting to mine them in wrong order would produce incorrect Ethereum block. 3. Numbering of child chain blocks is independent of numbering of deposit blocks. Disappearing deposit block will not invalidate numbering of child chain blocks. ## Child chain server ### Collecting transactions The child chain server will collect transactions, executing the valid ones immediately. The child chain will have **transactions per block limit** - an upper limit for the number of transactions that can go in a single child chain block. If a submitted transaction would exceed that limit, it's going to be held off in a queue and scheduled for inclusion in the next block. That queue would be prioritized by transaction fee value. If there are too many transactions in the queue the ones with the lowest fees will be lost and must be resubmitted. > Transaction per block limit is assumed to be 2^16, per Minimal Viable Plasma ### Submitting and propagating blocks Every T amount of time the child chain will submit a block (in form of blocks' transactions merkle root hash) to root chain contract. After the child chain has submitted a block to root chain contract it must share the block contents on watcher's request. The watchers are responsible for taking in blocks and extracting whatever information they need from them (see **Watcher** section). If the child chain operator submits an invalid block or withholds a submitted block (i.e. doesn't share the block contents) everyone must exit. ### Transactions Transactions, their semantics and encoding are described in detail in the [Transactions section of the contracts integration document](https://github.com/omgnetwork/plasma-contracts/blob/master/plasma_framework/docs/integration-docs/integration-doc.md#transactions), **NOTE** To create a valid transaction, a user needs to have access to inputs pointers (UTXO positions or OutputIDs or other) of all the UTXOs that they intend to spend. The Child Chain server doesn't provide this data, it is the responsibility of the Watcher (or Watcher Info) service intended to be ran by the users. ### Fees The transaction's fee is implicit (think bitcoin), i.e. surplus of the amount being inputted over the amount being outputted (`sumAmount(spent UTXOs) - sumAmount(created UTXOs) >= 0`) is the fee that the child chain operator is eligible to claim later. This section only skims the transaction fee topic, for details see [fee design document](./fee_design.md). #### Accepting fees by the child chain server The child chain will have a configurable fixed min fee and will not accept any transactions below the fixed min fee. The fixed min fee will be derived from the average of N different apis (see [here](https://developer.makerdao.com/feeds/) for more info) pinging the central server so that it stays up to date on the current prices. #### Tracking and exiting fees Child chain operator is eligible to exit the fees accumulated from the root chain contract. See **Watcher** section for Watcher's role of tracking the correctness of fee exits. ## Watcher The watcher is assumed to be run by the users, or taken differently, to be trusted by users of the child chain. Proper functioning of the watcher is critical to the security of funds deposited. The watcher is responsible for pinging the child chain server to ensure that everything is valid. The watcher will watch the root chain contract for a `BlockSubmitted` event log (a submission of a child chain block). As soon as it receives a log it will ping the child chain for the full block and then make sure the block is valid and that it's root matches the child chain root submitted. The watcher will check for the following conditions of chain invalidity. Any of these make the watcher prompt for an exit of funds: 1. Invalid blocks: - With multiple transactions spending the same input. - Transactions spending an input spent in any prior block - Transactions spending exited inputs, if unchallenged or challenge failed or was too late - Transactions with deposits that haven't happened. - Transactions with invalid inputs. - Transactions with invalid signatures. 2. Fee exits by the child chain operator that take more fees than the operator has available to them. It's the watchers job to check that the operator never exits more fees than they're due, because the funds to cover the exited fees are drawn from the same pool, where the deposited funds are. In other words, if watchers overlook the child chain operator exiting too much fees, there might be not enough funds left in the root chain contract for them to exit. 3. Inability to acquire (for a long enough period of time) enough information to validate a child chain block that's been submitted to the root chain. 4. Any invalid claim done on the root chain contract (e.g. an invalid exit), that goes without challenge for too long and becomes a risk on the security of the funds held on the child chain. The watcher will check for the following conditions that (optionally) prompt for an exit challenge: 1. Exits during their challenge period referencing UTXOs that have already been spent on the child chain. 2. Invalid actions taken during the in-flight exit game, see [MoreVP protocol](morevp.md). As soon as one watcher detects the child chain to be invalid, all others will as well and everyone with assets on the child chain will be notified to exit immediately. ### Storage facilities of the watcher (aka Account information) Watcher takes on an additional responsibility: collecting and storing data relevant to secure handling of user's assets on the child chain: 1. UTXOs in possession of the address holding assets 2. Full transaction history (child chain blocks) ## Exchange See [here](./dex_design.md) for a high-level discussion about exchange designs on top of Tesuji plasma. ================================================ FILE: docs/transaction_validation.md ================================================ # Transaction validation NOTE: * input = utxo This document presents current way of stateless and stateful validation of `OMG.ChildChain.submit(encoded_signed_tx)` function. #### Stateless validation 1. Decoding of encoded singed transaction using `OMG.State.Transaction.Signed.decode` method * Decoding using `ExRLP.decode` method and if failed then `{:error, :malformed_transaction_rlp}` * Checking the transaction type and if not allowed then `{:error, :malformed_transaction}` * Decoding the raw structure of RLP items and if failed then `{:error, :malformed_transaction}` * Checking output type with respect to the parent transaction type and if failed then `{:error, :unrecognized_output_type}` * Checking addresses/inputs/outputs/metadata format and if failed then `{:error, :malformed_address}` / `{:error, :malformed_inputs}` / `{:error, :malformed_outputs}` / `{:error, :malformed_metadata}` respectively * Checking if outputs are non-empty and if failed then `{:error, :empty_outputs}` * Checking any integer values to be formatted validly and if failed then `{:error, :leading_zeros_in_encoded_uint}` or `{:error, :encoded_uint_too_big}` accordingly * Checking all amount-representing values to non-zero and if failed then `{:error, :amount_cant_be_zero}` 2. Checking and recovering (preprocessing) a decoded `Transaction.Signed` using `OMG.State.Transaction.Recovered` * Checking if transaction doesn't have duplicated inputs and if failed then `{:error, :duplicate_inputs}` * Checking if signatures are in correct format and lengths and if failed then `{:error, :malformed_witnesses}` * Checking if transaction has no missing signature for an input supplied and if failed then `{:error, :missing_signature}` * Checking if transaction has no missing input for a signature supplied and if failed then `{:error, :superfluous_signature}` * Recovering addresses of spenders from signatures and if failed then `{:error, :signature_corrupt}` #### Stateful validation 1. Validating block size * if the number of transactions in block exceeds limit then `{:error, :too_many_transactions_in_block}` 2. Checking correctness of input positions * if the input is from the future block then `{:error, :input_utxo_ahead_of_state}` * if the input does not exists then `{:error, :utxo_not_found}` * if the owner of input does not match with spender then `{:error, :unauthorized_spend}` 3. Checking if the amounts from the provided inputs adds up. * if not then `{:error, :amounts_do_not_add_up}` 4. (if in child chain server tx submission pipeline): see if the transaction pays the correct fee. * if not then `{:error, :fees_not_covered}` ================================================ FILE: docs/unified_api.md ================================================ # Unified API ## Problem Currently the Childchain API and Watcher API behave differently e.g. - Childchain API: - Must have jsonrpc and id in the request body - **Cannot** have 'Content-Type': 'application/json' header - On error, response contains response.error - Watcher API: - **Must** have 'Content-Type': 'application/json' header - On error, response contains response.result === 'error' Also on the roadmap is the **Informational API Service** that will provide non-critical convenience APIs. All three APIs should behave consistently. ## Proposal The eWallet already has a well defined API, using HTTP-RPC (rather than REST). - [eWallet Admin API](https://ewallet.staging.omisego.io/api/admin/docs.ui) - [eWallet Client API](https://ewallet.staging.omisego.io/api/client/docs.ui) We can follow the same model and ensure consistency across all OMG Network services. #### eWallet API characteristics The API is a collection of HTTP-RPC style method calls in the format ``` EWALLET_URL/api/METHOD_FAMILY.method ``` where `METHOD_FAMILY` is one of the functional parts of the API e.g. `account`, `transaction`, etc. Responses contain all data, metadata or errors in the body of the response. This means that HTTP calls always return `200`, even if the result is an error. One exception to this is if an internal server error occurs - in this case it will return `500` All HTTP calls are `POST` for consistency. Following this HTTP-RPC style means that the service can be used via websockets as well as HTTP. Example: ``` POST http://plasma-chain.network/api/account.get_balance BODY { "address": "0x40d6a26bd478e60f97755d62196f0d0f85c1be0d" } RESPONSE 200 { "version": "1", "success": true, "data": [ { "currency": "0x0000000000000000000000000000000000000000", "amount": 100 }, { "currency": "0x1234560000000000000000000000000000000000", "amount": 300000 } ] } RESPONSE 200 { "version": "1", "success": false, "data": { "object": "error", "code": "account:not_found", "description": "Account not found" } } RESPONSE 500 { "version": "1", "success": false, "data": { "object": "error", "code": "server:internal_server_error", "description": "Something went wrong on the server", "messages": { "error_key": "error_reason" } } } ``` ## OMG Network Plasma API There are three services involved ### 1. ChildChain Normally a user wouldn't call the ChildChain API directly, as doing so would lose the security features of the Watcher. However there may be some low-stake accounts that don't care or are fine with some amount of trust in the ChildChain operator. These users can call e.g. `submit` on the ChildChain directly. #### API endpoints ``` /block.get /transaction.submit ``` ### 2. Watcher The watcher first and foremost plays a critical security role in the system. The watcher monitors the child chain and root chain (Ethereum) for faulty activity. #### API endpoints ``` /transaction.get /transaction.get_in_flight_exit /transaction.submit /utxo.get_exit_data /utxo.get_challenge_data /status ``` #### Events ``` new_block new_transaction new_deposit exit_started exit_challenged in_flight_exit_started in_flight_exit_challenged exit_success piggyback invalid_block unchallenged_exit block_withholding invalid_fee_exit ``` ### 3. Informational/Convenience API This service may end up being included in the Watcher as optional functionality, but conceptually it can be seen as a separate service. It stores informational data about the chain, and provides convenience APIs to proxy to the child chain/root chain/watcher to ease integration and reduce duplicate code in libraries. #### API endpoints ``` /account.get_balance /account.get_utxos /account.get_transactions /transaction.all /transaction.create /transaction.get /transaction.get_in_flight_exit /block.all /block.get ... utxo management apis ... ``` #### Events ``` transaction_confirmed address_received address_spent ``` ## Architecture To be decided... ================================================ FILE: docs/watcher_db_design.md ================================================ # Watcher database design Watcher benefits from two databases approach: * rocksdb - key-value database that child chain state uses for transaction validation * watcherDb - PostgreSQL database that stores transactions and contains data needed for challenges and exits. It provides user interface with the data of user's concern (e.g: did I pay the electricity bill, who sent me money last week). [PostgreSQL Schema Diagram](https://docs.google.com/drawings/d/14_0bfUGGWarndNWwpzA2Nznll4PHLbefvQy05B2LB38/edit?usp=sharing) ## The blocks table Stores data about blocks: hash, timestamp when block was mined, Ethereum height is was published on. |**blocks**||| |:-:|:-|:-| |blknum|bigint|Pk| |hash|bytea|| |timestamp|integer|| |eth_height|bigint|| ## The transactions table Stores information about transactions. |**transactions**||| |:-:|:-|:-| |txhash|bytea|Pk| |blknum|integer|Fk(blocks, blknum), UI(blknum, txindex)| |txindex|integer|UI^| |txbytes|bytea|| |sent_at|timestamp|UTC (w/o TZ)| ## The transaction inputs and outputs table Stores inputs and outputs of transactions. Utxo is a record in `txoutputs` table where `spending_txhash` is `NULL`. `proof` field is needed for exiting an utxo. We compute a proof from all the transactions contained in the same block as the transaction that created the utxo. |**txoutputs**||| |:-:|:-|:-| |blknum|integer|Pk(blknum, txindex, oindex)| |txindex|integer|Pk^| |oindex|integer|Pk^| |creating_txhash|bytea)|Fk(transactions, (txhash)), NULL| |creating_deposit|bytea)|Fk(eth_events, (hash)), NULL| |spending_txhash|bytea|Fk(transactions, (txhash)), NULL| |spending_exit|bytea|Fk(eth_event, (hash)), NULL| |spending_tx_oindex|integer|| |owner|bytea|| |amount|numeric(81,0)|| |currency|bytea|| |proof|bytea|| |child_chain_utxohash|bytea|UI| |inserted_at|datetime|UTC (w/o TZ)| |updated_at|datetime|UTC (w/o TZ)| ## The Ethereum events table Stores events logged in root contract, such as _deposits_ or _exits_. |**ethevents**||| |:-:|:-|:-| |root_chain_txhash|bytea|Pk(root_chain_txhash, log_index)| |event_type|integer|Pk^| |event_type|varchar(124)|| |root_chain_txhash_event|bytea|UI| |inserted_at|datetime|UTC (w/o TZ)| |updated_at|datetime|UTC (w/o TZ)| ## The ethevents_txoutputs table A table for many-to-many relationships between Ethereum events and UTXOs. |**ethevents_txoutputs**||| |:-:|:-|:-| |root_chain_txhash_event|bytea|Pk(root_chain_txhash_event, child_chain_utxohash), FK(ethevents, (root_chain_txhash_event))| |child_chain_utxohash|bytea|Pk^, FK(txoutputs, (child_chain_utxohash))| |inserted_at|datetime|UTC (w/o TZ)| |updated_at|datetime|UTC (w/o TZ)| ## Examples of queries against the tables ### 1. get a transaction with inputs and outputs ``` select t.*, i.*, o.* from transactions t join TxOutput i on i.spending_txhash = t.txhash join TxOutput o on o.creating_txhash = t.txhash where t.txhash = @txhash ``` ### 2. get all transactions satisfying some criteria ... As in the previous example but with modified where clause. ### 3. get utxo position by owner address for spend ``` select o.blknum, o.txindex, o.oindex from txoutputs o where o.owner = @owner_address ``` ### 4. get utxo position by owner address for exit ... see previous point ### 5. add utxo when deposit detected ``` insert hash, deposit_blknum, deposit_txindex, event_type into ethevents values (@hash, @blknum, 0, "deposit") insert creating_deposit, blknum, txindex, oindex, owner, amount, currency into txoutputs values (@hash, @blknum, 0, 0, ...) ``` ### 6. spend utxo when exit finalized ``` insert hash, event_type into ethevents values (@hash, "exit") update txoutputs set spending_exit = @hash where ... ``` ### 7. get exit data by an utxo position For exit we need: proof that transaction is included in block. (**NOTE** that this is only optionally served by `WatcherDB`. Normally one expects this to be served by the `security-critical` Watcher mode, which doesn't run `WatcherDB`) ``` select (t.txhash, t.txbytes) into @out from transactions t join txoutputs o on o.creating_txhash = t.txhash where (t.blknum, t.txindex, o.oindex) = @position select t.txhash from transactions t where t.blknum == @blknum ``` ### 8. get all ethereum events for existing utxos ``` SELECT CASE WHEN t.child_chain_utxohash IS NULL THEN NULL WHEN t.child_chain_utxohash IS NOT NULL THEN concat('0x', encode(t.child_chain_utxohash::bytea, 'hex')) END AS child_chain_utxohash, CASE WHEN e.root_chain_txhash_event IS NULL THEN NULL WHEN e.root_chain_txhash_event IS NOT NULL THEN concat('0x', encode(e.root_chain_txhash_event::bytea, 'hex')) END AS root_chain_txhash_event, CASE WHEN e.root_chain_txhash IS NULL THEN NULL WHEN e.root_chain_txhash IS NOT NULL THEN concat('0x', encode(e.root_chain_txhash::bytea, 'hex')) END AS root_chain_txhash, blknum, txindex, oindex, amount, log_index, event_type FROM txoutputs t LEFT OUTER JOIN ethevents_txoutputs et ON t.child_chain_utxohash = et.child_chain_utxohash LEFT OUTER JOIN ethevents e ON et.root_chain_txhash_event = e.root_chain_txhash_event; ``` ================================================ FILE: dummy ================================================ ================================================ FILE: fees_setup.env ================================================ FEE_CLAIMER_ADDRESS=0x3b9f4c1dd26e0be593373b1d36cee2008cbeb837 FEE_FEED_URL=http://172.27.0.110:4000/api/v1 STORED_FEE_UPDATE_INTERVAL_MINUTES=1 FEE_CHANGE_TOLERANCE_PERCENT=1 FEE_BUFFER_DURATION_MS=30000 FEE_ADAPTER=file FEE_SPECS_FILE_PATH=/dev-artifacts/fee_specs.dev.json ================================================ FILE: mix.exs ================================================ defmodule OMG.Umbrella.MixProject do use Mix.Project def project() do [ # name the ap for the sake of `mix coveralls --umbrella` # see https://github.com/parroty/excoveralls/issues/23#issuecomment-339379061 apps_path: "apps", start_permanent: Mix.env() == :prod, deps: deps(), preferred_cli_env: [ coveralls: :test, "coveralls.detail": :test, "coveralls.html": :test, "coveralls.circle": :test, dialyzer: :test ], build_path: "_build" <> docker(), deps_path: "deps" <> docker(), dialyzer: dialyzer(), test_coverage: [tool: ExCoveralls], # gets all apps test folders for the sake of `mix coveralls --umbrella` test_paths: test_paths(), aliases: aliases(), # Docs source_url: "https://github.com/omgnetwork/elixir-omg", version: current_version(), releases: [ watcher: [ steps: steps(), applications: [ tools: :permanent, runtime_tools: :permanent, omg_watcher: :permanent, omg_watcher_rpc: :permanent, omg_status: :permanent, omg_db: :permanent, omg_eth: :permanent, omg_bus: :permanent ], config_providers: [ {OMG.Status.ReleaseTasks.SetSentry, [release: :watcher, current_version: current_version()]}, {OMG.Status.ReleaseTasks.SetTracer, [release: :watcher]}, {OMG.Status.ReleaseTasks.SetApplication, [release: :watcher, current_version: current_version()]}, {OMG.Eth.ReleaseTasks.SetEthereumEventsCheckInterval, []}, {OMG.Eth.ReleaseTasks.SetEthereumStalledSyncThreshold, []}, {OMG.Eth.ReleaseTasks.SetEthereumClient, []}, {OMG.Eth.ReleaseTasks.SetContract, []}, {OMG.DB.ReleaseTasks.SetKeyValueDB, [release: :watcher]}, {OMG.WatcherRPC.ReleaseTasks.SetEndpoint, []}, {OMG.WatcherRPC.ReleaseTasks.SetTracer, []}, {OMG.WatcherRPC.ReleaseTasks.SetApiMode, :watcher}, {OMG.Status.ReleaseTasks.SetLogger, []}, {OMG.Watcher.ReleaseTasks.SetEthereumEventsCheckInterval, []}, {OMG.Watcher.ReleaseTasks.SetExitProcessorSLAMargin, []}, {OMG.Watcher.ReleaseTasks.SetTracer, []}, {OMG.Watcher.ReleaseTasks.SetApplication, [release: :watcher, current_version: current_version()]} ] ], watcher_info: [ steps: steps(), version: current_version(), applications: [ tools: :permanent, runtime_tools: :permanent, omg_watcher: :permanent, omg_watcher_info: :permanent, omg_watcher_rpc: :permanent, omg_status: :permanent, omg_db: :permanent, omg_eth: :permanent, omg_bus: :permanent ], config_providers: [ {OMG.Status.ReleaseTasks.SetSentry, [release: :watcher_info, current_version: current_version()]}, {OMG.Status.ReleaseTasks.SetTracer, [release: :watcher_info]}, {OMG.Status.ReleaseTasks.SetApplication, [release: :watcher_info, current_version: current_version()]}, {OMG.Status.ReleaseTasks.SetLogger, []}, {OMG.Eth.ReleaseTasks.SetEthereumEventsCheckInterval, []}, {OMG.Eth.ReleaseTasks.SetEthereumStalledSyncThreshold, []}, {OMG.Eth.ReleaseTasks.SetEthereumClient, []}, {OMG.Eth.ReleaseTasks.SetContract, []}, {OMG.DB.ReleaseTasks.SetKeyValueDB, [release: :watcher_info]}, {OMG.WatcherRPC.ReleaseTasks.SetEndpoint, []}, {OMG.WatcherRPC.ReleaseTasks.SetTracer, []}, {OMG.WatcherRPC.ReleaseTasks.SetApiMode, :watcher_info}, {OMG.Watcher.ReleaseTasks.SetEthereumEventsCheckInterval, []}, {OMG.Watcher.ReleaseTasks.SetExitProcessorSLAMargin, []}, {OMG.Watcher.ReleaseTasks.SetTracer, []}, {OMG.Watcher.ReleaseTasks.SetApplication, [release: :watcher_info, current_version: current_version()]}, {OMG.WatcherInfo.ReleaseTasks.SetTracer, []} ] ] ] ] end defp test_paths() do "apps/*/test" |> Path.wildcard() |> Enum.sort() end defp deps() do [ {:mix_audit, "~> 0.1", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, {:credo, "~> 1.3", only: [:dev, :test], runtime: false}, # https://github.com/xadhoom/excoveralls.git `52c6c8e5d7fe9abb814e5e3e546c863b9b2b41b7` rebased on `master` # more or less around v0.11.1 {:excoveralls, "~> 0.12.3"}, {:licensir, "~> 0.2.0", only: :dev, runtime: false}, { :ex_unit_fixtures, git: "https://github.com/omgnetwork/ex_unit_fixtures", branch: "feature/require_files_not_load", only: [:test] }, {:ex_doc, "~> 0.20.2", only: :dev, runtime: false}, {:spandex, "~> 3.0.2"} ] end defp aliases() do [ test: ["ecto.create", "ecto.migrate", "test --no-start"], coveralls: ["coveralls --no-start"], "coveralls.html": ["coveralls.html --no-start"], "coveralls.detail": ["coveralls.detail --no-start"], "coveralls.post": ["coveralls.post --no-start"], "coveralls.circle": ["coveralls.circle --no-start"], "ecto.setup": ["ecto.create", "ecto.migrate"], "ecto.reset": ["ecto.drop", "ecto.setup"] ] end defp dialyzer() do [ flags: [:error_handling, :race_conditions, :underspecs, :unknown, :unmatched_returns], ignore_warnings: "dialyzer.ignore-warnings", list_unused_filters: true, plt_add_apps: plt_apps(), paths: Enum.map(File.ls!("apps"), fn app -> "_build#{docker()}/#{Mix.env()}/lib/#{app}/ebin" end) ] end defp plt_apps() do [ :briefly, :cowboy, :ex_machina, :ex_unit, :exexec, :fake_server, :iex, :jason, :mix, :plug, :ranch, :sentry, :vmstats ] end defp docker(), do: if(System.get_env("DOCKER"), do: "_docker", else: "") defp current_version() do "git" |> System.cmd(["describe", "--tags"]) |> elem(0) |> String.replace("\n", "") end defp steps() do case Mix.env() do :prod -> [:assemble, :tar] _ -> [:assemble] end end end ================================================ FILE: priv/dev-artifacts/README.md ================================================ # Dev artifacts Contains various development artifacts that are useful for development environment configs but not to be used in production environments. For example: - `fee_specs.dev.json`: The fee specs used by docker-compose.yml which is only intended for development setup. ================================================ FILE: priv/dev-artifacts/fee_specs.dev.json ================================================ { "1": { "0x0000000000000000000000000000000000000000": { "amount": 1, "pegged_amount": null, "pegged_currency": null, "pegged_subunit_to_unit": null, "subunit_to_unit": 1000000000000000000, "updated_at": "2019-01-01T10:10:00+00:00", "symbol": "ETH", "type": "fixed" } } } ================================================ FILE: priv/dev-artifacts/fee_specs.test.json ================================================ { } ================================================ FILE: priv/perf/.formatter.exs ================================================ # Used by "mix format" [ inputs: ["mix.exs", "config/*.exs"], line_length: 120, subdirectories: ["apps/*"] ] ================================================ FILE: priv/perf/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Open api auto generated elixir client directories /apps/child_chain_api/ /apps/watcher_info_api/ /apps/watcher_security_critical_api/ # Load test results /results/ ================================================ FILE: priv/perf/Dockerfile ================================================ FROM elixir:1.11.2-alpine RUN apk add --no-cache rust \ cargo \ git \ curl \ bash \ maven jq \ autoconf \ automake \ gmp \ gmp-dev \ libtool \ gcc \ cmake \ gnupg \ alpine-sdk COPY ./ ./elixir-omg WORKDIR ./elixir-omg RUN mkdir -p priv/openapitools \ && curl https://raw.githubusercontent.com/OpenAPITools/openapi-generator/v4.3.1/bin/utils/openapi-generator-cli.sh > priv/openapitools/openapi-generator-cli \ && chmod u+x priv/openapitools/openapi-generator-cli RUN priv/openapitools/openapi-generator-cli generate \ -i https://raw.githubusercontent.com/omgnetwork/omg-childchain-v1/master/apps/omg_child_chain_rpc/priv/swagger/operator_api_specs.yaml \ -g elixir \ -o priv/perf/apps/child_chain_api/ RUN priv/openapitools/openapi-generator-cli generate \ -i apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml \ -g elixir \ -o priv/perf/apps/watcher_security_critical_api/ RUN priv/openapitools/openapi-generator-cli generate \ -i apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml \ -g elixir \ -o priv/perf/apps/watcher_info_api/ RUN mix local.hex --force && mix local.rebar --force WORKDIR ./priv/perf RUN mix deps.get && mix compile ================================================ FILE: priv/perf/Makefile ================================================ .PHONY: list COMPOSE_FULL_SERVICES=-f ../../docker-compose.yml -f ../../docker-compose.datadog.yml list: @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' clean: docker-compose $(COMPOSE_FULL_SERVICES) down && docker volume prune --force start-services: cd ../../ && \ SNAPSHOT=SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120 make init-contracts && \ cd priv/perf/ && \ docker-compose $(COMPOSE_FULL_SERVICES) up -d stop-services: docker-compose $(COMPOSE_FULL_SERVICES) down log-services: docker-compose $(COMPOSE_FULL_SERVICES) logs -f childchain feefeed watcher watcher_info geth init: . scripts/generate_api_client.sh mix deps.get test: # runs test against child chain and geth provided in test docker containers LOAD_TEST_FAUCET_PRIVATE_KEY=0xd885a307e35738f773d8c9c63c7a3f3977819274638d04aaf934a1e1158513ce mix test format-code-check-warnings: LOAD_TEST_FAUCET_PRIVATE_KEY=0xd885a307e35738f773d8c9c63c7a3f3977819274638d04aaf934a1e1158513ce MIX_ENV=test mix do compile --warnings-as-errors --ignore-module-conflict --force, test --exclude test ================================================ FILE: priv/perf/README.md ================================================ # Perf Umbrella app for performance/load/stress tests ## How to run the tests ### 1. Set up the environment vars ``` export CHILD_CHAIN_URL= export WATCHER_INFO_URL= export ETHEREUM_RPC_URL= export CONTRACT_ADDRESS_PLASMA_FRAMEWORK=
export CONTRACT_ADDRESS_ETH_VAULT=
export CONTRACT_ADDRESS_ERC20_VAULT=
export LOAD_TEST_FAUCET_PRIVATE_KEY= ``` ### 2. Generate the open-api client ``` make init ``` ## Tests with assertions These tests check the integrity of the system during their run. They meant to be run with the given rate (tests/second) over the given period (seconds). During test runs, the perf project sends metrics to datadog. After a test finishes its executions, datadog monitor events are checked. If some metrics exceed values set in monitors, the test is marked as failed. Currently there two tests are implemented: `deposits` and `transactions` tests. ### Deposits tests A single iteration of this test consists of the following steps: 1. It creates two accounts: the depositor and the receiver. 2. It funds depositor with the specified amount (`initial_amount`) on the rootchain. 3. It creates deposit (`deposited_amount`) with gas price `gas_price` for the depositor account on the childchain and checks balances on the rootchain and the childchain after this deposit. 4. The depositor account sends the specifed amount (`transferred_amount`) on the childchain to the receiver and checks its balance on the childchain. ### Transactions tests A single iteration of this test consists of the following steps: 1.1 Two accounts are created - the sender and the receiver 2.1 The sender account is funded with `initial_amount` `token` 2.2 The balance on the childchain of the sender is validated using WatcherInfoAPI.Api.Account.account_get_balance API. 2.3 Utxos of the sender are validated using WatcherInfoAPI.Api.Account.account_get_utxos API 3.1 The sender sends all his tokens to the receiver with fee `fee` 3.2 The balance on the childchain of the sender is validated 3.3 The balance on the childchain of the receiver is validated 3.4 Utxos of the sender are validated 3.5 Utxos of the receiver are validated ### Basic usage If you want to run `deposits` with 5 tests / second rate over 20 seconds, you should run the following command: ```bash mix run -e "LoadTest.TestRunner.run()" -- deposits 5 20 ``` ### Help and documentation To see up-to-date docs, run: ```bash mix run -e "LoadTest.TestRunner.run()" -- help ``` To see info about a specific test, run: ```bash mix run -e "LoadTest.TestRunner.run()" -- help transactions ``` ### Docker The perf project is packaged into a docker image. So instead of using the project directly, you can run all commands with docker container. For example, help command looks like this: ```bash docker run -it omisego/perf:latest mix run -e "LoadTest.TestRunner.run()" -- help ``` To run deposits tests, use: ```bash docker run -it --env-file ./localchain_contract_addresses.env --network host omisego/perf:latest mix run -e "LoadTest.TestRunner.run()" -- "deposits" 10 1 ``` ## Tests without assertions ### 1. Configure the tests Edit the config file (e.g. `config/dev.exs`) set the test parameters e.g. ``` childchain_transactions_test_config: %{ concurrent_sessions: 100, transactions_per_session: 600, transaction_delay: 1000 } ``` Note that by default the tests use ETH both as the currency spent and as the fee. This makes the code simpler as it doesn't have to manage separate fee utxos. However, if necessary you can configure the tests to use a different currency. e.g. ``` config :load_test, test_currency: "0x942f123b3587EDe66193aa52CF2bF9264C564F87", fee_amount: 6_000_000_000_000_000, ``` ### 2. Run the tests ``` MIX_ENV= mix test ``` Or just `mix test` if you want to run against local services. You can specify a particular test on the command line e.g. ``` MIX_ENV=dev mix test apps/load_test/test/load_tests/runner/childchain_test.exs ``` **Important** After each test run, you need to wait ~15 seconds before running it again. This is necessary to wait for the faucet account's utxos to be spendable. Depending on the watcher-info load, it can take longer than this. If you get an error like this ``` module=LoadTest.Service.Faucet Funding user 0x76f0a3aade31c19d306bc91b46817b95072a8cbd with 2 from utxo: 10800070000⋅ module=LoadTest.ChildChain.Transaction Transaction submission has failed, reason: "submit:utxo_not_found"⋅ ``` then you haven't waited long enough. Kill it, wait some more, try again. ### Increase connection pool size and connection One can override the setup in config to increase the `pool_size` and `max_connection`. If you found the latency on the api calls are high but the data dog latency shows way smaller, it might be latency from setting up the connection instead of real api latency. ### Retrying on errors The Tesla HTTP middleware can be configured to retry on error. By default this is disabled, but it can be enabled by modifying the `retry?` function in `connection_defaults.ex`. For example, to retry any 500 response: ``` defp retry?() do fn {:ok, %{status: status}} when status in 500..599 -> true {:ok, _} -> false {:error, _} -> false end end ``` See [Tesla.Middleware.Retry](https://hexdocs.pm/tesla/Tesla.Middleware.Retry.html) for more details. ================================================ FILE: priv/perf/apps/load_test/.formatter.exs ================================================ # Used by "mix format" [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], line_length: 120 ] ================================================ FILE: priv/perf/apps/load_test/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). load_test-*.tar # Ignore Chaperon report outputs /results/ ================================================ FILE: priv/perf/apps/load_test/README.md ================================================ # LoadTest Load test is load test! ================================================ FILE: priv/perf/apps/load_test/lib/application.ex ================================================ defmodule LoadTest.Application do @moduledoc """ Application for the load test """ use Application alias LoadTest.Connection.ConnectionDefaults alias LoadTest.Ethereum.NonceTracker alias LoadTest.Service.Datadog alias LoadTest.Service.Faucet def start(_type, _args) do :ok = start_hackney_pool() NonceTracker.init() children = [{Faucet, fetch_faucet_config()}, {Datadog, []}] Supervisor.start_link(children, strategy: :one_for_one, restart: :temporary) end defp start_hackney_pool() do pool_size = Application.fetch_env!(:load_test, :pool_size) max_connections = Application.fetch_env!(:load_test, :max_connection) :hackney_pool.start_pool( ConnectionDefaults.pool_name(), timeout: 180_000, connect_timeout: 30_000, pool_size: pool_size, max_connections: max_connections ) end def stop(_app) do :hackney_pool.stop_pool(:perf_pool) end defp fetch_faucet_config() do faucet_config_keys = [ :faucet_private_key, :fee_amount, :faucet_deposit_amount, :deposit_finality_margin, :gas_price ] Enum.map(faucet_config_keys, fn key -> {key, Application.fetch_env!(:load_test, key)} end) end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/abi/abi_event_selector.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Abi.AbiEventSelector do @moduledoc """ We define Solidity Event selectors that help us decode returned values from function calls. Function names are to be used as inputs to Event Fetcher. Function names describe the type of the event Event Fetcher will retrieve. """ @spec deposit_created() :: ABI.FunctionSelector.t() def deposit_created() do %ABI.FunctionSelector{ function: "DepositCreated", input_names: ["depositor", "blknum", "token", "amount"], inputs_indexed: [true, true, true, false], method_id: <<24, 86, 145, 34>>, returns: [], type: :event, types: [:address, {:uint, 256}, :address, {:uint, 256}] } end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/abi/abi_function_selector.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Abi.AbiFunctionSelector do @moduledoc """ We define Solidity Function selectors that help us decode returned values from function calls """ # workaround for https://github.com/omgnetwork/elixir-omg/issues/1632 def blocks() do %ABI.FunctionSelector{ function: "blocks", input_names: ["block_hash", "block_timestamp"], inputs_indexed: nil, method_id: <<242, 91, 63, 153>>, # returns: [bytes: 32, uint: 256], type: :function, # types: [uint: 256] types: [bytes: 32, uint: 256] } end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/abi/fields.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Abi.Fields do @moduledoc """ Adapt to naming from contracts to elixir-omg. I need to do this even though I'm bleeding out of my eyes. """ def rename(data, %ABI.FunctionSelector{function: "DepositCreated"}) do # key is naming coming from plasma contracts # value is what we use contracts_naming = [{"token", :currency}, {"depositor", :owner}, {"blknum", :blknum}, {"amount", :amount}] reduce_naming(data, contracts_naming) end defp reduce_naming(data, contracts_naming) do Enum.reduce(contracts_naming, %{}, fn {old_name, new_name}, acc -> value = Map.get(data, old_name) acc |> Map.put_new(new_name, value) |> Map.delete(old_name) end) end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/abi.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Abi do @moduledoc """ Functions that provide ethereum log decoding """ alias ExPlasma.Encoding alias LoadTest.ChildChain.Abi.AbiEventSelector alias LoadTest.ChildChain.Abi.AbiFunctionSelector alias LoadTest.ChildChain.Abi.Fields def decode_function(enriched_data, signature) do "0x" <> data = enriched_data <> = elem(ExKeccak.hash_256(signature), 1) method_id |> Encoding.to_hex() |> Kernel.<>(data) |> Encoding.to_binary() |> decode_function() end def decode_function(enriched_data) do function_specs = Enum.reduce(AbiFunctionSelector.module_info(:exports), [], fn {:module_info, 0}, acc -> acc {function, 0}, acc -> [apply(AbiFunctionSelector, function, []) | acc] _, acc -> acc end) {function_spec, data} = ABI.find_and_decode(function_specs, enriched_data) decode_function_call_result(function_spec, data) end def decode_log(log) do event_specs = Enum.reduce(AbiEventSelector.module_info(:exports), [], fn {:module_info, 0}, acc -> acc {function, 0}, acc -> [apply(AbiEventSelector, function, []) | acc] _, acc -> acc end) topics = Enum.map(log["topics"], fn nil -> nil topic -> Encoding.to_binary(topic) end) data = Encoding.to_binary(log["data"]) {event_spec, data} = ABI.Event.find_and_decode( event_specs, Enum.at(topics, 0), Enum.at(topics, 1), Enum.at(topics, 2), Enum.at(topics, 3), data ) data |> Enum.into(%{}, fn {key, _type, _indexed, value} -> {key, value} end) |> Fields.rename(event_spec) |> common_parse_event(log) end def common_parse_event( result, %{"blockNumber" => eth_height, "transactionHash" => root_chain_txhash, "logIndex" => log_index} = event ) do # NOTE: we're using `put_new` here, because `merge` would allow us to overwrite data fields in case of conflict result |> Map.put_new(:eth_height, Encoding.to_int(eth_height)) |> Map.put_new(:root_chain_txhash, Encoding.to_binary(root_chain_txhash)) |> Map.put_new(:log_index, Encoding.to_int(log_index)) # just copy `event_signature` over, if it's present (could use tidying up) |> Map.put_new(:event_signature, event[:event_signature]) end defp decode_function_call_result(function_spec, [values]) when is_tuple(values) do function_spec.input_names |> Enum.zip(Tuple.to_list(values)) |> Enum.into(%{}) |> Fields.rename(function_spec) end # workaround for https://github.com/omgnetwork/elixir-omg/issues/1632 defp decode_function_call_result(%{function: "startExit"} = function_spec, values) do function_spec.input_names |> Enum.zip(values) |> Enum.into(%{}) |> Fields.rename(function_spec) end defp decode_function_call_result(function_spec, values) do function_spec.input_names |> Enum.zip(values) |> Enum.into(%{}) end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/deposit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Deposit do @moduledoc """ Utility functions for deposits on a child chain """ require Logger alias ExPlasma.Encoding alias ExPlasma.Transaction.Deposit alias ExPlasma.Utxo alias LoadTest.Ethereum alias LoadTest.Ethereum.Account alias LoadTest.Service.Sync @eth <<0::160>> @doc """ Deposits funds into the childchain. If currency is ETH, funds will be deposited into the EthVault. If currency is ERC20, 'approve()' will be called before depositing funds into the Erc20Vault. This function accepts three required parameters: 1. depositor account 2. the amount to be deposited 3. currency 4. the number of verifications 5. gas price of the transaction 6. return - it can be :utxo or :txhash Returns the utxo created by the deposit or the hash of the the deposit transaction. """ @spec deposit_from(Account.t(), pos_integer(), Account.t(), non_neg_integer(), non_neg_integer, atom()) :: Utxo.t() | binary() def deposit_from(depositor, amount, currency, deposit_finality_margin, gas_price, return) do deposit_utxo = %Utxo{amount: amount, owner: depositor.addr, currency: currency} {:ok, deposit} = Deposit.new(deposit_utxo) {:ok, {deposit_blknum, eth_blknum, eth_txhash}} = send_deposit(deposit, depositor, amount, currency, gas_price) :ok = wait_deposit_finality(eth_blknum, deposit_finality_margin) case return do :utxo -> Utxo.new(%{blknum: deposit_blknum, txindex: 0, oindex: 0, amount: amount}) _ -> eth_txhash end end defp send_deposit(deposit, account, value, @eth, gas_price) do vault_address = Application.fetch_env!(:load_test, :eth_vault_address) do_deposit(vault_address, deposit, account, value, gas_price) end defp send_deposit(deposit, account, value, erc20_contract, gas_price) do vault_address = Application.fetch_env!(:load_test, :erc20_vault_address) # First have to approve the token {:ok, tx_hash} = approve(erc20_contract, vault_address, account, value, gas_price) {:ok, _} = Ethereum.transact_sync(tx_hash) # Note that when depositing erc20 tokens, then tx value must be 0 do_deposit(vault_address, deposit, account, 0, gas_price) end defp do_deposit(vault_address, deposit, account, value, gas_price) do %{data: deposit_data} = LoadTest.Utils.Encoding.encode_deposit(deposit) tx = %LoadTest.Ethereum.Transaction{ to: Encoding.to_binary(vault_address), value: value, gas_price: gas_price, gas_limit: 200_000, data: Encoding.to_binary(deposit_data) } {:ok, tx_hash} = Ethereum.send_raw_transaction(tx, account) {:ok, %{"blockNumber" => eth_blknum}} = Ethereum.transact_sync(tx_hash) {:ok, %{"logs" => logs}} = Ethereumex.HttpClient.eth_get_transaction_receipt(tx_hash) %{"topics" => [_topic, _addr, blknum | _]} = Enum.find(logs, fn %{"address" => address} -> address == vault_address end) {:ok, {Encoding.to_int(blknum), eth_blknum, tx_hash}} end defp wait_deposit_finality(deposit_eth_blknum, finality_margin) do func = fn -> {:ok, current_blknum} = Ethereumex.HttpClient.eth_block_number() current_blknum = Encoding.to_int(current_blknum) if current_blknum >= deposit_eth_blknum + finality_margin do :ok end end Sync.repeat_until_success(func, :infinity, "Waiting for deposit finality") end defp approve(contract, vault_address, account, value, gas_price) do data = ABI.encode("approve(address,uint256)", [Encoding.to_binary(vault_address), value]) tx = %LoadTest.Ethereum.Transaction{ to: contract, gas_price: gas_price, gas_limit: 200_000, data: data } Ethereum.send_raw_transaction(tx, account) end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/exit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Exit do @moduledoc """ Utility functions for exits on a child chain. """ require Logger alias ExPlasma.Encoding alias ExPlasma.Utxo alias LoadTest.ChildChain.Transaction alias LoadTest.Ethereum alias LoadTest.Ethereum.Account alias LoadTest.Ethereum.Crypto alias LoadTest.Service.Sync @gas_start_exit 500_000 @gas_challenge_exit 300_000 @gas_add_exit_queue 800_000 @standard_exit_bond 14_000_000_000_000_000 @doc """ Returns the exit data of a utxo. """ @spec get_exit_data(Utxo.t()) :: any() def get_exit_data(%Utxo{} = utxo), do: get_exit_data(Utxo.pos(utxo)) @spec get_exit_data(non_neg_integer()) :: any() def get_exit_data(utxo_pos) do body = %WatcherSecurityCriticalAPI.Model.UtxoPositionBodySchema{ utxo_pos: utxo_pos } {:ok, response} = WatcherSecurityCriticalAPI.Api.UTXO.utxo_get_exit_data( LoadTest.Connection.WatcherSecurity.client(), body ) data = Jason.decode!(response.body)["data"] %{ proof: data["proof"], txbytes: data["txbytes"], utxo_pos: data["utxo_pos"] } end @doc """ Retries until the exit data of a utxo is found. """ @spec wait_for_exit_data(Utxo.t(), pos_integer()) :: any() def wait_for_exit_data(utxo_pos, timeout \\ 100_000) do func = fn -> data = get_exit_data(utxo_pos) if not is_nil(data.proof) do {:ok, data} end end {:ok, result} = Sync.repeat_until_success(func, timeout, "waiting for exit data") result end @doc """ Starts an exit. """ @spec start_exit(any(), Account.t(), pos_integer()) :: any() def start_exit(exit_data, from, gas_price) do data = ABI.encode( "startStandardExit((uint256,bytes,bytes))", [ { exit_data.utxo_pos, Encoding.to_binary(exit_data.txbytes), Encoding.to_binary(exit_data.proof) } ] ) tx = %Ethereum.Transaction{ to: contract_address_payment_exit_game(), value: @standard_exit_bond, gas_price: gas_price, gas_limit: @gas_start_exit, data: data } {:ok, tx_hash} = Ethereum.send_raw_transaction(tx, from) tx_hash end def add_exit_queue(vault_id, token, from, gas_price) do if has_exit_queue?(vault_id, token) do _ = Logger.info("Exit queue was already added.") else _ = Logger.info("Exit queue missing. Adding...") data = ABI.encode( "addExitQueue(uint256,address)", [vault_id, token] ) tx = %Ethereum.Transaction{ to: Encoding.to_binary(Application.fetch_env!(:load_test, :contract_address_plasma_framework)), gas_price: gas_price, gas_limit: @gas_add_exit_queue, data: data } {:ok, receipt_hash} = Ethereum.send_raw_transaction(tx, from) Ethereum.transact_sync(receipt_hash) wait_for_exit_queue(vault_id, token) receipt_hash end end defp wait_for_exit_queue(vault_id, token, timeout \\ 100_000) do func = fn -> if has_exit_queue?(vault_id, token) do :ok end end Sync.repeat_until_success(func, timeout, "waiting for exit queue") end defp has_exit_queue?(vault_id, token) do data = ABI.encode( "hasExitQueue(uint256,address)", [vault_id, token] ) {:ok, receipt_enc} = Ethereumex.HttpClient.eth_call(%{ from: Application.fetch_env!(:load_test, :contract_address_plasma_framework), to: Application.fetch_env!(:load_test, :contract_address_plasma_framework), data: Encoding.to_hex(data) }) receipt_enc |> Encoding.to_binary() |> ABI.TypeDecoder.decode([:bool]) |> hd() end def challenge_exit(exit_id, exiting_tx, challenge_tx, input_index, challenge_tx_sig, from) do opts = Keyword.put(tx_defaults(), :gas, @gas_challenge_exit) sender_data = Crypto.hash(from) contract = contract_address_payment_exit_game() signature = "challengeStandardExit((uint160,bytes,bytes,uint16,bytes,bytes32))" args = [{exit_id, exiting_tx, challenge_tx, input_index, challenge_tx_sig, sender_data}] {:ok, transaction_hash} = Ethereum.contract_transact(from, contract, signature, args, opts) Encoding.to_hex(transaction_hash) end def tx_defaults() do Transaction.tx_defaults() end defp contract_address_payment_exit_game() do :load_test |> Application.fetch_env!(:contract_address_payment_exit_game) |> Encoding.to_binary() end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Transaction do @moduledoc """ Utility functions for sending transaction to child chain """ require Logger alias ChildChainAPI.Api alias ChildChainAPI.Model alias ExPlasma.Encoding alias ExPlasma.Transaction alias ExPlasma.Utxo alias LoadTest.Connection.ChildChain, as: Connection alias LoadTest.Service.Metrics alias LoadTest.Service.Sync # safe, reasonable amount, equal to the testnet block gas limit @lots_of_gas 5_712_388 @gas_price 1_000_000_000 @doc """ Spends a utxo. Creates, signs and submits a transaction using the utxo as the input, one output with the amount and receiver address and another output if there is any change. Returns the utxos created by the transaction. If a change utxo was created, it will be the first in the list. Note that input must cover fees, so the currency must be a fee paying currency. """ @spec spend_utxo( Utxo.t(), pos_integer(), pos_integer(), LoadTest.Ethereum.Account.t(), LoadTest.Ethereum.Account.t(), Utxo.address_binary(), pos_integer() ) :: list(Utxo.t()) def spend_utxo(utxo, amount, fee, signer, receiver, currency, retries \\ 120_000) def spend_utxo(utxo, amount, fee, signer, receiver, currency, timeout) when byte_size(currency) == 20 do change_amount = utxo.amount - amount - fee receiver_output = %Utxo{owner: receiver.addr, currency: currency, amount: amount} do_spend(utxo, receiver_output, change_amount, currency, signer, timeout) end def spend_utxo(utxo, amount, fee, signer, receiver, currency, timeout) do spend_utxo(utxo, amount, fee, signer, receiver, Encoding.to_binary(currency), timeout) end def tx_defaults() do Enum.map([value: 0, gasPrice: @gas_price, gas: @lots_of_gas], fn {k, v} -> {k, Encoding.to_hex(v)} end) end @doc """ Submits a transaction Creates a transaction from the given inputs and outputs, signs it and submits it to the childchain. Returns the utxos created by the transaction. """ @spec submit_tx( list(Utxo.output_map()), list(Utxo.input_map()), list(LoadTest.Ethereum.Account.t()), pos_integer() ) :: list(Utxo.t()) def submit_tx(inputs, outputs, signers, retries \\ 120_000) do {:ok, tx} = Transaction.Payment.new(%{inputs: inputs, outputs: outputs}) keys = signers |> Enum.map(&Map.get(&1, :priv)) |> Enum.map(&Encoding.to_hex/1) {:ok, blknum, txindex} = tx |> Transaction.sign(keys: keys) |> try_submit_tx(retries) outputs |> Enum.with_index() |> Enum.map(fn {output, i} -> %Utxo{blknum: blknum, txindex: txindex, oindex: i, amount: output.amount, currency: output.currency} end) end def recover(encoded_signed_tx) do {:ok, trx} = encoded_signed_tx |> Encoding.to_binary() |> ExRLP.decode() |> reconstruct() trx end defp reconstruct([raw_witnesses | typed_tx_rlp_decoded_chunks]) do with true <- is_list(raw_witnesses) || {:error, :malformed_witnesses}, true <- Enum.all?(raw_witnesses, &valid_witness?/1) || {:error, :malformed_witnesses}, {:ok, raw_tx} <- reconstruct_transaction(typed_tx_rlp_decoded_chunks) do {:ok, %{raw_tx: raw_tx, sigs: raw_witnesses}} end end defp do_spend(_input, _output, change_amount, _currency, _signer, _retries) when change_amount < 0 do :error_insufficient_funds end defp do_spend(input, output, 0, _currency, signer, retries) do submit_tx([input], [output], [signer], retries) end defp do_spend(input, output, change_amount, currency, signer, retries) do change_output = %Utxo{owner: signer.addr, currency: currency, amount: change_amount} submit_tx([input], [change_output, output], [signer], retries) end defp valid_witness?(witness) when is_binary(witness), do: byte_size(witness) == 65 defp valid_witness?(_), do: false defp reconstruct_transaction([raw_type, inputs_rlp, outputs_rlp, tx_data_rlp, metadata_rlp]) when is_binary(raw_type) do with {:ok, 1} <- parse_uint256(raw_type), {:ok, inputs} <- parse_inputs(inputs_rlp), {:ok, outputs} <- parse_outputs(outputs_rlp), {:ok, tx_data} <- parse_uint256(tx_data_rlp), 0 <- tx_data, {:ok, metadata} <- validate_metadata(metadata_rlp) do {:ok, %{tx_type: 1, inputs: inputs, outputs: outputs, metadata: metadata}} else _ -> {:error, :unrecognized_transaction_type} end end defp reconstruct_transaction([tx_type, outputs_rlp, nonce_rlp]) do with {:ok, 3} <- parse_uint256(tx_type), {:ok, outputs} <- parse_outputs(outputs_rlp), {:ok, nonce} <- reconstruct_nonce(nonce_rlp) do {:ok, %{tx_type: 3, outputs: outputs, nonce: nonce}} end end defp reconstruct_nonce(nonce) when is_binary(nonce) and byte_size(nonce) == 32, do: {:ok, nonce} defp reconstruct_nonce(_), do: {:error, :malformed_nonce} defp validate_metadata(metadata) when is_binary(metadata) and byte_size(metadata) == 32, do: {:ok, metadata} defp validate_metadata(_), do: {:error, :malformed_metadata} defp parse_inputs(inputs_rlp) do with true <- Enum.count(inputs_rlp) <= 4 || {:error, :too_many_inputs}, # NOTE: workaround for https://github.com/omgnetwork/ex_plasma/issues/19. # remove, when this is blocked on `ex_plasma` end true <- Enum.all?(inputs_rlp, &(&1 != <<0::256>>)) || {:error, :malformed_inputs}, do: {:ok, Enum.map(inputs_rlp, &parse_input!/1)} rescue _ -> {:error, :malformed_inputs} end defp parse_input!(encoded) do {:ok, result} = decode_position(encoded) result end defp decode_position(encoded) when is_number(encoded) and encoded <= 0, do: {:error, :encoded_utxo_position_too_low} defp decode_position(encoded) when is_integer(encoded) and encoded > 0, do: do_decode_position(encoded) defp decode_position(encoded) when is_binary(encoded) and byte_size(encoded) == 32, do: do_decode_position(encoded) defp do_decode_position(encoded) do ExPlasma.Utxo.new(encoded) end defp parse_outputs(outputs_rlp) do outputs = Enum.map(outputs_rlp, &parse_output!/1) with true <- Enum.count(outputs) <= 4 || {:error, :too_many_outputs}, nil <- Enum.find(outputs, &match?({:error, _}, &1)), do: {:ok, outputs} rescue _ -> {:error, :malformed_outputs} end defp parse_output!(rlp_data) do {:ok, result} = ExPlasma.Utxo.new(rlp_data) result end defp parse_uint256(<<0>> <> _binary), do: {:error, :leading_zeros_in_encoded_uint} defp parse_uint256(binary) when byte_size(binary) <= 32, do: {:ok, :binary.decode_unsigned(binary, :big)} defp parse_uint256(binary) when byte_size(binary) > 32, do: {:error, :encoded_uint_too_big} defp parse_uint256(_), do: {:error, :malformed_uint256} defp try_submit_tx(tx, timeout) do {:ok, {blknum, txindex}} = Sync.repeat_until_success(fn -> do_submit_tx(tx) end, timeout, "Failed to submit transaction") {:ok, blknum, txindex} end defp do_submit_tx(tx) do Metrics.run_with_metrics( fn -> submit_request(tx) end, "Childchain.submit" ) end defp submit_request(tx) do {:ok, response} = tx |> Transaction.encode() |> do_submit_tx_rpc() response |> Map.fetch!(:body) |> Jason.decode!() |> Map.fetch!("data") |> case do %{"blknum" => blknum, "txindex" => txindex} -> _ = Logger.debug("[Transaction submitted successfully {#{inspect(blknum)}, #{inspect(txindex)}}") {:ok, {blknum, txindex}} %{"code" => reason} -> _ = Logger.warn("Transaction submission has failed, reason: #{inspect(reason)}, tx inputs: #{inspect(tx.inputs)}") {:error, reason} end end @spec do_submit_tx_rpc(binary) :: {:ok, map} | {:error, any} defp do_submit_tx_rpc(encoded_tx) do body = %Model.TransactionSubmitBodySchema{ transaction: Encoding.to_hex(encoded_tx) } Api.Transaction.submit(Connection.client(), body) end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/utxos.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.Utxos do @moduledoc """ Utility functions for utxos """ require Logger alias ExPlasma.Encoding alias ExPlasma.Utxo alias LoadTest.ChildChain.Transaction alias LoadTest.Service.Sync @doc """ Returns an addresses utxos. """ @spec get_utxos(Utxo.address_binary()) :: list(Utxo.t()) def get_utxos(address) do body = %WatcherInfoAPI.Model.AddressBodySchema1{ address: Encoding.to_hex(address) } {:ok, response} = WatcherInfoAPI.Api.Account.account_get_utxos( LoadTest.Connection.WatcherInfo.client(), body ) utxos = Jason.decode!(response.body)["data"] Enum.map( utxos, fn x -> %Utxo{ blknum: x["blknum"], txindex: x["txindex"], oindex: x["oindex"], currency: x["currency"], amount: x["amount"] } end ) end @doc """ Returns the highest value utxo of a given currency """ @spec get_largest_utxo(list(Utxo.t()), Utxo.address_binary()) :: Utxo.t() def get_largest_utxo([], _currency), do: nil def get_largest_utxo(utxos, currency) do utxos |> Enum.filter(fn utxo -> currency == LoadTest.Utils.Encoding.from_hex(utxo.currency) end) |> Enum.max_by(fn x -> x.amount end, fn -> nil end) end @doc """ Merges all the given utxos into one. Note that this can take several iterations to complete. """ @spec merge(list(Utxo.t()), Utxo.address_binary(), Account.t()) :: Utxo.t() def merge(utxos, currency, faucet_account) do utxos |> Enum.filter(fn utxo -> LoadTest.Utils.Encoding.from_hex(utxo.currency) == currency end) |> merge(faucet_account) end @spec merge(list(Utxo.t()), Account.t()) :: Utxo.t() defp merge([], _faucet_account), do: :error_empty_utxo_list defp merge([single_utxo], _faucet_account), do: single_utxo defp merge(utxos, faucet_account) when length(utxos) > 4 do utxos |> Enum.chunk_every(4) |> Enum.map(fn inputs -> merge(inputs, faucet_account) end) |> merge(faucet_account) end defp merge([%{currency: currency} | _] = inputs, faucet_account) do tx_amount = Enum.reduce(inputs, 0, fn x, acc -> x.amount + acc end) output = %Utxo{amount: tx_amount, currency: currency, owner: faucet_account.addr} [utxo] = Transaction.submit_tx( inputs, [output], List.duplicate(faucet_account, length(inputs)) ) utxo end @doc """ Retries until the utxo is found. """ @spec wait_for_utxo(Utxo.address_binary(), Utxo.t(), pos_integer()) :: :ok def wait_for_utxo(address, utxo, timeout \\ 100_000) do func = fn -> find_utxo(address, utxo) end Sync.repeat_until_success(func, timeout, "waiting for utxo") end defp find_utxo(address, utxo) do utxos = get_utxos(address) if Enum.find(utxos, fn x -> Utxo.pos(x) == Utxo.pos(utxo) end) do :ok end end end ================================================ FILE: priv/perf/apps/load_test/lib/child_chain/watcher_sync.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.ChildChain.WatcherSync do @moduledoc """ Wait for the watcher to sync to a certain root chain block height """ require Logger alias LoadTest.Service.Sync @doc """ Blocks the caller until the watcher configured reports to be fully synced up (both child chain blocks and eth events) Options: - :root_chain_height - if not `nil`, in addition to synchronizing to current top mined child chain block, it will sync up till all the Watcher's services report at at least this Ethereum height """ @spec watcher_synchronize(keyword()) :: :ok def watcher_synchronize(opts \\ []) do root_chain_height = Keyword.get(opts, :root_chain_height, nil) service = Keyword.get(opts, :service, nil) _ = Logger.info("Waiting for the watcher to synchronize") :ok = Sync.repeat_until_success( fn -> watcher_synchronized?(root_chain_height, service) end, 500_000, "Failed to sync watcher" ) # NOTE: allowing some more time for the dust to settle on the synced Watcher # otherwise some of the freshest UTXOs to exit will appear as missing on the Watcher # related issue to remove this `sleep` and fix properly is https://github.com/omgnetwork/elixir-omg/issues/1031 Process.sleep(2000) _ = Logger.info("Watcher synchronized") end # This function is prepared to be called in `Sync`. # It repeatedly ask for Watcher's `/status.get` until Watcher consume mined block defp watcher_synchronized?(root_chain_height, service) do {:ok, status_response} = WatcherSecurityCriticalAPI.Api.Status.status_get(LoadTest.Connection.WatcherSecurity.client()) status = Jason.decode!(status_response.body)["data"] with true <- watcher_synchronized_to_mined_block?(status), true <- root_chain_synced?(root_chain_height, status, service) do :ok else _ -> :repeat end end defp root_chain_synced?(nil, _, _), do: true defp root_chain_synced?(root_chain_height, status, nil) do status |> Map.get("services_synced_heights") |> Enum.reject(fn height -> service = height["service"] # these service heights are stuck on circle ci, but they work fine locally # I think ci machine is not powerful enough service == "block_getter" || service == "exit_finalizer" || service == "ife_exit_finalizer" end) |> Enum.all?(&(&1["height"] >= root_chain_height)) end defp root_chain_synced?(root_chain_height, status, service) do heights = Map.get(status, "services_synced_heights") found_root_chain_height = Enum.find(heights, fn height -> height["service"] == service end) found_root_chain_height && found_root_chain_height["height"] >= root_chain_height end defp watcher_synchronized_to_mined_block?(%{ "last_mined_child_block_number" => last_mined_child_block_number, "last_validated_child_block_number" => last_validated_child_block_number }) when last_mined_child_block_number == last_validated_child_block_number and last_mined_child_block_number > 0 do _ = Logger.debug("Synced to blknum: #{last_validated_child_block_number}") true end defp watcher_synchronized_to_mined_block?(_params) do :not_synchronized end end ================================================ FILE: priv/perf/apps/load_test/lib/connection/child_chain.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Connection.ChildChain do @moduledoc """ Module that overrides the Tesla middleware with the url from config. """ alias LoadTest.Connection.ConnectionDefaults def client() do base_url = Application.fetch_env!(:load_test, :child_chain_url) middleware = [{Tesla.Middleware.BaseUrl, base_url} | ConnectionDefaults.middleware()] Tesla.client(middleware) end end ================================================ FILE: priv/perf/apps/load_test/lib/connection/connection_defaults.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Connection.ConnectionDefaults do @moduledoc """ Utility functions shared between all connection modules """ @doc """ Returns Tesla middleware common for all connections. """ def middleware() do [ {Tesla.Middleware.EncodeJson, engine: Poison}, {Tesla.Middleware.Headers, [{"user-agent", "Load-Test"}, {"Content-Type", "application/json"}]}, {Tesla.Middleware.Retry, delay: 500, max_retries: 10, max_delay: 45_000, should_retry: retry?()}, {Tesla.Middleware.Timeout, timeout: 30_000}, {Tesla.Middleware.Opts, [adapter: [recv_timeout: 30_000, pool: pool_name()]]} ] end @doc """ Returns connection pool name """ def pool_name(), do: :perf_pool # Don't automatically retry on error # It _can_ sometimes be useful to retry though, so if you need it return true here # See README.md for more info defp retry?() do fn _ -> false end end end ================================================ FILE: priv/perf/apps/load_test/lib/connection/watcher_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Connection.WatcherInfo do @moduledoc """ Module that overrides the Tesla middleware with the url from config. """ alias LoadTest.Connection.ConnectionDefaults def client() do base_url = Application.fetch_env!(:load_test, :watcher_info_url) middleware = [{Tesla.Middleware.BaseUrl, base_url} | ConnectionDefaults.middleware()] Tesla.client(middleware) end end ================================================ FILE: priv/perf/apps/load_test/lib/connection/watcher_security.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Connection.WatcherSecurity do @moduledoc """ Module that overrides the Tesla middleware with the url from config. """ alias LoadTest.Connection.ConnectionDefaults def client() do base_url = Application.fetch_env!(:load_test, :watcher_security_url) middleware = [{Tesla.Middleware.BaseUrl, base_url} | ConnectionDefaults.middleware()] Tesla.client(middleware) end end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/account.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Ethereum.Account do @moduledoc """ Functions for creating Ethereum accounts """ require Logger alias ExPlasma.Encoding alias LoadTest.Ethereum.Crypto @type private_key_t :: <<_::256>> @type private_key_hex_t :: <<_::512>> | <<_::528>> @type public_key_t :: <<_::512>> @type addr_t :: <<_::160>> @type t :: %__MODULE__{ priv: private_key_t(), pub: public_key_t(), addr: addr_t() } defstruct [:priv, :pub, :addr] @spec new(private_key_t()) :: {:ok, t()} | {:error, atom()} def new(private_key) when byte_size(private_key) == 32 do {:ok, der_public_key} = compute_public_key(private_key) public_key = der_to_raw(der_public_key) {:ok, address} = compute_address(public_key) {:ok, struct!(__MODULE__, priv: private_key, pub: public_key, addr: address)} end @spec new(private_key_hex_t()) :: {:ok, t()} | {:error, atom()} def new(private_key_hex) do private_key_hex |> Encoding.to_binary() |> new() end @spec new() :: {:ok, t()} | {:error, atom()} def new() do {:ok, priv} = Crypto.generate_private_key() new(priv) end defp compute_public_key(private_key) do ExSecp256k1.create_public_key(private_key) end defp compute_address(<>) do <<_::binary-size(12), address::binary-size(20)>> = Crypto.hash(pub) {:ok, address} end defp der_to_raw(<<4::integer-size(8), data::binary>>), do: data end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/bit_helper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Ethereum.BitHelper do @moduledoc """ Helpers for common operations on the blockchain. Extracted from: https://github.com/exthereum/blockchain """ use Bitwise @type keccak_hash :: binary() @doc """ Returns the keccak sha256 of a given input. """ @spec kec(binary()) :: keccak_hash def kec(data) do elem(ExKeccak.hash_256(data), 1) end @doc """ Similar to `:binary.encode_unsigned/1`, except we encode `0` as `<<>>`, the empty string. This is because the specification says that we cannot have any leading zeros, and so having <<0>> by itself is leading with a zero and prohibited. """ @spec encode_unsigned(non_neg_integer()) :: binary() def encode_unsigned(0), do: <<>> def encode_unsigned(n), do: :binary.encode_unsigned(n) end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/crypto.ex ================================================ defmodule LoadTest.Ethereum.Crypto do @moduledoc """ Cryptography related utility functions """ @type hash_t() :: <<_::256>> @type priv_key_t :: <<_::256>> @doc """ Produces a KECCAK digest for the message. see https://hexdocs.pm/exth_crypto/ExthCrypto.Hash.html#kec/0 """ @spec hash(binary) :: hash_t() def hash(message), do: elem(ExKeccak.hash_256(message), 1) @doc """ Generates private key. Internally uses OpenSSL RAND_bytes. May throw if there is not enough entropy. """ @spec generate_private_key() :: {:ok, priv_key_t()} def generate_private_key, do: {:ok, :crypto.strong_rand_bytes(32)} end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/ethereum.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Ethereum do @moduledoc """ Support for synchronous transactions. """ require Logger alias ExPlasma.Encoding alias LoadTest.ChildChain.Abi alias LoadTest.Ethereum.Account alias LoadTest.Ethereum.NonceTracker alias LoadTest.Ethereum.Transaction alias LoadTest.Ethereum.Transaction.Signature alias LoadTest.Service.Sync @about_4_blocks_time 120_000 @poll_timeout 60_000 @type hash_t() :: <<_::256>> @doc """ Send transaction to be singed by a key managed by Ethereum node, geth or parity. For geth, account must be unlocked externally. If using parity, account passphrase must be provided directly or via config. """ @spec contract_transact(<<_::160>>, <<_::160>>, binary, [any]) :: {:ok, <<_::256>>} | {:error, any} def contract_transact(from, to, signature, args, opts \\ []) do data = encode_tx_data(signature, args) txmap = %{from: Encoding.to_hex(from), to: Encoding.to_hex(to), data: data} |> Map.merge(Map.new(opts)) |> encode_all_integer_opts() case Ethereumex.HttpClient.eth_send_transaction(txmap) do {:ok, receipt_enc} -> {:ok, Encoding.to_binary(receipt_enc)} other -> other end end @spec get_gas_used(String.t()) :: non_neg_integer() def get_gas_used(receipt_hash) do {{:ok, %{"gasUsed" => gas_used}}, {:ok, %{"gasPrice" => gas_price}}} = {Ethereumex.HttpClient.eth_get_transaction_receipt(receipt_hash), Ethereumex.HttpClient.eth_get_transaction_by_hash(receipt_hash)} {gas_price_value, ""} = gas_price |> String.replace_prefix("0x", "") |> Integer.parse(16) {gas_used_value, ""} = gas_used |> String.replace_prefix("0x", "") |> Integer.parse(16) gas_price_value * gas_used_value end @doc """ Waits until transaction is mined Returns transaction receipt updated with Ethereum block number in which the transaction was mined """ @spec transact_sync(hash_t(), pos_integer()) :: {:ok, map()} def transact_sync(txhash, timeout \\ @about_4_blocks_time) do {:ok, %{"status" => "0x1"} = receipt} = eth_receipt(txhash, timeout) {:ok, Map.update!(receipt, "blockNumber", &Encoding.to_int(&1))} end def block_hash(mined_num) do contract_address = Application.fetch_env!(:load_test, :contract_address_plasma_framework) %{"block_hash" => block_hash, "block_timestamp" => block_timestamp} = get_external_data(contract_address, "blocks(uint256)", [mined_num]) {block_hash, block_timestamp} end def send_raw_transaction(txmap, sender) do nonce = NonceTracker.get_next_nonce(sender.addr) txmap |> Map.merge(%{nonce: nonce}) |> Signature.sign_transaction(sender.priv) |> Transaction.serialize() |> ExRLP.encode() |> Encoding.to_hex() |> Ethereumex.HttpClient.eth_send_raw_transaction() end def get_next_nonce_for_account(address) when byte_size(address) == 20 do address |> ExPlasma.Encoding.to_hex() |> get_next_nonce_for_account() end def get_next_nonce_for_account("0x" <> _ = address) do {:ok, nonce} = Ethereumex.HttpClient.eth_get_transaction_count(address) Encoding.to_int(nonce) end def wait_for_root_chain_block(awaited_eth_height, timeout \\ 600_000) do f = fn -> {:ok, eth_height} = case Ethereumex.HttpClient.eth_block_number() do {:ok, height_hex} -> {:ok, Encoding.to_int(height_hex)} other -> other end if eth_height < awaited_eth_height, do: :repeat, else: {:ok, eth_height} end Sync.repeat_until_success(f, timeout, "Failed to fetch eth block number") end @spec fetch_balance(Account.addr_t(), Account.addr_t()) :: non_neg_integer() | no_return() def fetch_balance(address, <<0::160>>) do {:ok, initial_balance} = Sync.repeat_until_success( fn -> address |> Encoding.to_hex() |> eth_account_get_balance() end, @poll_timeout, "Failed to fetch eth balance from rootchain" ) {initial_balance, ""} = initial_balance |> String.replace_prefix("0x", "") |> Integer.parse(16) initial_balance end def fetch_balance(address, currency) do Sync.repeat_until_success( fn -> do_root_chain_get_erc20_balance(address, currency) end, @poll_timeout, "Failed to fetch erc20 balance from rootchain" ) end defp eth_account_get_balance(address) do Ethereumex.HttpClient.eth_get_balance(address) end defp do_root_chain_get_erc20_balance(address, currency) do data = ABI.encode("balanceOf(address)", [Encoding.to_binary(address)]) case Ethereumex.HttpClient.eth_call(%{ from: Encoding.to_hex(currency), to: Encoding.to_hex(currency), data: Encoding.to_hex(data) }) do {:ok, result} -> balance = result |> Encoding.to_binary() |> ABI.TypeDecoder.decode([{:uint, 256}]) |> hd() {:ok, balance} error -> error end end defp get_external_data(address, signature, params) do data = signature |> ABI.encode(params) |> Encoding.to_hex() {:ok, data} = Ethereumex.HttpClient.eth_call(%{from: address, to: address, data: data}) Abi.decode_function(data, signature) end defp eth_receipt(txhash, timeout) do f = fn -> txhash |> Ethereumex.HttpClient.eth_get_transaction_receipt() |> case do {:ok, receipt} when receipt != nil -> {:ok, receipt} _ -> :repeat end end Sync.repeat_until_success(f, timeout, "Failed to fetch eth receipt") end defp encode_tx_data(signature, args) do signature |> ABI.encode(args) |> Encoding.to_hex() end defp encode_all_integer_opts(opts) do opts |> Enum.filter(fn {_k, v} -> is_integer(v) end) |> Enum.into(opts, fn {k, v} -> {k, Encoding.to_hex(v)} end) end end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/hash.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Ethereum.Hash do @moduledoc """ Defines helper functions for signing and getting the signature of a transaction, as defined in Appendix F of the Yellow Paper. For any of the following functions, if chain_id is specified, it's assumed that we're post-fork and we should follow the specification EIP-155 from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md Extracted from: https://github.com/exthereum/blockchain """ alias LoadTest.Ethereum.BitHelper alias LoadTest.Ethereum.Transaction @base_recovery_id 27 @base_recovery_id_eip_155 35 @type private_key :: <<_::256>> @type hash_v :: integer() @type hash_r :: integer() @type hash_s :: integer() @doc """ Returns a hash of a given transaction according to the formula defined in Eq.(214) and Eq.(215) of the Yellow Paper. Note: As per EIP-155 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md), we will append the chain-id and nil elements to the serialized transaction. ## Examples iex> LoadTest.Ethereum.Hash.transaction_hash(%LoadTest.Ethereum.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}) <<127, 113, 209, 76, 19, 196, 2, 206, 19, 198, 240, 99, 184, 62, 8, 95, 9, 122, 135, 142, 51, 22, 61, 97, 70, 206, 206, 39, 121, 54, 83, 27>> iex> LoadTest.Ethereum.Hash.transaction_hash(%LoadTest.Ethereum.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1>>, value: 5, data: <<1>>}) <<225, 195, 128, 181, 3, 211, 32, 231, 34, 10, 166, 198, 153, 71, 210, 118, 51, 117, 22, 242, 87, 212, 229, 37, 71, 226, 150, 160, 50, 203, 127, 180>> iex> LoadTest.Ethereum.Hash.transaction_hash(%LoadTest.Ethereum.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1>>, value: 5, data: <<1>>}, 1) <<132, 79, 28, 4, 212, 58, 235, 38, 66, 211, 167, 102, 36, 58, 229, 88, 238, 251, 153, 23, 121, 163, 212, 64, 83, 111, 200, 206, 54, 43, 112, 53>> """ @spec transaction_hash(Transaction.t(), integer() | nil) :: BitHelper.keccak_hash() def transaction_hash(trx, chain_id \\ nil) do trx |> Transaction.serialize(false) # See EIP-155 |> Kernel.++(if chain_id, do: [:binary.encode_unsigned(chain_id), <<>>, <<>>], else: []) |> ExRLP.encode() |> BitHelper.kec() end @doc """ Returns a ECDSA signature (v,r,s) for a given hashed value. This implementes Eq.(207) of the Yellow Paper. ## Examples iex> LoadTest.Ethereum.Hash.sign_hash(<<2::256>>, <<1::256>>) {28, 38938543279057362855969661240129897219713373336787331739561340553100525404231, 23772455091703794797226342343520955590158385983376086035257995824653222457926} iex> LoadTest.Ethereum.Hash.sign_hash(<<5::256>>, <<1::256>>) {27, 74927840775756275467012999236208995857356645681540064312847180029125478834483, 56037731387691402801139111075060162264934372456622294904359821823785637523849} iex> data = Base.decode16!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080", case: :lower) iex> hash = LoadTest.Ethereum.BitHelper.kec(data) iex> private_key = Base.decode16!("4646464646464646464646464646464646464646464646464646464646464646", case: :lower) iex> LoadTest.Ethereum.Hash.sign_hash(hash, private_key, 1) { 37, 18515461264373351373200002665853028612451056578545711640558177340181847433846, 46948507304638947509940763649030358759909902576025900602547168820602576006531 } """ @spec sign_hash(BitHelper.keccak_hash(), private_key, integer() | nil) :: {hash_v, hash_r, hash_s} def sign_hash(hash, private_key, chain_id \\ nil) do {:ok, {<>, recovery_id}} = ExSecp256k1.sign_compact(hash, private_key) # Fork Ψ EIP-155 recovery_id = if chain_id do chain_id * 2 + @base_recovery_id_eip_155 + recovery_id else @base_recovery_id + recovery_id end {recovery_id, r, s} end @doc """ Packs a {v,r,s} signature as 65-bytes binary. """ @spec pack_signature({hash_v, hash_r, hash_s}) :: binary def pack_signature({v, r, s}) do <> end end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/nonce_tracker.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Ethereum.NonceTracker do @moduledoc """ Nonce tracker for sending ethereum transactions """ alias ExPlasma.Encoding def init() do :ets.new(:nonce_tracker, [:set, :public, :named_table]) end def get_next_nonce(address) do if Enum.empty?(:ets.lookup(:nonce_tracker, address)) do current_nonce = address |> Encoding.to_hex() |> Ethereumex.HttpClient.eth_get_transaction_count("pending") |> elem(1) |> Encoding.to_int() # it might happen that this is called more than once, but # we rely on :ets.update_counter being atomic, so starting value is not changed :ets.update_counter(:nonce_tracker, address, 1, {0, current_nonce - 1}) else :ets.update_counter(:nonce_tracker, address, 1) end end end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/transaction/signature.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Ethereum.Transaction.Signature do @moduledoc """ Defines helper functions for signing and getting the signature of a transaction, as defined in Appendix F of the Yellow Paper. For any of the following functions, if chain_id is specified, it's assumed that we're post-fork and we should follow the specification EIP-155 from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md Extracted from: https://github.com/exthereum/blockchain """ require Integer alias LoadTest.Ethereum.Hash alias LoadTest.Ethereum.Transaction @type private_key :: <<_::256>> @doc """ Takes a given transaction and returns a version signed with the given private key. This is defined in Eq.(216) and Eq.(217) of the Yellow Paper. """ @spec sign_transaction(Transaction.t(), private_key, integer() | nil) :: Transaction.t() def sign_transaction(trx, private_key, chain_id \\ nil) do {v, r, s} = trx |> Hash.transaction_hash(chain_id) |> Hash.sign_hash(private_key, chain_id) %{trx | v: v, r: r, s: s} end end ================================================ FILE: priv/perf/apps/load_test/lib/ethereum/transaction/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Ethereum.Transaction do alias LoadTest.Ethereum.BitHelper @moduledoc """ This module encodes the transaction object, defined in Section 4.3 of the Yellow Paper (http://gavwood.com/Paper.pdf). We are focused on implementing 𝛶, as defined in Eq.(1). Extracted from: https://github.com/exthereum/blockchain """ defstruct nonce: 0, # Tn # Tp gas_price: 0, # Tg gas_limit: 0, # Tt to: <<>>, # Tv value: 0, # Tw v: nil, # Tr r: nil, # Ts s: nil, # Ti init: <<>>, # Td data: <<>> @type t :: %__MODULE__{ nonce: integer(), gas_price: integer(), gas_limit: integer(), to: <<_::160>> | <<_::0>>, value: integer(), v: integer(), r: integer(), s: integer(), init: binary(), data: binary() } @doc """ Encodes a transaction such that it can be RLP-encoded. This is defined at L_T Eq.(14) in the Yellow Paper. ## Examples iex> LoadTest.Ethereum.Transaction.serialize(%LoadTest.Ethereum.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1::160>>, value: 8, v: 27, r: 9, s: 10, data: "hi"}) [<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>] iex> LoadTest.Ethereum.Transaction.serialize(%LoadTest.Ethereum.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 8, v: 27, r: 9, s: 10, init: <<1, 2, 3>>}) [<<5>>, <<6>>, <<7>>, <<>>, <<8>>, <<1, 2, 3>>, <<27>>, <<9>>, <<10>>] iex> LoadTest.Ethereum.Transaction.serialize(%LoadTest.Ethereum.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 8, v: 27, r: 9, s: 10, init: <<1, 2, 3>>}, false) [<<5>>, <<6>>, <<7>>, <<>>, <<8>>, <<1, 2, 3>>] iex> LoadTest.Ethereum.Transaction.serialize(%LoadTest.Ethereum.Transaction{ data: "", gas_limit: 21000, gas_price: 20000000000, init: "", nonce: 9, r: 0, s: 0, to: "55555555555555555555", v: 1, value: 1000000000000000000 }) ["\t", <<4, 168, 23, 200, 0>>, "R\b", "55555555555555555555", <<13, 224, 182, 179, 167, 100, 0, 0>>, "", <<1>>, "", ""] """ @spec serialize(t) :: ExRLP.t() def serialize(trx, include_vrs \\ true) do base = [ BitHelper.encode_unsigned(trx.nonce), BitHelper.encode_unsigned(trx.gas_price), BitHelper.encode_unsigned(trx.gas_limit), trx.to, BitHelper.encode_unsigned(trx.value), if(trx.to == <<>>, do: trx.init, else: trx.data) ] if include_vrs do base ++ [ BitHelper.encode_unsigned(trx.v), BitHelper.encode_unsigned(trx.r), BitHelper.encode_unsigned(trx.s) ] else base end end end ================================================ FILE: priv/perf/apps/load_test/lib/performance.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Performance do @moduledoc """ OMG network performance tests. Provides general setup and utilities to do the perf tests. """ defmacro __using__(_opt) do quote do alias LoadTest.Common.ByzantineEvents alias LoadTest.Common.ExtendedPerftest alias LoadTest.Common.Generators alias LoadTest.Performance import Performance, only: [timeit: 1] require Performance require Logger :ok end end @doc """ Utility macro which causes the expression given to be timed, the timing logged (`info`) and the original result of the call to be returned ## Examples iex> use LoadTest.Performance iex> timeit 1+2 3 """ defmacro timeit(call) do quote do {duration, result} = :timer.tc(fn -> unquote(call) end) duration_s = duration / 1_000_000 _ = Logger.info("Lasted #{inspect(duration_s)} seconds") result end end end ================================================ FILE: priv/perf/apps/load_test/lib/runner/childchain.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.ChildChainTransactions do @moduledoc """ Creates load on the child chain by submitting transactions as fast as possible. Run with `mix test apps/load_test/test/load_tests/runner/childchain_test.exs` In each session, the test creates a new address and funds it from the faucet. It then creates transactions from this address to another temporary address. Transactions are chained i.e. using the returned blocknum and tx_pos from `transaction.submit` we can calculate the next utxo to be spent without waiting for the block to finalize. This allows us to submit transactions as fast as possible, limited only by latency of `transaction.submit` Note that the latency of `transaction.submit` can be high enough to mean that one account sending transactions in this way is not enough to stress the childchain. It is necessary to run many concurrent sessions to provide meaningful load. """ use Chaperon.LoadTest alias LoadTest.Ethereum.Account @default_config %{ concurrent_sessions: 1, transactions_per_session: 1, transaction_delay: 0 } def default_config() do Application.get_env(:load_test, :childchain_transactions_test_config, @default_config) end def scenarios() do test_currency = Application.fetch_env!(:load_test, :test_currency) fee_amount = Application.fetch_env!(:load_test, :fee_amount) config = default_config() {:ok, sender} = Account.new() {:ok, receiver} = Account.new() amount = 1 ntx_to_send = config.transactions_per_session initial_funds = (amount + fee_amount) * ntx_to_send [ {{config.concurrent_sessions, [LoadTest.Scenario.FundAccount, LoadTest.Scenario.SpendEthUtxo]}, %{ account: sender, initial_funds: initial_funds, sender: sender, receiver: receiver, amount: amount, test_currency: test_currency }} ] end end ================================================ FILE: priv/perf/apps/load_test/lib/runner/deposits.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.Deposits do @moduledoc """ Deposits tests runner. """ use Chaperon.LoadTest def scenarios do [ {{1, LoadTest.Scenario.Deposits}, %{}} ] end end ================================================ FILE: priv/perf/apps/load_test/lib/runner/smoke.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.Smoke do @moduledoc """ Smoke test to verify that the childchain, watcher and watcher-info are up and running Run with `mix test apps/load_test/test/load_tests/runner/smoke_test.exs` """ use Chaperon.LoadTest def scenarios do [ {{1, LoadTest.Scenario.Smoke}, %{}} ] end end ================================================ FILE: priv/perf/apps/load_test/lib/runner/standard_exits.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.StandardExits do @moduledoc """ Runs the Standard Exit load test scenarios. Run with `mix test apps/load_test/test/load_tests/runner/standard_exit_test.exs` The ManyStandardExits scenarios first creates and funds a new address, then creates many utxos and then starts an exits on each one. It then waits for the Watcher to sync with the root chain. Finally, it calls Watcher status.get to measure the timing. """ use Chaperon.LoadTest alias ExPlasma.Encoding alias LoadTest.ChildChain.Exit alias LoadTest.Service.Faucet @default_config %{ concurrent_sessions: 1, exits_per_session: 1 } def default_config() do Application.get_env(:load_test, :standard_exit_test_config, @default_config) end def scenarios() do test_currency = Application.fetch_env!(:load_test, :test_currency) gas_price = Application.fetch_env!(:load_test, :gas_price) config = default_config() # Use the faucet account to add the token's exit queue if necessary {:ok, faucet} = Faucet.get_faucet() _ = Exit.add_exit_queue(1, Encoding.to_binary(test_currency), faucet, gas_price) [ {{config.concurrent_sessions, [LoadTest.Scenario.ManyStandardExits, LoadTest.Scenario.WatcherStatus]}, %{ gas_price: gas_price, test_currency: test_currency }} ] end end ================================================ FILE: priv/perf/apps/load_test/lib/runner/transactions.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.Transactions do @moduledoc """ Transactions tests runner. """ use Chaperon.LoadTest def scenarios do [ {{1, LoadTest.Scenario.Transactions}, %{}} ] end end ================================================ FILE: priv/perf/apps/load_test/lib/runner/utxos_load.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.UtxosLoad do @moduledoc """ Creates utxos and submits transactions to test how child chain performs when there are many utxos in its state. Run with `mix test apps/load_test/test/load_tests/runner/utxos_load_test.exs` This test first creates a number of utxos by funding a new address from the faucet and then successively splitting its utxos into 4 until the desired number of utxos is reached. It then creates a number of transactions from the address, measuring the time taken. """ use Chaperon.LoadTest alias LoadTest.Ethereum.Account @default_config %{ concurrent_sessions: 1, utxos_to_create_per_session: 30, transactions_per_session: 10 } def default_config() do utxo_load_test_config = Application.get_env(:load_test, :utxo_load_test_config, @default_config) %{ concurrent_sessions: utxo_load_test_config[:concurrent_sessions], utxos_to_create_per_session: utxo_load_test_config[:utxos_to_create_per_session], transactions_per_session: utxo_load_test_config[:transactions_per_session] } end def scenarios() do {:ok, sender} = Account.new() %{concurrent_sessions: concurrent_sessions} = default_config() [ {{concurrent_sessions, [LoadTest.Scenario.CreateUtxos, LoadTest.Scenario.SpendEthUtxo]}, %{ sender: sender, receiver: sender }} ] end end ================================================ FILE: priv/perf/apps/load_test/lib/runner/watcher_info.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.WatcherInfoAccountApi do @moduledoc """ Tests all the `account.*` apis on the watcher-info Run with `mix test apps/load_test/test/load_tests/runner/watcher_info_test.exs` This test first creates a new address and funds from the faucet. Next it calls the watcher-info apis: - `account.get_balance` - `account.get_utxos` - `account.get_transactions` It then creates a transaction from the address, measuring the time taken. """ use Chaperon.LoadTest @default_config %{ concurrent_sessions: 1, iterations: 1, merge_scenario_sessions: true } def default_config() do Application.get_env(:load_test, :watcher_info_test_config, @default_config) end def scenarios() do %{concurrent_sessions: concurrent_sessions} = default_config() [ {{concurrent_sessions, LoadTest.Scenario.AccountTransactions}, %{}} ] end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/account_transactions.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.AccountTransactions do @moduledoc """ This scenario tests watcher info apis when number of transaction increases for an account. """ use Chaperon.Scenario alias Chaperon.Timing alias LoadTest.Connection.WatcherInfo, as: Connection alias LoadTest.Ethereum.Account alias LoadTest.Ethereum.Hash alias LoadTest.Service.Faucet alias LoadTest.Utils.Encoding alias WatcherInfoAPI.Api alias WatcherInfoAPI.Model @poll_interval 15_000 @default_retry_attempts 15 @retry_delay 30 @eth <<0::160>> @test_output_amount 1 @spec init(Chaperon.Session.t()) :: Chaperon.Session.t() def init(session) do session |> log_info("start init with random delay...") |> random_delay(Timing.seconds(5)) end def run(session) do iterations = config(session, [:iterations]) fee_amount = Application.fetch_env!(:load_test, :fee_amount) amount = iterations * (@test_output_amount + fee_amount) {:ok, sender} = Account.new() {:ok, _} = Faucet.fund_child_chain_account(sender, amount, @eth) {:ok, faucet} = Faucet.get_faucet() session |> assign(faucet: faucet, iteration: 1) |> wait_for_balance_update(sender) |> log_info("user created: " <> Encoding.to_hex(sender.addr)) |> repeat(:repeat_task, [sender], iterations) |> log_info("end...") end def repeat_task(session, sender) do session |> log_info("running iteration #{session.assigned.iteration}") |> retry_on_error( :test_apis, [sender], retries: @default_retry_attempts, random_delay: seconds(@retry_delay) ) |> update_assign(iteration: &(&1 + 1)) end def test_apis(session, sender) do session |> measure_get_balance(sender) |> measure_get_utxos(sender) |> measure_get_transactions(sender) |> measure_create_and_submit_transactions(sender) end defp wait_for_balance_update(session, sender, retry \\ @default_retry_attempts) do {:ok, session} = do_wait_for_balance_update(session, sender, retry) session end defp measure(session, sender, api_call, metric_name) do start = Timing.timestamp() {:ok, _} = api_call.(sender) add_metric( session, {:call, {LoadTest.Scenario.AccountTransactions, metric_name}}, Timing.timestamp() - start ) end defp measure_get_balance(session, sender) do measure(session, sender, &get_balance/1, "/account.get_balance") end defp measure_get_utxos(session, sender) do measure(session, sender, &get_utxos/1, "/account.get_utxos") end defp measure_get_transactions(session, sender) do measure(session, sender, &get_transactions/1, "/account.get_transactions") end defp measure_create_and_submit_transactions(session, sender) do start = Timing.timestamp() {:ok, [inputs, sign_hash, typed_data, _txbytes]} = create_transaction(session, sender) session = add_metric( session, {:call, {LoadTest.Scenario.AccountTransactions, '/transaction.create'}}, Timing.timestamp() - start ) typed_data_signed = sign_tx(inputs, sign_hash, typed_data, sender) start = Timing.timestamp() {:ok, response} = Api.Transaction.submit_typed(Connection.client(), typed_data_signed) session = add_metric( session, {:call, {LoadTest.Scenario.AccountTransactions, '/transaction.submit_typed'}}, Timing.timestamp() - start ) %{ "txhash" => tx_id } = Jason.decode!(response.body)["data"] wait_until_tx_sync_to_watcher(session, tx_id) end defp create_transaction(session, sender) do {:ok, response} = Api.Transaction.create_transaction( Connection.client(), %Model.CreateTransactionsBodySchema{ owner: Encoding.to_hex(sender.addr), fee: %Model.TransactionCreateFee{ currency: Encoding.to_hex(@eth) }, payments: [ %Model.TransactionCreatePayments{ amount: @test_output_amount, currency: Encoding.to_hex(@eth), owner: Encoding.to_hex(session.assigned.faucet.addr) } ] } ) %{ "result" => "complete", "transactions" => [ %{ "inputs" => inputs, "sign_hash" => sign_hash, "typed_data" => typed_data, "txbytes" => txbytes } ] } = Jason.decode!(response.body)["data"] {:ok, [inputs, sign_hash, typed_data, txbytes]} end defp get_balance(sender) do {:ok, response} = Api.Account.account_get_balance( Connection.client(), %Model.AddressBodySchema{ address: Encoding.to_hex(sender.addr) } ) {:ok, response.body} end defp get_utxos(sender) do {:ok, response} = Api.Account.account_get_utxos( Connection.client(), %Model.AddressBodySchema1{ address: Encoding.to_hex(sender.addr) } ) {:ok, response.body} end defp get_transactions(_sender) do # There is an issue openapi generated client does not work well with optional request body # https://github.com/OpenAPITools/openapi-generator/issues/5234 # So we are not filtering by sender now {:ok, response} = Api.Account.account_get_transactions( Connection.client(), %Model.GetAllTransactionsBodySchema{} ) {:ok, response.body} end defp sign_tx(inputs, sign_hash, typed_data, sender) do signature = sign_hash |> Encoding.to_binary() |> Hash.sign_hash(sender.priv) |> Hash.pack_signature() |> Encoding.to_hex() signatures = Enum.map(inputs, fn _ -> signature end) Map.put_new(typed_data, "signatures", signatures) end defp do_wait_for_balance_update(_session, _sender, 0), do: :wait_for_balance_failed defp do_wait_for_balance_update(session, sender, retry) do {:ok, response_body} = get_balance(sender) utxos = Jason.decode!(response_body)["data"] if Enum.empty?(utxos) do Process.sleep(@poll_interval) session |> log_debug("retry for the balance update for sender: #{Encoding.to_hex(sender.addr)}") |> do_wait_for_balance_update(sender, retry - 1) else {:ok, session} end end defp wait_until_tx_sync_to_watcher(session, tx_id) do {:ok, session} = do_wait_until_tx_sync_to_watcher(session, tx_id, @default_retry_attempts) session end defp do_wait_until_tx_sync_to_watcher(_session, _tx_id, 0), do: :wait_until_tx_sync_failed defp do_wait_until_tx_sync_to_watcher(session, tx_id, retry) do {:ok, response} = Api.Transaction.transaction_get( Connection.client(), %Model.GetTransactionBodySchema{ id: tx_id } ) case Jason.decode!(response.body) do %{"success" => true} -> {:ok, session} _ -> Process.sleep(@poll_interval) session |> log_debug("retry for watcher info to sync the submitted tx_id: #{tx_id}") |> do_wait_until_tx_sync_to_watcher(tx_id, retry - 1) end end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/create_utxos.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.CreateUtxos do @moduledoc """ Funds an account and then splits the resulting utxo into many more utxos. ## configuration values - `sender` the owner of the utxos - `utxos_to_create_per_session` the amount of utxos to create """ use Chaperon.Scenario alias Chaperon.Session alias ExPlasma.Utxo @spawned_outputs_per_transaction 3 @spec run(Session.t()) :: Session.t() def run(session) do fee_amount = Application.fetch_env!(:load_test, :fee_amount) session = Session.assign(session, fee_amount: fee_amount) test_currency = Application.fetch_env!(:load_test, :test_currency) session = Session.assign(session, test_currency: test_currency) sender = config(session, [:sender]) utxos_to_create_per_session = config(session, [:utxos_to_create_per_session]) number_of_transactions = div(utxos_to_create_per_session, 3) transactions_per_session = config(session, [:transactions_per_session]) min_final_change = transactions_per_session * fee_amount + 1 amount_per_utxo = get_amount_per_created_utxo(fee_amount) initial_funds = number_of_transactions * fee_amount + utxos_to_create_per_session * amount_per_utxo + min_final_change session |> run_scenario(LoadTest.Scenario.FundAccount, %{ account: sender, initial_funds: initial_funds, test_currency: test_currency }) |> repeat(:submit_transaction, [sender], number_of_transactions) end def submit_transaction(session, sender) do {inputs, outputs} = create_transaction( sender, session.assigned.utxo, session.assigned.test_currency, session.assigned.fee_amount ) new_outputs = LoadTest.ChildChain.Transaction.submit_tx(inputs, outputs, [sender]) Session.assign(session, utxo: List.last(new_outputs)) end defp create_transaction(sender, input, currency, fee_amount) do amount_per_utxo = get_amount_per_created_utxo(fee_amount) change = input.amount - @spawned_outputs_per_transaction * amount_per_utxo - fee_amount created_output = %Utxo{owner: sender.addr, currency: currency, amount: amount_per_utxo} change_output = %Utxo{owner: sender.addr, currency: currency, amount: change} {[input], List.duplicate(created_output, @spawned_outputs_per_transaction) ++ [change_output]} end defp get_amount_per_created_utxo(fee_amount), do: fee_amount + 2 end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/deposits.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.Deposits do @moduledoc """ The scenario for deposits tests: 1. It creates two accounts: the depositor and the receiver. 2. It funds depositor with the specified amount on the rootchain. 3. It creates deposit for the depositor account on the childchain and checks balances on the rootchain and the childchain after this deposit. 4. The depositor account sends the specifed amount on the childchain to the receiver and checks its balance on the childchain. """ use Chaperon.Scenario alias Chaperon.Session alias LoadTest.ChildChain.Deposit alias LoadTest.Ethereum alias LoadTest.Ethereum.Account alias LoadTest.Service.Faucet alias LoadTest.Service.Metrics alias LoadTest.WatcherInfo.Balance alias LoadTest.WatcherInfo.Transaction @spec run(Session.t()) :: Session.t() def run(session) do tps = config(session, [:run_config, :tps]) period_in_seconds = config(session, [:run_config, :period_in_seconds]) total_number_of_transactions = tps * period_in_seconds period_in_mseconds = period_in_seconds * 1_000 session |> cc_spread( :create_deposit_and_make_assertions, total_number_of_transactions, period_in_mseconds ) |> await_all(:create_deposit_and_make_assertions) end def create_deposit_and_make_assertions(session) do {_, session} = Metrics.run_with_metrics( fn -> do_create_deposit_and_make_assertions(session) end, "deposits_test" ) session end defp do_create_deposit_and_make_assertions(session) do with {:ok, from, to} <- create_accounts(session), :ok <- create_deposit(from, session), :ok <- send_value_to_receiver(from, to, session) do {:ok, session} else _error -> {:error, session} end end defp create_accounts(session) do initial_amount = config(session, [:chain_config, :initial_amount]) {:ok, from_address} = Account.new() {:ok, to_address} = Account.new() {:ok, _} = Faucet.fund_root_chain_account(from_address.addr, initial_amount) {:ok, from_address, to_address} end defp create_deposit(from_address, session) do token = config(session, [:chain_config, :token]) deposited_amount = config(session, [:chain_config, :deposited_amount]) initial_amount = config(session, [:chain_config, :initial_amount]) gas_price = config(session, [:chain_config, :gas_price]) txhash = Deposit.deposit_from(from_address, deposited_amount, token, 10, gas_price, :txhash) gas_used = Ethereum.get_gas_used(txhash) with :ok <- fetch_childchain_balance(from_address, amount: deposited_amount, token: token, error: :wrong_childchain_from_balance_after_deposit ), :ok <- fetch_rootchain_balance( from_address, amount: initial_amount - deposited_amount - gas_used, token: token, error: :wrong_rootchain_balance_after_deposit ) do :ok end end defp send_value_to_receiver(from_address, to_address, session) do token = config(session, [:chain_config, :token]) transferred_amount = config(session, [:chain_config, :transferred_amount]) with _ <- send_amount_on_childchain(from_address, to_address, token, transferred_amount), :ok <- fetch_childchain_balance( to_address, amount: transferred_amount, token: token, error: :wrong_childchain_to_balance_after_sending_deposit ) do :ok end end defp send_amount_on_childchain(from, to, token, amount) do {:ok, [sign_hash, typed_data, _txbytes]} = Transaction.create_transaction( amount, from.addr, to.addr, token ) Transaction.submit_transaction(typed_data, sign_hash, [from.priv]) end defp fetch_childchain_balance(account, amount: amount, token: token, error: error) do childchain_balance = Balance.fetch_balance(account.addr, amount, token) case childchain_balance["amount"] do ^amount -> :ok _ -> error end end defp fetch_rootchain_balance(account, amount: amount, token: token, error: error) do rootchain_balance = Ethereum.fetch_balance(account.addr, token) case rootchain_balance do ^amount -> :ok _ -> error end end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/fund_account.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.FundAccount do @moduledoc """ Funds an account with some ether from the faucet. Returns the new utxo in the session. ## configuration values - `account` the account to fund - `initial_funds` the amount to fund (in wei) """ use Chaperon.Scenario alias Chaperon.Session alias LoadTest.Service.Faucet @spec run(Session.t()) :: Session.t() def run(session) do account = config(session, [:account]) initial_funds = config(session, [:initial_funds]) test_currency = config(session, [:test_currency]) {:ok, utxo} = Faucet.fund_child_chain_account(account, initial_funds, test_currency) Session.assign(session, utxo: utxo) end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/many_standard_exits.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.ManyStandardExits do @moduledoc """ Creates and funds an account, creates many utxos and starts a standard exit on each utxo ## configuration values - `exits_per_session` the number od utxos to create and then exit """ use Chaperon.Scenario alias LoadTest.ChildChain.WatcherSync alias LoadTest.Ethereum alias LoadTest.Ethereum.Account alias LoadTest.Service.Faucet @gas_start_exit 500_000 @standard_exit_bond 14_000_000_000_000_000 def run(session) do exits_per_session = config(session, [:exits_per_session]) gas_price = config(session, [:gas_price]) # Create a new exiter account {:ok, exiter} = Account.new() amount = (@gas_start_exit * gas_price + @standard_exit_bond) * exits_per_session # Fund the exiter with some root chain eth {:ok, _} = Faucet.fund_root_chain_account(exiter.addr, amount) # Create many utxos on the child chain session = run_scenario(session, LoadTest.Scenario.CreateUtxos, %{ sender: exiter, transactions_per_session: 1, utxos_to_create_per_session: exits_per_session }) # Wait for the last utxo to seen by the watcher :ok = LoadTest.ChildChain.Utxos.wait_for_utxo(exiter.addr, session.assigned.utxo) # Start a standard exit on each of the exiter's utxos session = exiter.addr |> LoadTest.ChildChain.Utxos.get_utxos() |> Enum.map(&exit_utxo(session, &1, exiter)) |> List.last() last_tx_hash = session.assigned.tx_hash {:ok, %{"status" => "0x1", "blockNumber" => last_exit_height}} = Ethereum.transact_sync(last_tx_hash) :ok = WatcherSync.watcher_synchronize(root_chain_height: last_exit_height) log_info(session, "Many Standard Exits Test done.") end def exit_utxo(session, utxo, exiter) do run_scenario( session, LoadTest.Scenario.StartStandardExit, %{ exiter: exiter, utxo: utxo } ) end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/smoke.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.Smoke do @moduledoc """ Smoke test scenario to ensure services are up """ use Chaperon.Scenario def run(session) do log_info(session, "run smoke test to make sure services are up...") check_child_chain_up() check_watcher_security_up() check_watcher_info_up() log_info(session, "smoke test done...") end defp check_child_chain_up() do {:ok, response} = ChildChainAPI.Api.Configuration.configuration_get(LoadTest.Connection.ChildChain.client()) # some sanity check %{ "data" => %{ "contract_semver" => _contract_semver, "deposit_finality_margin" => _deposit_finality_margin, "network" => _network }, "service_name" => "child_chain" } = Jason.decode!(response.body) end defp check_watcher_security_up() do {:ok, response} = WatcherSecurityCriticalAPI.Api.Status.status_get(LoadTest.Connection.WatcherSecurity.client()) # some sanity check %{ "data" => %{ "byzantine_events" => _byzantine_events, "contract_addr" => _contract_addr }, "service_name" => "watcher" } = Jason.decode!(response.body) end defp check_watcher_info_up() do {:ok, response} = WatcherInfoAPI.Api.Stats.stats_get(LoadTest.Connection.WatcherInfo.client()) # some sanity check %{ "data" => %{ "average_block_interval_seconds" => _average_block_interval, "block_count" => _block_count }, "service_name" => "watcher_info" } = Jason.decode!(response.body) end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/spend_eth_utxo.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.SpendEthUtxo do @moduledoc """ Spends a utxo in a transaction. Can be done repeatedly by setting `transactions_per_session` Returns the first output of the spent transaction in the session. Normally this will be the change output. ## configuration values - `sender` the owner of the utxo - `receiver` the receiver's account - `amount` the amount to spend. If amount + fee is less than the value of the utxo then the change will be sent back to the sender - `transactions_per_session` the number of transactions to send. Each transaction after the first will spend the change output of the previous transaction - `transaction_delay` delay in milliseconds before sending the transaction. Used to control the tx rate. """ use Chaperon.Scenario alias Chaperon.Session alias Chaperon.Timing def run(session) do fee_amount = Application.fetch_env!(:load_test, :fee_amount) sender = config(session, [:sender]) receiver = config(session, [:receiver]) amount = config(session, [:amount], nil) test_currency = config(session, [:test_currency], nil) delay = config(session, [:transaction_delay], 0) transactions_per_session = config(session, [:transactions_per_session]) repeat( session, :submit_transaction, [amount, fee_amount, sender, receiver, test_currency, delay], transactions_per_session ) end def submit_transaction(session, nil, fee_amount, sender, receiver, currency, delay) do utxo = session.assigned.utxo amount = utxo.amount - fee_amount submit_transaction(session, amount, fee_amount, sender, receiver, currency, delay) end def submit_transaction(session, amount, fee_amount, sender, receiver, currency, delay) do Process.sleep(delay) utxo = session.assigned.utxo start = Timing.timestamp() [next_utxo | _] = LoadTest.ChildChain.Transaction.spend_utxo(utxo, amount, fee_amount, sender, receiver, currency) session |> Session.assign(utxo: next_utxo) |> Session.add_metric( {:call, {LoadTest.Scenario.SpendEthUtxo, "submit_transaction"}}, Timing.timestamp() - start ) end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/start_standard_exit.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.StartStandardExit do @moduledoc """ Starts a standard exit. ## configuration values - `exiter` the account that's starting the exit - `utxo` the utxo to exit """ use Chaperon.Scenario alias Chaperon.Session alias LoadTest.ChildChain.Exit def run(session) do exiter = config(session, [:exiter]) utxo = config(session, [:utxo]) gas_price = config(session, [:gas_price]) tx_hash = utxo |> Exit.wait_for_exit_data() |> Exit.start_exit(exiter, gas_price) Session.assign(session, tx_hash: tx_hash) end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/transactions.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.Transactions do @moduledoc """ The scenario for transactions tests: 1. It creates two accounts: the sender and the receiver. 2. It funds sender with the specified amount on the childchain, checks utxos and balance. 3. The sender account sends the specifed amount on the childchain to the receiver, checks its balance on the childchain and utxos for both accounts. """ use Chaperon.Scenario alias Chaperon.Session alias ExPlasma.Encoding alias LoadTest.ChildChain.Transaction alias LoadTest.Ethereum.Account alias LoadTest.Service.Faucet alias LoadTest.Service.Metrics alias LoadTest.WatcherInfo.Balance alias LoadTest.WatcherInfo.Utxo @spec run(Session.t()) :: Session.t() def run(session) do tps = config(session, [:run_config, :tps]) period_in_seconds = config(session, [:run_config, :period_in_seconds]) total_number_of_transactions = tps * period_in_seconds period_in_mseconds = period_in_seconds * 1_000 session |> cc_spread( :create_utxos_and_make_assertions, total_number_of_transactions, period_in_mseconds ) |> await_all(:create_utxos_and_make_assertions) end def create_utxos_and_make_assertions(session) do {_, session} = Metrics.run_with_metrics( fn -> do_create_utxos_and_make_assertions(session) end, "transactions_test" ) session end defp do_create_utxos_and_make_assertions(session) do with {:ok, sender, receiver} <- create_accounts(), {:ok, utxo} <- fund_account(session, sender), :ok <- spend_utxo(session, utxo, sender, receiver) do {:ok, session} else _ -> {:error, session} end end defp create_accounts() do {:ok, sender_address} = Account.new() {:ok, receiver_address} = Account.new() {:ok, sender_address, receiver_address} end defp fund_account(session, account) do initial_amount = config(session, [:chain_config, :initial_amount]) token = config(session, [:chain_config, :token]) with {:ok, utxo} <- fund_childchain_account(account, initial_amount, token), :ok <- fetch_childchain_balance(account, amount: initial_amount, token: Encoding.to_binary(token), error: :wrong_childchain_after_funding ), :ok <- validate_utxos(account, %{utxo | owner: account.addr}) do {:ok, utxo} end end defp validate_utxos(account, utxo) do utxo_with_owner = case utxo do :empty -> :empty _ -> %{utxo | owner: account.addr} end case Utxo.get_utxos(account, utxo_with_owner) do {:ok, _} -> :ok _other -> :invalid_utxos end end defp fund_childchain_account(address, amount, token) do case Faucet.fund_child_chain_account(address, amount, token) do {:ok, utxo} -> {:ok, utxo} _ -> :failed_to_fund_childchain_account end end defp spend_utxo(session, utxo, sender, receiver) do amount = config(session, [:chain_config, :initial_amount]) token = config(session, [:chain_config, :token]) fee = config(session, [:chain_config, :fee]) amount_to_transfer = amount - fee with [new_utxo] <- Transaction.spend_utxo(utxo, amount_to_transfer, fee, sender, receiver, token), :ok <- validate_utxos(sender, :empty), :ok <- validate_utxos(receiver, %{new_utxo | owner: receiver.addr}), :ok <- fetch_childchain_balance(sender, amount: 0, token: Encoding.to_binary(token), error: :wrong_sender_balance), :ok <- fetch_childchain_balance(receiver, amount: amount_to_transfer, token: Encoding.to_binary(token), error: :wrong_sender_balance ) do :ok end end defp fetch_childchain_balance(account, amount: amount, token: token, error: error) do childchain_balance = Balance.fetch_balance(account.addr, amount, token) case childchain_balance do nil when amount == 0 -> :ok %{"amount" => ^amount} -> :ok _ -> error end end end ================================================ FILE: priv/perf/apps/load_test/lib/scenario/watcher_status.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Scenario.WatcherStatus do @moduledoc """ Calls Watcher status.get """ use Chaperon.Scenario alias Chaperon.Session def run(session) do {:ok, response} = WatcherSecurityCriticalAPI.Api.Status.status_get(LoadTest.Connection.WatcherSecurity.client()) watcher_status = Jason.decode!(response.body) Session.assign(session, watcher_status: watcher_status) end end ================================================ FILE: priv/perf/apps/load_test/lib/service/datadog/api.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Datadog.API do @moduledoc """ Funcions for fetching monitor events from datadog. """ require Logger @datadog_events_api_path "api/v1/events" @datadog_monitor_resolve_path "monitor/bulk_resolve" @datadog_app_url "https://app.datadoghq.com" def assert_metrics(environment, start_unix, end_unix) when is_integer(start_unix) and is_integer(end_unix) do # events aren't pulished instantly, so we will poll them do_assert_metrics(environment, start_unix, end_unix) end def assert_metrics(environment, start_datetime, end_datetime) do start_unix = DateTime.to_unix(start_datetime) end_unix = DateTime.to_unix(end_datetime) assert_metrics(environment, start_unix, end_unix) end defp do_assert_metrics(environment, start_unix, end_unix, poll_count \\ 60) defp do_assert_metrics(_environment, _start_unix, _end_unix, 0), do: :ok defp do_assert_metrics(environment, start_unix, end_unix, poll_count) do case fetch_events(start_unix, end_unix, environment) do {:ok, []} -> Process.sleep(1_000) do_assert_metrics(environment, start_unix, end_unix, poll_count - 1) {:ok, events} -> # failed monitors with the same tags do not emit events, so we're resolving # failed monitors for future runs resolve_monitors(events) {:error, events} other -> other end end defp fetch_events(start_time, end_time, environment, retries \\ 5) defp fetch_events(start_time, end_time, environment, 0) do Logger.error("failed to fetch events #{inspect({start_time, end_time, environment})}") {:error, :failed_to_fetch_event} end defp fetch_events(start_time, end_time, environment, retries) do params = %{ start: start_time, end: end_time, tags: environment, unaggregated: true } url = api_url() <> @datadog_events_api_path <> "?" <> URI.encode_query(params) case HTTPoison.get(url, headers()) do {:ok, %{status_code: 200, body: body}} -> events = body |> Jason.decode!() |> parse_events(environment) {:ok, events} {:ok, %{body: body}} -> Logger.warn("failed to fetch events #{inspect(body)}. retrying") fetch_events(start_time, end_time, environment, retries - 1) {:error, error} -> Logger.warn("failed to fetch events #{inspect(error)}. retrying") fetch_events(start_time, end_time, environment, retries - 1) end end defp parse_events(events_response, environment) do events_response["events"] |> Enum.filter(fn event -> event["alert_type"] == "error" and String.contains?(event["text"], environment) end) |> Enum.map(fn event -> {:ok, date} = DateTime.from_unix(event["date_happened"]) %{ "title" => event["title"], "url" => @datadog_app_url <> event["url"], "date" => date, "monitor_id" => find_monitor_id(event["text"]) } end) end defp resolve_monitors(events) do params = events |> Enum.map(fn event -> event["monitor_id"] end) |> Enum.filter(fn id -> !(is_nil(id) or id == "") end) |> Enum.uniq() |> Enum.map(fn id -> %{id => "ALL_GROUPS"} end) do_resolve_monitors(params) end defp do_resolve_monitors(params, retries \\ 5) defp do_resolve_monitors([], _), do: :ok defp do_resolve_monitors(params, 0) do Logger.error("failed to resolve monitors #{params}") {:error, :failed_to_resolve_monitors} end defp do_resolve_monitors(params, retries) do payload = Jason.encode!(%{"resolve" => params}) url = api_url() <> @datadog_monitor_resolve_path case HTTPoison.post(url, payload, headers()) do {:ok, %{status_code: 200}} -> :ok {:ok, %{body: body}} -> Logger.warn("failed to resolve monitors #{inspect(body)}. retrying") Process.sleep(1_000) do_resolve_monitors(params, retries - 1) {:error, error} -> Logger.warn("failed to resolve monitors #{inspect(error)}. retrying") Process.sleep(1_000) do_resolve_monitors(params, retries - 1) end end defp find_monitor_id(text) do case String.split(text, ["monitors#", "?to_ts"], parts: 3) do [_, id, _] -> String.to_integer(id) _ -> nil end end defp headers() do %{ "Content-Type" => "application/json", "DD-API-KEY" => api_key(), "DD-APPLICATION-KEY" => app_key() } end defp api_key() do Keyword.fetch!(datadog_config(), :api_key) end defp app_key() do Keyword.fetch!(datadog_config(), :app_key) end defp api_url() do Keyword.fetch!(datadog_config(), :api_url) end defp datadog_config() do Application.fetch_env!(:load_test, :datadog) end end ================================================ FILE: priv/perf/apps/load_test/lib/service/datadog/dummy_statix.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Datadog.DummyStatix do @moduledoc """ Useful for overwritting Statix behaviour. """ defmacro __using__(_opts) do quote location: :keep do @behaviour Statix def connect(options \\ []), do: :ok def increment(_), do: :ok def increment(_, _, options \\ []), do: :ok def decrement(_, val \\ 1, options \\ []), do: :ok def gauge(_, val, options \\ []), do: :ok def histogram(_, val, options \\ []), do: :ok def timing(_, val, options \\ []), do: :ok def measure(key, options \\ [], fun), do: :ok def set(key, val, options \\ []), do: :ok def event(key, val, options), do: :ok def service_check(key, val, options), do: :ok def current_conn(), do: %Statix.Conn{sock: __MODULE__} end end end ================================================ FILE: priv/perf/apps/load_test/lib/service/datadog.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Datadog do @moduledoc """ Datadog connection wrapper """ # we want to override Statix # because we don't want to send metrics in unittests case Application.get_env(:load_test, :record_metrics) do true -> use Statix, runtime_config: true _ -> use LoadTest.Service.Datadog.DummyStatix end use GenServer require Logger def start_link(_params), do: GenServer.start_link(__MODULE__, [], []) def init(_opts) do _ = Process.flag(:trap_exit, true) _ = Logger.info("Starting #{inspect(__MODULE__)} and connecting to Datadog.") :ok = __MODULE__.connect() _ = Logger.info("Datadog Connection for Statix was opened") {:ok, []} end def handle_info({:EXIT, port, reason}, %Statix.Conn{sock: __MODULE__} = state) do _ = Logger.error("Port in #{inspect(__MODULE__)} #{inspect(port)} exited with reason #{reason}") {:stop, :normal, state} end end ================================================ FILE: priv/perf/apps/load_test/lib/service/faucet.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Faucet do @moduledoc """ Handles funding accounts on child chain. For simplicity, the faucet will always use its largest value utxo to fund other accounts. If its largest value utxo is insufficient (or if it has no utxos) it will do a deposit, wait for it to finalize and then use that deposit utxo for funding accounts. This means that faucet account must have sufficient funds on the root chain. After a few test runs, the faucet can end up with a large amount of utxos. This is not a problem per se, but keeping the number of utxos down can speed things up. You can merge utxos periodically by running merge_utxos e.g. MIX_ENV=test mix run -e "LoadTest.Service.Faucet.merge_utxos(<<0::160>>)" """ require Logger use GenServer alias ExPlasma.Encoding alias ExPlasma.Utxo alias LoadTest.ChildChain.Deposit alias LoadTest.ChildChain.Transaction alias LoadTest.ChildChain.Utxos alias LoadTest.Ethereum alias LoadTest.Ethereum.Account # Submitting a transaction to the childchain can fail if it is under heavy load, # allow the faucet to retry to avoid failing the test prematurely. @fund_child_chain_account_timeout 100_000 @type state :: %__MODULE__{ faucet_account: Account.t(), fee: pos_integer(), faucet_deposit_amount: pos_integer(), deposit_finality_margin: pos_integer(), gas_price: pos_integer(), utxos: map() } defstruct [:faucet_account, :fee, :faucet_deposit_amount, :deposit_finality_margin, :gas_price, utxos: %{}] @doc """ Sends funds to an account on the rootchain. """ @spec fund_root_chain_account(<<_::160>>, pos_integer()) :: Utxo.t() def fund_root_chain_account(receiver, amount) do GenServer.call(__MODULE__, {:fund_root_chain, receiver, amount}, :infinity) end @doc """ Sends funds to an account on the childchain. If the faucet doesn't have enough funds it will deposit more. Note that this can take some time to finalize. """ @spec fund_child_chain_account(Account.t(), pos_integer(), Utxo.address_binary()) :: Utxo.t() def fund_child_chain_account(receiver, amount, currency) when byte_size(currency) == 20 do GenServer.call(__MODULE__, {:fund_child_chain, receiver, amount, currency}, :infinity) end def fund_child_chain_account(receiver, amount, currency) do fund_child_chain_account(receiver, amount, Encoding.to_binary(currency)) end @doc """ Returns the faucet account. """ @spec get_faucet() :: Account.t() def get_faucet() do GenServer.call(__MODULE__, :get_faucet) end @doc """ Merges all the utxos of the given currency into one. Note that this can take some time. """ @spec merge_utxos(Utxo.address_binary()) :: Utxo.t() def merge_utxos(currency) when byte_size(currency) == 20 do GenServer.call(__MODULE__, {:merge_utxos, currency}, :infinity) end @spec merge_utxos(Utxo.address_hex()) :: Utxo.t() def merge_utxos(currency), do: merge_utxos(Encoding.to_binary(currency)) def start_link(config) do GenServer.start_link(__MODULE__, config, name: __MODULE__) end def init(config) do {:ok, faucet_account} = Account.new(Keyword.fetch!(config, :faucet_private_key)) Logger.debug("Using faucet: #{Encoding.to_hex(faucet_account.addr)}") state = struct!( __MODULE__, faucet_account: faucet_account, fee: Keyword.fetch!(config, :fee_amount), faucet_deposit_amount: Keyword.fetch!(config, :faucet_deposit_amount), deposit_finality_margin: Keyword.fetch!(config, :deposit_finality_margin), gas_price: Keyword.fetch!(config, :gas_price) ) {:ok, state} end def handle_call(:get_faucet, _from, state) do {:reply, {:ok, state.faucet_account}, state} end def handle_call({:fund_child_chain, receiver, amount, currency}, _from, state) do utxo = get_funding_utxo(state, currency, amount) Logger.debug("Funding user #{Encoding.to_hex(receiver.addr)} with #{amount} from utxo: #{Utxo.pos(utxo)}") outputs = Transaction.spend_utxo( utxo, amount, state.fee, state.faucet_account, receiver, currency, @fund_child_chain_account_timeout ) [next_faucet_utxo, user_utxo] = case outputs do [single_output] -> [nil, single_output] [change_output, user_output] -> [change_output, user_output] end updated_state = Map.put(state, :utxos, Map.put(state.utxos, currency, next_faucet_utxo)) {:reply, {:ok, user_utxo}, updated_state} end def handle_call({:fund_root_chain, receiver, amount}, _from, state) do Logger.debug("Funding user #{Encoding.to_hex(receiver)} with #{amount} on root chain}") tx = %LoadTest.Ethereum.Transaction{ to: receiver, value: amount, gas_price: state.gas_price, gas_limit: 21_000 } {:ok, tx_hash} = Ethereum.send_raw_transaction(tx, state.faucet_account) {:ok, _} = Ethereum.transact_sync(tx_hash) {:reply, {:ok, tx_hash}, state} end def handle_call({:merge_utxos, currency}, _from, state) do utxos = Utxos.get_utxos(state.faucet_account.addr) Logger.debug("Merging #{length(utxos)} utxos of #{Encoding.to_hex(currency)}") utxo = Utxos.merge(utxos, currency, state.faucet_account) {:reply, {:ok, utxo}, state} end @spec get_funding_utxo(state(), Utxo.address_binary(), pos_integer()) :: Utxo.t() defp get_funding_utxo(state, currency, amount) do utxo = choose_largest_utxo(state.utxos[currency], state.faucet_account, currency) if utxo == nil or utxo.amount - amount - state.fee < 0 do deposit( state.faucet_account, max(state.faucet_deposit_amount, amount + state.fee), currency, state.deposit_finality_margin, state.gas_price ) else utxo end end defp choose_largest_utxo(nil, account, currency) do account.addr |> Utxos.get_utxos() |> Utxos.get_largest_utxo(currency) end defp choose_largest_utxo(utxo, _account, _currency), do: utxo @spec deposit(Account.t(), pos_integer(), Utxo.address_binary(), pos_integer(), pos_integer()) :: Utxo.t() defp deposit(faucet_account, amount, currency, deposit_finality_margin, gas_price) do Logger.debug("Not enough funds in the faucet, depositing #{amount} from the root chain") {:ok, utxo} = Deposit.deposit_from(faucet_account, amount, currency, deposit_finality_margin, gas_price, :utxo) utxo end end ================================================ FILE: priv/perf/apps/load_test/lib/service/metrics.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Metrics do @moduledoc """ Functions for aggregating metrics. """ alias LoadTest.Service.Datadog alias LoadTest.Service.Datadog.API def run_with_metrics(func, property) do case Application.get_env(:load_test, :record_metrics) do true -> do_run_with_metrics(func, property) false -> func.() end end def assert_metrics(start_datetime, end_datetime) do env = :statix |> Application.get_env(:tags) |> List.first() API.assert_metrics(env, start_datetime, end_datetime) end defp do_run_with_metrics(func, property) do {time, result} = :timer.tc(func) time_ms = time / 1_000 case result do {:ok, _} -> record_success(property, time_ms) :ok -> record_success(property, time_ms) {:error, :data_not_found} -> record_success(property, time_ms) _ -> record_failure(property, time_ms) end result end defp record_success(property, time) do record_datadog(property, time, "_success") end defp record_failure(property, time) do record_datadog(property, time, "_failure") end defp record_datadog(property, time, postfix) do property_name = property <> postfix total_property_name = property <> "_count" :ok = Datadog.gauge(property_name, time, tags: tags()) increment_counter(property_name <> "_count") increment_counter(total_property_name) end defp increment_counter(property) do :ok = Datadog.increment(property, 1, tags: tags()) end defp tags() do Application.get_env(:statix, :tags) end end ================================================ FILE: priv/perf/apps/load_test/lib/service/sleeper.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Sleeper do @moduledoc """ Sleeps the current process for the given timeout and print the given warning. """ require Logger @spec sleep(String.t()) :: :ok def sleep(message) do _ = Logger.info(message) Process.sleep(retry_sleep()) end defp retry_sleep() do Application.fetch_env!(:load_test, :retry_sleep) end end ================================================ FILE: priv/perf/apps/load_test/lib/service/sync.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Sync do @moduledoc """ Provides a function for repeating a function call until a given criteria is met. """ require Logger alias LoadTest.Service.Sleeper @doc """ Repeats f until f returns {:ok, ...}, :ok OR exception is raised (see :erlang.exit, :erlang.error) OR timeout after `timeout` milliseconds specified Simple throws and :badmatch are treated as signals to repeat """ def repeat_until_success(f, timeout, message) do fn -> do_repeat_until_success(f, message) end |> Task.async() |> Task.await(timeout) end defp do_repeat_until_success(f, message) do case f.() do :ok -> :ok {:ok, _} = return -> return result -> repeat(f, message, result) end catch result -> repeat(f, message, result) end defp repeat(f, message, result) do Sleeper.sleep(message <> " #{inspect(result)}") do_repeat_until_success(f, message) end end ================================================ FILE: priv/perf/apps/load_test/lib/test_runner/config.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.TestRunner.Config do @moduledoc """ Command line args parser for TestRunner. """ alias ExPlasma.Encoding alias LoadTest.TestRunner.Help @tests %{ "deposits" => LoadTest.Runner.Deposits, "transactions" => LoadTest.Runner.Transactions } @configs %{ "deposits" => %{ token: {:binary, "0x0000000000000000000000000000000000000000"}, initial_amount: 500_000_000_000_000_000, deposited_amount: 200_000_000_000_000_000, transferred_amount: 100_000_000_000_000_000, gas_price: 2_000_000_000 }, "transactions" => %{ token: "0x0000000000000000000000000000000000000000", initial_amount: 760, fee: 75 } } def parse() do case System.argv() do ["make_assertions", start_time, end_time] -> start_time_integer = String.to_integer(start_time) end_time_integer = String.to_integer(end_time) {:make_assertions, start_time_integer, end_time_integer} [test, rate, period] -> {:run_tests, config(test, rate, period, "true")} [test, rate, period, make_assertions] -> {:run_tests, config(test, rate, period, make_assertions)} ["help"] -> Help.help() ["help", "env"] -> Help.help("env") ["help", name] -> Help.help(name) end end defp config(test, rate, period, make_assertions) do rate_int = String.to_integer(rate) period_int = String.to_integer(period) runner_module = Map.fetch!(@tests, test) # Chaperon's SpreadAsyns (https://github.com/polleverywhere/chaperon/blob/13cc4a2d2a7baacddf20c46397064b5e42a48d97/lib/chaperon/action/spread_async.ex) # spawns a separate process for each execution. VM may fail if too many processes are spawned if rate_int * period_int > 200_000, do: raise("too many processes") run_config = %{ tps: rate_int, period_in_seconds: period_int } chain_config = read_config!(test) config = %{ run_config: run_config, chain_config: chain_config, make_assertions: parse_boolean(make_assertions), timeout: :infinity } {runner_module, config} end defp parse_boolean(bool) do case bool do "true" -> true _ -> false end end defp read_config!(test) do config_path = System.get_env("TEST_CONFIG_PATH") case config_path do nil -> @configs |> Map.fetch!(test) |> parse_config_values() _ -> parse_config_file!(config_path, test) end end defp parse_config_file!(file_path, test) do default_config = Map.fetch!(@configs, test) config = file_path |> File.read!() |> Jason.decode!() default_config |> Enum.map(fn {key, default_value} -> string_key = Atom.to_string(key) value = case default_value do {type, value} -> {type, config[string_key] || value} value -> config[string_key] || value end {key, value} end) |> Map.new() |> parse_config_values() end defp parse_config_values(config) do config |> Enum.map(fn {key, value} -> parsed_value = case value do {:binary, string} -> Encoding.to_binary(string) value -> value end {key, parsed_value} end) |> Map.new() end end ================================================ FILE: priv/perf/apps/load_test/lib/test_runner/help.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.TestRunner.Help do @moduledoc """ Shows help info for TestRunner. """ require Logger @help """ `LoadTest.TestRunner` accepts three required parameters: 1. Test name (`transactions` or `deposits`) 2. Rate in tests per second 3. Period in seconds For example, if you want to run `deposits` with 5 tests / second rate over 20 seconds, you should run the following command: ``` mix run -e "LoadTest.TestRunner.run()" -- deposits 5 20 ``` To modify tests values use `TEST_CONFIG_PATH`. It should contain a path to json file containing test values: ``` TEST_CONFIG_PATH=./my_file mix run -e "LoadTest.TestRunner.run()" -- deposits 1 5 ``` To see which values can be overridden, use ``` mix run -e "LoadTest.TestRunner.run()" -- help test_name ``` To see env variable, use: ``` mix run -e "LoadTest.TestRunner.run()" -- help env ``` Additonal notes. These tests use datadog to collect metrics so you need to set: - STATIX_TAG - env tag used by statsd/datadog-agent. different test runs are distinguished by this tag in datadog dashboard - DD_API_KEY - datadog api key - DD_APP_KEY - datadog app key Available dashboards are: - https://app.datadoghq.com/dashboard/rpx-xu2-b2g/deposits-perf-tests - deposits tests - https://app.datadoghq.com/dashboard/7kh-xx4-9qu/transactions-perf-tests - transactions tests Since `LoadTest.Ethereum.NonceTracker` is used to track nonces for addresses in the Ethereum, it's not possible to run multiple instances of these tests using the same addresses. It may cause race conditions. Creating new tests. 1. Create Chaperon runner (see `LoadTest.Runner.Transactions`) 2. Create Chaperon scenario (see `LoadTest.Scenario.Transactions`) 3. Wrap functions that you want to collect metrics for with `LoadTest.Service.Metrics.run_with_metrics/2`) 4. Run tests so metrics are sent to Datadog 5. Create dashboards and monitors in Datadog. Running tests without assertions. You can run tests without assertions by passing `false` as the last parameter: ``` STATIX_TAG="env:perf_circleci" mix run -e "LoadTest.TestRunner.run()" -- "transactions" 1 80 false ``` To just check if there are any events in the given period of time run passing start time and end time: ``` STATIX_TAG="env:perf_circleci" mix run -e "LoadTest.TestRunner.run()" -- "make_assertions" 1605775276 1605785276 ``` """ @help_test %{ "deposits" => """ A single iteration of this test consists of the following steps: 1. It creates two accounts: the depositor and the receiver. 2. It funds depositor with the specified amount (`initial_amount`) on the rootchain. 3. It creates deposit (`deposited_amount`) with gas price `gas_price` for the depositor account on the childchain and checks balances on the rootchain and the childchain after this deposit. 4. The depositor account sends the specifed amount (`transferred_amount`) on the childchain to the receiver and checks its balance on the childchain. Overridable parameters are: - token. default value is "0x0000000000000000000000000000000000000000" - initial_amount. default value is 500_000_000_000_000_000 - deposited_amount default value is 200_000_000_000_000_000 - transferred_amount. default value is 100_000_000_000_000_000 - gas_price. default value is 2_000_000_000 """, "transactions" => """ A single iteration of this test consists of the following steps: 1.1 Two accounts are created - the sender and the receiver 2.1 The sender account is funded with `initial_amount` `token` 2.2 The balance on the childchain of the sender is validated using WatcherInfoAPI.Api.Account.account_get_balance API. 2.3 Utxos of the sender are validated using WatcherInfoAPI.Api.Account.account_get_utxos API 3.1 The sender sends all his tokens to the receiver with fee `fee` 3.2 The balance on the childchain of the sender is validated 3.3 The balance on the childchain of the receiver is validated 3.4 Utxos of the sender are validated 3.5 Utxos of the receiver are validated Overridable parameters are: - initial_balance. default value is 760 - token. default value is "0x0000000000000000000000000000000000000000" - fee. default value is 75 """ } @env """ ETHEREUM_RPC_URL - Ethereum Json RPC url. Default value is http://localhost:8545 RETRY_SLEEP - Sleeping period used when polling data. Default value is 1000 (ms) CHILD_CHAIN_URL - Childcahin url. Default value is http://localhost:9656 WATCHER_SECURITY_URL - Watcher security url. Default value is http://localhost:7434 WATCHER_INFO_URL - Watcehr info url. Default value is http://localhost:7534 LOAD_TEST_FAUCET_PRIVATE_KEY - Faucet private key. Default value is 0xd885a307e35738f773d8c9c63c7a3f3977819274638d04aaf934a1e1158513ce CONTRACT_ADDRESS_ETH_VAULT - Eth vault contact address CONTRACT_ADDRESS_PAYMENT_EXIT_GAME - Payment exit game contract address CHILD_BLOCK_INTERVAL - Block generation interval. Default value is 1000 (ms) CONTRACT_ADDRESS_PLASMA_FRAMEWORK - Plasma framework contract address CONTRACT_ADDRESS_ERC20_VAULT - Erc 20 vault contract address FEE_AMOUNT - Fee amount used by faucet when funding accounts. Default value is 75 DEPOSIT_FINALITY_MARGIN - Number of comfirmation for a deposit. Default value is 10 """ def help() do IO.puts(@help) end def help("env") do IO.puts(@env) end def help(test_name) do case @help_test[test_name] do nil -> tests = @help_test |> Map.keys() |> Enum.join(", ") IO.puts(""" Documentation for `#{test_name}` is not found. Available tests are #{tests}. """) doc -> IO.puts(doc) end end end ================================================ FILE: priv/perf/apps/load_test/lib/test_runner.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.TestRunner do @moduledoc """ This module runs tests using `mix run`. For example: mix run -e "LoadTest.TestRunner.run()" -- "deposits" "1" "5" It accepts three arguments: - test name - transactions per seconds - period in seconds You can also modify values for tests by providing `TEST_CONFIG_PATH` env variable, it should contain the path to json file. For example: TEST_CONFIG_PATH=./my_file mix run -e "LoadTest.TestRunner.run()" -- "deposits" "1" "5" 90 It fetches all configuration params from env vars. """ alias LoadTest.Service.Metrics alias LoadTest.TestRunner.Config @circleci_tag "env:perf_circleci" def run() do case Config.parse() do {:run_tests, {runner_module, config}} -> run_test(runner_module, config) {:make_assertions, start_time, end_time} -> make_assertions(start_time, end_time) :ok -> :ok end end defp run_test(runner_module, config) do case System.get_env("STATIX_TAG") do nil -> raise("STATIX_TAG is not set") _ -> :ok end start_datetime = DateTime.utc_now() maybe_add_custom_tag(start_datetime) Chaperon.run_load_test(runner_module, print_results: true, config: config) end_datetime = DateTime.utc_now() case config.make_assertions do true -> make_assertions(start_datetime, end_datetime) _ -> :ok end end defp make_assertions(start_time, end_time) do case Metrics.assert_metrics(start_time, end_time) do :ok -> System.halt(0) {:error, errors} -> # credo:disable-for-next-line IO.inspect("errors: #{inspect(errors)}") System.halt(1) end end defp maybe_add_custom_tag(start_date) do tags = Application.get_env(:statix, :tags) tags |> Enum.find(fn value -> value == @circleci_tag end) |> case do nil -> :ok _ -> postfix = start_date |> to_string |> String.replace(" ", "-") |> String.downcase() new_tag = @circleci_tag <> ":" <> postfix Application.put_env(:statix, :tags, [new_tag | tags]) end end end ================================================ FILE: priv/perf/apps/load_test/lib/utils/encoding.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Utils.Encoding do @moduledoc """ Utility module for converting between hex strings and other types. """ def to_binary(hex) do hex |> String.replace_prefix("0x", "") |> String.upcase() |> Base.decode16!() end @spec to_hex(binary | non_neg_integer) :: binary def to_hex(non_hex) def to_hex(raw) when is_binary(raw), do: "0x" <> Base.encode16(raw, case: :lower) def to_hex(int) when is_integer(int), do: "0x" <> Integer.to_string(int, 16) # because https://github.com/rrrene/credo/issues/583, we need to: # credo:disable-for-next-line Credo.Check.Consistency.SpaceAroundOperators @spec from_hex(<<_::16, _::_*8>>) :: binary def from_hex("0x" <> encoded), do: Base.decode16!(encoded, case: :lower) @spec encode_deposit(ExPlasma.Transaction.t()) :: %{data: binary()} def encode_deposit(transaction) do tx_bytes = ExPlasma.Transaction.encode(transaction) data = encode_data("deposit(bytes)", [tx_bytes]) %{data: data} end @spec encode_data(String.t(), list()) :: binary defp encode_data(function_signature, data) do data = ABI.encode(function_signature, data) "0x" <> Base.encode16(data, case: :lower) end end ================================================ FILE: priv/perf/apps/load_test/lib/watcher_info/balance.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.WatcherInfo.Balance do @moduledoc """ Functions related to balances on the childchain """ require Logger alias ExPlasma.Encoding alias LoadTest.Ethereum.Account alias LoadTest.Service.Sync alias LoadTest.WatcherInfo.Client @poll_timeout 60_000 @spec fetch_balance(Account.addr_t(), non_neg_integer(), Account.addr_t()) :: non_neg_integer() | :error | nil | map() def fetch_balance(address, amount, currency \\ <<0::160>>) do {:ok, result} = Sync.repeat_until_success( fn -> do_fetch_balance(Encoding.to_hex(address), amount, Encoding.to_hex(currency)) end, @poll_timeout, "Failed to fetch childchain balance" ) result end defp do_fetch_balance(address, amount, currency) do response = case Client.get_balances(address) do {:ok, decoded_response} -> Enum.find(decoded_response["data"], fn data -> data["currency"] == currency end) result -> Logger.error("Failed to fetch balance from childchain #{inspect(result)}") :error end case response do # empty response is considered no account balance! nil when amount == 0 -> {:ok, nil} %{"amount" => ^amount} = balance -> {:ok, balance} response -> {:error, response} end end end ================================================ FILE: priv/perf/apps/load_test/lib/watcher_info/client.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.WatcherInfo.Client do @moduledoc """ Interface to Watcher info which collects metrics. """ alias LoadTest.Service.Metrics alias WatcherInfoAPI.Api.Account def get_balances(address) do Metrics.run_with_metrics( fn -> request(fn -> Account.account_get_balance(client(), %{address: address}) end) end, "WatcherInfo.get_balances" ) end def get_utxos(address) do Metrics.run_with_metrics( fn -> request(fn -> Account.account_get_utxos(client(), %{address: address}) end) end, "WatcherInfo.get_utxos" ) end defp request(func) do case func.() do {:ok, response} -> case Jason.decode!(response.body) do %{"data" => %{"object" => "error"}} = failure -> {:error, failure} decoded_response -> {:ok, decoded_response} end other -> other end end defp client() do LoadTest.Connection.WatcherInfo.client() end end ================================================ FILE: priv/perf/apps/load_test/lib/watcher_info/transaction.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.WatcherInfo.Transaction do @moduledoc """ Functions for working with transactions WatherInfo API """ alias ExPlasma.Encoding alias LoadTest.Ethereum.Account alias LoadTest.Service.Metrics alias LoadTest.Service.Sync @poll_timeout 60_000 @spec create_transaction( non_neg_integer(), Account.addr_t(), Account.addr_t(), Account.addr_t(), non_neg_integer() ) :: {:ok, [binary()]} | {:error, map()} def create_transaction(amount_in_wei, input_address, output_address, currency \\ <<0::160>>, timeout \\ 120_000) do func = fn -> Metrics.run_with_metrics( fn -> do_create_transaction(amount_in_wei, input_address, output_address, currency) end, "WatcherInfo.create_transaction" ) end Sync.repeat_until_success(func, timeout, "Failed to create a transaction") end @spec submit_transaction(binary(), binary(), [binary()]) :: map() def submit_transaction(typed_data, sign_hash, private_keys) do signatures = Enum.map(private_keys, fn private_key -> sign_hash |> to_binary() |> signature_digest(private_key) |> Encoding.to_hex() end) typed_data_signed = Map.put_new(typed_data, "signatures", signatures) Sync.repeat_until_success( fn -> Metrics.run_with_metrics( fn -> submit_typed(typed_data_signed) end, "WatcherInfo.submit_typed" ) end, @poll_timeout, "Failed to submit transaction" ) end defp do_create_transaction(amount_in_wei, input_address, output_address, currency) do transaction = %WatcherInfoAPI.Model.CreateTransactionsBodySchema{ owner: Encoding.to_hex(input_address), payments: [ %WatcherInfoAPI.Model.TransactionCreatePayments{ amount: amount_in_wei, currency: Encoding.to_hex(currency), owner: Encoding.to_hex(output_address) } ], fee: %WatcherInfoAPI.Model.TransactionCreateFee{currency: Encoding.to_hex(currency)} } {:ok, response} = WatcherInfoAPI.Api.Transaction.create_transaction(LoadTest.Connection.WatcherInfo.client(), transaction) result = Jason.decode!(response.body)["data"] process_transaction_result(result) end defp submit_typed(typed_data_signed) do {:ok, response} = execute_submit_typed(typed_data_signed) decoded_response = Jason.decode!(response.body)["data"] case decoded_response do %{"messages" => %{"code" => "submit:utxo_not_found"}} -> {:error, :data_not_found} %{"messages" => %{"code" => "operation:service_unavailable"}} = error -> {:error, error} %{"txhash" => _} -> {:ok, decoded_response} end end defp execute_submit_typed(typed_data_signed) do WatcherInfoAPI.Api.Transaction.submit_typed(LoadTest.Connection.WatcherInfo.client(), typed_data_signed) end defp process_transaction_result(result) do case result do %{"code" => "create:client_error"} -> {:error, result} %{ "result" => "complete", "transactions" => [ %{ "sign_hash" => sign_hash, "typed_data" => typed_data, "txbytes" => txbytes } ] } -> {:ok, [sign_hash, typed_data, txbytes]} error -> {:error, error} end end defp signature_digest(hash_digest, private_key) do {:ok, {<>, recovery_id}} = ExSecp256k1.sign_compact(hash_digest, private_key) # EIP-155 # See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md base_recovery_id = 27 recovery_id = base_recovery_id + recovery_id <> end defp to_binary(hex) do hex |> String.replace_prefix("0x", "") |> String.upcase() |> Base.decode16!() end end ================================================ FILE: priv/perf/apps/load_test/lib/watcher_info/utxo.ex ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.w defmodule LoadTest.WatcherInfo.Utxo do @moduledoc """ Functions for retrieving utxos through WatcherInfo API. """ require Logger alias LoadTest.Ethereum.Account alias LoadTest.Service.Sync alias LoadTest.Utils.Encoding alias LoadTest.WatcherInfo.Client @poll_timeout 60_000 @spec get_utxos(Account.addr_t(), ExPlasma.Utxo.t() | nil | :empty) :: {:ok, [] | ExPlasma.Utxo.t()} | no_return def get_utxos(sender, utxo \\ nil) do Sync.repeat_until_success( fn -> fetch_utxos(sender, utxo) end, @poll_timeout, "Failed to fetch utxos" ) end defp fetch_utxos(sender, utxo) do address = Encoding.to_hex(sender.addr) case Client.get_utxos(address) do {:ok, result} -> find_utxo(result, utxo) other -> other end end defp find_utxo(decoded_response, nil) do {:ok, decoded_response} end defp find_utxo(%{"data" => []}, :empty) do {:ok, []} end defp find_utxo(decoded_response, :empty) do {:error, decoded_response} end defp find_utxo(decoded_response, utxo) do do_find_utxo(decoded_response, utxo) end defp do_find_utxo(response, utxo) do found_utxo = Enum.find(response["data"], fn %{ "amount" => amount, "blknum" => blknum, "currency" => currency, "oindex" => oindex, "otype" => otype, "owner" => owner, "txindex" => txindex } -> current_utxo = %ExPlasma.Utxo{ amount: amount, blknum: blknum, currency: Encoding.to_binary(currency), oindex: oindex, output_type: otype, owner: Encoding.to_binary(owner), txindex: txindex } current_utxo == utxo end) case found_utxo do nil -> {:error, response} _ -> {:ok, found_utxo} end end end ================================================ FILE: priv/perf/apps/load_test/mix.exs ================================================ defmodule LoadTest.MixProject do use Mix.Project def project do [ app: :load_test, version: "0.1.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps() ] end # Run "mix help compile.app" to learn about applications. def application do [ extra_applications: [:logger, :ex_secp256k1], mod: {LoadTest.Application, []} ] end # Specifies which paths to compile per environment. defp elixirc_paths(_), do: ["lib"] # Run "mix help deps" to learn about dependencies. defp deps do [ {:ex_rlp, "~> 0.5.3"}, {:ex_keccak, "~> 0.1.2"}, {:ex_abi, "~> 0.5.1"}, {:briefly, "~> 0.3"}, {:chaperon, "~> 0.3.1"}, {:statix, "~> 1.4"}, {:histogrex, "~> 0.0.5"}, {:tesla, "~> 1.3.0"}, {:httpoison, "~> 1.7", override: true}, {:hackney, git: "https://github.com/SergeTupchiy/hackney", ref: "2bf38f92f647de00c4850202f37d4eaab93ed834", override: true}, {:ex_plasma, git: "https://github.com/omgnetwork/ex_plasma", ref: "5e94c4fc82dbf26cb457b30911505ec45ec534ea", override: true}, {:ex_secp256k1, "~> 0.1.2"}, {:telemetry, "~> 0.4.1"}, {:fake_server, "~> 2.1", only: :test}, {:watcher_info_api, in_umbrella: true}, {:watcher_security_critical_api, in_umbrella: true}, {:child_chain_api, in_umbrella: true} ] end end ================================================ FILE: priv/perf/apps/load_test/test/load_test/runner/childchain_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.ChildChainTest do @moduledoc """ child chain load test """ use ExUnit.Case @tag timeout: 6_000_000 test "childchain test" do Chaperon.run_load_test(LoadTest.Runner.ChildChainTransactions, print_results: true) end end ================================================ FILE: priv/perf/apps/load_test/test/load_test/runner/smoke_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.SmokeTest do @moduledoc """ Runs a smoke test for the perf tests setup """ use ExUnit.Case test "smoke test" do Chaperon.run_load_test(LoadTest.Runner.Smoke, print_results: false) end end ================================================ FILE: priv/perf/apps/load_test/test/load_test/runner/standard_exit_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.StandardExitTest do @moduledoc """ Runs a smoke test for utxos load test """ use ExUnit.Case @tag timeout: 6_000_000 test "should run standard exit load test" do Chaperon.run_load_test(LoadTest.Runner.StandardExits, print_results: true) end end ================================================ FILE: priv/perf/apps/load_test/test/load_test/runner/utxos_load_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.UtxosLoadTest do @moduledoc """ Runs a smoke test for utxos load test """ use ExUnit.Case @tag timeout: 6_000_000 test "smoke test - should run utxos load test" do Chaperon.run_load_test(LoadTest.Runner.UtxosLoad, print_results: true) end end ================================================ FILE: priv/perf/apps/load_test/test/load_test/runner/watcher_info_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Runner.WatcherInfoTest do @moduledoc """ watcher info load test """ use ExUnit.Case @tag timeout: 6_000_000 test "watcher info test" do Chaperon.run_load_test(LoadTest.Runner.WatcherInfoAccountApi, print_results: true) end end ================================================ FILE: priv/perf/apps/load_test/test/load_test/service/datadog/api_test.exs ================================================ # Copyright 2019-2020 OMG Network Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule LoadTest.Service.Datadog.APITest do use ExUnit.Case require FakeServer alias FakeServer.Response alias LoadTest.Service.Datadog.API @server_name :datadog @event %{ "alert_type" => "error", "title" => "[Triggered] WatcherInfo.get_balances takes more than 40ms", "url" => "/event/event?id=5718361627942929581", "text" => "tag", "date_happened" => 1_605_191_364 } setup do {:ok, server} = FakeServer.start(@server_name) {:ok, port} = FakeServer.port(@server_name) fakeserver_address = "http://localhost:" <> to_string(port) <> "/" datadog_params = Application.get_env(:load_test, :datadog) new_datadog_params = Keyword.put(datadog_params, :api_url, fakeserver_address) Application.put_env(:load_test, :datadog, new_datadog_params) FakeServer.put_route(@server_name, "/api/v1/events", Response.new(200, Jason.encode!(%{"events" => [@event]}))) on_exit(fn -> FakeServer.stop(server) Application.put_env(:load_test, :datadog, datadog_params) end) :ok end describe "assert_metrics/3" do test "fetches events from datadog" do current_time = DateTime.utc_now() assert {:error, [event]} = API.assert_metrics("tag", current_time, current_time) assert @event["title"] == event["title"] assert "https://app.datadoghq.com" <> @event["url"] == event["url"] end end end ================================================ FILE: priv/perf/apps/load_test/test/test_helper.exs ================================================ {:ok, _} = Application.ensure_all_started(:fake_server) ExUnit.start(exclude: [:skip]) ================================================ FILE: priv/perf/config/.credo.exs ================================================ # This file contains the configuration for Credo and you are probably reading # this after creating it with `mix credo.gen.config`. # # If you find anything wrong or unclear in this file, please report an # issue on GitHub: https://github.com/rrrene/credo/issues # %{ # # You can have as many configs as you like in the `configs:` field. configs: [ %{ # # Run any exec using `mix credo -C `. If no exec name is given # "default" is used. # name: "default", # # These are the files included in the analysis: files: %{ # # You can give explicit globs or simply directories. # In the latter case `**/*.{ex,exs}` will be used. # included: ["lib/", "src/", "test/", "web/", "apps/", "config/", "mix.exs"], excluded: [ ~r"/_build/", ~r"/deps/", ~r"/node_modules/", ~r"/results/", ~r"/apps/child_chain_api", ~r"/apps/watcher_security_critical_api", ~r"/apps/watcher_info_api" ] }, # # If you create your own checks, you must specify the source files for # them here, so they can be loaded by Credo before running the analysis. # requires: ["config/credo/license_header.ex"], # # If you want to enforce a style guide and need a more traditional linting # experience, you can change `strict` to `true` below: # strict: true, # # If you want to use uncolored output by default, you can change `color` # to `false` below: # color: true, # # You can customize the parameters of any check by adding a second element # to the tuple. # # To disable a check put `false` as second element: # # {Credo.Check.Design.DuplicatedCode, false} # checks: [ {Credo.Check.Refactor.MapInto, false}, # custom checks {Credo.Check.Custom.LicenseHeader}, # ## Consistency Checks # {Credo.Check.Consistency.ExceptionNames, []}, {Credo.Check.Consistency.LineEndings, []}, {Credo.Check.Consistency.ParameterPatternMatching, []}, {Credo.Check.Consistency.SpaceAroundOperators, []}, {Credo.Check.Consistency.SpaceInParentheses, []}, {Credo.Check.Consistency.TabsOrSpaces, []}, # ## Design Checks # # You can customize the priority of any check # Priority values are: `low, normal, high, higher` # {Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 3, if_called_more_often_than: 1]}, # You can also customize the exit_status of each check. # If you don't want TODO comments to cause `mix credo` to fail, just # set this value to 0 (zero). # {Credo.Check.Design.TagTODO, [exit_status: 0]}, {Credo.Check.Design.TagFIXME, []}, # ## Readability Checks # {Credo.Check.Readability.AliasOrder, []}, {Credo.Check.Readability.FunctionNames, []}, {Credo.Check.Readability.LargeNumbers, []}, {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, {Credo.Check.Readability.ModuleAttributeNames, []}, {Credo.Check.Readability.ModuleDoc, []}, {Credo.Check.Readability.ModuleNames, []}, {Credo.Check.Readability.ParenthesesInCondition, []}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false}, {Credo.Check.Readability.PredicateFunctionNames, []}, {Credo.Check.Readability.PreferImplicitTry, []}, {Credo.Check.Readability.RedundantBlankLines, []}, {Credo.Check.Readability.Semicolons, []}, {Credo.Check.Readability.SpaceAfterCommas, []}, {Credo.Check.Readability.StringSigils, []}, {Credo.Check.Readability.TrailingBlankLine, []}, {Credo.Check.Readability.TrailingWhiteSpace, []}, {Credo.Check.Readability.VariableNames, []}, # ## Refactoring Opportunities # {Credo.Check.Refactor.CondStatements, []}, {Credo.Check.Refactor.CyclomaticComplexity, []}, {Credo.Check.Refactor.FunctionArity, []}, {Credo.Check.Refactor.LongQuoteBlocks, []}, {Credo.Check.Refactor.MatchInCondition, []}, {Credo.Check.Refactor.NegatedConditionsInUnless, []}, {Credo.Check.Refactor.NegatedConditionsWithElse, []}, {Credo.Check.Refactor.Nesting, []}, {Credo.Check.Refactor.PipeChainStart, false}, {Credo.Check.Refactor.UnlessWithElse, []}, # ## Warnings # {Credo.Check.Warning.BoolOperationOnSameValues, []}, {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, {Credo.Check.Warning.IExPry, []}, {Credo.Check.Warning.IoInspect, []}, {Credo.Check.Warning.LazyLogging, false}, {Credo.Check.Warning.OperationOnSameValues, []}, {Credo.Check.Warning.OperationWithConstantResult, []}, {Credo.Check.Warning.RaiseInsideRescue, []}, {Credo.Check.Warning.UnusedEnumOperation, []}, {Credo.Check.Warning.UnusedFileOperation, []}, {Credo.Check.Warning.UnusedKeywordOperation, []}, {Credo.Check.Warning.UnusedListOperation, []}, {Credo.Check.Warning.UnusedPathOperation, []}, {Credo.Check.Warning.UnusedRegexOperation, []}, {Credo.Check.Warning.UnusedStringOperation, []}, {Credo.Check.Warning.UnusedTupleOperation, []}, # # Controversial and experimental checks (opt-in, just remove `, false`) # {Credo.Check.Readability.SinglePipe, []}, {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, {Credo.Check.Design.DuplicatedCode, false}, {Credo.Check.Readability.Specs, false}, {Credo.Check.Refactor.ABCSize, false}, {Credo.Check.Refactor.AppendSingleItem, false}, {Credo.Check.Refactor.DoubleBooleanNegation, false}, {Credo.Check.Refactor.VariableRebinding, false}, {Credo.Check.Warning.MapGetUnsafePass, false}, {Credo.Check.Warning.UnsafeToAtom, false} # # Custom checks can be created using `mix credo.gen.check`. # ] } ] } ================================================ FILE: priv/perf/config/config.exs ================================================ use Mix.Config # Better adapter for tesla. # default httpc would fail when doing post request without param. # https://github.com/googleapis/elixir-google-api/issues/26#issuecomment-360209019 config :tesla, adapter: Tesla.Adapter.Hackney ethereum_client_timeout_ms = 20_000 config :ethereumex, http_options: [recv_timeout: ethereum_client_timeout_ms], url: System.get_env("ETHEREUM_RPC_URL") || "http://localhost:8545" config :load_test, pool_size: 5000, max_connection: 5000, retry_sleep: "RETRY_SLEEP" |> System.get_env("1000") |> String.to_integer(), child_chain_url: System.get_env("CHILD_CHAIN_URL") || "http://localhost:9656", watcher_security_url: System.get_env("WATCHER_SECURITY_URL") || "http://localhost:7434", watcher_info_url: System.get_env("WATCHER_INFO_URL") || "http://localhost:7534", faucet_private_key: System.get_env("LOAD_TEST_FAUCET_PRIVATE_KEY") || "0xd885a307e35738f773d8c9c63c7a3f3977819274638d04aaf934a1e1158513ce", eth_vault_address: System.get_env("CONTRACT_ADDRESS_ETH_VAULT"), contract_address_payment_exit_game: System.get_env("CONTRACT_ADDRESS_PAYMENT_EXIT_GAME"), child_block_interval: "CHILD_BLOCK_INTERVAL" |> System.get_env("1000") |> String.to_integer(), contract_address_plasma_framework: System.get_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK"), erc20_vault_address: System.get_env("CONTRACT_ADDRESS_ERC20_VAULT"), test_currency: "0x0000000000000000000000000000000000000000", faucet_deposit_amount: trunc(:math.pow(10, 14)), fee_amount: "FEE_AMOUNT" |> System.get_env("75") |> String.to_integer(), deposit_finality_margin: "DEPOSIT_FINALITY_MARGIN" |> System.get_env("10") |> String.to_integer(), gas_price: "GAS_PRICE" |> System.get_env("2000000000") |> String.to_integer(), record_metrics: true config :ex_plasma, eip_712_domain: [ name: "OMG Network", salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", verifying_contract: System.get_env("CONTRACT_ADDRESS_PLASMA_FRAMEWORK"), version: "1" ] config :logger, :console, format: "$date $time [$level] $metadata⋅$message⋅\n", discard_threshold: 2000, metadata: [:module, :function, :request_id, :trace_id, :span_id] config :statix, host: "localhost", port: 8125, tags: [System.get_env("STATIX_TAG")] config :load_test, :datadog, api_key: System.get_env("DD_API_KEY"), app_key: System.get_env("DD_APP_KEY"), api_url: "https://app.datadoghq.com/" import_config "#{Mix.env()}.exs" ================================================ FILE: priv/perf/config/dev.exs ================================================ use Mix.Config ================================================ FILE: priv/perf/config/stress.exs ================================================ use Mix.Config # Target tps. # Note that this is only a rough estimate and depends on the response time from the childchain. # If the childchain is under high load, tps will drop. tps = 100 # Must be >= than tps, _should_ be at least 2x tps concurrency = 200 # Minutes that the test should run. # Again, a rough estimate - if the childchain is under high load the test will take longer to finish test_duration = 1 tx_delay = trunc(concurrency / tps) * 1000 tx_per_session = trunc(test_duration * 60 / trunc(tx_delay / 1000)) config :load_test, utxo_load_test_config: %{ concurrent_sessions: 1, utxos_to_create_per_session: 30, transactions_per_session: 4 }, childchain_transactions_test_config: %{ concurrent_sessions: concurrency, transactions_per_session: tx_per_session, transaction_delay: tx_delay }, watcher_info_test_config: %{ concurrent_sessions: 100, iterations: 10, merge_scenario_sessions: true }, standard_exit_test_config: %{ concurrent_sessions: 1, exits_per_session: 10 } ================================================ FILE: priv/perf/config/test.exs ================================================ use Mix.Config config :ethereumex, url: "http://localhost:8545" config :load_test, child_chain_url: "http://localhost:9656", watcher_security_url: "http://localhost:7434", watcher_info_url: "http://localhost:7534", faucet_deposit_amount: trunc(:math.pow(10, 18) * 10), # fee testing setup: https://github.com/omgnetwork/fee-rules-public/blob/master/fee_rules.json fee_amount: 75, utxo_load_test_config: %{ concurrent_sessions: 10, utxos_to_create_per_session: 5, transactions_per_session: 5 }, childchain_transactions_test_config: %{ concurrent_sessions: 10, transactions_per_session: 10 }, watcher_info_test_config: %{ concurrent_sessions: 2, iterations: 2, merge_scenario_sessions: true }, standard_exit_test_config: %{ concurrent_sessions: 1, exits_per_session: 4 }, record_metrics: false ================================================ FILE: priv/perf/mix.exs ================================================ defmodule Perf.MixProject do use Mix.Project def project do [ app: :perf, apps_path: "apps", start_permanent: Mix.env() == :prod, deps: deps() ] end defp deps do [ {:credo, "~> 1.5", only: [:dev, :test], runtime: false} ] end end ================================================ FILE: priv/perf/scripts/generate_api_client.sh ================================================ #!/bin/bash # Script that generates the elixir client code to communicate with child chain and watcher services. # Auto generated elixir client would be put directly under apps/ directory. # # The client come with a default base url from the swagger spec file. # To connect to different environment, override the middleware in runtime: https://github.com/teamon/tesla#runtime-middleware set -e echo "Generate api client script starts..." echo "------------------------------------------------------" echo "Cleaning up .api_specs/ and generated client codes..." echo "------------------------------------------------------" rm -rf apps/child_chain_api rm -rf apps/watcher_security_critical_api rm -rf apps/watcher_info_api echo "------------------------------------------------------" echo "Generating childchain clients..." echo "------------------------------------------------------" docker run --rm \ -v ${PWD}/apps:/apps \ --user $(id -u):$(id -g) \ openapitools/openapi-generator-cli generate \ -i https://raw.githubusercontent.com/omgnetwork/omg-childchain-v1/master/apps/omg_child_chain_rpc/priv/swagger/operator_api_specs.yaml \ -g elixir \ -o /apps/child_chain_api/ echo "------------------------------------------------------" echo "Generating watcher security clients..." echo "------------------------------------------------------" docker run --rm \ -v ${PWD}/apps:/apps \ -v ${PWD}/../../apps/omg_watcher_rpc/priv/swagger/:/swagger \ --user $(id -u):$(id -g) \ openapitools/openapi-generator-cli generate \ -i /swagger/security_critical_api_specs.yaml \ -g elixir \ -o /apps/watcher_security_critical_api/ echo "------------------------------------------------------" echo "Generating watcher info clients..." echo "------------------------------------------------------" docker run --rm \ -v ${PWD}/apps:/apps \ -v ${PWD}/../../apps/omg_watcher_rpc/priv/swagger/:/swagger \ --user $(id -u):$(id -g) \ openapitools/openapi-generator-cli generate \ -i /swagger/info_api_specs.yaml \ -g elixir \ -o /apps/watcher_info_api/ ================================================ FILE: rel/env.sh.eex ================================================ #!/bin/sh # Sets and enables heart (recommended only in daemon mode) # case $RELEASE_COMMAND in # daemon*) # HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" # export HEART_COMMAND # export ELIXIR_ERL_OPTIONS="-heart" # ;; # *) # ;; # esac # Set the release to work across nodes. If using the long name format like # the one below (my_app@127.0.0.1), you need to also uncomment the # RELEASE_DISTRIBUTION variable below. Must be "sname", "name" or "none". export RELEASE_DISTRIBUTION=name export RELEASE_NODE=<%= @release.name %>@$NODE_HOST ================================================ FILE: rootfs/watcher_entrypoint ================================================ #!/bin/execlineb -S0 with-contenv cd /app s6-setuidgid watcher ## NODE_HOST ## ## Used in clustering; combine with an app name, this is used for identifying ## and connecting each nodes. By default use the container hostname. ## backtick -n default_host { s6-hostname } importas -iu default_host default_host importas -D $default_host NODE_HOST NODE_HOST s6-env NODE_HOST=$NODE_HOST ## ERLANG_COOKIE ## ## Used in clustering; all nodes in the cluster is required to have the same ## Erlang Cookie set. Randomly generated default cookie is only useful for ## the single instance scenario. ## backtick -n default_cookie { pipeline { s6-head -c 6 /dev/urandom } foreground { base64 } } importas -iu default_cookie default_cookie importas -D $default_cookie ERLANG_COOKIE ERLANG_COOKIE s6-env ERLANG_COOKIE=$ERLANG_COOKIE ## NODE_DNS ## ## Used in clustering; by default wallet is configured to discover nodes via ## DNS. Registering nodes to the cluster is a responsibility of cluster tools ## e.g. Kubernetes. ## importas -D localhost NODE_DNS NODE_DNS s6-env NODE_DNS=$NODE_DNS ## HOME ## ## Home is required to be set since some tools use it to create e.g. cache. ## By default Watcher will run with a limited privileges so HOME need to be ## somewhere writable. ## s6-env HOME=/app ## REPLACE_OS_VARS ## ## Used by Distillery to replace environment variables in settings to the ## value of environment variable. ## s6-env REPLACE_OS_VARS=yes ## Known commands ## ifelse { test $1 == "full_local" } { /bin/bash -c "/app/bin/watcher eval \"OMG.DB.ReleaseTasks.InitKeyValueDB.run()\" && /app/bin/watcher start" } ifelse { test $1 == "start_iex" } { /app/bin/watcher $@ } ifelse { test $1 == "start" } { /app/bin/watcher $@ } ## Fallback; if we're not running the known command, just run it as is. ## This is to allow e.g. administrator attaching a shell into the container. ## exec $@ ================================================ FILE: rootfs/watcher_info_entrypoint ================================================ #!/bin/execlineb -S0 with-contenv cd /app s6-setuidgid watcher ## NODE_HOST ## ## Used in clustering; combine with an app name, this is used for identifying ## and connecting each nodes. By default use the container hostname. ## backtick -n default_host { s6-hostname } importas -iu default_host default_host importas -D $default_host NODE_HOST NODE_HOST s6-env NODE_HOST=$NODE_HOST ## ERLANG_COOKIE ## ## Used in clustering; all nodes in the cluster is required to have the same ## Erlang Cookie set. Randomly generated default cookie is only useful for ## the single instance scenario. ## backtick -n default_cookie { pipeline { s6-head -c 6 /dev/urandom } foreground { base64 } } importas -iu default_cookie default_cookie importas -D $default_cookie ERLANG_COOKIE ERLANG_COOKIE s6-env ERLANG_COOKIE=$ERLANG_COOKIE ## NODE_DNS ## ## Used in clustering; by default wallet is configured to discover nodes via ## DNS. Registering nodes to the cluster is a responsibility of cluster tools ## e.g. Kubernetes. ## importas -D localhost NODE_DNS NODE_DNS s6-env NODE_DNS=$NODE_DNS ## HOME ## ## Home is required to be set since some tools use it to create e.g. cache. ## By default Watcher will run with a limited privileges so HOME need to be ## somewhere writable. ## s6-env HOME=/app ## REPLACE_OS_VARS ## ## Used by Distillery to replace environment variables in settings to the ## value of environment variable. ## s6-env REPLACE_OS_VARS=yes ## Known commands ## ifelse { test $1 == "full_local" } { /bin/bash -c "/app/bin/watcher_info eval \"OMG.WatcherInfo.ReleaseTasks.InitPostgresqlDB.migrate()\" && \ /app/bin/watcher_info eval \"OMG.DB.ReleaseTasks.InitKeyValueDB.run()\" && \ /app/bin/watcher_info start" } ifelse { test $1 == "start_iex" } { /app/bin/watcher_info $@ } ifelse { test $1 == "start" } { /app/bin/watcher_info $@ } ## Fallback; if we're not running the known command, just run it as is. ## This is to allow e.g. administrator attaching a shell into the container. ## exec $@ ================================================ FILE: snapshot_reorg.env ================================================ SNAPSHOT=https://storage.googleapis.com/circleci-docker-artifacts/data-elixir-omg-tester-plasma-deployer-dev-10cafba-MIN_EXIT_PERIOD-120-PLASMA_CONTRACTS_SHA-a69c763f239b81c5eb46b0bbdef3459f764360dc-reorg.tar.gz CONTRACT_SHA=a69c763f239b81c5eb46b0bbdef3459f764360dc ================================================ FILE: snapshots.env ================================================ SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_20=https://storage.googleapis.com/circleci-docker-artifacts/data-elixir-omg-tester-plasma-deployer-stable-20201207-MIN_EXIT_PERIOD-20-PLASMA_CONTRACTS_SHA-b3a5c8d5232edfab8617f6939733b08b67863c8a.tar.gz SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120=https://storage.googleapis.com/circleci-docker-artifacts/data-elixir-omg-tester-plasma-deployer-stable-20201207-MIN_EXIT_PERIOD-120-PLASMA_CONTRACTS_SHA-b3a5c8d5232edfab8617f6939733b08b67863c8a.tar.gz SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_240=https://storage.googleapis.com/circleci-docker-artifacts/data-elixir-omg-tester-plasma-deployer-stable-20201207-MIN_EXIT_PERIOD-240-PLASMA_CONTRACTS_SHA-b3a5c8d5232edfab8617f6939733b08b67863c8a.tar.gz CONTRACT_SHA=b3a5c8d5232edfab8617f6939733b08b67863c8a