├── .circleci └── config.yml ├── .credo.exs ├── .formatter.exs ├── .gitignore ├── .tool-versions ├── LICENSE ├── Makefile ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── dialyzer.ignore-warnings ├── docker-compose.conformance.yml ├── docker-compose.yml ├── docker └── migrations │ ├── 1_deploy_712_lib_mock.js │ └── 2_deploy_exit_id_wrapper.js ├── lib ├── ex_plasma.ex └── ex_plasma │ ├── block.ex │ ├── builder.ex │ ├── configuration.ex │ ├── configuration │ └── validator.ex │ ├── crypto.ex │ ├── encoding.ex │ ├── in_flight_exit.ex │ ├── merkle.ex │ ├── output.ex │ ├── output │ ├── position.ex │ ├── position │ │ └── position_validator.ex │ └── type │ │ ├── abstract_payment.ex │ │ ├── abstract_payment │ │ └── abstract_payment_validator.ex │ │ ├── empty.ex │ │ ├── fee.ex │ │ └── payment_v1.ex │ ├── signature.ex │ ├── transaction.ex │ ├── transaction │ ├── signed.ex │ ├── type │ │ ├── fee.ex │ │ ├── fee │ │ │ └── fee_validator.ex │ │ ├── payment_v1.ex │ │ └── payment_v1 │ │ │ └── payment_v1_validator.ex │ ├── type_mapper.ex │ └── witness.ex │ ├── typed_data.ex │ ├── typed_data │ ├── output.ex │ └── transaction.ex │ └── utils │ └── rlp_decoder.ex ├── mix.exs ├── mix.lock ├── plasma-contract.Dockerfile └── test ├── conformance ├── in_flight_exit_test.exs └── signatures_test.exs ├── ex_plasma ├── block_test.exs ├── builder_test.exs ├── configuration │ └── validator_test.exs ├── configuration_test.exs ├── crypto_test.exs ├── encoding_test.exs ├── in_flight_exit_test.exs ├── merkle_test.exs ├── output │ ├── position │ │ └── position_validator_test.exs │ ├── position_test.exs │ └── types │ │ ├── abstract_payment │ │ └── abstract_payment_validator_test.exs │ │ └── abstract_payment_test.exs ├── output_test.exs ├── signature_test.exs ├── transaction │ ├── signed_test.exs │ ├── type │ │ ├── fee │ │ │ └── fee_validator_test.exs │ │ ├── fee_test.exs │ │ ├── payment_v1 │ │ │ └── payment_v1_validator_test.exs │ │ └── payment_v1_test.exs │ └── witness_test.exs ├── transaction_test.exs ├── typed_data │ ├── output_test.exs │ └── transaction_test.exs └── utils │ └── rlp_decoder_test.exs ├── ex_plasma_test.exs ├── support └── entity.ex └── test_helper.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | commands: 3 | add_rust_to_path: 4 | description: "Add path to PATH env var" 5 | steps: 6 | - run: 7 | name: Add rust to PATH env 8 | command: echo 'export PATH=~/.cargo/bin/:$PATH' >> $BASH_ENV 9 | install_rust: 10 | description: "Install Rust" 11 | steps: 12 | - run: 13 | name: Install Rust 14 | command: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 15 | - add_rust_to_path 16 | 17 | jobs: 18 | build: 19 | working_directory: ~/ex_plasma 20 | docker: 21 | - image: circleci/elixir:1.10.2 22 | environment: 23 | MIX_ENV=test 24 | steps: 25 | - checkout 26 | - install_rust 27 | - run: mix local.rebar --force && mix local.hex --force 28 | - restore_cache: 29 | key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 30 | - run: 31 | command: mix do deps.get, compile --warnings-as-errors --force 32 | no_output_timeout: 20m 33 | - save_cache: 34 | key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 35 | paths: 36 | - "deps" 37 | - "_build" 38 | - "~/.cargo/" 39 | test: 40 | working_directory: ~/ex_plasma 41 | docker: 42 | - image: circleci/elixir:1.10.2 43 | environment: 44 | MIX_ENV=test 45 | steps: 46 | - checkout 47 | - add_rust_to_path 48 | - run: mix local.rebar --force && mix local.hex --force 49 | - restore_cache: 50 | key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 51 | - run: mix do deps.get, test --exclude skip 52 | - run: mix coveralls.circle 53 | credo: 54 | working_directory: ~/ex_plasma 55 | docker: 56 | - image: circleci/elixir:1.10.2 57 | environment: 58 | MIX_ENV=test 59 | steps: 60 | - checkout 61 | - run: mix local.rebar --force && mix local.hex --force 62 | - restore_cache: 63 | key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 64 | - run: mix do deps.get, credo, format --check-formatted --dry-run 65 | conformance: 66 | machine: 67 | image: ubuntu-1604:201903-01 68 | working_directory: ~/ex_plasma 69 | steps: 70 | - checkout 71 | - install_rust 72 | - run: 73 | name: Install Erlang and Elixir 74 | command: | 75 | set -e 76 | wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb 77 | sudo dpkg -i erlang-solutions_2.0_all.deb 78 | sudo apt-get update 79 | sudo apt-get install esl-erlang=1:21.3.8.10-1 elixir=1.10.2-1 80 | - run: mix local.rebar --force && mix local.hex --force 81 | - run: make up-mocks 82 | - restore_cache: 83 | key: v1-conformance-deps-cache-{{ checksum "mix.lock" }} 84 | - run: mix do deps.get, compile --warnings-as-errors --ignore-module-conflict --force 85 | - save_cache: 86 | key: v1-conformance-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 87 | paths: 88 | - "deps" 89 | - "_build" 90 | - "~/.cargo/" 91 | - run: mix test --only conformance 92 | 93 | dialyzer: 94 | working_directory: ~/ex_plasma 95 | docker: 96 | - image: circleci/elixir:1.10.2 97 | environment: 98 | MIX_ENV=dev 99 | steps: 100 | - checkout 101 | - install_rust 102 | - run: mix local.rebar --force && mix local.hex --force 103 | - restore_cache: 104 | key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 105 | - restore_cache: 106 | key: v1-dialyzer-plts-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 107 | - run: 108 | name: Unpack PLT cache 109 | command: | 110 | mkdir -p _build/dev 111 | cp plts/dialyxir*.plt _build/dev/ || true 112 | mkdir -p ~/.mix 113 | cp plts/dialyxir*.plt ~/.mix/ || true 114 | - run: 115 | command: mix do deps.get, dialyzer --halt-exit-status 116 | no_output_timeout: 30m 117 | - run: 118 | name: Pack PLT cache 119 | command: | 120 | mkdir -p plts 121 | cp _build/dev/dialyxir*.plt plts/ 122 | cp ~/.mix/dialyxir*.plt plts/ 123 | - save_cache: 124 | key: v1-dialyzer-plts-cache-{{ .Branch }}-{{ checksum "mix.lock" }} 125 | paths: 126 | - "plts" 127 | workflows: 128 | version: 2 129 | build-test-lint: 130 | jobs: 131 | - build 132 | - test: 133 | requires: 134 | - build 135 | - credo: 136 | requires: 137 | - build 138 | - dialyzer: 139 | requires: 140 | - build 141 | - conformance 142 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: [ 68 | # 69 | ## Consistency Checks 70 | # 71 | {Credo.Check.Consistency.ExceptionNames, []}, 72 | {Credo.Check.Consistency.LineEndings, []}, 73 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 74 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 75 | {Credo.Check.Consistency.SpaceInParentheses, []}, 76 | {Credo.Check.Consistency.TabsOrSpaces, []}, 77 | 78 | # 79 | ## Design Checks 80 | # 81 | # You can customize the priority of any check 82 | # Priority values are: `low, normal, high, higher` 83 | # 84 | {Credo.Check.Design.AliasUsage, 85 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 86 | # You can also customize the exit_status of each check. 87 | # If you don't want TODO comments to cause `mix credo` to fail, just 88 | # set this value to 0 (zero). 89 | # 90 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 91 | {Credo.Check.Design.TagFIXME, []}, 92 | 93 | # 94 | ## Readability Checks 95 | # 96 | {Credo.Check.Readability.AliasOrder, []}, 97 | {Credo.Check.Readability.FunctionNames, []}, 98 | {Credo.Check.Readability.LargeNumbers, []}, 99 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 100 | {Credo.Check.Readability.ModuleAttributeNames, []}, 101 | {Credo.Check.Readability.ModuleDoc, []}, 102 | {Credo.Check.Readability.ModuleNames, []}, 103 | {Credo.Check.Readability.ParenthesesInCondition, []}, 104 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, [parens: true]}, 105 | {Credo.Check.Readability.PredicateFunctionNames, []}, 106 | {Credo.Check.Readability.PreferImplicitTry, []}, 107 | {Credo.Check.Readability.RedundantBlankLines, []}, 108 | {Credo.Check.Readability.Semicolons, []}, 109 | {Credo.Check.Readability.SpaceAfterCommas, []}, 110 | {Credo.Check.Readability.StringSigils, []}, 111 | {Credo.Check.Readability.TrailingBlankLine, []}, 112 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 113 | # TODO: enable by default in Credo 1.1 114 | {Credo.Check.Readability.UnnecessaryAliasExpansion, false}, 115 | {Credo.Check.Readability.VariableNames, []}, 116 | 117 | # 118 | ## Refactoring Opportunities 119 | # 120 | {Credo.Check.Refactor.CondStatements, []}, 121 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 122 | {Credo.Check.Refactor.FunctionArity, []}, 123 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 124 | {Credo.Check.Refactor.MapInto, false}, 125 | {Credo.Check.Refactor.MatchInCondition, []}, 126 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 127 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 128 | {Credo.Check.Refactor.Nesting, []}, 129 | {Credo.Check.Refactor.UnlessWithElse, []}, 130 | {Credo.Check.Refactor.WithClauses, []}, 131 | 132 | # 133 | ## Warnings 134 | # 135 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 136 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 137 | {Credo.Check.Warning.IExPry, []}, 138 | {Credo.Check.Warning.IoInspect, []}, 139 | {Credo.Check.Warning.LazyLogging, false}, 140 | {Credo.Check.Warning.MixEnv, false}, 141 | {Credo.Check.Warning.OperationOnSameValues, []}, 142 | {Credo.Check.Warning.OperationWithConstantResult, []}, 143 | {Credo.Check.Warning.RaiseInsideRescue, []}, 144 | {Credo.Check.Warning.UnusedEnumOperation, []}, 145 | {Credo.Check.Warning.UnusedFileOperation, []}, 146 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 147 | {Credo.Check.Warning.UnusedListOperation, []}, 148 | {Credo.Check.Warning.UnusedPathOperation, []}, 149 | {Credo.Check.Warning.UnusedRegexOperation, []}, 150 | {Credo.Check.Warning.UnusedStringOperation, []}, 151 | {Credo.Check.Warning.UnusedTupleOperation, []}, 152 | 153 | # 154 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 155 | 156 | # 157 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 158 | # 159 | {Credo.Check.Readability.StrictModuleLayout, []}, 160 | {Credo.Check.Readability.SinglePipe, []}, 161 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 162 | {Credo.Check.Consistency.UnusedVariableNames, false}, 163 | {Credo.Check.Design.DuplicatedCode, false}, 164 | {Credo.Check.Readability.AliasAs, false}, 165 | {Credo.Check.Readability.MultiAlias, false}, 166 | {Credo.Check.Readability.Specs, false}, 167 | {Credo.Check.Refactor.ABCSize, false}, 168 | {Credo.Check.Refactor.AppendSingleItem, false}, 169 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 170 | {Credo.Check.Refactor.ModuleDependencies, false}, 171 | {Credo.Check.Refactor.NegatedIsNil, []}, 172 | {Credo.Check.Refactor.PipeChainStart, []}, 173 | {Credo.Check.Refactor.VariableRebinding, []}, 174 | {Credo.Check.Warning.MapGetUnsafePass, false}, 175 | {Credo.Check.Warning.UnsafeToAtom, []} 176 | 177 | # 178 | # Custom checks can be created using `mix credo.gen.check`. 179 | # 180 | ] 181 | } 182 | ] 183 | } 184 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 120 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ex_plasma-*.tar 24 | 25 | # Elixirls data directory 26 | .elixir_ls 27 | 28 | .history -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.11.2 2 | erlang 23.1.4 3 | rust 1.46.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2019] [OmiseGO] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | mix test 5 | 6 | logs: 7 | docker-compose logs -f 8 | up: 9 | docker-compose up -d 10 | up-mocks: 11 | docker-compose -f docker-compose.yml -f docker-compose.conformance.yml up -d ganache mock-contracts 12 | down: 13 | docker-compose down 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ExPlasma 2 | [![Build Status](https://circleci.com/gh/omgnetwork/ex_plasma.svg?style=svg)](https://circleci.com/gh/omgnetwork/ex_plasma) 3 | 4 | ExPlasma is an Elixir library for encoding, decoding and validating transactions used for the OMG Network Plasma contracts. 5 | 6 | ## Installation 7 | 8 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 9 | by adding `ex_plasma` to your list of dependencies in `mix.exs`: 10 | 11 | ```elixir 12 | def deps do 13 | [ 14 | {:ex_plasma, "~> 0.2.0"} 15 | ] 16 | end 17 | ``` 18 | 19 | You will also need to specify some configurations in your [config/config.exs](): 20 | 21 | ```elixir 22 | config :ex_plasma, 23 | eip_712_domain: %{ 24 | name: "OMG Network", 25 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 26 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 27 | version: "1" 28 | } 29 | ``` 30 | 31 | ## Setup 32 | 33 | `ExPlasma` requires Rust to be installed because it uses Rust NIFs for keccak hash and secp256k1. 34 | 35 | 1. Clone the repo to your desktop `git@github.com:omgnetwork/ex_plasma.git` 36 | 2. Run `mix compile` in your terminal. 37 | 3. If there are any unavailable dependencies, run `mix deps.get`. 38 | 39 | ## Usage 40 | 41 | To build a transaction use `ExPlasma.Builder` module: 42 | 43 | ``` elixir 44 | {:ok, txn} = 45 | ExPlasma.payment_v1() 46 | |> ExPlasma.Builder.new() 47 | |> ExPlasma.Builder.add_input(blknum: 1, txindex: 0, oindex: 0) 48 | |> ExPlasma.Builder.add_output(output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1}) 49 | |> ExPlasma.Builder.sign(["0x79298b0292bbfa9b15705c56b6133201c62b798f102d7d096d31d7637f9b2382"]) 50 | {:ok, 51 | %ExPlasma.Transaction{ 52 | inputs: [ 53 | %ExPlasma.Output{ 54 | output_data: nil, 55 | output_id: %{blknum: 1, oindex: 0, txindex: 0}, 56 | output_type: nil 57 | } 58 | ], 59 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 61 | nonce: nil, 62 | outputs: [ 63 | %ExPlasma.Output{ 64 | output_data: %{ 65 | amount: 1, 66 | output_guard: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 67 | 0, 1>>, 68 | token: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 69 | }, 70 | output_id: nil, 71 | output_type: 1 72 | } 73 | ], 74 | sigs: [ 75 | <<236, 177, 165, 5, 109, 208, 210, 116, 68, 176, 199, 17, 168, 29, 30, 198, 76 | 77, 45, 233, 147, 149, 38, 93, 136, 24, 98, 53, 218, 52, 177, 200, 127, 77 | 26, 6, 138, 17, 36, 52, 97, 152, 240, 222, ...>> 78 | ], 79 | tx_data: 0, 80 | tx_type: 1, 81 | witnesses: [] 82 | }} 83 | ``` 84 | 85 | You can encode a transaction using `ExPlasma.encode/2`: 86 | 87 | ``` elixir 88 | {:ok, rlp} = ExPlasma.encode(txn, signed: false) 89 | {:ok, 90 | <<248, 116, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 154, 202, 0, 238, 237, 1, 235, 148, 0, 0, 92 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 148, 0, 0, 0, 0, 0, 0, 93 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 128, 160, 0, 0, 0, 0, 0, 0, 0, 94 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>} 95 | ``` 96 | 97 | You can decode a transaction using `ExPlasma.decode/2`: 98 | 99 | ``` elixir 100 | {:ok, txn} = ExPlasma.decode(rlp, signed: false) 101 | {:ok, 102 | %ExPlasma.Transaction{ 103 | inputs: [ 104 | %ExPlasma.Output{ 105 | output_data: nil, 106 | output_id: %{blknum: 1, oindex: 0, position: 1000000000, txindex: 0}, 107 | output_type: nil 108 | } 109 | ], 110 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 111 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 112 | nonce: nil, 113 | outputs: [ 114 | %ExPlasma.Output{ 115 | output_data: %{ 116 | amount: 1, 117 | output_guard: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 118 | 0, 1>>, 119 | token: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 120 | }, 121 | output_id: nil, 122 | output_type: 1 123 | } 124 | ], 125 | sigs: [], 126 | tx_data: 0, 127 | tx_type: 1, 128 | witnesses: [] 129 | }} 130 | ``` 131 | 132 | You can validate a transaction using `ExPlasma.validate/1`: 133 | 134 | ``` elixir 135 | ExPlasma.validate(txn) 136 | ``` 137 | 138 | View the [documentation](https://hexdocs.pm/ex_plasma) 139 | 140 | ## Testing 141 | 142 | You can run the tests by running; 143 | 144 | ```sh 145 | mix test 146 | mix credo 147 | mix dialyzer 148 | ``` 149 | 150 | This will load up Ganche and the plasma contracts to deploy. 151 | 152 | 153 | ### Conformance test 154 | 155 | To ensure we can encode/decode according to the contracts, we have a separate suite of conformance tests that 156 | loads up mock contracts to compare encoding results. You can run the test by: 157 | 158 | ```sh 159 | make up-mocks 160 | mix test --only conformance 161 | ``` 162 | 163 | This will spin up ganache and deploy the mock contracts. 164 | 165 | ## Contributing 166 | 167 | 1. [Fork it!](https://github.com/omgnetwork/ex_plasma) 168 | 2. Create your feature branch (`git checkout -b my-new-feature`) 169 | 3. Commit your changes (`git commit -am 'Add some feature'`) 170 | 4. Push to the branch (`git push origin my-new-feature`) 171 | 5. Create new Pull Request 172 | 173 | ## License 174 | 175 | ExPlasma is released under the Apache-2.0 License. 176 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :ex_plasma, 6 | # 168 for v2 7 | exit_id_size: 160, 8 | eip_712_domain: %{ 9 | name: "OMG Network", 10 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 11 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 12 | version: "1" 13 | } 14 | 15 | # This configuration is loaded before any dependency and is restricted 16 | # to this project. If another project depends on this project, this 17 | # file won't be loaded nor affect the parent project. For this reason, 18 | # if you want to provide default values for your application for 19 | # third-party users, it should be done in your "mix.exs" file. 20 | 21 | # You can configure your application as: 22 | # 23 | # config :ex_plasma, key: :value 24 | # 25 | # and access this configuration in your application as: 26 | # 27 | # Application.get_env(:ex_plasma, :key) 28 | # 29 | # You can also configure a third-party app: 30 | # 31 | # config :logger, level: :info 32 | # 33 | 34 | # It is also possible to import configuration files, relative to this 35 | # directory. For example, you can emulate configuration per environment 36 | # by uncommenting the line below and defining dev.exs, test.exs and such. 37 | # Configuration from the imported file will override the ones defined 38 | # here (which is why it is important to import them last). 39 | # 40 | import_config "#{Mix.env()}.exs" 41 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ethereumex, 4 | id_reset: true, 5 | url: "http://localhost:8545" 6 | -------------------------------------------------------------------------------- /dialyzer.ignore-warnings: -------------------------------------------------------------------------------- 1 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Atom':'__impl__'/1 2 | :0: Unknown function 'Elixir.ExPlasma.TypedData.BitString':'__impl__'/1 3 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Float':'__impl__'/1 4 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Function':'__impl__'/1 5 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Integer':'__impl__'/1 6 | :0: Unknown function 'Elixir.ExPlasma.TypedData.List':'__impl__'/1 7 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Map':'__impl__'/1 8 | :0: Unknown function 'Elixir.ExPlasma.TypedData.PID':'__impl__'/1 9 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Port':'__impl__'/1 10 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Reference':'__impl__'/1 11 | :0: Unknown function 'Elixir.ExPlasma.TypedData.Tuple':'__impl__'/1 12 | :0: Unknown function 'Elixir.ExKeccak':hash_256/1 13 | :0: Unknown function 'Elixir.ExSecp256k1':recover/4 14 | :0: Unknown function 'Elixir.ExSecp256k1':sign/2 -------------------------------------------------------------------------------- /docker-compose.conformance.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | mock-contracts: 4 | build: 5 | context: . 6 | dockerfile: plasma-contract.Dockerfile 7 | command: /bin/sh -c "cd /home/node/plasma-contracts/plasma_framework && npx truffle migrate --network local" 8 | environment: 9 | - ETH_CLIENT_HOST=ganache 10 | - ETH_CLIENT_PORT=8545 11 | - MIN_EXIT_PERIOD=20 12 | - DEPLOYER_ADDRESS=0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0 13 | - DEPLOYER_PRIVATEKEY=0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1 14 | - USE_EXISTING_AUTHORITY_ADDRESS=1 15 | - AUTHORITY_ADDRESS=0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b 16 | - AUTHORITY_PRIVATEKEY=0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c 17 | volumes: 18 | - ./docker/migrations/:/home/node/plasma-contracts/plasma_framework/migrations 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | ganache: 4 | image: trufflesuite/ganache-cli:latest 5 | command: ganache-cli -d -e 100000 -m "myth like bonus scare over problem client lizard pioneer submit female collect" 6 | ports: 7 | - "8545:8545" 8 | - "8546:8546" 9 | plasma-contracts: 10 | build: 11 | context: . 12 | dockerfile: plasma-contract.Dockerfile 13 | command: /bin/sh -c "cd /home/node/plasma-contracts/plasma_framework && npx truffle migrate --network local" 14 | environment: 15 | - ETH_CLIENT_HOST=ganache 16 | - ETH_CLIENT_PORT=8545 17 | - MIN_EXIT_PERIOD=20 18 | - DEPLOYER_ADDRESS=0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0 19 | - DEPLOYER_PRIVATEKEY=0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1 20 | - USE_EXISTING_AUTHORITY_ADDRESS=1 21 | - AUTHORITY_ADDRESS=0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b 22 | - AUTHORITY_PRIVATEKEY=0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c 23 | -------------------------------------------------------------------------------- /docker/migrations/1_deploy_712_lib_mock.js: -------------------------------------------------------------------------------- 1 | var Contract = artifacts.require("PaymentEip712LibMock"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Contract); 5 | }; 6 | -------------------------------------------------------------------------------- /docker/migrations/2_deploy_exit_id_wrapper.js: -------------------------------------------------------------------------------- 1 | var Contract = artifacts.require("ExitIdWrapper"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Contract); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/ex_plasma.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma do 2 | @moduledoc """ 3 | Documentation for ExPlasma. 4 | """ 5 | 6 | alias ExPlasma.Transaction 7 | alias ExPlasma.Transaction.TypeMapper 8 | 9 | # constants that identify payment types, make sure that 10 | # when we introduce a new payment type, you name it `paymentV2` 11 | # https://github.com/omisego/plasma-contracts/blob/6ab35256b805e25cfc30d85f95f0616415220b20/plasma_framework/docs/design/tx-types-dependencies.md 12 | @payment_v1 TypeMapper.tx_type_for(:tx_payment_v1) 13 | @fee TypeMapper.tx_type_for(:tx_fee_token_claim) 14 | 15 | @type transaction_type :: non_neg_integer() 16 | 17 | @doc """ 18 | Simple payment type V1 19 | 20 | ## Example 21 | 22 | iex> ExPlasma.payment_v1() 23 | 1 24 | """ 25 | @spec payment_v1() :: 1 26 | def payment_v1(), do: @payment_v1 27 | 28 | @doc """ 29 | Transaction fee claim V1 30 | 31 | ## Example 32 | 33 | iex> ExPlasma.fee() 34 | 3 35 | """ 36 | @spec fee() :: 3 37 | def fee(), do: @fee 38 | 39 | @doc """ 40 | Transaction types 41 | 42 | ## Example 43 | 44 | iex> ExPlasma.transaction_types() 45 | [1, 3] 46 | """ 47 | @spec transaction_types :: [1 | 3, ...] 48 | def transaction_types(), do: [payment_v1(), fee()] 49 | 50 | @doc """ 51 | Encode the given Transaction into an RLP encodable list. 52 | 53 | If `signed: false` is given in the list of opts, will encode the transaction without its signatures. 54 | 55 | ## Example 56 | 57 | iex> txn = 58 | ...> %ExPlasma.Transaction{ 59 | ...> inputs: [ 60 | ...> %ExPlasma.Output{ 61 | ...> output_data: nil, 62 | ...> output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 63 | ...> output_type: nil 64 | ...> } 65 | ...> ], 66 | ...> metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 67 | ...> outputs: [ 68 | ...> %ExPlasma.Output{ 69 | ...> output_data: %{ 70 | ...> amount: 1, 71 | ...> output_guard: <<29, 246, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 72 | ...> 217, 206, 65, 226, 241, 55, 0, 110>>, 73 | ...> token: <<46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 74 | ...> 65, 226, 241, 55, 0, 110>> 75 | ...> }, 76 | ...> output_id: nil, 77 | ...> output_type: 1 78 | ...> } 79 | ...> ], 80 | ...> sigs: [], 81 | ...> tx_data: 0, 82 | ...> tx_type: 1 83 | ...> } 84 | iex> ExPlasma.encode(txn, signed: false) 85 | {:ok, <<248, 116, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 237, 1, 235, 148, 29, 246, 47, 87 | 41, 27, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110, 88 | 148, 46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 89 | 241, 55, 0, 110, 1, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>} 91 | """ 92 | defdelegate encode(transaction, opts \\ []), to: Transaction 93 | 94 | @doc """ 95 | Throwing version of encode/2 96 | """ 97 | defdelegate encode!(transaction, opts \\ []), to: Transaction 98 | 99 | @doc """ 100 | Attempt to decode the given RLP list into a Transaction. 101 | 102 | If `signed: false` is given in the list of opts, expects the underlying RLP to not contain signatures. 103 | 104 | Only validates that the RLP is structurally correct and that the tx type is supported. 105 | Does not perform any other kind of validation, use validate/1 for that. 106 | 107 | ## Example 108 | 109 | iex> rlp = <<248, 116, 1, 225, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 110 | ...> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 237, 1, 235, 148, 111 | ...> 29, 246, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 112 | ...> 55, 0, 110, 148, 46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 113 | ...> 206, 65, 226, 241, 55, 0, 110, 1, 128, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114 | ...> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 115 | iex> ExPlasma.decode(rlp, signed: false) 116 | {:ok, 117 | %ExPlasma.Transaction{ 118 | inputs: [ 119 | %ExPlasma.Output{ 120 | output_data: nil, 121 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 122 | output_type: nil 123 | } 124 | ], 125 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 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 | outputs: [ 127 | %ExPlasma.Output{ 128 | output_data: %{ 129 | amount: 1, 130 | output_guard: <<29, 246, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 131 | 217, 206, 65, 226, 241, 55, 0, 110>>, 132 | token: <<46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 133 | 65, 226, 241, 55, 0, 110>> 134 | }, 135 | output_id: nil, 136 | output_type: 1 137 | } 138 | ], 139 | witnesses: [], 140 | sigs: [], 141 | tx_data: 0, 142 | tx_type: 1 143 | } 144 | } 145 | """ 146 | defdelegate decode(tx_bytes, opts \\ []), to: Transaction 147 | 148 | @doc """ 149 | Keccak hash the Transaction. This is used in the contracts and events to to reference transactions. 150 | 151 | 152 | ## Example 153 | 154 | iex> rlp = <<248, 74, 192, 1, 193, 128, 239, 174, 237, 1, 235, 148, 29, 246, 47, 41, 27, 155 | ...> 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110, 148, 46, 156 | ...> 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 157 | ...> 0, 110, 1, 128, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158 | ...> 0>> 159 | iex> ExPlasma.hash(rlp) 160 | {:ok, <<87, 132, 239, 36, 144, 239, 129, 88, 63, 88, 116, 147, 164, 200, 113, 191, 161 | 124, 14, 55, 131, 119, 96, 112, 13, 28, 178, 251, 49, 16, 127, 58, 96>>} 162 | """ 163 | defdelegate hash(transaction), to: Transaction 164 | 165 | @doc """ 166 | Statelessly validate a transation. 167 | 168 | Returns :ok if valid or {:error, {atom, atom}} otherwise 169 | """ 170 | defdelegate validate(transaction), to: Transaction 171 | end 172 | -------------------------------------------------------------------------------- /lib/ex_plasma/block.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Block do 2 | @moduledoc """ 3 | Encapsulates the block data we receive from the contract. It returns two things: 4 | 5 | * hash - The merkle root block hash of the plasma blocks. 6 | * transactions - the list of Transactions associated with this given block 7 | """ 8 | 9 | alias ExPlasma.Merkle 10 | alias ExPlasma.Transaction 11 | 12 | @type t() :: %__MODULE__{ 13 | hash: binary(), 14 | transactions: maybe_improper_list() 15 | } 16 | 17 | defstruct hash: nil, transactions: [] 18 | 19 | # TODO 20 | # 21 | # Perhaps we need to do some validation check to prevent Deposit transactions from being included? 22 | @doc """ 23 | Generate a new `Block` from a list of transactions 24 | 25 | ## Example 26 | 27 | iex> %ExPlasma.Transaction{tx_type: 1} |> List.wrap() |> ExPlasma.Block.new() 28 | %ExPlasma.Block{ 29 | hash: <<168, 54, 172, 201, 1, 212, 18, 167, 34, 57, 232, 89, 151, 225, 172, 30 | 150, 208, 77, 194, 12, 174, 250, 146, 254, 93, 42, 28, 253, 203, 237, 247, 31 | 62>>, 32 | transactions: [ 33 | %ExPlasma.Transaction{ 34 | sigs: [], 35 | witnesses: [], 36 | inputs: [], 37 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 39 | outputs: [], 40 | tx_data: 0, 41 | tx_type: 1 42 | } 43 | ] 44 | } 45 | """ 46 | @spec new(maybe_improper_list()) :: t() 47 | def new(transactions) do 48 | %__MODULE__{transactions: transactions, hash: merkle_root_hash(transactions)} 49 | end 50 | 51 | # Encode the transactions and merkle root hash them. 52 | defp merkle_root_hash(transactions) do 53 | transactions 54 | |> Enum.map(fn tx -> Transaction.encode!(tx, signed: false) end) 55 | |> Merkle.root_hash() 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ex_plasma/builder.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Builder do 2 | @moduledoc """ 3 | Helper module to make crafting plasma transactions much simpler. 4 | """ 5 | 6 | alias ExPlasma.Output 7 | alias ExPlasma.Transaction 8 | alias ExPlasma.Transaction.Signed 9 | 10 | @type tx_opts :: [ 11 | inputs: Transaction.outputs(), 12 | outputs: Transaction.outputs(), 13 | tx_data: any(), 14 | metadata: Transaction.metadata() 15 | ] 16 | 17 | @type input_opts :: [ 18 | position: pos_integer(), 19 | blknum: non_neg_integer(), 20 | txindex: non_neg_integer(), 21 | oindex: non_neg_integer() 22 | ] 23 | 24 | @doc """ 25 | Create a new Transaction 26 | 27 | ## Example 28 | 29 | # Empty payment v1 transaction 30 | iex> new(ExPlasma.payment_v1()) 31 | %ExPlasma.Transaction{tx_type: 1, inputs: [], outputs: [], metadata: <<0::256>>} 32 | 33 | # New payment v1 transaction with metadata 34 | iex> new(ExPlasma.payment_v1(), metadata: <<1::256>>) 35 | %ExPlasma.Transaction{tx_type: 1, inputs: [], outputs: [], metadata: <<1::256>>} 36 | """ 37 | @spec new(ExPlasma.transaction_type(), tx_opts()) :: Transaction.t() 38 | def new(tx_type, opts \\ []), do: struct(%Transaction{tx_type: tx_type}, opts) 39 | 40 | @doc """ 41 | Decorates the transaction with a nonce when given valid params for the type. 42 | 43 | ## Example 44 | 45 | iex> tx = new(ExPlasma.fee()) 46 | iex> with_nonce(tx, %{blknum: 1000, token: <<0::160>>}) 47 | {:ok, %ExPlasma.Transaction{ 48 | inputs: [], 49 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50 | 0, 0, 0, 0, 0, 0, 0, 0>>, 51 | nonce: <<61, 119, 206, 68, 25, 203, 29, 23, 147, 224, 136, 32, 198, 128, 177, 74, 52 | 227, 250, 194, 173, 146, 182, 251, 152, 123, 172, 26, 83, 175, 194, 213, 238>>, 53 | outputs: [], 54 | sigs: [], 55 | tx_data: 0, 56 | tx_type: 3, 57 | witnesses: [] 58 | }} 59 | """ 60 | @spec with_nonce(Transaction.t(), map()) :: {:ok, Transaction.t()} | {:error, atom()} 61 | defdelegate with_nonce(transaction, params), to: Transaction 62 | 63 | @spec with_nonce!(Transaction.t(), map()) :: Transaction.t() | no_return() 64 | def with_nonce!(transaction, params) do 65 | {:ok, transaction} = Transaction.with_nonce(transaction, params) 66 | transaction 67 | end 68 | 69 | @doc """ 70 | Adds an input to the Transaction 71 | 72 | ## Example 73 | 74 | iex> ExPlasma.payment_v1() 75 | ...> |> new() 76 | ...> |> add_input(blknum: 1, txindex: 0, oindex: 0) 77 | ...> |> add_input(blknum: 2, txindex: 0, oindex: 0) 78 | %ExPlasma.Transaction{ 79 | tx_type: 1, 80 | inputs: [ 81 | %ExPlasma.Output{output_id: %{blknum: 1, txindex: 0, oindex: 0}}, 82 | %ExPlasma.Output{output_id: %{blknum: 2, txindex: 0, oindex: 0}}, 83 | ] 84 | } 85 | """ 86 | @spec add_input(Transaction.t(), input_opts()) :: Transaction.t() 87 | def add_input(txn, opts) do 88 | input = %Output{output_id: Enum.into(opts, %{})} 89 | %{txn | inputs: txn.inputs ++ [input]} 90 | end 91 | 92 | @doc """ 93 | Adds an output to the Transaction 94 | 95 | ## Example 96 | 97 | iex> ExPlasma.payment_v1() 98 | ...> |> new() 99 | ...> |> add_output(output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1}) 100 | ...> |> add_output(output_guard: <<1::160>>, token: <<0::160>>, amount: 2) 101 | %ExPlasma.Transaction{ 102 | tx_type: 1, 103 | outputs: [ 104 | %ExPlasma.Output{output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1}}, 105 | %ExPlasma.Output{output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 2}}, 106 | ] 107 | } 108 | """ 109 | @spec add_output(Transaction.t(), list()) :: Transaction.t() 110 | def add_output(txn, output_type: type, output_data: data) do 111 | output = %Output{output_type: type, output_data: data} 112 | %{txn | outputs: txn.outputs ++ [output]} 113 | end 114 | 115 | def add_output(txn, opts) when is_list(opts) do 116 | output = %Output{output_type: 1, output_data: Enum.into(opts, %{})} 117 | %{txn | outputs: txn.outputs ++ [output]} 118 | end 119 | 120 | @doc """ 121 | Sign the inputs of the transaction with the given keys in the corresponding order. 122 | 123 | Returns a tuple {:ok, transaction} if success or {:error, atom} otherwise. 124 | 125 | ## Example 126 | 127 | iex> key = "0x79298b0292bbfa9b15705c56b6133201c62b798f102d7d096d31d7637f9b2382" 128 | ...> ExPlasma.payment_v1() 129 | ...> |> new() 130 | ...> |> sign([key]) 131 | {:ok, %ExPlasma.Transaction{ 132 | inputs: [], 133 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 135 | outputs: [], 136 | tx_data: 0, 137 | sigs: [ 138 | <<129, 213, 32, 15, 183, 218, 255, 22, 82, 95, 22, 86, 103, 227, 92, 109, 9, 139 | 89, 7, 142, 235, 107, 203, 29, 20, 231, 91, 168, 255, 119, 204, 239, 44, 140 | 125, 76, 109, 200, 196, 204, 230, 224, 241, 84, 75, 9, 3, 160, 177, 37, 141 | 181, 174, 98, 51, 15, 136, 235, 47, 96, 15, 209, 45, 85, 153, 2, 28>> 142 | ], 143 | tx_type: 1 144 | }} 145 | """ 146 | defdelegate sign(txn, sigs), to: Transaction 147 | 148 | @spec sign!(Transaction.t(), Signed.sigs()) :: Transaction.t() | no_return() 149 | def sign!(txn, sigs) do 150 | {:ok, signed} = Transaction.sign(txn, sigs) 151 | signed 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/ex_plasma/configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Configuration do 2 | @moduledoc """ 3 | Provides access to applications configuration with additional validation. 4 | 5 | Application variables should be retrieved using this module 6 | instead of calling Application.get_env/3 7 | """ 8 | 9 | alias ExPlasma.Configuration.Validator 10 | 11 | @app :ex_plasma 12 | 13 | @doc """ 14 | Retrieve the eip 712 domain from the config and validates its format. 15 | The expected format is: 16 | %{ 17 | name: "OMG Network", 18 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 19 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 20 | version: "1" 21 | } 22 | 23 | Returns the domain if valid, or raise an exception otherwise. 24 | 25 | ## Example 26 | 27 | iex> Application.put_env(:ex_plasma, :eip_712_domain, %{ 28 | ...> name: "OMG Network", 29 | ...> salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 30 | ...> verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 31 | ...> version: "1" 32 | ...>}) 33 | iex> ExPlasma.Configuration.eip_712_domain() 34 | %{ 35 | name: "OMG Network", 36 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 37 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 38 | version: "1" 39 | } 40 | """ 41 | @spec eip_712_domain() :: Validator.eip_712_domain_t() | no_return() 42 | def eip_712_domain() do 43 | @app 44 | |> Application.get_env(:eip_712_domain) 45 | |> Validator.validate_eip_712_domain() 46 | end 47 | 48 | @spec exit_id_size() :: 160 | 168 49 | def exit_id_size() do 50 | Application.get_env(@app, :exit_id_size) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ex_plasma/configuration/validator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Configuration.Validator do 2 | @moduledoc """ 3 | Provides validation to application variables 4 | """ 5 | 6 | @type eip_712_domain_t() :: %{ 7 | name: String.t(), 8 | salt: String.t(), 9 | verifying_contract: String.t(), 10 | version: String.t() 11 | } 12 | 13 | @doc """ 14 | Validates the eip 712 domain format. 15 | The expected format is: 16 | %{ 17 | name: "OMG Network", 18 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 19 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 20 | version: "1" 21 | } 22 | 23 | Returns the domain if valid, or raise an exception otherwise. 24 | 25 | ## Example 26 | 27 | iex> ExPlasma.Configuration.Validator.validate_eip_712_domain(%{ 28 | ...> name: "OMG Network", 29 | ...> salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 30 | ...> verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 31 | ...> version: "1" 32 | ...>}) 33 | %{ 34 | name: "OMG Network", 35 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 36 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 37 | version: "1" 38 | } 39 | """ 40 | @spec validate_eip_712_domain(any()) :: eip_712_domain_t() | no_return() 41 | def validate_eip_712_domain(%{name: name, salt: "0x" <> _, verifying_contract: "0x" <> _, version: version} = domain) 42 | when is_binary(name) and is_binary(version) do 43 | domain 44 | end 45 | 46 | def validate_eip_712_domain(_) do 47 | raise RuntimeError, """ 48 | :eip_712_domain config is invalid. It must be in the following format: 49 | %{ 50 | name: "OMG Network", 51 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 52 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 53 | version: "1" 54 | } 55 | """ 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ex_plasma/crypto.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Crypto do 2 | @moduledoc """ 3 | Signs and validates signatures. Constructed signatures can be used directly 4 | in Ethereum with `ecrecover` call. 5 | """ 6 | alias ExPlasma.Signature 7 | 8 | @type sig_t() :: <<_::520>> 9 | @type pub_key_t() :: <<_::512>> 10 | @type address_t() :: <<_::160>> 11 | @type hash_t() :: <<_::256>> 12 | 13 | @type recover_address_error() :: :corrupted_witness | :invalid_message 14 | 15 | @doc """ 16 | Produces a KECCAK digest for the message. 17 | 18 | ## Example 19 | 20 | iex> ExPlasma.Crypto.keccak_hash("omg!") 21 | <<241, 85, 204, 147, 187, 239, 139, 133, 69, 248, 239, 233, 219, 51, 189, 54, 22 | 171, 76, 106, 229, 69, 102, 203, 7, 21, 134, 230, 92, 23, 209, 187, 12>> 23 | """ 24 | @spec keccak_hash(binary()) :: hash_t() 25 | def keccak_hash(message) do 26 | {:ok, hash} = ExKeccak.hash_256(message) 27 | 28 | hash 29 | end 30 | 31 | @doc """ 32 | Recovers the address of the signer from a binary-encoded signature. 33 | """ 34 | @spec recover_address(hash_t(), sig_t()) :: {:ok, address_t()} | {:error, recover_address_error()} 35 | def recover_address(<>, <>) do 36 | case Signature.recover_public(digest, packed_signature) do 37 | {:ok, pub} -> 38 | generate_address(pub) 39 | 40 | {:error, _} -> 41 | {:error, :corrupted_witness} 42 | end 43 | end 44 | 45 | def recover_address(<<_digest::binary-size(32)>>, _signature), do: {:error, :corrupted_witness} 46 | 47 | def recover_address(_message, _signature), do: {:error, :invalid_message} 48 | 49 | @doc """ 50 | Given public key, returns an address. 51 | """ 52 | @spec generate_address(pub_key_t()) :: {:ok, address_t()} 53 | def generate_address(<>) do 54 | <<_::binary-size(12), address::binary-size(20)>> = keccak_hash(pub) 55 | {:ok, address} 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ex_plasma/encoding.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Encoding do 2 | @moduledoc """ 3 | Provides the common encoding functionality we use across 4 | all the transactions and clients. 5 | """ 6 | 7 | @doc """ 8 | Converts binary and integer values into its hex string 9 | equivalent. 10 | 11 | ## Examples 12 | 13 | # Convert a raw binary to hex 14 | iex> raw = <<29, 246, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>> 15 | iex> ExPlasma.Encoding.to_hex(raw) 16 | "0x1df62f291b2e969fb0849d99d9ce41e2f137006e" 17 | 18 | # Convert an integer to hex 19 | iex> ExPlasma.Encoding.to_hex(1) 20 | "0x1" 21 | """ 22 | @spec to_hex(binary | non_neg_integer) :: String.t() 23 | def to_hex(non_hex) 24 | def to_hex(raw) when is_binary(raw), do: "0x" <> Base.encode16(raw, case: :lower) 25 | def to_hex(int) when is_integer(int), do: "0x" <> Integer.to_string(int, 16) 26 | 27 | @doc """ 28 | Converts a hex string into the integer value. 29 | 30 | ## Examples 31 | 32 | # Convert a hex string into an integer 33 | iex> ExPlasma.Encoding.to_int("0xb") 34 | 11 35 | 36 | # Convert a binary into an integer 37 | iex> ExPlasma.Encoding.to_int(<<11>>) 38 | 11 39 | """ 40 | @spec to_int(String.t()) :: non_neg_integer 41 | def to_int("0x" <> encoded) do 42 | {return, ""} = Integer.parse(encoded, 16) 43 | return 44 | end 45 | 46 | def to_int(encoded) when is_binary(encoded), do: :binary.decode_unsigned(encoded, :big) 47 | 48 | @doc """ 49 | Converts a hex string into a binary. 50 | 51 | ## Examples 52 | 53 | iex> ExPlasma.Encoding.to_binary("0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e") 54 | {:ok, <<29, 246, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55 | 55, 0, 110>>} 56 | """ 57 | @spec to_binary(String.t()) :: {:ok, binary} | {:error, :decoding_error} 58 | def to_binary("0x" <> unprefixed_hex) do 59 | case unprefixed_hex |> String.upcase() |> Base.decode16() do 60 | {:ok, binary} -> {:ok, binary} 61 | :error -> {:error, :decoding_error} 62 | end 63 | end 64 | 65 | @doc """ 66 | Throwing version of to_binary/1 67 | 68 | ## Examples 69 | 70 | iex> ExPlasma.Encoding.to_binary!("0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e") 71 | <<29, 246, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 72 | 55, 0, 110>> 73 | """ 74 | @spec to_binary!(String.t()) :: binary | no_return() 75 | def to_binary!("0x" <> _unprefixed_hex = prefixed_hex) do 76 | {:ok, binary} = to_binary(prefixed_hex) 77 | 78 | binary 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/ex_plasma/in_flight_exit.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.InFlightExit do 2 | @moduledoc """ 3 | Represents an in-flight exit (IFE). 4 | """ 5 | use Bitwise, only_operators: true 6 | 7 | alias ExPlasma.Crypto 8 | 9 | @doc """ 10 | Derive the in-flight exit ID from its transaction bytes. 11 | 12 | See https://github.com/omisego/plasma-contracts/blob/v1.0.3/plasma_framework/contracts/src/exits/utils/ExitId.sol#L53-L55 13 | """ 14 | @spec tx_bytes_to_id(binary()) :: pos_integer() 15 | def tx_bytes_to_id(tx_bytes) do 16 | tx_bytes 17 | |> Crypto.keccak_hash() 18 | |> :binary.decode_unsigned() 19 | |> Bitwise.>>>(get_bit_shift_size()) 20 | |> set_bit(get_set_bit()) 21 | end 22 | 23 | defp set_bit(data, bit_position) do 24 | data ||| 1 <<< bit_position 25 | end 26 | 27 | defp get_bit_shift_size() do 28 | case ExPlasma.Configuration.exit_id_size() do 29 | 160 -> 105 30 | 168 -> 89 31 | end 32 | end 33 | 34 | defp get_set_bit() do 35 | case ExPlasma.Configuration.exit_id_size() do 36 | 160 -> 151 37 | 168 -> 167 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/ex_plasma/merkle.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Merkle do 2 | @moduledoc """ 3 | Encapsulates all the interactions with the MerkleTree library. 4 | """ 5 | 6 | alias ExPlasma.Crypto 7 | 8 | @transaction_merkle_tree_height 16 9 | @default_leaf <<0::256>> 10 | 11 | @doc """ 12 | Generate a Merkle Root hash for the given list of transactions in encoded byte form. 13 | 14 | ## Examples 15 | 16 | iex> txns = %ExPlasma.Transaction{tx_type: 1} |> ExPlasma.encode!(signed: false) |> List.wrap() 17 | iex> ExPlasma.Merkle.root_hash(txns) 18 | <<168, 54, 172, 201, 1, 212, 18, 167, 34, 57, 232, 89, 151, 225, 172, 150, 208, 19 | 77, 194, 12, 174, 250, 146, 254, 93, 42, 28, 253, 203, 237, 247, 62>> 20 | """ 21 | @spec root_hash([binary(), ...]) :: binary() 22 | def root_hash(encoded_transactions) do 23 | MerkleTree.fast_root(encoded_transactions, 24 | hash_function: &Crypto.keccak_hash/1, 25 | height: @transaction_merkle_tree_height, 26 | default_data_block: @default_leaf 27 | ) 28 | end 29 | 30 | # Creates a Merkle proof that transaction under a given transaction index 31 | # is included in block consisting of hashed transactions 32 | @spec proof([binary(), ...], non_neg_integer()) :: binary() 33 | def proof(encoded_transactions, txindex) do 34 | encoded_transactions 35 | |> build() 36 | |> prove(txindex) 37 | |> Enum.reverse() 38 | |> Enum.join() 39 | end 40 | 41 | defp build(encoded_transactions) do 42 | MerkleTree.build(encoded_transactions, 43 | hash_function: &Crypto.keccak_hash/1, 44 | height: @transaction_merkle_tree_height, 45 | default_data_block: @default_leaf 46 | ) 47 | end 48 | 49 | defp prove(hash, txindex) do 50 | MerkleTree.Proof.prove(hash, txindex) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ex_plasma/output/position.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Position do 2 | @moduledoc """ 3 | Generates an Output position if given the: 4 | 5 | `blknum` - The block number for this output 6 | `txindex` - The index of the Transaction in the block. 7 | `oindex` - The index of the Output in the Transaction. 8 | """ 9 | 10 | @behaviour ExPlasma.Output 11 | 12 | alias __MODULE__.Validator 13 | alias ExPlasma.Output 14 | alias ExPlasma.Utils.RlpDecoder 15 | 16 | @type position() :: pos_integer() 17 | 18 | @type t() :: %{ 19 | blknum: non_neg_integer(), 20 | txindex: non_neg_integer(), 21 | oindex: non_neg_integer() 22 | } 23 | 24 | @type with_position() :: %{ 25 | position: position(), 26 | blknum: non_neg_integer(), 27 | txindex: non_neg_integer(), 28 | oindex: non_neg_integer() 29 | } 30 | 31 | @type validation_responses() :: 32 | :ok 33 | | {:error, 34 | Validator.blknum_validation_errors() 35 | | Validator.oindex_validation_errors() 36 | | Validator.txindex_validation_errors()} 37 | 38 | # Contract settings 39 | # These are being hard-coded from the same values on the contracts. 40 | # See: https://github.com/omisego/plasma-contracts/blob/master/plasma_framework/contracts/src/utils/PosLib.sol#L16-L23 41 | @block_offset 1_000_000_000 42 | @transaction_offset 10_000 43 | 44 | def block_offset(), do: @block_offset 45 | def transaction_offset(), do: @transaction_offset 46 | 47 | @doc """ 48 | Creates an output_id from a block number, a tx index and an output index. 49 | 50 | ## Example 51 | 52 | iex> ExPlasma.Output.Position.new(1, 0, 0) 53 | %{blknum: 1, txindex: 0, oindex: 0, position: 1_000_000_000} 54 | """ 55 | @spec new(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: with_position() 56 | def new(blknum, txindex, oindex) when is_integer(blknum) and is_integer(txindex) and is_integer(oindex) do 57 | output_id = %{blknum: blknum, txindex: txindex, oindex: oindex} 58 | Map.put(output_id, :position, pos(output_id)) 59 | end 60 | 61 | @doc """ 62 | Encodes the blknum, txindex, and oindex into a single integer. 63 | 64 | ## Example 65 | 66 | iex> pos = %{blknum: 1, txindex: 0, oindex: 0} 67 | iex> ExPlasma.Output.Position.pos(pos) 68 | 1_000_000_000 69 | """ 70 | @spec pos(t() | with_position()) :: position() 71 | def pos(%{blknum: blknum, txindex: txindex, oindex: oindex}) do 72 | blknum * @block_offset + txindex * @transaction_offset + oindex 73 | end 74 | 75 | @doc """ 76 | Transforms the output position into a positive integer representing the position. 77 | 78 | ## Example 79 | 80 | iex> output_id = %{blknum: 1, txindex: 0, oindex: 0} 81 | iex> ExPlasma.Output.Position.to_rlp(output_id) 82 | <<0, 0, 0, 0, 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>> 83 | """ 84 | @impl Output 85 | @spec to_rlp(t() | with_position()) :: binary() 86 | def to_rlp(output_id), do: output_id |> pos() |> encode() 87 | 88 | @doc """ 89 | Encodes the output position into an RLP encodable object. 90 | 91 | ## Example 92 | 93 | iex> pos = ExPlasma.Output.Position.pos(%{blknum: 1, txindex: 0, oindex: 0}) 94 | iex> ExPlasma.Output.Position.encode(pos) 95 | <<0, 0, 0, 0, 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>> 96 | """ 97 | @spec encode(position()) :: binary() 98 | def encode(position) do 99 | position |> :binary.encode_unsigned(:big) |> pad_binary() 100 | end 101 | 102 | @doc """ 103 | Returns a map of the decoded position. 104 | 105 | ## Example 106 | 107 | iex> pos = 1_000_000_000 108 | iex> ExPlasma.Output.Position.to_map(pos) 109 | {:ok, %{position: 1_000_000_000, blknum: 1, txindex: 0, oindex: 0}} 110 | """ 111 | @impl Output 112 | @spec to_map(position()) :: {:ok, with_position()} | {:error, :malformed_output_position} 113 | def to_map(pos) when is_integer(pos) do 114 | blknum = div(pos, @block_offset) 115 | txindex = pos |> rem(@block_offset) |> div(@transaction_offset) 116 | oindex = rem(pos, @transaction_offset) 117 | 118 | {:ok, %{position: pos, blknum: blknum, txindex: txindex, oindex: oindex}} 119 | end 120 | 121 | def to_map(_), do: {:error, :malformed_output_position} 122 | 123 | @doc """ 124 | Decodes and returns the integer position. 125 | 126 | ## Example 127 | 128 | iex> 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, 154, 202, 0>> 129 | iex> ExPlasma.Output.Position.decode(pos) 130 | {:ok, 1_000_000_000} 131 | """ 132 | @spec decode(binary()) :: {:ok, position()} | {:error, :malformed_input_position_rlp} 133 | def decode(encoded_pos) do 134 | case RlpDecoder.parse_uint256_with_leading(encoded_pos) do 135 | {:ok, pos} -> {:ok, pos} 136 | _error -> {:error, :malformed_input_position_rlp} 137 | end 138 | end 139 | 140 | @doc """ 141 | Validates that values can give a valid position. 142 | 143 | ## Example 144 | 145 | iex> output_id = %{blknum: 1, txindex: 0, oindex: 0} 146 | iex> ExPlasma.Output.Position.validate(output_id) 147 | :ok 148 | """ 149 | @impl Output 150 | @spec validate(t() | with_position()) :: validation_responses() 151 | def validate(nil), do: :ok 152 | 153 | def validate(output_id) do 154 | with :ok <- Validator.validate_blknum(output_id.blknum), 155 | :ok <- Validator.validate_txindex(output_id.txindex), 156 | :ok <- Validator.validate_oindex(output_id.oindex) do 157 | :ok 158 | end 159 | end 160 | 161 | defp pad_binary(unpadded) do 162 | pad_size = (32 - byte_size(unpadded)) * 8 163 | <<0::size(pad_size)>> <> unpadded 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/ex_plasma/output/position/position_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Position.Validator do 2 | @moduledoc """ 3 | Contain stateless validation logic for position 4 | """ 5 | 6 | alias ExPlasma.Output.Position 7 | 8 | @block_offset Position.block_offset() 9 | @transaction_offset Position.transaction_offset() 10 | 11 | @max_txindex :math.pow(2, 16) - 1 12 | @max_blknum (:math.pow(2, 54) - 1 - @max_txindex) / (@block_offset / @transaction_offset) 13 | 14 | @type blknum_validation_errors() :: 15 | {:blknum, :cannot_be_nil} 16 | | {:blknum, :cannot_exceed_maximum_value} 17 | | {:blknum, :must_be_an_integer} 18 | @type txindex_validation_errors() :: 19 | {:txindex, :cannot_be_nil} 20 | | {:txindex, :cannot_exceed_maximum_value} 21 | | {:txindex, :must_be_an_integer} 22 | @type oindex_validation_errors() :: {:oindex, :cannot_be_nil} | {:oindex, :must_be_an_integer} 23 | 24 | @spec validate_blknum(pos_integer()) :: :ok | {:error, blknum_validation_errors()} 25 | def validate_blknum(nil), do: {:error, {:blknum, :cannot_be_nil}} 26 | 27 | def validate_blknum(blknum) when is_integer(blknum) and blknum > @max_blknum do 28 | {:error, {:blknum, :cannot_exceed_maximum_value}} 29 | end 30 | 31 | def validate_blknum(blknum) when is_integer(blknum), do: :ok 32 | def validate_blknum(_blknum), do: {:error, {:blknum, :must_be_an_integer}} 33 | 34 | @spec validate_txindex(non_neg_integer()) :: :ok | {:error, txindex_validation_errors()} 35 | def validate_txindex(nil), do: {:error, {:txindex, :cannot_be_nil}} 36 | 37 | def validate_txindex(txindex) when is_integer(txindex) and txindex > @max_txindex do 38 | {:error, {:txindex, :cannot_exceed_maximum_value}} 39 | end 40 | 41 | def validate_txindex(txindex) when is_integer(txindex), do: :ok 42 | def validate_txindex(_txindex), do: {:error, {:txindex, :must_be_an_integer}} 43 | 44 | @spec validate_oindex(non_neg_integer()) :: :ok | {:error, oindex_validation_errors()} 45 | def validate_oindex(nil), do: {:error, {:oindex, :cannot_be_nil}} 46 | def validate_oindex(oindex) when is_integer(oindex), do: :ok 47 | def validate_oindex(_oindex), do: {:error, {:oindex, :must_be_an_integer}} 48 | end 49 | -------------------------------------------------------------------------------- /lib/ex_plasma/output/type/abstract_payment.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Type.AbstractPayment do 2 | @moduledoc """ 3 | Abstract payment output type. 4 | """ 5 | 6 | @behaviour ExPlasma.Output 7 | 8 | alias __MODULE__.Validator 9 | alias ExPlasma.Output 10 | alias ExPlasma.Utils.RlpDecoder 11 | 12 | @type address() :: <<_::160>> 13 | @type output_guard() :: address() 14 | @type token() :: address() 15 | @type amount() :: non_neg_integer() 16 | @type mapping_errors() :: 17 | :malformed_output_guard 18 | | :malformed_output_token 19 | | :malformed_output_amount 20 | 21 | @type rlp() :: [<<_::8>> | [output_guard() | [token() | binary()]]] 22 | 23 | @type validation_responses() :: {:ok, t()} 24 | 25 | @type t() :: %{ 26 | output_guard: output_guard(), 27 | token: token(), 28 | amount: amount() 29 | } 30 | 31 | @type validation_error() :: 32 | Validator.amount_validation_errors() 33 | | Validator.token_validation_errors() 34 | | Validator.output_guard_validation_errors() 35 | 36 | @doc """ 37 | Encode a map of the output data into an RLP list. 38 | 39 | ## Example 40 | 41 | iex> output = %{output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<1::160>>, amount: 1}} 42 | iex> ExPlasma.Output.Type.AbstractPayment.to_rlp(output) 43 | [<<1>>, [<<1::160>>, <<1::160>>, <<1>>]] 44 | """ 45 | @impl Output 46 | @spec to_rlp(Output.t()) :: rlp() 47 | def to_rlp(output) do 48 | %{output_type: type, output_data: data} = output 49 | 50 | [ 51 | <>, 52 | [ 53 | data.output_guard, 54 | data.token, 55 | truncate_leading_zero(<>) 56 | ] 57 | ] 58 | end 59 | 60 | @doc """ 61 | Decode a map of the output data into the Abstract Payment format 62 | 63 | Only validates that the RLP is structurally correct. 64 | Does not perform any other kind of validation, use validate/1 for that. 65 | 66 | ## Example 67 | 68 | iex> data = [<<1>>, [<<1::160>>, <<1::160>>, <<1>>]] 69 | iex> ExPlasma.Output.Type.AbstractPayment.to_map(data) 70 | {:ok, %{ 71 | output_type: 1, 72 | output_data: %{output_guard: <<1::160>>, token: <<1::160>>, amount: 1} 73 | }} 74 | """ 75 | @impl Output 76 | @spec to_map(rlp()) :: {:ok, %{output_data: t(), output_type: non_neg_integer()}} | {:error, mapping_errors()} 77 | def to_map([<>, [output_guard_rlp, token_rlp, amount_rlp]]) do 78 | with {:ok, output_guard} <- decode_output_guard(output_guard_rlp), 79 | {:ok, token} <- decode_token(token_rlp), 80 | {:ok, amount} <- decode_amount(amount_rlp) do 81 | {:ok, 82 | %{ 83 | output_type: output_type, 84 | output_data: %{output_guard: output_guard, token: token, amount: amount} 85 | }} 86 | end 87 | end 88 | 89 | def to_map(_), do: {:error, :malformed_outputs} 90 | 91 | @doc """ 92 | Validates the output data 93 | 94 | ## Example 95 | 96 | iex> data = %{output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1}} 97 | iex> ExPlasma.Output.Type.AbstractPayment.validate(data) 98 | :ok 99 | """ 100 | @impl Output 101 | def validate(output) do 102 | with :ok <- Validator.validate_amount(output.output_data.amount), 103 | :ok <- Validator.validate_token(output.output_data.token), 104 | :ok <- Validator.validate_output_guard(output.output_data.output_guard) do 105 | :ok 106 | end 107 | end 108 | 109 | defp truncate_leading_zero(<<0>>), do: <<0>> 110 | defp truncate_leading_zero(<<0>> <> binary), do: truncate_leading_zero(binary) 111 | defp truncate_leading_zero(binary), do: binary 112 | 113 | defp decode_output_guard(output_guard_rlp) do 114 | case RlpDecoder.parse_address(output_guard_rlp) do 115 | {:ok, output_guard} -> {:ok, output_guard} 116 | _error -> {:error, :malformed_output_guard} 117 | end 118 | end 119 | 120 | defp decode_token(token_rlp) do 121 | case RlpDecoder.parse_address(token_rlp) do 122 | {:ok, token} -> {:ok, token} 123 | _error -> {:error, :malformed_output_token} 124 | end 125 | end 126 | 127 | defp decode_amount(amount_rlp) do 128 | case RlpDecoder.parse_uint256(amount_rlp) do 129 | {:ok, amount} -> {:ok, amount} 130 | _error -> {:error, :malformed_output_amount} 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/ex_plasma/output/type/abstract_payment/abstract_payment_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Type.AbstractPayment.Validator do 2 | @moduledoc """ 3 | Contain stateless validation logic for abstract payment outputs 4 | """ 5 | 6 | alias ExPlasma.Crypto 7 | 8 | @zero_address <<0::160>> 9 | 10 | @type amount_validation_errors() :: {:amount, :cannot_be_nil} | {:amount, :cannot_be_zero} 11 | @type token_validation_errors() :: {:token, :cannot_be_nil} 12 | @type output_guard_validation_errors() :: {:output_guard, :cannot_be_nil} | {:output_guard, :cannot_be_zero} 13 | 14 | @spec validate_amount(pos_integer()) :: :ok | {:error, amount_validation_errors()} 15 | def validate_amount(nil), do: {:error, {:amount, :cannot_be_nil}} 16 | def validate_amount(amount) when amount <= 0, do: {:error, {:amount, :cannot_be_zero}} 17 | def validate_amount(_amount), do: :ok 18 | 19 | @spec validate_token(Crypto.address_t()) :: :ok | {:error, token_validation_errors()} 20 | def validate_token(nil), do: {:error, {:token, :cannot_be_nil}} 21 | def validate_token(_token), do: :ok 22 | 23 | @spec validate_output_guard(Crypto.address_t()) :: :ok | {:error, output_guard_validation_errors()} 24 | def validate_output_guard(nil), do: {:error, {:output_guard, :cannot_be_nil}} 25 | def validate_output_guard(@zero_address), do: {:error, {:output_guard, :cannot_be_zero}} 26 | def validate_output_guard(_output_guard), do: :ok 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_plasma/output/type/empty.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Type.Empty do 2 | @moduledoc """ 3 | An empty invalid output type. Used to return a default 4 | response if given an invalid output key. 5 | """ 6 | 7 | @behaviour ExPlasma.Output 8 | alias ExPlasma.Output 9 | 10 | @impl Output 11 | @spec to_rlp(any()) :: [<<_::8>> | [], ...] 12 | def to_rlp(_), do: [<<0>>, []] 13 | 14 | @impl Output 15 | @spec to_map(any()) :: {:ok, %Output{output_type: 0, output_data: %{}, output_id: nil}} 16 | def to_map(_), do: {:ok, %Output{output_type: 0, output_data: %{}}} 17 | 18 | @impl Output 19 | @spec validate(any) :: {:error, {:output_type, :unknown}} 20 | def validate(_), do: {:error, {:output_type, :unknown}} 21 | end 22 | -------------------------------------------------------------------------------- /lib/ex_plasma/output/type/fee.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Type.Fee do 2 | @moduledoc """ 3 | Fee Output Type. 4 | """ 5 | @behaviour ExPlasma.Output 6 | 7 | alias ExPlasma.Output 8 | alias ExPlasma.Output.Type.AbstractPayment 9 | alias ExPlasma.Transaction.TypeMapper 10 | 11 | @type t() :: AbstractPayment.t() 12 | 13 | @fee_type TypeMapper.output_type_for(:output_fee_token_claim) 14 | 15 | @impl Output 16 | defdelegate to_rlp(output), to: AbstractPayment 17 | 18 | @impl Output 19 | defdelegate to_map(rlp), to: AbstractPayment 20 | 21 | @impl Output 22 | def validate(%{output_type: @fee_type} = output) do 23 | AbstractPayment.validate(output) 24 | end 25 | 26 | def validate(_output), do: {:error, {:output_type, :unrecognized_output_type}} 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_plasma/output/type/payment_v1.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Type.PaymentV1 do 2 | @moduledoc """ 3 | Payment V1 output type. 4 | """ 5 | @behaviour ExPlasma.Output 6 | 7 | alias ExPlasma.Output 8 | alias ExPlasma.Output.Type.AbstractPayment 9 | alias ExPlasma.Transaction.TypeMapper 10 | 11 | @type t() :: AbstractPayment.t() 12 | 13 | @payment_v1_type TypeMapper.output_type_for(:output_payment_v1) 14 | 15 | @impl Output 16 | defdelegate to_rlp(output), to: AbstractPayment 17 | 18 | @impl Output 19 | defdelegate to_map(rlp), to: AbstractPayment 20 | 21 | @impl Output 22 | def validate(%{output_type: @payment_v1_type} = output) do 23 | AbstractPayment.validate(output) 24 | end 25 | 26 | def validate(_output), do: {:error, {:output_type, :unrecognized_output_type}} 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_plasma/signature.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Signature do 2 | @moduledoc """ 3 | Adapted from https://github.com/exthereum/blockchain. 4 | Defines helper functions for signing and getting the signature 5 | of a transaction, as defined in Appendix F of the Yellow Paper. 6 | 7 | For any of the following functions, if chain_id is specified, 8 | it's assumed that we're post-fork and we should follow the 9 | specification EIP-155 from: 10 | 11 | https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md 12 | """ 13 | 14 | alias ExPlasma.Encoding 15 | 16 | @base_recovery_id 27 17 | @base_recovery_id_eip_155 35 18 | @signature_len 32 19 | 20 | @type keccak_hash :: binary() 21 | @type public_key :: <<_::512>> 22 | @type hash_v :: integer() 23 | @type hash_r :: integer() 24 | @type hash_s :: integer() 25 | @type signature_len :: unquote(@signature_len) 26 | 27 | @doc """ 28 | Produces a stand-alone, 65 bytes long, signature for message hash. 29 | """ 30 | @spec signature_digest(keccak_hash(), String.t()) :: ExPlasma.Crypto.sig_t() 31 | def signature_digest(hash_digest, private_key_hash) do 32 | private_key_binary = Encoding.to_binary!(private_key_hash) 33 | 34 | {:ok, {r, s, recovery_id}} = 35 | ExSecp256k1.sign( 36 | hash_digest, 37 | private_key_binary 38 | ) 39 | 40 | # EIP-155 41 | # See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md 42 | rid = @base_recovery_id + recovery_id 43 | 44 | r <> s <> <> 45 | end 46 | 47 | @doc """ 48 | Recovers a public key from a signed hash. 49 | 50 | This implements Eq.(208) of the Yellow Paper, adapted from https://stackoverflow.com/a/20000007 51 | 52 | ## Example 53 | 54 | iex(1)> ExPlasma.Signature.recover_public(<<2::256>>, 55 | ...(1)> 28, 56 | ...(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, 57 | ...(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 58 | ...(1)> ) 59 | {:ok, 60 | <<121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 61 | 217, 89, 242, 129, 91, 22, 248, 23, 152, 72, 58, 218, 119, 38, 163, 196, 101, 93, 164, 251, 252, 14, 17, 62 | 8, 168, 253, 23, 180, 72, 166, 133, 84, 25, 156, 71, 208, 143, 251, 16, 212, 184>>} 63 | """ 64 | @spec recover_public(keccak_hash(), hash_v, hash_r, hash_s, integer() | nil) :: 65 | {:ok, public_key} | {:error, String.t()} 66 | def recover_public(hash, v, r, s, chain_id \\ nil) do 67 | r_bin = pad(:binary.encode_unsigned(r), @signature_len) 68 | s_bin = pad(:binary.encode_unsigned(s), @signature_len) 69 | 70 | # Fork Ψ EIP-155 71 | recovery_id = 72 | if not is_nil(chain_id) and uses_chain_id?(v) do 73 | v - chain_id * 2 - @base_recovery_id_eip_155 74 | else 75 | v - @base_recovery_id 76 | end 77 | 78 | case ExSecp256k1.recover(hash, r_bin, s_bin, recovery_id) do 79 | {:ok, <<_byte::8, public_key::binary()>>} -> {:ok, public_key} 80 | {:error, reason} -> {:error, to_string(reason)} 81 | end 82 | end 83 | 84 | @doc """ 85 | Recovers a public key from a signed hash. 86 | 87 | This implements Eq.(208) of the Yellow Paper, adapted from https://stackoverflow.com/a/20000007 88 | 89 | ## Example 90 | 91 | iex(1)> ExPlasma.Signature.recover_public(<<2::256>>, <<168, 39, 110, 198, 11, 113, 141, 8, 168, 151, 22, 210, 198, 150, 24, 111, 23, 92 | ...(1)> 173, 42, 122, 59, 152, 143, 224, 214, 70, 96, 204, 31, 173, 154, 198, 97, 94, 93 | ...(1)> 203, 172, 169, 136, 182, 131, 11, 106, 54, 190, 96, 128, 227, 222, 248, 231, 94 | ...(1)> 75, 254, 141, 233, 113, 49, 74, 28, 189, 73, 249, 32, 89, 165, 27>>) 95 | {:ok, 96 | <<233, 102, 200, 175, 51, 251, 139, 85, 204, 181, 94, 133, 233, 88, 251, 156, 97 | 123, 157, 146, 192, 53, 73, 125, 213, 245, 12, 143, 102, 54, 70, 126, 35, 34, 98 | 167, 2, 255, 248, 68, 210, 117, 183, 156, 4, 185, 77, 27, 53, 239, 10, 57, 99 | 140, 63, 81, 87, 133, 241, 241, 210, 250, 35, 76, 232, 2, 153>>} 100 | """ 101 | def recover_public(hash, <>, chain_id \\ nil) do 102 | recover_public(hash, v, r, s, chain_id) 103 | end 104 | 105 | @spec uses_chain_id?(hash_v) :: boolean() 106 | defp uses_chain_id?(v) do 107 | v >= @base_recovery_id_eip_155 108 | end 109 | 110 | @spec pad(binary(), signature_len()) :: binary() 111 | defp pad(binary, desired_length) do 112 | desired_bits = desired_length * 8 113 | 114 | case byte_size(binary) do 115 | 0 -> 116 | <<0::size(desired_bits)>> 117 | 118 | x when x <= desired_length -> 119 | padding_bits = (desired_length - x) * 8 120 | <<0::size(padding_bits)>> <> binary 121 | 122 | _ -> 123 | raise "Binary too long for padding" 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/ex_plasma/transaction/signed.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Signed do 2 | @moduledoc """ 3 | Holds functions related to transactions containing signatures. 4 | """ 5 | 6 | alias ExPlasma.Crypto 7 | alias ExPlasma.Signature 8 | alias ExPlasma.Transaction 9 | alias ExPlasma.Transaction.Witness 10 | alias ExPlasma.TypedData 11 | alias ExPlasma.Utils.RlpDecoder 12 | 13 | @type tx_bytes() :: binary() 14 | @type decoding_error() :: :malformed_rlp | :malformed_witnesses 15 | @type validation_error() :: {:witnesses, :malformed_witnesses} 16 | @type sigs() :: list(Crypto.sig_t()) | [] 17 | 18 | @doc """ 19 | Decodes a binary expecting it to represent a signed transactions with 20 | the signatures being the first element of the decoded RLP list. 21 | 22 | Returns {:ok, signed_tx_rlp_items} if the encoded RLP can be decoded, 23 | or {:error, atom} otherwise. 24 | 25 | Only validates that the RLP is structurally correct. 26 | Does not perform any other kind of validation, use validate/1 for that. 27 | """ 28 | @spec decode(tx_bytes()) :: {:ok, list()} | {:error, decoding_error()} 29 | def decode(signed_tx_bytes) do 30 | with {:ok, tx_rlp_items} <- RlpDecoder.decode(signed_tx_bytes), 31 | {:ok, signed_tx_rlp_items} <- validate_rlp_items(tx_rlp_items) do 32 | {:ok, signed_tx_rlp_items} 33 | end 34 | end 35 | 36 | @doc """ 37 | Validate a signed transaction. 38 | 39 | Returns :ok if valid or {:error, {:witnesses, :malformed_witnesses}} otherwise. 40 | """ 41 | @spec validate(Transaction.t()) :: :ok | {:error, validation_error()} 42 | def validate(transaction), do: validate_sigs(transaction.sigs) 43 | 44 | @doc """ 45 | Recovers the witnesses for non-empty signatures, in the order they appear in transaction's signatures. 46 | 47 | Returns {:ok, witness_list} if witnesses are recoverable, 48 | or {:error, :corrupted_witness} otherwise. 49 | """ 50 | @spec get_witnesses(Transaction.t()) :: {:ok, list(Witness.t())} | {:error, Witness.recovery_error()} 51 | def get_witnesses(%Transaction{sigs: []}), do: {:ok, []} 52 | 53 | def get_witnesses(transaction) do 54 | hash = TypedData.hash(transaction) 55 | 56 | transaction.sigs 57 | |> Enum.reverse() 58 | |> Enum.reduce_while({:ok, []}, fn signature, {:ok, addresses} -> 59 | case Witness.recover(hash, signature) do 60 | {:ok, address} -> 61 | {:cont, {:ok, [address | addresses]}} 62 | 63 | error -> 64 | {:halt, error} 65 | end 66 | end) 67 | end 68 | 69 | @spec compute_signatures(Transaction.t(), list(String.t())) :: {:ok, sigs()} | {:error, :not_signable} 70 | def compute_signatures(transaction, keys) when is_list(keys) do 71 | case TypedData.impl_for(transaction) do 72 | nil -> 73 | {:error, :not_signable} 74 | 75 | _ -> 76 | eip712_hash = TypedData.hash(transaction) 77 | sigs = Enum.map(keys, fn key -> Signature.signature_digest(eip712_hash, key) end) 78 | {:ok, sigs} 79 | end 80 | end 81 | 82 | defp validate_rlp_items([sigs | _typed_tx_rlp_items] = rlp) when is_list(sigs), do: {:ok, rlp} 83 | defp validate_rlp_items([_sigs | _typed_tx_rlp_items]), do: {:error, :malformed_witnesses} 84 | defp validate_rlp_items(_), do: {:error, :malformed_transaction} 85 | 86 | defp validate_sigs([sig | rest]) do 87 | case Witness.valid?(sig) do 88 | true -> validate_sigs(rest) 89 | false -> {:error, {:witnesses, :malformed_witnesses}} 90 | end 91 | end 92 | 93 | defp validate_sigs([]), do: :ok 94 | end 95 | -------------------------------------------------------------------------------- /lib/ex_plasma/transaction/type/fee.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.Fee do 2 | @moduledoc """ 3 | Implementation of Transaction behaviour for Fee claiming type. 4 | """ 5 | 6 | @behaviour ExPlasma.Transaction 7 | 8 | import ABI.TypeEncoder, only: [encode_raw: 2] 9 | 10 | alias __MODULE__.Validator 11 | alias ExPlasma.Crypto 12 | alias ExPlasma.Output 13 | alias ExPlasma.Transaction 14 | alias ExPlasma.Transaction.TypeMapper 15 | 16 | @tx_type TypeMapper.tx_type_for(:tx_fee_token_claim) 17 | @output_type TypeMapper.output_type_for(:output_fee_token_claim) 18 | 19 | @type validation_error() :: Validator.outputs_validation_error() | {:nonce, :malformed_nonce} 20 | @type mapping_error() :: :malformed_transaction 21 | 22 | @doc """ 23 | Creates output for a fee transaction 24 | 25 | ## Example 26 | 27 | iex> output = new_output(<<1::160>>, <<0::160>>, 1) 28 | iex> %ExPlasma.Output{ 29 | ...> output_data: %{amount: 1, output_guard: <<1::160>>, token: <<0::160>>}, 30 | ...> output_id: nil, 31 | ...> output_type: 2 32 | ...> } = output 33 | """ 34 | @spec new_output(Crypto.address_t(), Crypto.address_t(), pos_integer()) :: Output.t() 35 | def new_output(fee_claimer, token, amount) do 36 | %Output{ 37 | output_type: @output_type, 38 | output_data: %{ 39 | amount: amount, 40 | output_guard: fee_claimer, 41 | token: token 42 | } 43 | } 44 | end 45 | 46 | @impl Transaction 47 | def build_nonce(%{blknum: blknum, token: token}) do 48 | blknum_bytes = encode_raw([blknum], [{:uint, 256}]) 49 | token_bytes = encode_raw([token], [:address]) 50 | 51 | {:ok, Crypto.keccak_hash(blknum_bytes <> token_bytes)} 52 | end 53 | 54 | def build_nonce(_), do: {:error, :invalid_nonce_params} 55 | 56 | @doc """ 57 | Turns a structure instance into a structure of RLP items, ready to be RLP encoded 58 | """ 59 | @impl Transaction 60 | def to_rlp(transaction) do 61 | case encode_outputs(transaction.outputs) do 62 | {:ok, outputs} -> 63 | {:ok, 64 | [ 65 | <<@tx_type>>, 66 | outputs, 67 | transaction.nonce 68 | ]} 69 | end 70 | end 71 | 72 | @doc """ 73 | Decodes an RLP list into a Fee Transaction. 74 | 75 | Only validates that the RLP is structurally correct. 76 | Does not perform any other kind of validation, use validate/1 for that. 77 | """ 78 | @impl Transaction 79 | def to_map([<<@tx_type>>, outputs_rlp, nonce_rlp]) do 80 | case map_outputs(outputs_rlp) do 81 | {:ok, outputs} -> 82 | {:ok, 83 | %Transaction{ 84 | tx_type: @tx_type, 85 | outputs: outputs, 86 | nonce: nonce_rlp 87 | }} 88 | 89 | {:error, _mapping_error_atom} = error -> 90 | error 91 | end 92 | end 93 | 94 | def to_map(_), do: {:error, :malformed_transaction} 95 | 96 | @doc """ 97 | Validates the Transaction. 98 | """ 99 | @impl Transaction 100 | def validate(transaction) do 101 | with :ok <- Validator.validate_outputs(transaction.outputs), 102 | :ok <- Validator.validate_nonce(transaction.nonce) do 103 | :ok 104 | end 105 | end 106 | 107 | defp encode_outputs(outputs) when is_list(outputs), do: reduce_outputs(outputs, [], &Output.to_rlp/1) 108 | defp encode_outputs(_outputs), do: {:error, :malformed_output_rlp} 109 | 110 | defp map_outputs(outputs) when is_list(outputs), do: reduce_outputs(outputs, [], &Output.to_map/1) 111 | defp map_outputs(_outputs), do: {:error, :malformed_output_rlp} 112 | 113 | defp reduce_outputs([], reduced, _reducing_func), do: {:ok, Enum.reverse(reduced)} 114 | 115 | defp reduce_outputs([output | rest], reduced, reducing_func) do 116 | case reducing_func.(output) do 117 | {:ok, item} -> reduce_outputs(rest, [item | reduced], reducing_func) 118 | error -> error 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/ex_plasma/transaction/type/fee/fee_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.Fee.Validator do 2 | @moduledoc """ 3 | Contain stateless validation logic for Fee transactions 4 | """ 5 | 6 | alias ExPlasma.Output 7 | alias ExPlasma.Transaction.TypeMapper 8 | 9 | @output_type TypeMapper.output_type_for(:output_fee_token_claim) 10 | 11 | @type outputs_validation_error() :: 12 | {:outputs, :wrong_number_of_fee_outputs} 13 | | {:outputs, :fee_output_amount_has_to_be_positive} 14 | | {:outputs, :invalid_output_type_for_transaction} 15 | 16 | @spec validate_outputs(list(Output)) :: :ok | {:error, outputs_validation_error()} 17 | def validate_outputs(outputs) do 18 | with {:ok, output} <- validate_outputs_count(outputs), 19 | :ok <- validate_generic_output(output), 20 | :ok <- validate_output_type(output), 21 | :ok <- validate_output_amount(output) do 22 | :ok 23 | end 24 | end 25 | 26 | defp validate_generic_output(output) do 27 | with :ok <- Output.validate(output), do: :ok 28 | end 29 | 30 | defp validate_outputs_count([output]), do: {:ok, output} 31 | defp validate_outputs_count(_outputs), do: {:error, {:outputs, :wrong_number_of_fee_outputs}} 32 | 33 | defp validate_output_type(%Output{output_type: @output_type}), do: :ok 34 | defp validate_output_type(_output), do: {:error, {:outputs, :invalid_output_type_for_transaction}} 35 | 36 | defp validate_output_amount(%Output{output_data: %{amount: amount}}) when amount > 0, do: :ok 37 | defp validate_output_amount(_output), do: {:error, {:outputs, :fee_output_amount_has_to_be_positive}} 38 | 39 | @spec validate_nonce(binary()) :: :ok | {:error, {:nonce, :malformed_nonce}} 40 | def validate_nonce(nonce) when is_binary(nonce) and byte_size(nonce) == 32, do: :ok 41 | def validate_nonce(_nonce), do: {:error, {:nonce, :malformed_nonce}} 42 | end 43 | -------------------------------------------------------------------------------- /lib/ex_plasma/transaction/type/payment_v1.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.PaymentV1 do 2 | @moduledoc """ 3 | Implementation of Transaction behaviour for Payment V1 type. 4 | """ 5 | 6 | @behaviour ExPlasma.Transaction 7 | 8 | alias __MODULE__.Validator 9 | alias ExPlasma.Crypto 10 | alias ExPlasma.Output 11 | alias ExPlasma.Transaction 12 | alias ExPlasma.Transaction.TypeMapper 13 | alias ExPlasma.Utils.RlpDecoder 14 | 15 | require __MODULE__.Validator 16 | 17 | @empty_metadata <<0::256>> 18 | @empty_tx_data 0 19 | 20 | @tx_type TypeMapper.tx_type_for(:tx_payment_v1) 21 | @output_type TypeMapper.output_type_for(:output_payment_v1) 22 | 23 | @type outputs() :: list(Output.t()) | [] 24 | @type metadata() :: <<_::256>> | nil 25 | 26 | @type validation_error() :: 27 | Validator.inputs_validation_error() 28 | | Validator.outputs_validation_error() 29 | | {:tx_data, :malformed_tx_data} 30 | | {:metadata, :malformed_metadata} 31 | 32 | @type mapping_error() :: 33 | :malformed_transaction 34 | | :malformed_tx_data 35 | | :malformed_input_position_rlp 36 | | :malformed_output_rlp 37 | 38 | @doc """ 39 | Creates output for a payment v1 transaction 40 | 41 | ## Example 42 | 43 | iex> output = new_output(<<1::160>>, <<0::160>>, 1) 44 | iex> %ExPlasma.Output{ 45 | ...> output_data: %{amount: 1, output_guard: <<1::160>>, token: <<0::160>>}, 46 | ...> output_id: nil, 47 | ...> output_type: 1 48 | ...> } = output 49 | """ 50 | @spec new_output(Crypto.address_t(), Crypto.address_t(), pos_integer()) :: Output.t() 51 | def new_output(owner, token, amount) do 52 | %Output{ 53 | output_type: @output_type, 54 | output_data: %{ 55 | amount: amount, 56 | output_guard: owner, 57 | token: token 58 | } 59 | } 60 | end 61 | 62 | @impl Transaction 63 | def build_nonce(_params), do: {:ok, nil} 64 | 65 | @doc """ 66 | Turns a structure instance into a structure of RLP items, ready to be RLP encoded 67 | """ 68 | @impl Transaction 69 | def to_rlp(transaction) do 70 | with {:ok, inputs} <- encode_inputs(transaction.inputs), 71 | {:ok, outputs} <- encode_outputs(transaction.outputs) do 72 | {:ok, 73 | [ 74 | <<@tx_type>>, 75 | inputs, 76 | outputs, 77 | @empty_tx_data, 78 | transaction.metadata || @empty_metadata 79 | ]} 80 | end 81 | end 82 | 83 | @doc """ 84 | Decodes an RLP list into a Payment V1 Transaction. 85 | 86 | Only validates that the RLP is structurally correct. 87 | Does not perform any other kind of validation, use validate/1 for that. 88 | """ 89 | @impl Transaction 90 | def to_map([<<@tx_type>>, inputs_rlp, outputs_rlp, tx_data_rlp, metadata_rlp]) do 91 | with {:ok, inputs} <- map_inputs(inputs_rlp), 92 | {:ok, outputs} <- map_outputs(outputs_rlp), 93 | {:ok, tx_data} <- decode_tx_data(tx_data_rlp), 94 | {:ok, metadata} <- decode_metadata(metadata_rlp) do 95 | {:ok, 96 | %Transaction{ 97 | tx_type: @tx_type, 98 | inputs: inputs, 99 | outputs: outputs, 100 | tx_data: tx_data, 101 | metadata: metadata 102 | }} 103 | end 104 | end 105 | 106 | def to_map(_), do: {:error, :malformed_transaction} 107 | 108 | @doc """ 109 | Validates the Transaction. 110 | """ 111 | @impl Transaction 112 | def validate(transaction) do 113 | with :ok <- Validator.validate_inputs(transaction.inputs), 114 | :ok <- Validator.validate_outputs(transaction.outputs), 115 | :ok <- Validator.validate_tx_data(transaction.tx_data), 116 | :ok <- Validator.validate_metadata(transaction.metadata) do 117 | :ok 118 | end 119 | end 120 | 121 | defp encode_inputs(inputs) when is_list(inputs), do: reduce_outputs(inputs, [], &Output.to_rlp_id/1) 122 | defp encode_inputs(_inputs), do: {:error, :malformed_input_position_rlp} 123 | 124 | defp encode_outputs(outputs) when is_list(outputs), do: reduce_outputs(outputs, [], &Output.to_rlp/1) 125 | defp encode_outputs(_outputs), do: {:error, :malformed_output_rlp} 126 | 127 | defp map_inputs(inputs) when is_list(inputs), do: reduce_outputs(inputs, [], &Output.decode_id/1) 128 | defp map_inputs(_inputs), do: {:error, :malformed_input_position_rlp} 129 | 130 | defp map_outputs(outputs) when is_list(outputs), do: reduce_outputs(outputs, [], &Output.to_map/1) 131 | defp map_outputs(_outputs), do: {:error, :malformed_output_rlp} 132 | 133 | defp reduce_outputs([], reduced, _reducing_func), do: {:ok, Enum.reverse(reduced)} 134 | 135 | defp reduce_outputs([output | rest], reduced, reducing_func) do 136 | case reducing_func.(output) do 137 | {:ok, item} -> reduce_outputs(rest, [item | reduced], reducing_func) 138 | error -> error 139 | end 140 | end 141 | 142 | defp decode_metadata(metadata_rlp) when Validator.is_metadata(metadata_rlp), do: {:ok, metadata_rlp} 143 | defp decode_metadata(_), do: {:error, :malformed_metadata} 144 | 145 | defp decode_tx_data(@empty_tx_data), do: {:ok, @empty_tx_data} 146 | 147 | defp decode_tx_data(tx_data_rlp) do 148 | case RlpDecoder.parse_uint256(tx_data_rlp) do 149 | {:ok, tx_data} -> {:ok, tx_data} 150 | _ -> {:error, :malformed_tx_data} 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/ex_plasma/transaction/type/payment_v1/payment_v1_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.PaymentV1.Validator do 2 | @moduledoc """ 3 | Contain stateless validation logic for Payment V1 transactions 4 | """ 5 | 6 | alias ExPlasma.Output 7 | alias ExPlasma.Transaction.TypeMapper 8 | 9 | @empty_tx_data 0 10 | @output_limit 4 11 | 12 | @output_type TypeMapper.output_type_for(:output_payment_v1) 13 | 14 | @type inputs_validation_error() :: {:inputs, :duplicate_inputs} | {:inputs, :cannot_exceed_maximum_value} 15 | 16 | @type outputs_validation_error() :: 17 | {:outputs, :cannot_exceed_maximum_value} 18 | | {:outputs, :cannot_subceed_minimum_value} 19 | | {:outputs, :invalid_output_type_for_transaction} 20 | 21 | defmacro is_metadata(metadata) do 22 | quote do 23 | is_binary(unquote(metadata)) and byte_size(unquote(metadata)) == 32 24 | end 25 | end 26 | 27 | @spec validate_inputs(list(Output)) :: :ok | {:error, inputs_validation_error()} 28 | def validate_inputs(inputs) do 29 | with :ok <- validate_generic_output(inputs), 30 | :ok <- validate_unique_inputs(inputs), 31 | :ok <- validate_outputs_count(:inputs, inputs, 0) do 32 | :ok 33 | end 34 | end 35 | 36 | @spec validate_outputs(list(Output)) :: :ok | {:error, outputs_validation_error()} 37 | def validate_outputs(outputs) do 38 | with :ok <- validate_generic_output(outputs), 39 | :ok <- validate_outputs_count(:outputs, outputs, 1), 40 | :ok <- validate_outputs_type(outputs) do 41 | :ok 42 | end 43 | end 44 | 45 | @spec validate_tx_data(any()) :: :ok | {:error, {:tx_data, :malformed_tx_data}} 46 | # txData is required to be zero in the contract 47 | def validate_tx_data(@empty_tx_data), do: :ok 48 | def validate_tx_data(_), do: {:error, {:tx_data, :malformed_tx_data}} 49 | 50 | @spec validate_metadata(<<_::256>>) :: :ok | {:error, {:metadata, :malformed_metadata}} 51 | def validate_metadata(metadata) when is_metadata(metadata), do: :ok 52 | def validate_metadata(_), do: {:error, {:metadata, :malformed_metadata}} 53 | 54 | defp validate_generic_output([output | rest]) do 55 | case Output.validate(output) do 56 | :ok -> validate_generic_output(rest) 57 | error -> error 58 | end 59 | end 60 | 61 | defp validate_generic_output([]), do: :ok 62 | 63 | defp validate_unique_inputs(inputs) do 64 | case inputs == Enum.uniq(inputs) do 65 | true -> :ok 66 | false -> {:error, {:inputs, :duplicate_inputs}} 67 | end 68 | end 69 | 70 | defp validate_outputs_count(field, list, _min_limit) when length(list) > @output_limit do 71 | {:error, {field, :cannot_exceed_maximum_value}} 72 | end 73 | 74 | defp validate_outputs_count(field, list, min_limit) when length(list) < min_limit do 75 | {:error, {field, :cannot_subceed_minimum_value}} 76 | end 77 | 78 | defp validate_outputs_count(_field, _list, _min_limit), do: :ok 79 | 80 | defp validate_outputs_type(outputs) do 81 | case Enum.all?(outputs, &(&1.output_type == @output_type)) do 82 | true -> :ok 83 | false -> {:error, {:outputs, :invalid_output_type_for_transaction}} 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/ex_plasma/transaction/type_mapper.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.TypeMapper do 2 | @moduledoc """ 3 | Provides wire format's tx/output type values and mapping to modules which decodes them. 4 | """ 5 | 6 | alias ExPlasma.Output 7 | alias ExPlasma.Transaction 8 | 9 | @type tx_type_to_tx_module_map() :: %{1 => Transaction.Type.PaymentV1, 3 => Transaction.Type.Fee} 10 | @type tx_type_to_output_module_map() :: %{ 11 | 0 => Output.Type.AbstractPayment, 12 | 1 => Output.Type.PaymentV1, 13 | 2 => Output.Type.Fee 14 | } 15 | 16 | @tx_type_values %{ 17 | tx_payment_v1: 1, 18 | tx_fee_token_claim: 3 19 | } 20 | 21 | @tx_type_modules %{ 22 | 1 => Transaction.Type.PaymentV1, 23 | 3 => Transaction.Type.Fee 24 | } 25 | 26 | @output_type_values %{ 27 | abstract_payment: 0, 28 | output_payment_v1: 1, 29 | output_fee_token_claim: 2 30 | } 31 | 32 | @output_type_modules %{ 33 | # NB: work-around the TypeData using a "zeroed-out" output to hash the eip712 struct with. 34 | 0 => Output.Type.AbstractPayment, 35 | 1 => Output.Type.PaymentV1, 36 | 2 => Output.Type.Fee 37 | } 38 | 39 | @known_tx_types Map.keys(@tx_type_values) 40 | @known_output_types Map.keys(@output_type_values) 41 | 42 | @doc """ 43 | Returns wire format type value of known transaction type 44 | """ 45 | @spec tx_type_for(tx_type :: atom()) :: non_neg_integer() 46 | def tx_type_for(tx_type) when tx_type in @known_tx_types, do: @tx_type_values[tx_type] 47 | 48 | @doc """ 49 | Returns module atom that is able to decode transaction of given type 50 | """ 51 | @spec tx_type_modules() :: tx_type_to_tx_module_map() 52 | def tx_type_modules(), do: @tx_type_modules 53 | 54 | @doc """ 55 | Returns wire format type value of known output type 56 | """ 57 | @spec output_type_for(output_type :: atom()) :: non_neg_integer() 58 | def output_type_for(output_type) when output_type in @known_output_types, do: @output_type_values[output_type] 59 | 60 | @doc """ 61 | Returns module atom that is able to decode output of given type 62 | """ 63 | @spec output_type_modules() :: tx_type_to_output_module_map() 64 | def output_type_modules(), do: @output_type_modules 65 | end 66 | -------------------------------------------------------------------------------- /lib/ex_plasma/transaction/witness.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Witness do 2 | @moduledoc """ 3 | Code required to validate and recover raw witnesses (e.g. signatures) goes here. 4 | """ 5 | 6 | alias ExPlasma.Crypto 7 | 8 | @signature_length 65 9 | 10 | @type t :: Crypto.address_t() 11 | @type recovery_error() :: :corrupted_witness | :malformed_witnesses 12 | 13 | @doc """ 14 | Pre-check done after decoding to quickly assert whether the witness has one of valid forms 15 | """ 16 | def valid?(witness) when is_binary(witness), do: has_valid_length?(witness) 17 | def valid?(_), do: false 18 | 19 | @doc """ 20 | Prepares the witness to be quickly used in stateful validation 21 | """ 22 | @spec recover(Crypto.hash_t(), Crypto.sig_t()) :: {:ok, Crypto.address_t()} | {:error, recovery_error()} 23 | def recover(raw_tx_hash, raw_witness) when is_binary(raw_witness) do 24 | Crypto.recover_address(raw_tx_hash, raw_witness) 25 | end 26 | 27 | def recover(_, _), do: {:error, :malformed_witnesses} 28 | 29 | defp has_valid_length?(sig) when byte_size(sig) == @signature_length, do: true 30 | defp has_valid_length?(_sig), do: false 31 | end 32 | -------------------------------------------------------------------------------- /lib/ex_plasma/typed_data.ex: -------------------------------------------------------------------------------- 1 | defprotocol ExPlasma.TypedData do 2 | @moduledoc """ 3 | EIP 712 signing encoding 4 | """ 5 | 6 | @doc """ 7 | The EIP712 encoded type data structure. 8 | """ 9 | @spec encode(any(), maybe_improper_list()) :: maybe_improper_list() 10 | def encode(data, options \\ []) 11 | 12 | @doc """ 13 | The keccak hash of the encoded data type. 14 | """ 15 | @spec hash(any(), maybe_improper_list()) :: binary() 16 | def hash(data, options \\ []) 17 | end 18 | -------------------------------------------------------------------------------- /lib/ex_plasma/typed_data/output.ex: -------------------------------------------------------------------------------- 1 | defimpl ExPlasma.TypedData, for: ExPlasma.Output do 2 | import ExPlasma.Crypto, only: [keccak_hash: 1] 3 | import ABI.TypeEncoder, only: [encode_raw: 2] 4 | 5 | alias ExPlasma.Output 6 | 7 | @output_signature "Output(uint256 outputType,bytes20 outputGuard,address currency,uint256 amount)" 8 | @input_signature "Input(uint256 blknum,uint256 txindex,uint256 oindex)" 9 | 10 | @spec encode(Output.t(), as: atom()) :: list() 11 | def encode(output, as: :input), do: do_to_rlp_id(output.output_id) 12 | def encode(output, as: :output), do: do_encode(output) 13 | 14 | @spec hash(Output.t(), [{:as, :input | :output}, ...]) :: <<_::256>> 15 | def hash(output, options), do: output |> encode(options) |> do_hash(options) 16 | 17 | defp do_encode(%{output_type: type, output_data: data}) do 18 | [ 19 | @output_signature, 20 | encode_raw([type], [{:uint, 256}]), 21 | encode_raw([data.output_guard], [{:bytes, 20}]), 22 | encode_raw([data.token], [:address]), 23 | encode_raw([data.amount], [{:uint, 256}]) 24 | ] 25 | end 26 | 27 | defp do_to_rlp_id(%{blknum: blknum, txindex: txindex, oindex: oindex}) do 28 | [ 29 | @input_signature, 30 | encode_raw([blknum], [{:uint, 256}]), 31 | encode_raw([txindex], [{:uint, 256}]), 32 | encode_raw([oindex], [{:uint, 256}]) 33 | ] 34 | end 35 | 36 | defp do_hash([signature | encoded_list], _options) do 37 | data = [keccak_hash(signature) | encoded_list] 38 | 39 | data 40 | |> Enum.join() 41 | |> keccak_hash() 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/ex_plasma/typed_data/transaction.ex: -------------------------------------------------------------------------------- 1 | defimpl ExPlasma.TypedData, for: ExPlasma.Transaction do 2 | import ExPlasma.Crypto, only: [keccak_hash: 1] 3 | import ExPlasma.Encoding, only: [to_binary!: 1] 4 | import ABI.TypeEncoder, only: [encode_raw: 2] 5 | 6 | alias ExPlasma.Configuration 7 | alias ExPlasma.Output 8 | alias ExPlasma.TypedData 9 | 10 | # Prefix and version byte motivated by http://eips.ethereum.org/EIPS/eip-191 11 | @eip_191_prefix <<0x19, 0x01>> 12 | 13 | @domain_signature "EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)" 14 | @signature "Transaction(uint256 txType,Input input0,Input input1,Input input2,Input input3,Output output0,Output output1,Output output2,Output output3,uint256 txData,bytes32 metadata)" 15 | @output_signature "Output(uint256 outputType,bytes20 outputGuard,address currency,uint256 amount)" 16 | @input_signature "Input(uint256 blknum,uint256 txindex,uint256 oindex)" 17 | 18 | # The full encoded signature for the transaction 19 | @encoded_signature @signature <> @input_signature <> @output_signature 20 | 21 | # NB: Currently we only support 1 type of transaction: Payment. 22 | @max_output_count 4 23 | 24 | @empty_input Output.decode_id!(<<0>>) 25 | @empty_input_hash TypedData.hash(@empty_input, as: :input) 26 | 27 | @empty_output %Output{ 28 | output_type: 0, 29 | output_data: %{output_guard: <<0::160>>, token: <<0::160>>, amount: 0} 30 | } 31 | @empty_output_hash TypedData.hash(@empty_output, as: :output) 32 | 33 | def encode(%{} = transaction, _options) do 34 | encoded_inputs = Enum.map(transaction.inputs, &encode_as_input/1) 35 | encoded_outputs = Enum.map(transaction.outputs, &encode_as_output/1) 36 | encoded_transaction_type = encode_raw([transaction.tx_type], [{:uint, 256}]) 37 | encoded_transaction_data = encode_raw([transaction.tx_data], [{:uint, 256}]) 38 | encoded_metadata = encode_raw([transaction.metadata], [{:bytes, 32}]) 39 | 40 | [ 41 | @eip_191_prefix, 42 | domain_separator(), 43 | @encoded_signature, 44 | encoded_transaction_type, 45 | encoded_inputs, 46 | encoded_outputs, 47 | encoded_transaction_data, 48 | encoded_metadata 49 | ] 50 | end 51 | 52 | def hash(%{} = transaction, options), do: transaction |> encode(options) |> hash(options) 53 | 54 | def hash([prefix, domain_separator | encoded_transaction], _options) do 55 | keccak_hash(prefix <> hash_domain(domain_separator) <> hash_encoded(encoded_transaction)) 56 | end 57 | 58 | defp domain_separator() do 59 | domain = Configuration.eip_712_domain() 60 | 61 | [ 62 | @domain_signature, 63 | domain.name, 64 | domain.version, 65 | domain.verifying_contract, 66 | domain.salt 67 | ] 68 | end 69 | 70 | defp hash_domain([signature, name, version, verifying_contract, salt]) do 71 | [ 72 | keccak_hash(signature), 73 | keccak_hash(name), 74 | keccak_hash(version), 75 | encode_raw([to_binary!(verifying_contract)], [:address]), 76 | encode_raw([to_binary!(salt)], [{:bytes, 32}]) 77 | ] 78 | |> Enum.join() 79 | |> keccak_hash() 80 | end 81 | 82 | defp hash_encoded([signature, transaction_type, inputs, outputs, transaction_data, metadata]) do 83 | [ 84 | keccak_hash(signature), 85 | transaction_type, 86 | hash_inputs(inputs), 87 | hash_outputs(outputs), 88 | transaction_data, 89 | metadata 90 | ] 91 | |> List.flatten() 92 | |> Enum.join() 93 | |> keccak_hash() 94 | end 95 | 96 | defp encode_as_input(output), do: TypedData.encode(output, as: :input) 97 | defp encode_as_output(output), do: TypedData.encode(output, as: :output) 98 | 99 | defp hash_inputs(inputs) do 100 | inputs 101 | |> Stream.map(&hash_output/1) 102 | |> Stream.concat(Stream.cycle([@empty_input_hash])) 103 | |> Enum.take(@max_output_count) 104 | end 105 | 106 | defp hash_outputs(outputs) do 107 | outputs 108 | |> Stream.map(&hash_output/1) 109 | |> Stream.concat(Stream.cycle([@empty_output_hash])) 110 | |> Enum.take(@max_output_count) 111 | end 112 | 113 | defp hash_output([signature | encoded_list]) do 114 | data = [keccak_hash(signature) | encoded_list] 115 | 116 | data 117 | |> Enum.join() 118 | |> keccak_hash() 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/ex_plasma/utils/rlp_decoder.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Utils.RlpDecoder do 2 | @moduledoc """ 3 | Provides functions to decode various data types from RLP raw format 4 | """ 5 | 6 | alias ExPlasma.Crypto 7 | 8 | @type parse_uint256_errors() :: :encoded_uint_too_big | :malformed_uint256 9 | 10 | @doc """ 11 | Attempt to decode the given binary to a list of RLP items 12 | """ 13 | @spec decode(binary()) :: {:ok, list()} | {:error, :malformed_rlp} 14 | def decode(tx_bytes) do 15 | {:ok, ExRLP.decode(tx_bytes)} 16 | rescue 17 | _ -> {:error, :malformed_rlp} 18 | end 19 | 20 | @doc """ 21 | Parses 20-bytes address 22 | Case `<<>>` is necessary, because RLP handles empty string equally to integer 0 23 | """ 24 | @spec parse_address(<<>> | Crypto.address_t()) :: {:ok, Crypto.address_t()} | {:error, :malformed_address} 25 | def parse_address(binary) 26 | def parse_address(<<_::160>> = address_bytes), do: {:ok, address_bytes} 27 | def parse_address(_), do: {:error, :malformed_address} 28 | 29 | @doc """ 30 | Parses unsigned at-most 32-bytes integer. Leading zeros are disallowed 31 | """ 32 | @spec parse_uint256(binary()) :: 33 | {:ok, non_neg_integer()} | {:error, parse_uint256_errors() | :leading_zeros_in_encoded_uint} 34 | def parse_uint256(<<0>>), do: {:ok, 0} 35 | def parse_uint256(<<0>> <> _binary), do: {:error, :leading_zeros_in_encoded_uint} 36 | def parse_uint256(binary), do: parse_uint256_with_leading(binary) 37 | 38 | @doc """ 39 | Parses unsigned at-most 32-bytes integer. Leading zeros are allowed 40 | """ 41 | @spec parse_uint256_with_leading(binary()) :: {:ok, non_neg_integer()} | {:error, parse_uint256_errors()} 42 | def parse_uint256_with_leading(binary) when byte_size(binary) <= 32, do: {:ok, :binary.decode_unsigned(binary, :big)} 43 | def parse_uint256_with_leading(binary) when byte_size(binary) > 32, do: {:error, :encoded_uint_too_big} 44 | def parse_uint256_with_leading(_), do: {:error, :malformed_uint256} 45 | end 46 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ex_plasma, 7 | version: "0.3.0", 8 | elixir: "~> 1.8", 9 | start_permanent: Mix.env() == :prod, 10 | elixirc_paths: elixirc_paths(Mix.env()), 11 | description: description(), 12 | deps: deps(), 13 | package: package(), 14 | name: "ExPlasma", 15 | docs: docs(), 16 | test_coverage: [tool: ExCoveralls], 17 | preferred_cli_env: [ 18 | coveralls: :test, 19 | "coveralls.detail": :test, 20 | "coveralls.post": :test, 21 | "coveralls.html": :test 22 | ], 23 | dialyzer: dialyzer() 24 | ] 25 | end 26 | 27 | defp description do 28 | """ 29 | A library for encoding, decoding and validating transactions used for the OMG Network Plasma contracts. 30 | """ 31 | end 32 | 33 | defp package do 34 | [ 35 | name: :ex_plasma, 36 | maintainers: ["OMG Network team"], 37 | licenses: ["Apache-2.0 License"], 38 | links: %{"GitHub" => "https://github.com/omgnetwork/ex_plasma"} 39 | ] 40 | end 41 | 42 | defp docs do 43 | [ 44 | main: "readme", 45 | extras: [ 46 | "README.md" 47 | ] 48 | ] 49 | end 50 | 51 | # Run "mix help compile.app" to learn about applications. 52 | def application do 53 | [ 54 | applications: [:ex_abi, :ex_rlp, :ex_keccak, :ex_secp256k1, :merkle_tree], 55 | extra_applications: [:logger] 56 | ] 57 | end 58 | 59 | defp elixirc_paths(:test), do: ["lib", "test/support"] 60 | defp elixirc_paths(_), do: ["lib"] 61 | 62 | # Run "mix help deps" to learn about dependencies. 63 | defp deps do 64 | [ 65 | {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, 66 | {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, 67 | {:ethereumex, "~> 0.6.0", only: [:test]}, 68 | {:ex_abi, "~> 0.5.1"}, 69 | {:ex_rlp, "~> 0.5.3"}, 70 | {:excoveralls, "~> 0.10", only: [:test]}, 71 | {:ex_keccak, "~> 0.1.2"}, 72 | {:ex_secp256k1, "~> 0.1.1"}, 73 | {:merkle_tree, "~> 2.0.0"}, 74 | {:stream_data, "~>0.4.3", only: [:test]}, 75 | {:telemetry, "~> 0.4", only: [:test]}, 76 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 77 | # {:dep_from_hexpm, "~> 0.3.0"}, 78 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 79 | ] 80 | end 81 | 82 | defp dialyzer do 83 | [ 84 | flags: [:error_handling, :race_conditions, :underspecs, :unknown, :unmatched_returns], 85 | ignore_warnings: "dialyzer.ignore-warnings", 86 | list_unused_filters: true, 87 | plt_add_apps: plt_apps() 88 | ] 89 | end 90 | 91 | defp plt_apps, 92 | do: [ 93 | :ex_abi, 94 | :ex_rlp, 95 | :merkle_tree 96 | ] 97 | end 98 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, 4 | "credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"}, 5 | "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 7 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 8 | "ethereumex": {:hex, :ethereumex, "0.6.4", "58e998acb13b45a2b76b954b1d503f2f5f0e8118c0a14769c59264ef3ee4c301", [:mix], [{:httpoison, "~> 1.6", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "abc0bed1ba691645700f55bc843be7d08a23284e20ad889cabe6279e13debb32"}, 9 | "ex_abi": {:hex, :ex_abi, "0.5.1", "e12373d4511de65c8c7e40011fa40ac05bda687de819eb0f41fc36d4e96e7b04", [:mix], [{:ex_keccak, "~> 0.1.2", [hex: :ex_keccak, repo: "hexpm", optional: false]}], "hexpm", "a2d8b8030cb6af03e69dba7395b1929422d7cb15b8275d89844c2495acf9da1f"}, 10 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 11 | "ex_keccak": {:hex, :ex_keccak, "0.1.2", "4548914dc250a919712f03b4574fd2d048cd6d1245481b559abe9e7b9adafbd3", [:mix], [{:rustler, "~> 0.21.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "da2a11b1f95727193865f9184b6af9e1665e82b84f3c531ab4b7323532dafebd"}, 12 | "ex_rlp": {:hex, :ex_rlp, "0.5.3", "9055bddade545ee3e734aaad62c4b4d08211834da3beb43ae269b75785909e5e", [:mix], [], "hexpm", "a755a5f8f9f66079f3ecbe021536b949077fac0df963d9e59a20321bab28722d"}, 13 | "ex_secp256k1": {:hex, :ex_secp256k1, "0.1.1", "84db200e2b05595e4cd922f01f5a133dc42e18966418e7b82fb80e866ff5881a", [:mix], [{:rustler, "~> 0.21.1 ", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "8b8adcc426995c48fdadab2a5bbd9a46e7809992539a8c4424d4ebc68e291456"}, 14 | "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, 15 | "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, 16 | "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, 17 | "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, 18 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 19 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 20 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, 21 | "merkle_tree": {:hex, :merkle_tree, "2.0.0", "84a9cbfd3d8fab545d69c320460ff713ddea2f64ec4c14edfcc4f8eba899d5ab", [:mix], [], "hexpm", "351a764e385ce75bd782a972fc9597d99bed4d692631903e4a08936d4939c895"}, 22 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 23 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 24 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 25 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 26 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, 27 | "rustler": {:hex, :rustler, "0.21.1", "5299980be32da997c54382e945bacaa015ed97a60745e1e639beaf6a7b278c65", [:mix], [{:toml, "~> 0.5.2", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "6ee1651e10645b2b2f3bb70502bf180341aa058709177e9bc28c105934094bc6"}, 28 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 29 | "stream_data": {:hex, :stream_data, "0.4.3", "62aafd870caff0849a5057a7ec270fad0eb86889f4d433b937d996de99e3db25", [:mix], [], "hexpm", "7dafd5a801f0bc897f74fcd414651632b77ca367a7ae4568778191fc3bf3a19a"}, 30 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 31 | "toml": {:hex, :toml, "0.5.2", "e471388a8726d1ce51a6b32f864b8228a1eb8edc907a0edf2bb50eab9321b526", [:mix], [], "hexpm", "f1e3dabef71fb510d015fad18c0e05e7c57281001141504c6b69d94e99750a07"}, 32 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, 33 | } 34 | -------------------------------------------------------------------------------- /plasma-contract.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | MAINTAINER OmiseGO Engineering 4 | 5 | WORKDIR /home/node 6 | 7 | RUN apk add --update \ 8 | python \ 9 | python-dev \ 10 | py-pip \ 11 | build-base \ 12 | git 13 | 14 | RUN git clone https://github.com/omisego/plasma-contracts.git 15 | RUN cd /home/node/plasma-contracts && git checkout 5ce7d0b 16 | RUN cd /home/node/plasma-contracts/plasma_framework && npm install 17 | -------------------------------------------------------------------------------- /test/conformance/in_flight_exit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Conformance.InFlightExitTest do 2 | @moduledoc """ 3 | Conformance tests that check our local in-flight exit implementation aligns with the plasma contracts. 4 | """ 5 | use ExUnit.Case, async: true 6 | alias ExPlasma.InFlightExit 7 | 8 | @moduletag :conformance 9 | 10 | # The address where ExitIDWrapper is deployed. Ganache keeps this deterministic.. 11 | @contract "0x32cf1f3a98aeaf57b88b3740875d19912a522c1a" 12 | 13 | @ife_tx_hexes [ 14 | "0x", 15 | "0x1234", 16 | "0xb26f143eb9e68e5b", 17 | "0x70de28d3cd1cb609", 18 | "0xc235a61a575eb3e2", 19 | "0x8fdeb13e6acdc74955fdcf0f345ae57a", 20 | "0x00000000000000000000000000000000", 21 | "0xffffffffffffffffffffffffffffffff" 22 | ] 23 | 24 | describe "tx_bytes_to_id/1" do 25 | test "matches PaymentExitGame.getInFlightExitId(bytes)" do 26 | Enum.each(@ife_tx_hexes, fn hex -> 27 | tx_bytes = ExPlasma.Encoding.to_binary!(hex) 28 | assert InFlightExit.tx_bytes_to_id(tx_bytes) == contract_get_in_flight_exit_id(tx_bytes) 29 | end) 30 | end 31 | end 32 | 33 | defp contract_get_in_flight_exit_id(tx_bytes) do 34 | eth_call("getInFlightExitId(bytes)", [tx_bytes], [to: @contract], fn resp -> 35 | resp |> decode_response([{:uint, ExPlasma.Configuration.exit_id_size()}]) |> hd() 36 | end) 37 | end 38 | 39 | defp eth_call(contract_signature, data, [to: to], callback) when is_list(data) do 40 | options = %{data: encode_data(contract_signature, data), to: to} 41 | 42 | case Ethereumex.HttpClient.eth_call(options) do 43 | {:ok, resp} -> callback.(resp) 44 | other -> other 45 | end 46 | end 47 | 48 | @spec encode_data(String.t(), list()) :: binary 49 | defp encode_data(function_signature, data) do 50 | data = ABI.encode(function_signature, data) 51 | "0x" <> Base.encode16(data, case: :lower) 52 | end 53 | 54 | @spec decode_response(String.t(), list()) :: list() 55 | defp decode_response("0x" <> unprefixed_hash_response, types) do 56 | unprefixed_hash_response 57 | |> Base.decode16!(case: :lower) 58 | |> ABI.TypeDecoder.decode_raw(types) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/conformance/signatures_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Conformance.SignaturesTest do 2 | @moduledoc """ 3 | Conformance tests that check our signing with the plasma contracts. 4 | """ 5 | 6 | use ExUnit.Case, async: false 7 | import ExPlasma.Encoding, only: [to_binary!: 1] 8 | 9 | alias ExPlasma.Output 10 | alias ExPlasma.Transaction 11 | alias ExPlasma.TypedData 12 | 13 | @moduletag :conformance 14 | 15 | # The address where eip 712 lib mock is deployed. Ganache keeps this 16 | # deterministic 17 | @contract "0xd3aa556287afe63102e5797bfddd2a1e8dbb3ea5" 18 | @authority "0x22d491bde2303f2f43325b2108d26f1eaba1e32b" 19 | @verifying "0xd17e1233a03affb9092d5109179b43d6a8828607" 20 | 21 | test "signs without inputs" do 22 | output = %Output{ 23 | output_type: 1, 24 | output_data: %{output_guard: to_binary!(@authority), token: <<0::160>>, amount: 1} 25 | } 26 | 27 | assert_signs_conform(%Transaction{tx_type: 1, outputs: [output]}) 28 | end 29 | 30 | test "signs with metadata" do 31 | %Transaction{tx_type: 1, metadata: <<1::160>>} 32 | end 33 | 34 | test "signs with a minimal transaction (1x1)" do 35 | input = %Output{output_id: %{blknum: 1, txindex: 0, oindex: 0}} 36 | 37 | output = %Output{ 38 | output_type: 1, 39 | output_data: %{output_guard: to_binary!(@authority), token: <<0::160>>, amount: 1} 40 | } 41 | 42 | assert_signs_conform(%Transaction{tx_type: 1, inputs: [input], outputs: [output]}) 43 | end 44 | 45 | test "signs with a minimal transaction (4x4)" do 46 | input = %Output{output_id: %{blknum: 1, txindex: 0, oindex: 0}} 47 | 48 | output = %Output{ 49 | output_type: 1, 50 | output_data: %{output_guard: to_binary!(@authority), token: <<0::160>>, amount: 1} 51 | } 52 | 53 | assert_signs_conform(%Transaction{ 54 | tx_type: 1, 55 | inputs: List.duplicate(input, 4), 56 | outputs: List.duplicate(output, 4) 57 | }) 58 | end 59 | 60 | defp assert_signs_conform(%Transaction{} = transaction) do 61 | tx_bytes = Transaction.encode!(transaction, signed: false) 62 | typed_data_hash = TypedData.hash(transaction) 63 | 64 | assert typed_data_hash == verify_hash(tx_bytes) 65 | end 66 | 67 | defp verify_hash(tx_bytes) do 68 | verifying_address = ExPlasma.Encoding.to_binary!(@verifying) 69 | 70 | eth_call("hashTx(address,bytes)", [verifying_address, tx_bytes], [to: @contract], fn resp -> 71 | resp |> decode_response([{:bytes, 32}]) |> hd() 72 | end) 73 | end 74 | 75 | defp eth_call(contract_signature, data_types, [to: to], callback) when is_list(data_types) do 76 | options = %{data: encode_data(contract_signature, data_types), to: to} 77 | 78 | case Ethereumex.HttpClient.eth_call(options) do 79 | {:ok, resp} -> callback.(resp) 80 | other -> other 81 | end 82 | end 83 | 84 | @spec encode_data(String.t(), list()) :: binary 85 | defp encode_data(function_signature, data) do 86 | data = ABI.encode(function_signature, data) 87 | "0x" <> Base.encode16(data, case: :lower) 88 | end 89 | 90 | @spec decode_response(String.t(), list()) :: list() 91 | defp decode_response("0x" <> unprefixed_hash_response, types) do 92 | unprefixed_hash_response 93 | |> Base.decode16!(case: :lower) 94 | |> ABI.TypeDecoder.decode_raw(types) 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test/ex_plasma/block_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.BlockTest do 2 | use ExUnit.Case, async: true 3 | doctest ExPlasma.Block 4 | 5 | alias ExPlasma.Block 6 | alias ExPlasma.Transaction 7 | 8 | describe "new/1" do 9 | test "creates a new block" do 10 | transaction = %Transaction{ 11 | inputs: [], 12 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 13 | tx_data: 0, 14 | tx_type: 1 15 | } 16 | 17 | result = Block.new([transaction]) 18 | 19 | assert result.transactions == [transaction] 20 | assert result.hash == ExPlasma.Merkle.root_hash([ExPlasma.encode!(transaction, signed: false)]) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/ex_plasma/builder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.BuilderTest do 2 | use ExUnit.Case, async: true 3 | doctest ExPlasma.Builder, import: true 4 | 5 | alias ExPlasma.Builder 6 | alias ExPlasma.Output 7 | alias ExPlasma.Support.TestEntity 8 | alias ExPlasma.Transaction 9 | alias ExPlasma.Transaction.Type.PaymentV1 10 | 11 | @alice TestEntity.alice() 12 | @bob TestEntity.bob() 13 | 14 | describe "new/1" do 15 | test "returns a transaction representing a payment v1 with given params" do 16 | input = %Output{output_id: %{blknum: 1, txindex: 0, oindex: 0}} 17 | output_1 = PaymentV1.new_output(<<1::160>>, <<0::160>>, 1) 18 | output_2 = PaymentV1.new_output(<<2::160>>, <<0::160>>, 2) 19 | metadata = <<1::256>> 20 | 21 | tx = Builder.new(ExPlasma.payment_v1(), metadata: <<1::256>>, inputs: [input], outputs: [output_1, output_2]) 22 | 23 | assert tx == %Transaction{ 24 | tx_type: 1, 25 | inputs: [input], 26 | metadata: metadata, 27 | outputs: [output_1, output_2], 28 | tx_data: 0 29 | } 30 | end 31 | 32 | test "returns an empty transaction struct of the given type when no param given" do 33 | tx = Builder.new(ExPlasma.payment_v1()) 34 | 35 | assert tx == %Transaction{ 36 | tx_type: 1, 37 | inputs: [], 38 | metadata: <<0::256>>, 39 | outputs: [], 40 | tx_data: 0 41 | } 42 | end 43 | end 44 | 45 | describe "with_nonce/2" do 46 | test "returns {:ok, transaction} with a nonce when valid" do 47 | tx = Builder.new(ExPlasma.fee()) 48 | 49 | assert tx.nonce == nil 50 | assert {:ok, tx_with_nonce} = Builder.with_nonce(tx, %{blknum: 1000, token: <<0::160>>}) 51 | assert %{nonce: nonce} = tx_with_nonce 52 | 53 | assert nonce == 54 | <<61, 119, 206, 68, 25, 203, 29, 23, 147, 224, 136, 32, 198, 128, 177, 74, 227, 250, 194, 173, 146, 182, 55 | 251, 152, 123, 172, 26, 83, 175, 194, 213, 238>> 56 | end 57 | 58 | test "returns {:error, atom} when not given valid params" do 59 | tx = Builder.new(ExPlasma.fee()) 60 | assert Builder.with_nonce(tx, %{}) == {:error, :invalid_nonce_params} 61 | end 62 | end 63 | 64 | describe "with_nonce!/2" do 65 | test "returns transaction with a nonce when valid" do 66 | tx = Builder.new(ExPlasma.fee()) 67 | 68 | assert tx.nonce == nil 69 | assert tx_with_nonce = Builder.with_nonce!(tx, %{blknum: 1000, token: <<0::160>>}) 70 | assert %{nonce: nonce} = tx_with_nonce 71 | 72 | assert nonce == 73 | <<61, 119, 206, 68, 25, 203, 29, 23, 147, 224, 136, 32, 198, 128, 177, 74, 227, 250, 194, 173, 146, 182, 74 | 251, 152, 123, 172, 26, 83, 175, 194, 213, 238>> 75 | end 76 | 77 | test "raises when not given valid params" do 78 | tx = Builder.new(ExPlasma.fee()) 79 | 80 | assert_raise MatchError, fn -> 81 | Builder.with_nonce!(tx, %{}) 82 | end 83 | end 84 | end 85 | 86 | describe "add_input/2" do 87 | test "adds input" do 88 | block_number = 99 89 | tx_index = 100 90 | oindex = 101 91 | 92 | assert %{inputs: [output]} = 93 | ExPlasma.payment_v1() 94 | |> Builder.new() 95 | |> Builder.add_input(blknum: block_number, txindex: tx_index, oindex: oindex) 96 | 97 | assert output.output_id.blknum == block_number 98 | assert output.output_id.txindex == tx_index 99 | assert output.output_id.oindex == oindex 100 | end 101 | 102 | test "appends new input" do 103 | block_number = 102 104 | tx_index = 103 105 | oindex = 104 106 | 107 | transaction = %Transaction{ 108 | inputs: [ 109 | %ExPlasma.Output{ 110 | output_data: nil, 111 | output_id: %{blknum: 99, oindex: 101, txindex: 100}, 112 | output_type: nil 113 | } 114 | ], 115 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 116 | outputs: [], 117 | tx_data: 0, 118 | tx_type: 1 119 | } 120 | 121 | assert %{inputs: [_output, output]} = 122 | Builder.add_input(transaction, blknum: block_number, txindex: tx_index, oindex: oindex) 123 | 124 | assert output.output_id.blknum == block_number 125 | assert output.output_id.txindex == tx_index 126 | assert output.output_id.oindex == oindex 127 | end 128 | end 129 | 130 | describe "add_output/2" do 131 | test "adds the given output when given `output_data` map to the existing transaction" do 132 | tx = Builder.new(ExPlasma.payment_v1()) 133 | 134 | assert tx.outputs == [] 135 | 136 | updated_tx = 137 | Builder.add_output(tx, output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1}) 138 | 139 | assert updated_tx.outputs == [ 140 | %Output{output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1}} 141 | ] 142 | end 143 | 144 | test "adds the given output when given output data to the existing transaction" do 145 | tx = Builder.new(ExPlasma.payment_v1()) 146 | 147 | assert tx.outputs == [] 148 | 149 | updated_tx = Builder.add_output(tx, output_guard: <<1::160>>, token: <<0::160>>, amount: 2) 150 | 151 | assert updated_tx.outputs == [ 152 | %Output{output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 2}} 153 | ] 154 | end 155 | end 156 | 157 | describe "sign/2" do 158 | test "returns {:ok, signed} when given valid keys" do 159 | %{priv_encoded: key_1} = @alice 160 | %{priv_encoded: key_2} = @bob 161 | 162 | assert {:ok, transaction} = 163 | ExPlasma.payment_v1() 164 | |> Builder.new() 165 | |> Builder.add_input(blknum: 1, txindex: 0, oindex: 0, position: 1_000_000_000) 166 | |> Builder.add_input(blknum: 2, txindex: 0, oindex: 0, position: 2_000_000_000) 167 | |> Builder.add_input(blknum: 3, txindex: 0, oindex: 0, position: 3_000_000_000) 168 | |> Builder.sign([key_1, key_1, key_2]) 169 | 170 | assert [_sig_1, _sig_2, _sig_3] = transaction.sigs 171 | end 172 | end 173 | 174 | describe "complete flow" do 175 | test "builds and sign a payment v1 transaction with both inputs and outputs" do 176 | %{priv_encoded: key_1} = @alice 177 | %{priv_encoded: key_2} = @bob 178 | 179 | assert {:ok, txn} = 180 | ExPlasma.payment_v1() 181 | |> Builder.new(metadata: <<1::160>>, tx_data: 0) 182 | |> Builder.add_input(blknum: 1, txindex: 0, oindex: 0) 183 | |> Builder.add_input(blknum: 2, txindex: 1, oindex: 0) 184 | |> Builder.add_input(blknum: 3, txindex: 0, oindex: 1) 185 | |> Builder.add_output( 186 | output_type: 1, 187 | output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1} 188 | ) 189 | |> Builder.add_output(output_guard: <<1::160>>, token: <<0::160>>, amount: 1) 190 | |> Builder.add_output(output_guard: <<2::160>>, token: <<0::160>>, amount: 2) 191 | |> Builder.sign([key_1, key_1, key_2]) 192 | 193 | assert txn == %Transaction{ 194 | inputs: [ 195 | %Output{ 196 | output_data: nil, 197 | output_id: %{blknum: 1, oindex: 0, txindex: 0}, 198 | output_type: nil 199 | }, 200 | %Output{ 201 | output_data: nil, 202 | output_id: %{blknum: 2, oindex: 0, txindex: 1}, 203 | output_type: nil 204 | }, 205 | %Output{ 206 | output_data: nil, 207 | output_id: %{blknum: 3, oindex: 1, txindex: 0}, 208 | output_type: nil 209 | } 210 | ], 211 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>, 212 | outputs: [ 213 | %Output{ 214 | output_data: %{ 215 | amount: 1, 216 | output_guard: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>, 217 | token: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 218 | }, 219 | output_id: nil, 220 | output_type: 1 221 | }, 222 | %Output{ 223 | output_data: %{ 224 | amount: 1, 225 | output_guard: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>, 226 | token: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 227 | }, 228 | output_id: nil, 229 | output_type: 1 230 | }, 231 | %Output{ 232 | output_data: %{ 233 | amount: 2, 234 | output_guard: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2>>, 235 | token: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 236 | }, 237 | output_id: nil, 238 | output_type: 1 239 | } 240 | ], 241 | tx_data: 0, 242 | tx_type: 1, 243 | sigs: [ 244 | <<130, 89, 40, 246, 220, 178, 131, 241, 16, 106, 27, 67, 32, 118, 106, 91, 161, 143, 162, 136, 87, 57, 245 | 17, 98, 208, 210, 218, 181, 15, 6, 163, 2, 26, 127, 8, 14, 150, 44, 116, 72, 46, 90, 255, 243, 144, 246 | 39, 175, 13, 47, 107, 179, 178, 240, 160, 166, 94, 252, 119, 38, 82, 49, 254, 192, 250, 27>>, 247 | <<130, 89, 40, 246, 220, 178, 131, 241, 16, 106, 27, 67, 32, 118, 106, 91, 161, 143, 162, 136, 87, 57, 248 | 17, 98, 208, 210, 218, 181, 15, 6, 163, 2, 26, 127, 8, 14, 150, 44, 116, 72, 46, 90, 255, 243, 144, 249 | 39, 175, 13, 47, 107, 179, 178, 240, 160, 166, 94, 252, 119, 38, 82, 49, 254, 192, 250, 27>>, 250 | <<50, 96, 3, 234, 168, 162, 52, 142, 174, 145, 201, 159, 24, 143, 251, 111, 1, 26, 48, 243, 140, 215, 251 | 21, 137, 161, 128, 184, 139, 183, 28, 161, 146, 22, 132, 29, 228, 34, 241, 196, 53, 155, 142, 69, 252 | 183, 16, 105, 65, 14, 185, 194, 147, 143, 146, 218, 206, 63, 233, 66, 151, 171, 32, 212, 234, 25, 253 | 28>> 254 | ] 255 | } 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /test/ex_plasma/configuration/validator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Configuration.ValidatorTest do 2 | use ExUnit.Case, async: true 3 | alias ExPlasma.Configuration.Validator 4 | 5 | doctest ExPlasma.Configuration.Validator 6 | 7 | describe "validate_eip_712_domain/1" do 8 | @valid_domain %{ 9 | name: "OMG Network", 10 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 11 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 12 | version: "1" 13 | } 14 | 15 | test "returns the domain if correctly valid" do 16 | assert Validator.validate_eip_712_domain(@valid_domain) == @valid_domain 17 | end 18 | 19 | test "raises when given an invalid domain" do 20 | invalid_values = %{name: 123, salt: "invalid", verifying_contract: "invalid", version: 1} 21 | 22 | Enum.each(invalid_values, fn {key, invalid_value} -> 23 | invalid_domain = Map.put(@valid_domain, key, invalid_value) 24 | 25 | assert_raise RuntimeError, ~r"eip_712_domain config is invalid.", fn -> 26 | Validator.validate_eip_712_domain(invalid_domain) 27 | end 28 | end) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/ex_plasma/configuration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.ConfigurationTest do 2 | # We need async: false as we are manipulating application variables which may be used by other tests 3 | use ExUnit.Case, async: false 4 | alias ExPlasma.Configuration 5 | 6 | doctest ExPlasma.Configuration 7 | 8 | @app :ex_plasma 9 | 10 | describe "eip_712_domain/0" do 11 | @valid_domain %{ 12 | name: "OMG Network", 13 | salt: "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83", 14 | verifying_contract: "0xd17e1233a03affb9092d5109179b43d6a8828607", 15 | version: "1" 16 | } 17 | 18 | setup do 19 | # Restore the default value when the test is done 20 | current_value = Application.get_env(@app, :eip_712_domain) 21 | 22 | on_exit(fn -> 23 | set_domain(current_value) 24 | end) 25 | end 26 | 27 | test "retrive the domain if correctly configured" do 28 | set_domain(@valid_domain) 29 | 30 | assert Configuration.eip_712_domain() == @valid_domain 31 | end 32 | 33 | test "raises when given invalid data" do 34 | set_domain(%{invalid: "domain"}) 35 | 36 | assert_raise RuntimeError, ~r"eip_712_domain config is invalid.", fn -> 37 | Configuration.eip_712_domain() 38 | end 39 | end 40 | 41 | defp set_domain(domain), do: Application.put_env(@app, :eip_712_domain, domain) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/ex_plasma/crypto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.CryptoTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.Crypto 6 | alias ExPlasma.Encoding 7 | alias ExPlasma.Signature 8 | 9 | describe "keccak_hash/1" do 10 | test "calculates keccak hash" do 11 | message = "Hello world" 12 | 13 | result = Crypto.keccak_hash(message) 14 | 15 | expected_result = 16 | <<237, 108, 17, 176, 181, 184, 8, 150, 13, 242, 111, 91, 252, 71, 29, 4, 193, 153, 91, 15, 253, 32, 85, 146, 90, 17 | 209, 190, 40, 214, 186, 173, 253>> 18 | 19 | assert result == expected_result 20 | end 21 | end 22 | 23 | describe "recover_address/2" do 24 | test "recovers address of the signer from a binary-encoded signature" do 25 | {:ok, priv} = generate_private_key() 26 | {:ok, pub} = generate_public_key(priv) 27 | {:ok, address} = Crypto.generate_address(pub) 28 | 29 | msg = :crypto.strong_rand_bytes(32) 30 | sig = Signature.signature_digest(msg, Encoding.to_hex(priv)) 31 | 32 | assert {:ok, ^address} = Crypto.recover_address(msg, sig) 33 | end 34 | 35 | test "fails to recover address with invalid signature" do 36 | assert Crypto.recover_address(<<2::256>>, <<1::65>>) == {:error, :corrupted_witness} 37 | end 38 | 39 | test "fails to recover address with invalid signature size" do 40 | assert Crypto.recover_address(<<2::256>>, <<1>>) == {:error, :corrupted_witness} 41 | end 42 | 43 | test "fails to recover address with invalid message" do 44 | assert Crypto.recover_address(<<2>>, <<1::65>>) == {:error, :invalid_message} 45 | end 46 | end 47 | 48 | describe "generate_address/1" do 49 | test "generates an address with SHA3" do 50 | # test vectors below were generated using pyethereum's sha3 and privtoaddr 51 | py_priv = "7880aec93413f117ef14bd4e6d130875ab2c7d7d55a064fac3c2f7bd51516380" 52 | py_pub = "c4d178249d840f548b09ad8269e8a3165ce2c170" 53 | priv = Crypto.keccak_hash(<<"11">>) 54 | 55 | {:ok, pub} = generate_public_key(priv) 56 | {:ok, address} = Crypto.generate_address(pub) 57 | {:ok, decoded_private} = Base.decode16(py_priv, case: :lower) 58 | {:ok, decoded_address} = Base.decode16(py_pub, case: :lower) 59 | 60 | assert ^decoded_private = priv 61 | assert ^address = decoded_address 62 | end 63 | 64 | test "generates an address with a public signature" do 65 | # test vector was generated using plasma.utils.utils.sign/2 from plasma-mvp 66 | py_signature = 67 | "b8670d619701733e1b4d10149bc90eb4eb276760d2f77a08a5428d4cbf2eadbd656f374c187b1ac80ce31d8c62076af26150e52ef1f33bfc07c6d244da7ca38c1c" 68 | 69 | msg = Crypto.keccak_hash("1234") 70 | priv = Crypto.keccak_hash("11") 71 | 72 | {:ok, pub} = generate_public_key(priv) 73 | {:ok, _} = Crypto.generate_address(pub) 74 | 75 | sig = Signature.signature_digest(msg, Encoding.to_hex(priv)) 76 | assert ^sig = Base.decode16!(py_signature, case: :lower) 77 | end 78 | end 79 | 80 | defp generate_private_key(), do: {:ok, :crypto.strong_rand_bytes(32)} 81 | 82 | defp generate_public_key(<>) do 83 | {:ok, der_pub} = get_public_key(priv) 84 | {:ok, der_to_raw(der_pub)} 85 | end 86 | 87 | defp der_to_raw(<<4::integer-size(8), data::binary>>), do: data 88 | 89 | defp get_public_key(private_key) do 90 | case ExSecp256k1.create_public_key(private_key) do 91 | {:ok, public_key} -> {:ok, public_key} 92 | {:error, reason} -> {:error, to_string(reason)} 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/ex_plasma/encoding_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.EncodingTest do 2 | use ExUnit.Case, async: true 3 | doctest ExPlasma.Encoding 4 | 5 | alias ExPlasma.Encoding 6 | 7 | describe "to_hex/1" do 8 | test "converts int to hex" do 9 | assert Encoding.to_hex(10) == "0xA" 10 | end 11 | 12 | test "converts binary to hex" do 13 | assert 10 14 | |> :binary.encode_unsigned() 15 | |> Encoding.to_hex() == "0x0a" 16 | end 17 | end 18 | 19 | describe "to_int/1" do 20 | test "converts hex to int" do 21 | assert Encoding.to_int("0xA") == 10 22 | end 23 | 24 | test "converts binary to int" do 25 | assert 10 26 | |> :binary.encode_unsigned() 27 | |> Encoding.to_int() == 10 28 | end 29 | end 30 | 31 | describe "to_binary/1" do 32 | test "converts hex to binary" do 33 | assert Encoding.to_binary("0x0a") == {:ok, "\n"} 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/ex_plasma/in_flight_exit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.InFlightExitTest do 2 | use ExUnit.Case, async: true 3 | import ExPlasma.Encoding, only: [to_binary!: 1] 4 | alias ExPlasma.InFlightExit 5 | 6 | doctest ExPlasma.InFlightExit 7 | 8 | describe "tx_bytes_to_id/1" do 9 | test "basic tx_bytes is converted to the correct IDs" do 10 | Application.put_env(:ex_plasma, :exit_id_size, 160) 11 | # The right hand side of these assertions are collected from this contract, a stripped down version 12 | # of https://github.com/omisego/plasma-contracts/blob/v1.0.3/plasma_framework/contracts/src/exits/utils/ExitId.sol#L53-L55 13 | # 14 | # pragma solidity 0.5.11; 15 | # 16 | # contract ExitId { 17 | # uint constant internal ONE = uint(1); 18 | # 19 | # function getInFlightExitId(bytes memory _txBytes) public pure returns(uint160) { 20 | # return uint160((uint256(keccak256(_txBytes)) >> 105).setBit(151)); 21 | # } 22 | # 23 | # function setBit(uint _self, uint8 _index) internal pure returns (uint) { 24 | # return _self | ONE << _index; 25 | # } 26 | # } 27 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x")) == 28 | 5_060_277_488_387_867_361_168_243_832_726_934_991_540_486_235 29 | 30 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x1234")) == 31 | 3_817_219_175_777_579_444_019_728_485_725_459_454_579_489_354 32 | 33 | assert InFlightExit.tx_bytes_to_id(to_binary!("0xb26f143eb9e68e5b")) == 34 | 4_423_187_252_251_026_420_447_811_542_410_043_191_383_319_711 35 | 36 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x70de28d3cd1cb609")) == 37 | 3_110_272_171_107_387_954_030_746_231_895_833_085_925_917_747 38 | 39 | assert InFlightExit.tx_bytes_to_id(to_binary!("0xc235a61a575eb3e2")) == 40 | 5_361_575_098_523_492_156_916_835_341_228_175_600_149_117_682 41 | 42 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x8fdeb13e6acdc74955fdcf0f345ae57a")) == 43 | 4_404_745_967_111_218_594_847_696_181_449_381_826_825_993_906 44 | 45 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x00000000000000000000000000000000")) == 46 | 5_581_496_182_896_756_123_499_329_818_246_993_621_247_309_773 47 | 48 | assert InFlightExit.tx_bytes_to_id(to_binary!("0xffffffffffffffffffffffffffffffff")) == 49 | 5_148_223_842_797_971_894_055_932_452_183_428_950_371_578_310 50 | 51 | Application.put_env(:ex_plasma, :exit_id_size, 160) 52 | end 53 | end 54 | 55 | describe "tx_bytes_to_id/1 168 bit size" do 56 | test "basic tx_bytes is converted to the correct IDs" do 57 | Application.put_env(:ex_plasma, :exit_id_size, 168) 58 | # The right hand side of these assertions are collected from this contract, a stripped down version 59 | # of https://github.com/omgnetwork/plasma-contracts/blob/v2.0.0/plasma_framework/contracts/src/exits/utils/ExitId.sol#L56-L58 60 | # 61 | # pragma solidity 0.5.11; 62 | # 63 | # contract ExitId { 64 | # uint constant internal ONE = uint(1); 65 | # uint8 constant private FIRST_BIT_LOCATION = 167; 66 | # 67 | # function getInFlightExitId(bytes memory _txBytes) public pure returns(uint168) { 68 | # return uint168((uint256(keccak256(txBytes)) >> (256 - FIRST_BIT_LOCATION)).setBit(FIRST_BIT_LOCATION)); 69 | # } 70 | # 71 | # function setBit(uint _self, uint8 _index) internal pure returns (uint) { 72 | # return _self | ONE << _index; 73 | # } 74 | # } 75 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x")) == 76 | 331_630_345_478_987_275_381_522_027_821_592_411_605_597_305_907_685 77 | 78 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x1234")) == 79 | 250_165_275_903_759_446_443_276_926_040_503_710_815_321_414_348_783 80 | 81 | assert InFlightExit.tx_bytes_to_id(to_binary!("0xb26f143eb9e68e5b")) == 82 | 289_877_999_763_523_267_490_467_777_243_384_590_590_497_240_631_486 83 | 84 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x70de28d3cd1cb609")) == 85 | 203_834_797_005_693_776_955_358_985_053_525_317_119_240_945_512_783 86 | 87 | assert InFlightExit.tx_bytes_to_id(to_binary!("0xc235a61a575eb3e2")) == 88 | 351_376_185_656_835_581_995_701_720_922_729_716_131_372_576_441_573 89 | 90 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x8fdeb13e6acdc74955fdcf0f345ae57a")) == 91 | 288_669_431_700_600_821_831_938_616_947_466_687_402_868_336_661_515 92 | 93 | assert InFlightExit.tx_bytes_to_id(to_binary!("0x00000000000000000000000000000000")) == 94 | 365_788_933_842_321_809_309_652_078_968_634_973_962_063_693_346_008 95 | 96 | assert InFlightExit.tx_bytes_to_id(to_binary!("0xffffffffffffffffffffffffffffffff")) == 97 | 337_393_997_761_607_886_048_849_589_186_293_199_691_551_756_158_617 98 | 99 | Application.put_env(:ex_plasma, :exit_id_size, 160) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/ex_plasma/merkle_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.MerkleTest do 2 | use ExUnit.Case, async: true 3 | doctest ExPlasma.Merkle 4 | 5 | alias ExPlasma.Merkle 6 | alias ExPlasma.Transaction 7 | 8 | describe "proof/2" do 9 | test "calculates merkle proof" do 10 | transactions = 11 | Enum.map(1..16, fn _ -> 12 | transaction = %Transaction{ 13 | inputs: [ 14 | %ExPlasma.Output{ 15 | output_data: nil, 16 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 17 | output_type: nil 18 | } 19 | ], 20 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 21 | outputs: [ 22 | %ExPlasma.Output{ 23 | output_data: %{ 24 | amount: 10, 25 | output_guard: 26 | <<21, 248, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>>, 27 | token: <<46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>> 28 | }, 29 | output_id: nil, 30 | output_type: 1 31 | } 32 | ], 33 | tx_data: <<0>>, 34 | tx_type: 1 35 | } 36 | 37 | ExPlasma.encode!(transaction, signed: false) 38 | end) 39 | 40 | assert Merkle.proof(transactions, 10) == 41 | <<88, 117, 125, 178, 47, 156, 139, 41, 145, 135, 156, 235, 103, 89, 209, 54, 125, 132, 197, 141, 43, 93, 42 | 90, 83, 10, 106, 73, 111, 69, 103, 226, 241, 237, 91, 120, 10, 69, 184, 118, 141, 48, 240, 227, 211, 43 | 218, 252, 84, 213, 97, 229, 29, 27, 202, 99, 177, 10, 68, 111, 153, 123, 58, 173, 235, 116, 95, 95, 44 | 184, 2, 50, 182, 148, 249, 240, 193, 69, 241, 219, 50, 174, 40, 8, 41, 35, 175, 121, 80, 79, 221, 194, 45 | 47, 29, 15, 221, 56, 175, 68, 252, 116, 152, 157, 27, 104, 56, 24, 212, 11, 248, 130, 159, 3, 139, 204, 46 | 179, 101, 181, 147, 219, 158, 22, 62, 86, 89, 109, 34, 199, 79, 51, 235, 94, 185, 73, 105, 10, 4, 4, 47 | 171, 244, 206, 186, 252, 124, 255, 250, 56, 33, 145, 183, 221, 158, 125, 247, 120, 88, 30, 111, 183, 48 | 142, 250, 179, 95, 211, 100, 201, 213, 218, 218, 212, 86, 155, 109, 212, 127, 127, 234, 186, 250, 53, 49 | 113, 248, 66, 67, 68, 37, 84, 131, 53, 172, 110, 105, 13, 208, 113, 104, 216, 188, 91, 119, 151, 156, 50 | 26, 103, 2, 51, 79, 82, 159, 87, 131, 247, 158, 148, 47, 210, 205, 3, 246, 229, 90, 194, 207, 73, 110, 51 | 132, 159, 222, 156, 68, 111, 171, 70, 168, 210, 125, 177, 227, 16, 15, 39, 90, 119, 125, 56, 91, 68, 52 | 227, 203, 192, 69, 202, 186, 201, 218, 54, 202, 224, 64, 173, 81, 96, 130, 50, 76, 150, 18, 124, 242, 53 | 159, 69, 53, 235, 91, 126, 186, 207, 226, 161, 214, 211, 170, 184, 236, 4, 131, 211, 32, 121, 168, 89, 54 | 255, 112, 249, 33, 89, 112, 168, 190, 235, 177, 193, 100, 196, 116, 232, 36, 56, 23, 76, 142, 235, 111, 55 | 188, 140, 180, 89, 75, 136, 201, 68, 143, 29, 64, 176, 155, 234, 236, 172, 91, 69, 219, 110, 65, 67, 56 | 74, 18, 43, 105, 92, 90, 133, 134, 45, 142, 174, 64, 179, 38, 143, 111, 55, 228, 20, 51, 123, 227, 142, 57 | 186, 122, 181, 187, 243, 3, 208, 31, 75, 122, 224, 127, 215, 62, 220, 47, 59, 224, 94, 67, 148, 138, 58 | 52, 65, 138, 50, 114, 80, 156, 67, 194, 129, 26, 130, 30, 92, 152, 43, 165, 24, 116, 172, 125, 201, 59 | 221, 121, 168, 12, 194, 240, 95, 111, 102, 76, 157, 187, 46, 69, 68, 53, 19, 125, 160, 108, 228, 77, 60 | 228, 85, 50, 165, 106, 58, 112, 7, 162, 208, 198, 180, 53, 247, 38, 249, 81, 4, 191, 166, 231, 7, 4, 61 | 111, 193, 84, 186, 233, 24, 152, 208, 58, 26, 10, 198, 249, 180, 94, 71, 22, 70, 226, 85, 90, 199, 158, 62 | 63, 232, 126, 177, 120, 30, 38, 242, 5, 0, 36, 12, 55, 146, 116, 254, 145, 9, 110, 96, 209, 84, 90, 63 | 128, 69, 87, 31, 218, 185, 181, 48, 208, 214, 231, 232, 116, 110, 120, 191, 159, 32, 244, 232, 111, 6>> 64 | end 65 | end 66 | 67 | describe "root_hash/1" do 68 | test "calculates merkle root hash" do 69 | transactions = 70 | Enum.map(1..5, fn _ -> 71 | transaction = %Transaction{ 72 | inputs: [ 73 | %ExPlasma.Output{ 74 | output_data: nil, 75 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 76 | output_type: nil 77 | } 78 | ], 79 | metadata: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 80 | outputs: [ 81 | %ExPlasma.Output{ 82 | output_data: %{ 83 | amount: 10, 84 | output_guard: 85 | <<21, 248, 47, 41, 27, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>>, 86 | token: <<46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>> 87 | }, 88 | output_id: nil, 89 | output_type: 1 90 | } 91 | ], 92 | tx_data: <<0>>, 93 | tx_type: 1 94 | } 95 | 96 | ExPlasma.encode!(transaction, signed: false) 97 | end) 98 | 99 | assert Merkle.root_hash(transactions) == 100 | <<62, 248, 190, 51, 105, 73, 187, 114, 207, 52, 1, 69, 68, 69, 144, 36, 104, 19, 115, 215, 67, 179, 210, 101 | 121, 233, 229, 41, 251, 28, 86, 129, 147>> 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/ex_plasma/output/position/position_validator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Position.ValidatorTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.Output.Position.Validator 6 | 7 | describe "validate_blknum/1" do 8 | test "returns :ok when valid" do 9 | assert Validator.validate_blknum(1000) == :ok 10 | end 11 | 12 | test "returns an error when blknum is nil" do 13 | assert Validator.validate_blknum(nil) == {:error, {:blknum, :cannot_be_nil}} 14 | end 15 | 16 | test "returns an error when blknum exceed maximum value" do 17 | assert Validator.validate_blknum(1_000_000_000_000_000_000) == {:error, {:blknum, :cannot_exceed_maximum_value}} 18 | end 19 | 20 | test "returns an error when blknum is not an integer" do 21 | assert Validator.validate_blknum("a") == {:error, {:blknum, :must_be_an_integer}} 22 | end 23 | end 24 | 25 | describe "validate_txindex/1" do 26 | test "returns :ok when valid" do 27 | assert Validator.validate_txindex(1000) == :ok 28 | end 29 | 30 | test "returns an error when txindex is nil" do 31 | assert Validator.validate_txindex(nil) == {:error, {:txindex, :cannot_be_nil}} 32 | end 33 | 34 | test "returns an error when txindex exceed maximum value" do 35 | assert Validator.validate_txindex(1_000_000_000_000_000_000) == {:error, {:txindex, :cannot_exceed_maximum_value}} 36 | end 37 | 38 | test "returns an error when txindex is not an integer" do 39 | assert Validator.validate_txindex("a") == {:error, {:txindex, :must_be_an_integer}} 40 | end 41 | end 42 | 43 | describe "validate_oindex/1" do 44 | test "returns :ok when valid" do 45 | assert Validator.validate_oindex(1000) == :ok 46 | end 47 | 48 | test "returns an error when oindex is nil" do 49 | assert Validator.validate_oindex(nil) == {:error, {:oindex, :cannot_be_nil}} 50 | end 51 | 52 | test "returns an error when oindex is not an integer" do 53 | assert Validator.validate_oindex("a") == {:error, {:oindex, :must_be_an_integer}} 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/ex_plasma/output/position_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.PositionTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | doctest ExPlasma.Output.Position 5 | 6 | alias ExPlasma.Output.Position 7 | 8 | describe "new/3" do 9 | test "creates an output_id" do 10 | assert Position.new(1, 2, 3) == %{blknum: 1, oindex: 3, position: 1_000_020_003, txindex: 2} 11 | end 12 | end 13 | 14 | describe "pos/1" do 15 | test "returns the position" do 16 | output_id = %{blknum: 1, txindex: 2, oindex: 3} 17 | assert Position.pos(output_id) == 1_000_020_003 18 | end 19 | end 20 | 21 | describe "to_rlp/1" do 22 | test "returns the encoded position from an output struct" do 23 | output_id = %{blknum: 1, txindex: 2, oindex: 3} 24 | result = Position.to_rlp(output_id) 25 | 26 | expected_result = 27 | <<0, 0, 0, 0, 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>> 28 | 29 | assert result == expected_result 30 | end 31 | end 32 | 33 | describe "encode/1" do 34 | test "returns the encoded position from a position" do 35 | result = Position.encode(1_000_020_003) 36 | 37 | expected_result = 38 | <<0, 0, 0, 0, 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>> 39 | 40 | assert result == expected_result 41 | end 42 | end 43 | 44 | describe "to_map/1" do 45 | test "returns an Output struct from the given position" do 46 | assert Position.to_map(1_000_020_003) == {:ok, %{blknum: 1, oindex: 3, position: 1_000_020_003, txindex: 2}} 47 | end 48 | end 49 | 50 | describe "decode/1" do 51 | test "returns the decoded position from the given binary" do 52 | encoded = <<59, 155, 24, 35>> 53 | assert Position.decode(encoded) == {:ok, 1_000_020_003} 54 | end 55 | 56 | test "returns an error when the given position is invalid" do 57 | assert Position.decode([]) == {:error, :malformed_input_position_rlp} 58 | end 59 | end 60 | 61 | describe "validate/1" do 62 | setup do 63 | output_id = %{blknum: 1, oindex: 3, position: 1_000_020_003, txindex: 2} 64 | 65 | {:ok, %{output_id: output_id}} 66 | end 67 | 68 | test "returns :ok when valid", %{output_id: output_id} do 69 | assert Position.validate(output_id) == :ok 70 | end 71 | 72 | test "returns an error when blknum is nil", %{output_id: output_id} do 73 | output_id = %{output_id | blknum: nil} 74 | assert_field(output_id, :blknum, :cannot_be_nil) 75 | end 76 | 77 | test "returns an error when blknum exceed maximum value", %{output_id: output_id} do 78 | output_id = %{output_id | blknum: 1_000_000_000_000_000_000} 79 | assert_field(output_id, :blknum, :cannot_exceed_maximum_value) 80 | end 81 | 82 | test "returns an error when blknum is not an integer", %{output_id: output_id} do 83 | output_id = %{output_id | blknum: "a"} 84 | assert_field(output_id, :blknum, :must_be_an_integer) 85 | end 86 | 87 | test "returns an error when txindex is nil", %{output_id: output_id} do 88 | output_id = %{output_id | txindex: nil} 89 | assert_field(output_id, :txindex, :cannot_be_nil) 90 | end 91 | 92 | test "returns an error when txindex exceed maximum value", %{output_id: output_id} do 93 | output_id = %{output_id | txindex: 1_000_000_000_000_000_000} 94 | assert_field(output_id, :txindex, :cannot_exceed_maximum_value) 95 | end 96 | 97 | test "returns an error when txindex is not an integer", %{output_id: output_id} do 98 | output_id = %{output_id | txindex: "a"} 99 | assert_field(output_id, :txindex, :must_be_an_integer) 100 | end 101 | 102 | test "returns an error when oindex is nil", %{output_id: output_id} do 103 | output_id = %{output_id | oindex: nil} 104 | assert_field(output_id, :oindex, :cannot_be_nil) 105 | end 106 | 107 | test "returns an error when oindex is not an integer", %{output_id: output_id} do 108 | output_id = %{output_id | oindex: "a"} 109 | assert_field(output_id, :oindex, :must_be_an_integer) 110 | end 111 | end 112 | 113 | defp assert_field(data, field, message) do 114 | assert {:error, {^field, ^message}} = Position.validate(data) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/ex_plasma/output/types/abstract_payment/abstract_payment_validator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Type.AbstractPayment.ValidatorTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.Output.Type.AbstractPayment.Validator 6 | 7 | describe "validate_amount/1" do 8 | test "returns :ok when valid" do 9 | assert Validator.validate_amount(1000) == :ok 10 | end 11 | 12 | test "returns an error when amount is nil" do 13 | assert Validator.validate_amount(nil) == {:error, {:amount, :cannot_be_nil}} 14 | end 15 | 16 | test "returns an error when amount is zero" do 17 | assert Validator.validate_amount(0) == {:error, {:amount, :cannot_be_zero}} 18 | end 19 | end 20 | 21 | describe "validate_token/1" do 22 | test "returns :ok when valid" do 23 | assert Validator.validate_token(<<0::160>>) == :ok 24 | end 25 | 26 | test "returns an error when token is nil" do 27 | assert Validator.validate_token(nil) == {:error, {:token, :cannot_be_nil}} 28 | end 29 | end 30 | 31 | describe "validate_output_guard/1" do 32 | test "returns :ok when valid" do 33 | assert Validator.validate_output_guard(<<1::160>>) == :ok 34 | end 35 | 36 | test "returns an error when output_guard is nil" do 37 | assert Validator.validate_output_guard(nil) == {:error, {:output_guard, :cannot_be_nil}} 38 | end 39 | 40 | test "returns an error when output_guard is zero" do 41 | assert Validator.validate_output_guard(<<0::160>>) == {:error, {:output_guard, :cannot_be_zero}} 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/ex_plasma/output/types/abstract_payment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Output.Type.AbstractPaymentTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | doctest ExPlasma.Output.Type.AbstractPayment 5 | 6 | alias ExPlasma.Output.Type.AbstractPayment 7 | 8 | describe "to_rlp/1" do 9 | test "RLP encodes the given output" do 10 | amounts = [1, 65_000, 1_000_000_000_000_000_000_000_000] 11 | 12 | Enum.map(amounts, fn amount -> 13 | output = %{output_type: 1, output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: amount}} 14 | encoded_amount = :binary.encode_unsigned(amount, :big) 15 | assert [_, [_, _, ^encoded_amount]] = AbstractPayment.to_rlp(output) 16 | end) 17 | end 18 | end 19 | 20 | describe "to_map/1" do 21 | test "maps a RLP list of items into an output for different amounts" do 22 | amounts = [1, 65_000, 1_000_000_000_000_000_000_000_000] 23 | 24 | Enum.map(amounts, fn amount -> 25 | rlp = [<<1>>, [<<0::160>>, <<0::160>>, :binary.encode_unsigned(amount, :big)]] 26 | 27 | assert {:ok, %{output_data: %{token: <<0::160>>, output_guard: <<0::160>>, amount: _amount}}} = 28 | AbstractPayment.to_map(rlp) 29 | end) 30 | end 31 | 32 | test "returns an error when output_guard is not a valid address" do 33 | invalid_output_guards = [<<0::80>>, "a", 123, []] 34 | 35 | Enum.map(invalid_output_guards, fn output_guard -> 36 | rlp = [<<1>>, [output_guard, <<0::160>>, <<1>>]] 37 | 38 | assert AbstractPayment.to_map(rlp) == {:error, :malformed_output_guard} 39 | end) 40 | end 41 | 42 | test "returns an error when token is not a valid address" do 43 | invalid_tokens = [<<0::80>>, "a", 123, []] 44 | 45 | Enum.map(invalid_tokens, fn token -> 46 | rlp = [<<1>>, [<<0::160>>, token, <<1>>]] 47 | 48 | assert AbstractPayment.to_map(rlp) == {:error, :malformed_output_token} 49 | end) 50 | end 51 | 52 | test "returns an error when amount is invalid" do 53 | invalid_amounts = [<<0::512>>, <<0::160>>, []] 54 | 55 | Enum.map(invalid_amounts, fn amount -> 56 | rlp = [<<1>>, [<<0::160>>, <<0::160>>, amount]] 57 | 58 | assert AbstractPayment.to_map(rlp) == {:error, :malformed_output_amount} 59 | end) 60 | end 61 | end 62 | 63 | describe "validate/1" do 64 | setup do 65 | output = %{ 66 | output_data: %{ 67 | amount: 1, 68 | output_guard: <<11, 246, 22, 41, 33, 46, 44, 159, 55, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>>, 69 | token: <<46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>> 70 | }, 71 | output_type: 1 72 | } 73 | 74 | {:ok, %{output: output}} 75 | end 76 | 77 | test "returns :ok when valid", %{output: output} do 78 | assert AbstractPayment.validate(output) == :ok 79 | end 80 | 81 | test "returns an error when amount is nil", %{output: output} do 82 | output = %{output | output_data: %{output.output_data | amount: nil}} 83 | assert_field(output, :amount, :cannot_be_nil) 84 | end 85 | 86 | test "returns an error when amount is zero", %{output: output} do 87 | output = %{output | output_data: %{output.output_data | amount: 0}} 88 | assert_field(output, :amount, :cannot_be_zero) 89 | end 90 | 91 | test "returns an error when token is nil", %{output: output} do 92 | output = %{output | output_data: %{output.output_data | token: nil}} 93 | assert_field(output, :token, :cannot_be_nil) 94 | end 95 | 96 | test "returns an error when output_guard is nil", %{output: output} do 97 | output = %{output | output_data: %{output.output_data | output_guard: nil}} 98 | assert_field(output, :output_guard, :cannot_be_nil) 99 | end 100 | 101 | test "returns an error when output_guard is zero", %{output: output} do 102 | output = %{output | output_data: %{output.output_data | output_guard: <<0::160>>}} 103 | assert_field(output, :output_guard, :cannot_be_zero) 104 | end 105 | end 106 | 107 | defp assert_field(data, field, message) do 108 | assert {:error, {^field, ^message}} = AbstractPayment.validate(data) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/ex_plasma/output_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.OutputTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest ExPlasma.Output 5 | 6 | alias ExPlasma.Output 7 | alias ExPlasma.Transaction.TypeMapper 8 | 9 | @payment_output_type TypeMapper.output_type_for(:output_payment_v1) 10 | 11 | setup_all do 12 | output = %Output{ 13 | output_data: %{ 14 | amount: 1, 15 | output_guard: <<11, 246, 22, 41, 33, 46, 44, 159, 55, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>>, 16 | token: <<46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>> 17 | }, 18 | output_id: nil, 19 | output_type: 1 20 | } 21 | 22 | input = %Output{ 23 | output_data: nil, 24 | output_id: %{ 25 | blknum: 1, 26 | oindex: 0, 27 | txindex: 0, 28 | position: 1_000_000_000 29 | }, 30 | output_type: nil 31 | } 32 | 33 | {:ok, encoded_input} = Output.encode(input, as: :input) 34 | {:ok, encoded_output} = Output.encode(output) 35 | 36 | {:ok, 37 | %{ 38 | input: input, 39 | output: output, 40 | encoded_input: encoded_input, 41 | encoded_output: encoded_output 42 | }} 43 | end 44 | 45 | describe "decode/1" do 46 | test "successfuly decodes a valid encoded output", %{encoded_output: encoded_output, output: output} do 47 | assert Output.decode(encoded_output) == {:ok, output} 48 | end 49 | 50 | test "returns a malformed_output_rlp error when rlp is not decodable", %{encoded_output: encoded_output} do 51 | assert Output.decode("A" <> encoded_output) == {:error, :malformed_output_rlp} 52 | 53 | <<_, malformed_1::binary>> = encoded_output 54 | assert Output.decode(malformed_1) == {:error, :malformed_output_rlp} 55 | 56 | cropped_size = byte_size(encoded_output) - 1 57 | <> = encoded_output 58 | assert Output.decode(malformed_2) == {:error, :malformed_output_rlp} 59 | end 60 | 61 | test "returns a malformed_outputs error when rlp is decodable, but doesn't represent a known output format" do 62 | assert Output.decode(<<192>>) == {:error, :malformed_outputs} 63 | assert Output.decode(<<0x80>>) == {:error, :malformed_outputs} 64 | assert Output.decode(<<>>) == {:error, :malformed_outputs} 65 | assert Output.decode(ExRLP.encode(23)) == {:error, :malformed_outputs} 66 | assert Output.decode(ExRLP.encode([1])) == {:error, :malformed_outputs} 67 | end 68 | 69 | test "returns a unrecognized_transaction_type error when given an unkown/invalid output type" do 70 | assert Output.decode(ExRLP.encode([<<10>>, []])) == {:error, :unrecognized_output_type} 71 | assert Output.decode(ExRLP.encode([["bad"], []])) == {:error, :unrecognized_output_type} 72 | assert Output.decode(ExRLP.encode([234_567, []])) == {:error, :unrecognized_output_type} 73 | end 74 | 75 | test "forward decoding errors to sub-output types" do 76 | assert Output.decode(ExRLP.encode([@payment_output_type, [<<0::160>>, <<0::160>>, 'a']])) == 77 | {:error, :malformed_output_amount} 78 | 79 | assert Output.decode(ExRLP.encode([@payment_output_type, [<<0::160>>, <<0::160>>, [1]]])) == 80 | {:error, :malformed_output_amount} 81 | 82 | assert Output.decode(ExRLP.encode([@payment_output_type, [<<0::80>>, <<0::160>>, 1]])) == 83 | {:error, :malformed_output_guard} 84 | 85 | assert Output.decode(ExRLP.encode([@payment_output_type, [<<0::160>>, <<0::80>>, 1]])) == 86 | {:error, :malformed_output_token} 87 | end 88 | end 89 | 90 | describe "decode!/1" do 91 | test "successfuly decodes a valid encoded output", %{encoded_output: encoded_output, output: output} do 92 | assert Output.decode!(encoded_output) == output 93 | end 94 | 95 | test "raises when there was an error while decoding" do 96 | assert_raise MatchError, fn -> 97 | Output.decode!(<<192>>) 98 | end 99 | end 100 | end 101 | 102 | describe "decode_id/1" do 103 | test "successfuly decodes an encoded position", %{encoded_input: encoded_input, input: input} do 104 | assert Output.decode_id(encoded_input) == {:ok, input} 105 | end 106 | 107 | test "successfuly decodes empty binary" do 108 | assert Output.decode_id(<<>>) == 109 | {:ok, 110 | %Output{ 111 | output_data: nil, 112 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 113 | output_type: nil 114 | }} 115 | end 116 | 117 | test "returns a malformed_input_position_rlp error when given malformated position" do 118 | assert Output.decode_id([]) == 119 | {:error, :malformed_input_position_rlp} 120 | 121 | assert Output.decode_id(["q"]) == 122 | {:error, :malformed_input_position_rlp} 123 | end 124 | end 125 | 126 | describe "decode_id!/1" do 127 | test "successfuly decodes a valid encoded position", %{encoded_input: encoded_input, input: input} do 128 | assert Output.decode_id!(encoded_input) == input 129 | end 130 | 131 | test "raises when there was an error while decoding" do 132 | assert_raise MatchError, fn -> 133 | Output.decode_id!([]) 134 | end 135 | end 136 | end 137 | 138 | describe "to_map/1" do 139 | test "maps an rlp list of output data into an Output structure", %{output: output} do 140 | {:ok, rlp} = Output.to_rlp(output) 141 | 142 | assert {:ok, mapped} = Output.to_map(rlp) 143 | assert mapped == output 144 | end 145 | 146 | test "returns malformed_outputs error when the output is malformed" do 147 | assert Output.to_map(123) == {:error, :malformed_outputs} 148 | assert Output.to_map([]) == {:error, :malformed_outputs} 149 | end 150 | 151 | test "returns `unrecognized_output_type` when the given type is not supported" do 152 | assert Output.to_map([<<1337>>, []]) == {:error, :unrecognized_output_type} 153 | end 154 | end 155 | 156 | describe "to_map_id/1" do 157 | test "maps a position into an Output structure for an input", %{input: input} do 158 | assert Output.to_map_id(input.output_id.position) == {:ok, input} 159 | end 160 | 161 | test "returns an error when position is not an integer" do 162 | assert Output.to_map_id("bad") == {:error, :malformed_output_position} 163 | end 164 | end 165 | 166 | describe "encode/2" do 167 | test "encodes an output struct for an output", %{output: output} do 168 | assert {:ok, result} = Output.encode(output) 169 | 170 | expected_result = 171 | <<237, 1, 235, 148, 11, 246, 22, 41, 33, 46, 44, 159, 55, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110, 172 | 148, 46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110, 1>> 173 | 174 | assert result == expected_result 175 | end 176 | 177 | test "encodes an output struct for an input", %{input: input} do 178 | assert {:ok, result} = Output.encode(input, as: :input) 179 | 180 | assert result == 181 | <<0, 0, 0, 0, 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>> 182 | end 183 | 184 | test "returns an error when output_type is nil for an output", %{input: input} do 185 | assert Output.encode(input) == {:error, :invalid_output_data} 186 | end 187 | 188 | test "returns an error when output_type is not valid for an output", %{output: output} do 189 | output = %Output{output | output_type: 9876} 190 | assert Output.encode(output) == {:error, :unrecognized_output_type} 191 | end 192 | 193 | test "returns an error when output_id is nil for an input", %{output: output} do 194 | assert Output.encode(output, as: :input) == {:error, :invalid_output_id} 195 | end 196 | end 197 | 198 | describe "to_rlp/1" do 199 | test "converts a valid output to rlp", %{output: output} do 200 | expected_result = [ 201 | <<1>>, 202 | [ 203 | <<11, 246, 22, 41, 33, 46, 44, 159, 55, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>>, 204 | <<46, 38, 45, 41, 28, 46, 150, 159, 176, 132, 157, 153, 217, 206, 65, 226, 241, 55, 0, 110>>, 205 | <<1>> 206 | ] 207 | ] 208 | 209 | assert {:ok, result} = Output.to_rlp(output) 210 | 211 | assert result == expected_result 212 | end 213 | 214 | test "returns an error when given an invalid output_type" do 215 | output = %Output{ 216 | output_id: nil, 217 | output_type: 100, 218 | output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1} 219 | } 220 | 221 | assert Output.to_rlp(output) == {:error, :unrecognized_output_type} 222 | end 223 | 224 | test "returns an error when given a nil output_type", %{input: input} do 225 | assert Output.to_rlp(input) == {:error, :invalid_output_data} 226 | end 227 | end 228 | 229 | describe "to_rlp_id/1" do 230 | test "returns rlp id given a valid output position", %{input: input} do 231 | assert Output.to_rlp_id(input) == 232 | {:ok, 233 | <<0, 0, 0, 0, 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>>} 234 | end 235 | 236 | test "returns an error if output_id is nil", %{output: output} do 237 | assert Output.to_rlp_id(output) == {:error, :invalid_output_id} 238 | end 239 | end 240 | 241 | describe "validate/1" do 242 | test "returns :ok when given a valid output", %{output: output} do 243 | assert Output.validate(output) == :ok 244 | end 245 | 246 | test "returns :ok when given a valid input", %{input: input} do 247 | assert Output.validate(input) == :ok 248 | end 249 | 250 | test "returns an error when output_type and output_id are missing" do 251 | output = %Output{} 252 | assert Output.validate(output) == {:error, {:output, :invalid_output}} 253 | end 254 | 255 | test "returns an error when output_type is invalid", %{output: output} do 256 | output = %Output{output | output_type: 9876} 257 | assert Output.validate(output) == {:error, {:output_type, :unrecognized_output_type}} 258 | end 259 | 260 | test "forward validation to sub-outputs for an output" do 261 | output = %Output{ 262 | output_id: nil, 263 | output_type: 1, 264 | output_data: %{output_guard: <<1::160>>, token: <<0::160>>, amount: 0} 265 | } 266 | 267 | assert Output.validate(output) == {:error, {:amount, :cannot_be_zero}} 268 | end 269 | 270 | test "forward validation to sub-outputs for an input" do 271 | {:ok, input} = Output.to_map_id(1_000_000_000_000_000_000_000) 272 | 273 | assert Output.validate(input) == {:error, {:blknum, :cannot_exceed_maximum_value}} 274 | end 275 | end 276 | end 277 | -------------------------------------------------------------------------------- /test/ex_plasma/signature_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.SignatureTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.Crypto 6 | alias ExPlasma.Signature 7 | 8 | describe "signature_digest/2" do 9 | test "calculates digest" do 10 | private_key = "0x8da4ef21b864d2cc526dbdb2a120bd2874c36c9d0a1fb7f8c63d7f7a8b41de8f" 11 | hash_digest = <<2::256>> 12 | 13 | result = Signature.signature_digest(hash_digest, private_key) 14 | 15 | expected_result = 16 | <<73, 102, 23, 43, 29, 88, 149, 68, 77, 65, 248, 57, 200, 155, 43, 249, 154, 95, 100, 185, 121, 244, 84, 178, 17 | 159, 90, 254, 45, 27, 177, 221, 218, 21, 214, 167, 20, 61, 86, 189, 86, 241, 39, 239, 70, 71, 66, 201, 140, 18 | 21, 23, 206, 201, 129, 255, 24, 20, 160, 152, 36, 114, 115, 245, 33, 208, 28>> 19 | 20 | assert result == expected_result 21 | end 22 | end 23 | 24 | describe "recover_public/3" do 25 | test "returns an error from an invalid hash" do 26 | {:error, "invalid_recovery_id"} = 27 | Signature.recover_public( 28 | <<2::256>>, 29 | 55, 30 | 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, 31 | 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 32 | ) 33 | end 34 | 35 | test "recovers from generating a signed hash 1" do 36 | data = 37 | Base.decode16!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080", 38 | case: :lower 39 | ) 40 | 41 | hash = Crypto.keccak_hash(data) 42 | v = 27 43 | 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 44 | 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 45 | {:ok, public_key} = Signature.recover_public(hash, v, r, s) 46 | 47 | assert public_key == 48 | <<75, 194, 163, 18, 101, 21, 63, 7, 231, 14, 11, 171, 8, 114, 78, 107, 133, 226, 23, 248, 205, 98, 140, 49 | 235, 98, 151, 66, 71, 187, 73, 51, 130, 206, 40, 202, 183, 154, 215, 17, 158, 225, 173, 62, 188, 219, 50 | 152, 161, 104, 5, 33, 21, 48, 236, 198, 207, 239, 161, 184, 142, 109, 255, 153, 35, 42>> 51 | end 52 | 53 | test "recovers from generating a signed hash 2" do 54 | {v, r, s} = 55 | {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, 56 | 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} 57 | 58 | data = 59 | Base.decode16!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080", 60 | case: :lower 61 | ) 62 | 63 | hash = Crypto.keccak_hash(data) 64 | {:ok, public_key} = Signature.recover_public(hash, v, r, s, 1) 65 | 66 | assert public_key == 67 | <<75, 194, 163, 18, 101, 21, 63, 7, 231, 14, 11, 171, 8, 114, 78, 107, 133, 226, 23, 248, 205, 98, 140, 68 | 235, 98, 151, 66, 71, 187, 73, 51, 130, 206, 40, 202, 183, 154, 215, 17, 158, 225, 173, 62, 188, 219, 69 | 152, 161, 104, 5, 33, 21, 48, 236, 198, 207, 239, 161, 184, 142, 109, 255, 153, 35, 42>> 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/ex_plasma/transaction/signed_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.SignedTest do 2 | @moduledoc false 3 | 4 | use ExUnit.Case, async: true 5 | 6 | alias ExPlasma.Builder 7 | alias ExPlasma.Support.TestEntity 8 | alias ExPlasma.Transaction 9 | alias ExPlasma.Transaction.Signed 10 | alias ExPlasma.Transaction.TypeMapper 11 | 12 | @alice TestEntity.alice() 13 | @bob TestEntity.bob() 14 | @eth <<0::160>> 15 | @zero_metadata <<0::256>> 16 | @payment_tx_type TypeMapper.tx_type_for(:tx_payment_v1) 17 | 18 | setup_all do 19 | %{priv_encoded: alice_priv, addr: alice_addr} = @alice 20 | %{addr: bob_addr} = @bob 21 | 22 | signed = 23 | ExPlasma.payment_v1() 24 | |> Builder.new() 25 | |> Builder.add_input(blknum: 1, txindex: 0, oindex: 0, position: 1_000_000_000) 26 | |> Builder.add_input(blknum: 2, txindex: 0, oindex: 0, position: 2_000_000_000) 27 | |> Builder.add_output(output_guard: bob_addr, token: @eth, amount: 12) 28 | |> Builder.sign!([alice_priv, alice_priv]) 29 | 30 | {:ok, encoded_signed_tx} = Transaction.encode(signed) 31 | 32 | {:ok, 33 | %{ 34 | alice_addr: alice_addr, 35 | signed: signed, 36 | encoded_signed_tx: encoded_signed_tx 37 | }} 38 | end 39 | 40 | describe "decode/1" do 41 | test "successfuly decodes a signed transaction", %{encoded_signed_tx: encoded_signed_tx} do 42 | assert {:ok, [_sigs, _type, _inputs, _outputs, _data, _metadata]} = Signed.decode(encoded_signed_tx) 43 | end 44 | 45 | test "returns a malformed_rlp error when rlp is not decodable", %{encoded_signed_tx: encoded_signed_tx} do 46 | assert Signed.decode("A" <> encoded_signed_tx) == {:error, :malformed_rlp} 47 | 48 | <<_, malformed_1::binary>> = encoded_signed_tx 49 | assert Signed.decode(malformed_1) == {:error, :malformed_rlp} 50 | 51 | cropped_size = byte_size(encoded_signed_tx) - 1 52 | <> = encoded_signed_tx 53 | assert Signed.decode(malformed_2) == {:error, :malformed_rlp} 54 | end 55 | 56 | test "returns a malformed_transaction error when rlp is decodable, but doesn't represent a known transaction format" do 57 | assert Signed.decode(<<192>>) == {:error, :malformed_transaction} 58 | assert Signed.decode(<<0x80>>) == {:error, :malformed_transaction} 59 | assert Signed.decode(<<>>) == {:error, :malformed_transaction} 60 | assert Signed.decode(ExRLP.encode(23)) == {:error, :malformed_transaction} 61 | end 62 | 63 | test "returns a malformed_witnesses error when given something else than a list for witnesses" do 64 | assert Signed.decode(ExRLP.encode([<<1>>, @payment_tx_type, [], [], 0, @zero_metadata])) == 65 | {:error, :malformed_witnesses} 66 | end 67 | end 68 | 69 | describe "validate/1" do 70 | test "returns :ok when the signatures are valid", %{signed: signed} do 71 | assert Signed.validate(signed) == :ok 72 | end 73 | 74 | test "returns a malformed_witness error when not given list of valid length binary for sigs", %{ 75 | signed: signed 76 | } do 77 | error = {:error, {:witnesses, :malformed_witnesses}} 78 | 79 | assert Signed.validate(%Transaction{signed | sigs: [[1], [2]]}) == error 80 | assert Signed.validate(%Transaction{signed | sigs: [[1, 2]]}) == error 81 | assert Signed.validate(%Transaction{signed | sigs: [1, 2]}) == error 82 | assert Signed.validate(%Transaction{signed | sigs: [<<1>>, <<1>>]}) == error 83 | end 84 | end 85 | 86 | describe "get_witnesses/1" do 87 | test "returns {:ok, addresses} when signatures are valid", %{signed: signed, alice_addr: alice_addr} do 88 | assert {:ok, witnesses} = Signed.get_witnesses(signed) 89 | assert witnesses == [alice_addr, alice_addr] 90 | end 91 | 92 | test "returns a corrupted_witness error when given a list containing a malformed witness", %{ 93 | signed: signed 94 | } do 95 | [sig_1, sig_2] = signed.sigs 96 | error = {:error, :corrupted_witness} 97 | 98 | assert Transaction.with_witnesses(%{signed | sigs: [<<1>>, <<1>>]}) == error 99 | assert Transaction.with_witnesses(%{signed | sigs: [sig_1, <<1::size(520)>>]}) == error 100 | assert Transaction.with_witnesses(%{signed | sigs: [<<1::size(520)>>, sig_2]}) == error 101 | end 102 | end 103 | 104 | describe "compute_signatures/2" do 105 | test "returns {:ok, sigs} when given valid keys" do 106 | %{priv_encoded: key_1} = @alice 107 | %{priv_encoded: key_2} = @bob 108 | 109 | tx = 110 | ExPlasma.payment_v1() 111 | |> Builder.new() 112 | |> Builder.add_input(blknum: 1, txindex: 0, oindex: 0, position: 1_000_000_000) 113 | |> Builder.add_input(blknum: 2, txindex: 0, oindex: 0, position: 2_000_000_000) 114 | |> Builder.add_input(blknum: 3, txindex: 0, oindex: 0, position: 3_000_000_000) 115 | 116 | assert {:ok, [_sig_1, _sig_2, _sig_3]} = Signed.compute_signatures(tx, [key_1, key_1, key_2]) 117 | end 118 | 119 | test "returns {:error, :not_signable} when given an invalid struct" do 120 | %{priv_encoded: key_1} = @alice 121 | %{priv_encoded: key_2} = @bob 122 | 123 | assert Signed.compute_signatures(%{}, [key_1, key_1, key_2]) == {:error, :not_signable} 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/ex_plasma/transaction/type/fee/fee_validator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.Fee.ValidatorTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.Output 6 | alias ExPlasma.Transaction.Type.Fee 7 | alias ExPlasma.Transaction.Type.Fee.Validator 8 | 9 | describe "validate_outputs/1" do 10 | test "returns :ok when valid" do 11 | outputs = [ 12 | Fee.new_output(<<1::160>>, <<0::160>>, 10) 13 | ] 14 | 15 | assert Validator.validate_outputs(outputs) == :ok 16 | end 17 | 18 | test "returns an error when generic output is not valid" do 19 | outputs = [ 20 | Fee.new_output(<<1::160>>, <<0::160>>, 0) 21 | ] 22 | 23 | assert Validator.validate_outputs(outputs) == {:error, {:amount, :cannot_be_zero}} 24 | end 25 | 26 | test "returns an error when outputs count is greater than 1" do 27 | outputs = 28 | Enum.reduce(1..6, [], fn i, acc -> 29 | [Fee.new_output(<<1::160>>, <<0::160>>, i) | acc] 30 | end) 31 | 32 | assert Validator.validate_outputs(outputs) == {:error, {:outputs, :wrong_number_of_fee_outputs}} 33 | end 34 | 35 | test "returns an error when outputs count is 0" do 36 | assert Validator.validate_outputs([]) == {:error, {:outputs, :wrong_number_of_fee_outputs}} 37 | end 38 | 39 | test "returns an error when output type is not a payment v1" do 40 | outputs = [ 41 | %Output{ 42 | output_data: %{amount: 2, output_guard: <<2::160>>, token: <<0::160>>}, 43 | output_id: nil, 44 | output_type: 0 45 | } 46 | ] 47 | 48 | assert Validator.validate_outputs(outputs) == {:error, {:outputs, :invalid_output_type_for_transaction}} 49 | end 50 | end 51 | 52 | describe "validate_nonce/1" do 53 | test "returns :ok when valid" do 54 | assert Validator.validate_nonce(<<0::256>>) == :ok 55 | end 56 | 57 | test "returns an error when length is not 256" do 58 | assert Validator.validate_nonce(<<0::254>>) == {:error, {:nonce, :malformed_nonce}} 59 | end 60 | 61 | test "returns an error when the nonce is not a binary" do 62 | assert Validator.validate_nonce(1234) == {:error, {:nonce, :malformed_nonce}} 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/ex_plasma/transaction/type/fee_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.FeeTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | doctest ExPlasma.Transaction.Type.Fee, import: true 5 | 6 | alias ExPlasma.Builder 7 | alias ExPlasma.Output 8 | alias ExPlasma.Transaction 9 | alias ExPlasma.Transaction.Type.Fee 10 | 11 | describe "new_output/3" do 12 | test "returns a new fee output with the given params" do 13 | output = Fee.new_output(<<0::160>>, <<0::160>>, 1337) 14 | 15 | assert output == %Output{ 16 | output_data: %{amount: 1337, output_guard: <<0::160>>, token: <<0::160>>}, 17 | output_id: nil, 18 | output_type: 2 19 | } 20 | end 21 | end 22 | 23 | describe "build_nonce/1" do 24 | test "builds a valid nonce with the given params" do 25 | assert {:ok, nonce} = Fee.build_nonce(%{blknum: 1000, token: <<0::160>>}) 26 | 27 | assert nonce == 28 | <<61, 119, 206, 68, 25, 203, 29, 23, 147, 224, 136, 32, 198, 128, 177, 74, 227, 250, 194, 173, 146, 182, 29 | 251, 152, 123, 172, 26, 83, 175, 194, 213, 238>> 30 | end 31 | 32 | test "returns an error when not given correct params" do 33 | assert Fee.build_nonce(%{}) == {:error, :invalid_nonce_params} 34 | end 35 | end 36 | 37 | describe "to_rlp/1" do 38 | test "returns the rlp item list of the given fee transaction" do 39 | blknum = 1000 40 | token = <<0::160>> 41 | output = Fee.new_output(<<1::160>>, token, 1) 42 | 43 | tx = 44 | ExPlasma.fee() 45 | |> Builder.new(outputs: [output]) 46 | |> Builder.with_nonce!(%{token: token, blknum: blknum}) 47 | 48 | assert {:ok, rlp} = Fee.to_rlp(tx) 49 | 50 | assert rlp == [ 51 | # tx type 52 | <<3>>, 53 | [ 54 | [ 55 | # Output type 56 | <<2>>, 57 | [ 58 | # Output guard 59 | <<1::160>>, 60 | # Output token 61 | <<0::160>>, 62 | # Output amount 63 | <<1>> 64 | ] 65 | ] 66 | ], 67 | # nonce 68 | <<61, 119, 206, 68, 25, 203, 29, 23, 147, 224, 136, 32, 198, 128, 177, 74, 227, 250, 194, 173, 146, 182, 69 | 251, 152, 123, 172, 26, 83, 175, 194, 213, 238>> 70 | ] 71 | end 72 | end 73 | 74 | describe "to_map/1" do 75 | test "returns a transaction struct from an rlp list when valid" do 76 | rlp = [ 77 | # tx type 78 | <<3>>, 79 | [ 80 | [ 81 | # Output type 82 | <<2>>, 83 | [ 84 | # Output guard 85 | <<1::160>>, 86 | # Output token 87 | <<0::160>>, 88 | # Output amount 89 | <<1>> 90 | ] 91 | ] 92 | ], 93 | # nonce 94 | <<61, 119, 206, 68, 25, 203, 29, 23, 147, 224, 136, 32, 198, 128, 177, 74, 227, 250, 194, 173, 146, 182, 251, 95 | 152, 123, 172, 26, 83, 175, 194, 213, 238>> 96 | ] 97 | 98 | assert {:ok, tx} = Fee.to_map(rlp) 99 | 100 | assert tx == %Transaction{ 101 | nonce: 102 | <<61, 119, 206, 68, 25, 203, 29, 23, 147, 224, 136, 32, 198, 128, 177, 74, 227, 250, 194, 173, 146, 103 | 182, 251, 152, 123, 172, 26, 83, 175, 194, 213, 238>>, 104 | outputs: [ 105 | %Output{ 106 | output_data: %{amount: 1, output_guard: <<1::160>>, token: <<0::160>>}, 107 | output_id: nil, 108 | output_type: 2 109 | } 110 | ], 111 | tx_type: 3 112 | } 113 | end 114 | 115 | test "returns a `malformed_transaction` error when the rlp is invalid" do 116 | assert Fee.to_map([<<3>>, <<1>>]) == {:error, :malformed_transaction} 117 | end 118 | 119 | test "returns a `malformed_output_rlp` error when the outputs are not a list" do 120 | assert Fee.to_map([<<3>>, 123, <<1>>]) == {:error, :malformed_output_rlp} 121 | end 122 | 123 | test "returns a `malformed_outputs` error when the outputs are not a valid encoded output" do 124 | assert Fee.to_map([<<3>>, [123], <<1>>]) == {:error, :malformed_outputs} 125 | end 126 | end 127 | 128 | describe "validate/1" do 129 | test "returns :ok when valid" do 130 | token = <<0::160>> 131 | output = Fee.new_output(<<1::160>>, token, 1) 132 | {:ok, nonce} = Fee.build_nonce(%{blknum: 1000, token: token}) 133 | tx = Builder.new(ExPlasma.fee(), nonce: nonce, outputs: [output]) 134 | 135 | assert Fee.validate(tx) == :ok 136 | end 137 | 138 | test "returns an error when generic output is not valid" do 139 | output = Fee.new_output(<<1::160>>, <<0::160>>, 0) 140 | tx = Builder.new(ExPlasma.fee(), nonce: <<0>>, outputs: [output]) 141 | 142 | assert_field(tx, :amount, :cannot_be_zero) 143 | end 144 | 145 | test "returns an error when outputs count is greater than 1" do 146 | outputs = 147 | Enum.reduce(1..6, [], fn i, acc -> 148 | [Fee.new_output(<<1::160>>, <<0::160>>, i) | acc] 149 | end) 150 | 151 | tx = Builder.new(ExPlasma.fee(), nonce: <<0>>, outputs: outputs) 152 | 153 | assert_field(tx, :outputs, :wrong_number_of_fee_outputs) 154 | end 155 | 156 | test "returns an error when outputs count is 0" do 157 | tx = Builder.new(ExPlasma.fee(), nonce: <<0>>, outputs: []) 158 | 159 | assert_field(tx, :outputs, :wrong_number_of_fee_outputs) 160 | end 161 | 162 | test "returns an error when output type is not a fee" do 163 | output = %Output{ 164 | output_data: %{amount: 2, output_guard: <<2::160>>, token: <<0::160>>}, 165 | output_id: nil, 166 | output_type: 1 167 | } 168 | 169 | tx = Builder.new(ExPlasma.fee(), nonce: <<0>>, outputs: [output]) 170 | 171 | assert_field(tx, :outputs, :invalid_output_type_for_transaction) 172 | end 173 | 174 | test "returns an error when the nonce is not in the right format" do 175 | output = %Output{ 176 | output_data: %{amount: 2, output_guard: <<2::160>>, token: <<0::160>>}, 177 | output_id: nil, 178 | output_type: 2 179 | } 180 | 181 | tx = Builder.new(ExPlasma.fee(), nonce: <<0>>, outputs: [output]) 182 | 183 | assert_field(tx, :nonce, :malformed_nonce) 184 | end 185 | end 186 | 187 | defp assert_field(data, field, message) do 188 | assert {:error, {^field, ^message}} = Fee.validate(data) 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /test/ex_plasma/transaction/type/payment_v1/payment_v1_validator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.PaymentV1.ValidatorTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.Output 6 | alias ExPlasma.Transaction.Type.PaymentV1 7 | alias ExPlasma.Transaction.Type.PaymentV1.Validator 8 | 9 | describe "validate_inputs/1" do 10 | test "returns :ok when valid" do 11 | inputs = [ 12 | %Output{ 13 | output_data: [], 14 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 15 | output_type: nil 16 | } 17 | ] 18 | 19 | assert Validator.validate_inputs(inputs) == :ok 20 | end 21 | 22 | test "returns an error when generic output is not valid" do 23 | inputs = [ 24 | %Output{ 25 | output_data: [], 26 | output_id: %{blknum: nil, oindex: 0, position: 0, txindex: 0}, 27 | output_type: nil 28 | } 29 | ] 30 | 31 | assert Validator.validate_inputs(inputs) == {:error, {:blknum, :cannot_be_nil}} 32 | end 33 | 34 | test "returns an error when inputs are not unique" do 35 | input_1 = %Output{ 36 | output_data: [], 37 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 38 | output_type: nil 39 | } 40 | 41 | input_2 = %Output{ 42 | output_data: [], 43 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 44 | output_type: nil 45 | } 46 | 47 | assert Validator.validate_inputs([input_1, input_2]) == {:error, {:inputs, :duplicate_inputs}} 48 | end 49 | 50 | test "returns an error when inputs count is greater than 4" do 51 | inputs = 52 | Enum.reduce(0..5, [], fn i, acc -> 53 | [ 54 | %Output{ 55 | output_data: [], 56 | output_id: %{blknum: i, oindex: 0, position: 0, txindex: 0}, 57 | output_type: nil 58 | } 59 | | acc 60 | ] 61 | end) 62 | 63 | assert Validator.validate_inputs(inputs) == {:error, {:inputs, :cannot_exceed_maximum_value}} 64 | end 65 | end 66 | 67 | describe "validate_outputs/1" do 68 | test "returns :ok when valid" do 69 | outputs = [ 70 | PaymentV1.new_output(<<1::160>>, <<0::160>>, 10) 71 | ] 72 | 73 | assert Validator.validate_outputs(outputs) == :ok 74 | end 75 | 76 | test "returns an error when outputs count is greater than 4" do 77 | outputs = 78 | Enum.reduce(1..6, [], fn i, acc -> 79 | [PaymentV1.new_output(<<1::160>>, <<0::160>>, i) | acc] 80 | end) 81 | 82 | assert Validator.validate_outputs(outputs) == {:error, {:outputs, :cannot_exceed_maximum_value}} 83 | end 84 | 85 | test "returns an error when outputs count is 0" do 86 | assert Validator.validate_outputs([]) == {:error, {:outputs, :cannot_subceed_minimum_value}} 87 | end 88 | 89 | test "returns an error when output type is not a payment v1" do 90 | outputs = [ 91 | %Output{ 92 | output_data: %{amount: 2, output_guard: <<2::160>>, token: <<0::160>>}, 93 | output_id: nil, 94 | output_type: 0 95 | } 96 | ] 97 | 98 | assert Validator.validate_outputs(outputs) == {:error, {:outputs, :invalid_output_type_for_transaction}} 99 | end 100 | end 101 | 102 | describe "validate_tx_data/1" do 103 | test "returns :ok when valid" do 104 | assert Validator.validate_tx_data(0) == :ok 105 | end 106 | 107 | test "returns an error when invalid" do 108 | assert Validator.validate_tx_data("1234") == {:error, {:tx_data, :malformed_tx_data}} 109 | end 110 | end 111 | 112 | describe "validate_metadata/1" do 113 | test "returns :ok when valid" do 114 | assert Validator.validate_metadata(<<0::256>>) == :ok 115 | end 116 | 117 | test "returns an error when length is not 256" do 118 | assert Validator.validate_metadata(<<0::254>>) == {:error, {:metadata, :malformed_metadata}} 119 | end 120 | 121 | test "returns an error when not a binary" do 122 | assert Validator.validate_metadata(123) == {:error, {:metadata, :malformed_metadata}} 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/ex_plasma/transaction/type/payment_v1_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.Type.PaymentV1Test do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | doctest ExPlasma.Transaction.Type.PaymentV1, import: true 5 | 6 | alias ExPlasma.Builder 7 | alias ExPlasma.Output 8 | alias ExPlasma.Transaction 9 | alias ExPlasma.Transaction.Type.PaymentV1 10 | 11 | describe "new_output/3" do 12 | test "returns a new payment v1 output with the given params" do 13 | output = PaymentV1.new_output(<<0::160>>, <<0::160>>, 123) 14 | 15 | assert output == %Output{ 16 | output_data: %{amount: 123, output_guard: <<0::160>>, token: <<0::160>>}, 17 | output_id: nil, 18 | output_type: 1 19 | } 20 | end 21 | end 22 | 23 | describe "build_nonce/1" do 24 | test "returns a nil nonce" do 25 | assert PaymentV1.build_nonce(%{}) == {:ok, nil} 26 | end 27 | end 28 | 29 | describe "to_rlp/1" do 30 | test "returns the rlp item list of the given payment v1 transaction" do 31 | input = %Output{ 32 | output_data: [], 33 | output_id: %{blknum: 1, oindex: 2, txindex: 3, position: 1_000_020_003}, 34 | output_type: nil 35 | } 36 | 37 | output = PaymentV1.new_output(<<1::160>>, <<0::160>>, 1) 38 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [input], outputs: [output]) 39 | 40 | assert {:ok, rlp} = PaymentV1.to_rlp(tx) 41 | 42 | assert rlp == [ 43 | # tx type 44 | <<1>>, 45 | [ 46 | # input position 47 | <<0, 0, 0, 0, 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, 63, 50>> 48 | ], 49 | [ 50 | [ 51 | # Output type 52 | <<1>>, 53 | [ 54 | # Output guard 55 | <<1::160>>, 56 | # Output token 57 | <<0::160>>, 58 | # Output amount 59 | <<1>> 60 | ] 61 | ] 62 | ], 63 | # tx data 64 | 0, 65 | # metadata 66 | <<0::256>> 67 | ] 68 | end 69 | end 70 | 71 | describe "to_map/1" do 72 | test "returns a transaction struct from an rlp list when valid" do 73 | rlp = [ 74 | # tx type 75 | <<1>>, 76 | [ 77 | # input position 78 | <<0, 0, 0, 0, 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, 63, 50>> 79 | ], 80 | [ 81 | [ 82 | # Output type 83 | <<1>>, 84 | [ 85 | # Output guard 86 | <<1::160>>, 87 | # Output token 88 | <<0::160>>, 89 | # Output amount 90 | <<1>> 91 | ] 92 | ] 93 | ], 94 | # tx data 95 | <<0>>, 96 | # metadata 97 | <<0::256>> 98 | ] 99 | 100 | assert {:ok, tx} = PaymentV1.to_map(rlp) 101 | 102 | assert tx == %Transaction{ 103 | inputs: [ 104 | %Output{ 105 | output_data: nil, 106 | output_id: %{blknum: 1, oindex: 2, position: 1_000_030_002, txindex: 3}, 107 | output_type: nil 108 | } 109 | ], 110 | metadata: <<0::256>>, 111 | outputs: [ 112 | %Output{ 113 | output_data: %{amount: 1, output_guard: <<1::160>>, token: <<0::160>>}, 114 | output_id: nil, 115 | output_type: 1 116 | } 117 | ], 118 | tx_data: 0, 119 | tx_type: 1, 120 | sigs: [], 121 | witnesses: [] 122 | } 123 | end 124 | 125 | test "returns a `malformed_transaction` error when the rlp is invalid" do 126 | assert PaymentV1.to_map([<<1>>, <<1>>]) == {:error, :malformed_transaction} 127 | end 128 | 129 | test "returns a `malformed_tx_data` error when the tx data is invalid" do 130 | assert PaymentV1.to_map([<<1>>, [], [], <<0, 1>>, <<0::256>>]) == {:error, :malformed_tx_data} 131 | end 132 | 133 | test "returns a `malformed_input_position_rlp` error when the inputs are not a list" do 134 | assert PaymentV1.to_map([<<1>>, 123, [], <<0>>, <<0::256>>]) == {:error, :malformed_input_position_rlp} 135 | end 136 | 137 | test "returns a `malformed_input_position_rlp` error when the inputs are not an encoded position" do 138 | assert PaymentV1.to_map([<<1>>, [123, 123], [], <<0>>, <<0::256>>]) == {:error, :malformed_input_position_rlp} 139 | end 140 | 141 | test "returns a `malformed_output_rlp` error when the outputs are not a list" do 142 | assert PaymentV1.to_map([<<1>>, [], 123, <<0>>, <<0::256>>]) == {:error, :malformed_output_rlp} 143 | end 144 | 145 | test "returns a `malformed_outputs` error when the outputs are not an encoded output data" do 146 | assert PaymentV1.to_map([<<1>>, [], [123], <<0>>, <<0::256>>]) == {:error, :malformed_outputs} 147 | end 148 | 149 | test "returns a `malformed_metadata` error when metadata is not a 32 bytes binary" do 150 | assert PaymentV1.to_map([<<1>>, [], [], <<0>>, 123]) == {:error, :malformed_metadata} 151 | end 152 | end 153 | 154 | describe "validate/1" do 155 | test "returns :ok when valid" do 156 | input_1 = %Output{ 157 | output_data: nil, 158 | output_id: %{blknum: 1, oindex: 2, position: 1_000_030_002, txindex: 3}, 159 | output_type: nil 160 | } 161 | 162 | input_2 = %Output{ 163 | output_data: nil, 164 | output_id: %{blknum: 1, oindex: 3, position: 1_000_030_003, txindex: 3}, 165 | output_type: nil 166 | } 167 | 168 | output_1 = PaymentV1.new_output(<<1::160>>, <<0::160>>, 1) 169 | output_2 = PaymentV1.new_output(<<1::160>>, <<0::160>>, 2) 170 | output_3 = PaymentV1.new_output(<<2::160>>, <<0::160>>, 3) 171 | 172 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [input_1, input_2], outputs: [output_1, output_2, output_3]) 173 | 174 | assert PaymentV1.validate(tx) == :ok 175 | end 176 | 177 | test "returns an error when generic output is not valid" do 178 | output = PaymentV1.new_output(<<1::160>>, <<0::160>>, 0) 179 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [], outputs: [output]) 180 | 181 | assert_field(tx, :amount, :cannot_be_zero) 182 | end 183 | 184 | test "returns an error when inputs are not unique" do 185 | input = %Output{ 186 | output_data: [], 187 | output_id: %{blknum: 0, oindex: 0, position: 0, txindex: 0}, 188 | output_type: nil 189 | } 190 | 191 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [input, input], outputs: []) 192 | 193 | assert_field(tx, :inputs, :duplicate_inputs) 194 | end 195 | 196 | test "returns an error when inputs count is greater than 4" do 197 | inputs = 198 | Enum.reduce(0..5, [], fn i, acc -> 199 | [ 200 | %Output{ 201 | output_data: [], 202 | output_id: %{blknum: i, oindex: 0, position: 0, txindex: 0}, 203 | output_type: nil 204 | } 205 | | acc 206 | ] 207 | end) 208 | 209 | output = PaymentV1.new_output(<<1::160>>, <<0::160>>, 0) 210 | 211 | tx = Builder.new(ExPlasma.payment_v1(), inputs: inputs, outputs: [output]) 212 | 213 | assert_field(tx, :inputs, :cannot_exceed_maximum_value) 214 | end 215 | 216 | test "returns an error when outputs count is greater than 4" do 217 | outputs = 218 | Enum.reduce(1..6, [], fn i, acc -> 219 | [PaymentV1.new_output(<<1::160>>, <<0::160>>, i) | acc] 220 | end) 221 | 222 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [], outputs: outputs) 223 | 224 | assert_field(tx, :outputs, :cannot_exceed_maximum_value) 225 | end 226 | 227 | test "returns an error when outputs count is 0" do 228 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [], outputs: []) 229 | 230 | assert_field(tx, :outputs, :cannot_subceed_minimum_value) 231 | end 232 | 233 | test "returns an error when output type is valid but not a payment v1" do 234 | output = %Output{ 235 | output_data: %{amount: 2, output_guard: <<2::160>>, token: <<0::160>>}, 236 | output_id: nil, 237 | output_type: 0 238 | } 239 | 240 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [], outputs: [output]) 241 | 242 | assert_field(tx, :outputs, :invalid_output_type_for_transaction) 243 | end 244 | 245 | test "returns an error when output type is invalid" do 246 | output = %Output{ 247 | output_data: %{amount: 2, output_guard: <<2::160>>, token: <<0::160>>}, 248 | output_id: nil, 249 | output_type: 98_123_983 250 | } 251 | 252 | tx = Builder.new(ExPlasma.payment_v1(), inputs: [], outputs: [output]) 253 | assert_field(tx, :output_type, :unrecognized_output_type) 254 | end 255 | end 256 | 257 | defp assert_field(data, field, message) do 258 | assert {:error, {^field, ^message}} = PaymentV1.validate(data) 259 | end 260 | end 261 | -------------------------------------------------------------------------------- /test/ex_plasma/transaction/witness_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Transaction.WitnessTest do 2 | @moduledoc false 3 | 4 | use ExUnit.Case, async: true 5 | 6 | alias ExPlasma.Transaction.Witness 7 | 8 | describe "valid?/1" do 9 | test "returns true when is binary and 65 bytes long" do 10 | assert Witness.valid?(<<0::520>>) 11 | end 12 | 13 | test "returns false when not a binary" do 14 | refute Witness.valid?([<<0>>]) 15 | end 16 | 17 | test "returns false when not 65 bytes long" do 18 | refute Witness.valid?(<<0>>) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/ex_plasma/typed_data/output_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.TypedData.OutputTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.TypedData 6 | 7 | describe "encode/2" do 8 | test "builds an EIP712 encodable output" do 9 | output = %ExPlasma.Output{ 10 | output_type: 1, 11 | output_data: %{ 12 | output_guard: <<0::160>>, 13 | token: <<0::160>>, 14 | amount: 10 15 | } 16 | } 17 | 18 | encoded = TypedData.encode(output, as: :output) 19 | 20 | assert encoded == 21 | [ 22 | "Output(uint256 outputType,bytes20 outputGuard,address currency,uint256 amount)", 23 | <<0, 0, 0, 0, 0, 0, 0, 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>>, 24 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 25 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 26 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10>> 27 | ] 28 | end 29 | 30 | test "builds an EIP712 encodable input" do 31 | output = %ExPlasma.Output{ 32 | output_id: %{blknum: 1000, txindex: 0, oindex: 0} 33 | } 34 | 35 | encoded = ExPlasma.TypedData.encode(output, as: :input) 36 | 37 | assert encoded == 38 | [ 39 | "Input(uint256 blknum,uint256 txindex,uint256 oindex)", 40 | <<0, 0, 0, 0, 0, 0, 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, 232>>, 41 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 42 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 43 | ] 44 | end 45 | end 46 | 47 | describe "hash/2" do 48 | test "hashes an eip712 encoded output identifier" do 49 | output = %ExPlasma.Output{ 50 | output_id: %{blknum: 0, txindex: 0, oindex: 0} 51 | } 52 | 53 | hashed = ExPlasma.TypedData.hash(output, as: :input) 54 | 55 | assert hashed == 56 | <<26, 89, 51, 235, 11, 50, 35, 176, 80, 15, 187, 231, 3, 156, 171, 155, 173, 192, 6, 173, 218, 108, 243, 57 | 211, 55, 117, 20, 18, 253, 122, 75, 97>> 58 | end 59 | 60 | test "hashes an eip712 encoded output" do 61 | output = %ExPlasma.Output{ 62 | output_type: 1, 63 | output_data: %{ 64 | output_guard: <<0::160>>, 65 | token: <<0::160>>, 66 | amount: 10 67 | } 68 | } 69 | 70 | hashed = ExPlasma.TypedData.hash(output, as: :output) 71 | 72 | assert hashed == 73 | <<215, 8, 60, 19, 55, 10, 155, 112, 243, 199, 49, 150, 131, 140, 14, 12, 157, 118, 195, 214, 198, 94, 74 | 223, 77, 159, 186, 45, 211, 125, 37, 234, 32>> 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/ex_plasma/typed_data/transaction_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.TypedData.TransactionTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExPlasma.Transaction 6 | 7 | describe "encode/1" do 8 | test "builds a eip712 transaction object" do 9 | encoded = ExPlasma.TypedData.encode(%Transaction{tx_type: 1}) 10 | 11 | assert encoded == [ 12 | <<25, 1>>, 13 | [ 14 | "EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)", 15 | "OMG Network", 16 | "1", 17 | "0xd17e1233a03affb9092d5109179b43d6a8828607", 18 | "0xfad5c7f626d80f9256ef01929f3beb96e058b8b4b0e3fe52d84f054c0e2a7a83" 19 | ], 20 | "Transaction(uint256 txType,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)", 21 | <<0, 0, 0, 0, 0, 0, 0, 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>>, 22 | [], 23 | [], 24 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, 25 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 26 | ] 27 | end 28 | end 29 | 30 | describe "hash/1" do 31 | test "hashes a eip 712 encoded object" do 32 | encoded_hash = ExPlasma.TypedData.hash(%Transaction{tx_type: 1}) 33 | 34 | assert encoded_hash == 35 | <<196, 145, 245, 73, 70, 135, 10, 204, 85, 216, 199, 89, 153, 191, 31, 94, 60, 22, 20, 81, 54, 74, 38, 36 | 48, 248, 239, 148, 10, 173, 134, 85, 114>> 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/ex_plasma/utils/rlp_decoder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Utils.RlpDecoderTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ExPlasma.Utils.RlpDecoder 5 | 6 | describe "decode/1" do 7 | test "returns {:ok, decoded} when valid" do 8 | assert {:ok, decoded} = 1 |> ExRLP.encode() |> RlpDecoder.decode() 9 | assert decoded == <<1>> 10 | end 11 | 12 | test "returns malformed_rlp when given invalid bytes" do 13 | assert RlpDecoder.decode(1) == {:error, :malformed_rlp} 14 | end 15 | end 16 | 17 | describe "parse_uint256/1" do 18 | test "rejects integer greater than 32-bytes" do 19 | large = 2.0 |> :math.pow(8 * 32) |> Kernel.trunc() 20 | [too_large] = [large] |> ExRLP.encode() |> ExRLP.decode() 21 | 22 | assert {:error, :encoded_uint_too_big} == RlpDecoder.parse_uint256(too_large) 23 | end 24 | 25 | test "rejects leading zeros encoded numbers" do 26 | [one] = [1] |> ExRLP.encode() |> ExRLP.decode() 27 | 28 | assert {:error, :leading_zeros_in_encoded_uint} == RlpDecoder.parse_uint256(<<0>> <> one) 29 | end 30 | 31 | test "rejects if not a binary" do 32 | assert {:error, :malformed_uint256} == RlpDecoder.parse_uint256(123) 33 | end 34 | 35 | test "accepts 32-bytes positive integers" do 36 | large = 2.0 |> :math.pow(8 * 32) |> Kernel.trunc() 37 | big_just_enough = large - 1 38 | 39 | [one, big] = [1, big_just_enough] |> ExRLP.encode() |> ExRLP.decode() 40 | 41 | assert {:ok, 1} == RlpDecoder.parse_uint256(one) 42 | assert {:ok, big_just_enough} == RlpDecoder.parse_uint256(big) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/ex_plasma_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExPlasmaTest do 2 | use ExUnit.Case, async: true 3 | doctest ExPlasma 4 | end 5 | -------------------------------------------------------------------------------- /test/support/entity.ex: -------------------------------------------------------------------------------- 1 | defmodule ExPlasma.Support.TestEntity do 2 | @moduledoc """ 3 | Stable entities that have a valid private key/address. 4 | """ 5 | 6 | def alice() do 7 | %{ 8 | addr: <<99, 100, 231, 104, 170, 156, 129, 68, 252, 45, 124, 232, 218, 107, 175, 51, 13, 180, 254, 40>>, 9 | addr_encoded: "0x6364e768aa9c8144fc2d7ce8da6baf330db4fe28", 10 | priv: 11 | <<252, 157, 117, 210, 154, 177, 98, 65, 111, 244, 232, 247, 38, 113, 36, 4, 61, 191, 110, 125, 222, 219, 194, 12 | 221, 199, 251, 100, 63, 160, 194, 126, 126>>, 13 | priv_encoded: "0xfc9d75d29ab162416ff4e8f7267124043dbf6e7ddedbc2ddc7fb643fa0c27e7e" 14 | } 15 | end 16 | 17 | def bob() do 18 | %{ 19 | addr: <<70, 55, 228, 199, 167, 80, 4, 228, 159, 169, 40, 95, 34, 176, 220, 96, 12, 124, 194, 203>>, 20 | addr_encoded: "0x4637e4c7a75004e49fa9285f22b0dc600c7cc2cb", 21 | priv: 22 | <<165, 205, 127, 128, 156, 32, 196, 83, 131, 79, 62, 37, 89, 67, 34, 193, 223, 11, 15, 242, 218, 143, 99, 111, 23 | 78, 57, 106, 157, 68, 46, 14, 26>>, 24 | priv_encoded: "0xa5cd7f809c20c453834f3e25594322c1df0b0ff2da8f636f4e396a9d442e0e1a" 25 | } 26 | end 27 | 28 | def carol() do 29 | %{ 30 | addr: <<240, 54, 26, 40, 211, 42, 228, 46, 237, 159, 242, 9, 238, 29, 5, 63, 118, 62, 24, 248>>, 31 | addr_encoded: "0xf0361a28d32ae42eed9ff209ee1d053f763e18f8", 32 | priv: 33 | <<110, 195, 211, 42, 134, 51, 211, 75, 18, 102, 23, 110, 31, 252, 242, 234, 183, 78, 108, 21, 234, 15, 4, 47, 34 | 211, 255, 219, 30, 238, 109, 228, 64>>, 35 | priv_encoded: "0x6ec3d32a8633d34b1266176e1ffcf2eab74e6c15ea0f042fd3ffdb1eee6de440" 36 | } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Application.ensure_all_started(:ethereumex) 2 | Application.ensure_all_started(:telemetry) 3 | ExUnit.start(exclude: [:skip, :conformance]) 4 | --------------------------------------------------------------------------------