├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── .tool-versions ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── SECURITY.md ├── UNLICENSE ├── _plts └── .gitkeep ├── bench └── transaction_bench.exs ├── config └── config.exs ├── lib ├── address.ex ├── base58.ex ├── bech32.ex ├── bitcoinex.ex ├── extendedkey.ex ├── lightning_network │ ├── hop_hint.ex │ ├── invoice.ex │ └── lightning_network.ex ├── network.ex ├── opcode.ex ├── psbt.ex ├── script.ex ├── secp256k1 │ ├── ecdsa.ex │ ├── math.ex │ ├── params.ex │ ├── point.ex │ ├── privatekey.ex │ ├── schnorr.ex │ └── secp256k1.ex ├── segwit.ex ├── transaction.ex └── utils.ex ├── mix.exs ├── mix.lock ├── scripts └── decode_psbt.exs └── test ├── address_test.exs ├── base58_test.exs ├── bech32_test.exs ├── extendedkey_test.exs ├── lightning_network └── invoice_test.exs ├── network.ex ├── psbt_test.exs ├── script_test.exs ├── secp256k1 ├── ecdsa_test.exs ├── point_test.exs ├── privatekey_test.exs ├── schnorr_test.exs └── secp256k1_test.exs ├── segwit_test.exs ├── test_helper.exs └── transaction_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_with_opt_26: 7 | name: Build and test ${{matrix.elixir}}-otp-${{matrix.otp}} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | otp: ['27.0'] 12 | elixir: ['1.18.1'] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Elixir 16 | uses: erlef/setup-elixir@v1 17 | with: 18 | otp-version: ${{matrix.otp}} 19 | elixir-version: ${{matrix.elixir}} 20 | - name: Restore dependencies cache 21 | uses: actions/cache@v4 22 | with: 23 | path: deps 24 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 25 | restore-keys: ${{ runner.os }}-mix- 26 | - name: Install dependencies 27 | run: mix deps.get 28 | - name: Run tests 29 | run: mix test 30 | - name: format and lint 31 | run: mix lint.all 32 | -------------------------------------------------------------------------------- /.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 | bitcoinex-*.tar 24 | 25 | # Ignore linter directories 26 | .elixir_ls/ 27 | 28 | # dialyzer plt 29 | /_plts/*.plt 30 | /_plts/*.plt.hash 31 | 32 | # idea 33 | .idea/ 34 | 35 | # bitcoin related files 36 | **/*.psbt -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.1-otp-27 2 | erlang 27.0.1 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | elixir: 4 | - 1.8 5 | 6 | otp_release: 7 | - 21.3 8 | 9 | matrix: 10 | exclude: 11 | - elixir: 1.7 12 | otp_release: 19.3 13 | - elixir: 1.7 14 | otp_release: 20.3 15 | - elixir: 1.8 16 | otp_release: 19.3 17 | - elixir: 1.8 18 | otp_release: 20.3 19 | - elixir: 1.8 20 | otp_release: 21.3 21 | 22 | before_script: 23 | - mix deps.get 24 | 25 | script: 26 | - mix lint.all 27 | - mix test -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.1.8] - 2024-03-01 8 | ### Added 9 | - Fix warning emitted from String.slice with negative step when parsing amountful BOLT11 invoices. 10 | 11 | ## [0.1.7] - 2023-01-16 12 | ### Added 13 | - Support for Schnorr signature creation and validation 14 | - Fixed bug which would not correctly parse a BOLT11 invoice with amount explicitly set to 0. 15 | - Bump Elixir & Erlang Requirements & dependencies 16 | 17 | ## [0.1.4] - 2021-05-19 18 | ### Added 19 | - BIP32 support with new modules for extended keys and derivation paths. 20 | - Security document for vulnerability reports. 21 | - Extra test for PSBT with 0 inputs and 0 outputs. 22 | 23 | ### Changed 24 | - Jason updated to 1.2.2 25 | 26 | ## [0.1.3] - 2021-04-19 27 | ### Added 28 | - Disclaimer to README. 29 | - Support for Bech32m. 30 | - Private key module with signing functionality. 31 | - hash160 added to utils. 32 | 33 | ### Changed 34 | - Decimal dependency. 35 | 36 | ## [0.1.2] - 2021-01-13 37 | ### Added 38 | - Code snippet examples to README. 39 | - Padding function to utils. 40 | 41 | ### Fixed 42 | - Padding to public keys and transaction IDs. 43 | 44 | ## [0.1.1] - 2020-12-21 45 | ### Added 46 | - Native Elixir Secp256k1 elliptic curve support with ECDSA public key recovery. 47 | 48 | ### Removed 49 | - libsecp25k1 and ex_doc dependencies. 50 | 51 | ## [0.1.0] - 2020-12-02 52 | ### Added 53 | - Bech32 and base58 encoding. 54 | - Address and lightning invoice serialization. 55 | - PSBT serialization. 56 | - Transaction module. 57 | 58 | 59 | [0.1.4]: https://diff.hex.pm/diff/bitcoinex/0.1.3..0.1.4 60 | [0.1.3]: https://diff.hex.pm/diff/bitcoinex/0.1.2..0.1.3 61 | [0.1.2]: https://diff.hex.pm/diff/bitcoinex/0.1.1..0.1.2 62 | [0.1.1]: https://diff.hex.pm/diff/bitcoinex/0.1.0..0.1.1 63 | [0.1.0]: https://preview.hex.pm/preview/bitcoinex/0.1.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Bitcoinex](https://user-images.githubusercontent.com/8378656/102842648-4f671380-43bc-11eb-8e0d-72c2a107e5ed.png) 2 | # Bitcoinex 3 | 4 | Bitcoinex is striving to be the best and most up-to-date Bitcoin Library for Elixir. 5 | 6 | ## Documentation 7 | Documentation is available on [hexdocs.pm](https://hexdocs.pm/bitcoinex/api-reference.html). 8 | 9 | ## Current Utilities 10 | * Serialization and validation for Bech32 and Base58. 11 | * Support for standard on-chain scripts (P2PKH..P2WPKH) and Bolt#11 Lightning Invoices. 12 | * Transaction serialization. 13 | * Basic PSBT (BIP174) parsing. 14 | 15 | ## Usage 16 | 17 | With [Hex](https://hex.pm/packages/bitcoinex): 18 | 19 | {:bitcoinex, "~> 0.1.7"} 20 | 21 | Local: 22 | 23 | $ mix deps.get 24 | $ mix compile 25 | 26 | ## Examples 27 | 28 | Decode a Lightning Network invoice: 29 | 30 | ```elixir 31 | Bitcoinex.LightningNetwork.decode_invoice("lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp") 32 | 33 | {:ok, 34 | %Bitcoinex.LightningNetwork.Invoice{ 35 | amount_msat: 250000000, 36 | description: "1 cup coffee", 37 | description_hash: nil, 38 | destination: "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad", 39 | expiry: 60, 40 | fallback_address: nil, 41 | min_final_cltv_expiry: 18, 42 | network: :mainnet, 43 | payment_hash: "0001020304050607080900010203040506070809000102030405060708090102", 44 | route_hints: [], 45 | timestamp: 1496314658 46 | }} 47 | ``` 48 | 49 | Parse a BIP-174 Partially Signed Bitcoin Transaction: 50 | 51 | ```elixir 52 | Bitcoinex.PSBT.decode("cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=") 53 | 54 | {:ok, 55 | %Bitcoinex.PSBT{ 56 | global: %Bitcoinex.PSBT.Global{ 57 | proprietary: nil, 58 | unsigned_tx: %Bitcoinex.Transaction{...}, 59 | inputs: [ 60 | %Bitcoinex.PSBT.In{ 61 | bip32_derivation: [ 62 | %{ 63 | derivation: "b4a6ba67000000800000008004000080", 64 | public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46" 65 | }, 66 | %{ 67 | derivation: "b4a6ba67000000800000008005000080", 68 | public_key: "03de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd" 69 | } 70 | ], 71 | final_scriptsig: nil, 72 | final_scriptwitness: nil, 73 | non_witness_utxo: nil, 74 | partial_sig: %{ 75 | public_key: "03b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46", 76 | signature: "304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01" 77 | }, 78 | por_commitment: nil, 79 | proprietary: nil, 80 | redeem_script: "0020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681", 81 | sighash_type: nil, 82 | witness_script: "522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae", 83 | witness_utxo: %Bitcoinex.Transaction.Out{ 84 | script_pub_key: "a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87", 85 | value: 199909013 86 | } 87 | } 88 | ], 89 | outputs: [] 90 | }} 91 | ``` 92 | 93 | 94 | Handle bitcoin addresses: 95 | 96 | ```elixir 97 | {:ok, {:mainnet, witness_version, witness_program}} = Bitcoinex.Segwit.decode_address("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") 98 | 99 | Bitcoinex.Segwit.encode_address(:mainnet, witness_version, witness_program) 100 | 101 | {:ok, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"} 102 | ``` 103 | 104 | # Decoding a PSBT 105 | 106 | We have a simple script for decoding a PSBT to view the inputs and outputs. Once you've installed elixir, erlang, and the mix dependencies, you can run the command below: 107 | 108 | ```bash 109 | mix run scripts/decode_psbt.exs 110 | ``` 111 | 112 | ## Roadmap 113 | Continued support for on-chain and off-chain functionality including: 114 | * Full script support including validation. 115 | * Block serialization. 116 | * Transaction creation. 117 | * Broader BIP support including BIP32. 118 | 119 | ## Contributing 120 | We have big goals and this library is still in a very early stage. Contributions and comments are very much welcome. 121 | 122 | 123 | ## DISCLAIMER 124 | 125 | This software is not recommended for use in handling real funds. The addresses, public keys, and signatures generated by this library are not recommended to be used for sending or receiving real funds. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | n/a 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Report security issues to security@river.com. 10 | 11 | The following keys may be used to communicate sensitive information to developers: 12 | 13 | | Name | Fingerprint | 14 | |------|-------------| 15 | | Philip Glazman | 1DED 561A 828F 6542 9FA9 7B08 C31B DE21 A1C0 170D | 16 | | bruteforcecat | 741C B8EC 98F6 8979 1125 0488 5FEF C5A8 B4A2 E122 | 17 | | Sachin Meier | 9AA7 515C 4818 C98C 0792  F074 C599 15FE 4B6D 6276 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /_plts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiverFinancial/bitcoinex/97a78097dd7e8dcd015c72efec9dfd3d52a4f614/_plts/.gitkeep -------------------------------------------------------------------------------- /bench/transaction_bench.exs: -------------------------------------------------------------------------------- 1 | alias Bitcoinex.Transaction 2 | 3 | transactions_hexs = [ 4 | "01000000010470c3139dc0f0882f98d75ae5bf957e68dadd32c5f81261c0b13e85f592ff7b0000000000ffffffff02b286a61e000000001976a9140f39a0043cf7bdbe429c17e8b514599e9ec53dea88ac01000000000000001976a9148a8c9fd79173f90cf76410615d2a52d12d27d21288ac00000000", 5 | "01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000", 6 | "01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000", 7 | "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000", 8 | "0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000" 9 | ] 10 | 11 | transactions = 12 | Enum.map(transactions_hexs, fn hex -> 13 | {:ok, tx} = Transaction.decode(hex) 14 | tx 15 | end) 16 | 17 | Benchee.run( 18 | %{ 19 | "&Transaction.transaction_id/1" => fn -> 20 | Enum.each(transactions, &Transaction.transaction_id/1) 21 | end 22 | }, 23 | time: 10, 24 | memory_time: 2 25 | ) 26 | -------------------------------------------------------------------------------- /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 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :bitcoinex, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:bitcoinex, :key) 18 | # 19 | # You can also configure a third-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/address.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Address do 2 | @moduledoc """ 3 | Bitcoinex.Address supports Base58 and Bech32 address encoding and validation. 4 | """ 5 | 6 | alias Bitcoinex.{Segwit, Base58, Network} 7 | 8 | @typedoc """ 9 | The address_type describes the address type to use. 10 | 11 | Four address types are supported: 12 | * p2pkh: Pay-to-Public-Key-Hash 13 | * p2sh: Pay-to-Script-Hash 14 | * p2wpkh: Pay-to-Witness-Public-Key-Hash 15 | * p2wsh: Pay-To-Witness-Script-Hash 16 | """ 17 | @address_types ~w(p2pkh p2sh p2wpkh p2wsh p2tr)a 18 | @type address_type :: :p2pkh | :p2sh | :p2wpkh | :p2wsh | :p2tr 19 | 20 | @doc """ 21 | Accepts a public key hash, network, and address_type and returns its address. 22 | """ 23 | @spec encode(binary, Bitcoinex.Network.network_name(), address_type) :: String.t() 24 | def encode(pubkey_hash, network_name, :p2pkh) do 25 | network = Network.get_network(network_name) 26 | decimal_prefix = network.p2pkh_version_decimal_prefix 27 | 28 | Base58.encode(<> <> pubkey_hash) 29 | end 30 | 31 | def encode(script_hash, network_name, :p2sh) do 32 | network = Network.get_network(network_name) 33 | decimal_prefix = network.p2sh_version_decimal_prefix 34 | Base58.encode(<> <> script_hash) 35 | end 36 | 37 | @doc """ 38 | Checks if the address is valid. 39 | Both encoding and network is checked. 40 | """ 41 | @spec is_valid?(String.t(), Bitcoinex.Network.network_name()) :: boolean 42 | def is_valid?(address, network_name) do 43 | Enum.any?(@address_types, &is_valid?(address, network_name, &1)) 44 | end 45 | 46 | @doc """ 47 | Checks if the address is valid and matches the given address_type. 48 | Both encoding and network is checked. 49 | """ 50 | @spec is_valid?(String.t(), Bitcoinex.Network.network_name(), address_type) :: boolean 51 | def is_valid?(address, network_name, :p2pkh) do 52 | network = apply(Bitcoinex.Network, network_name, []) 53 | is_valid_base58_check_address?(address, network.p2pkh_version_decimal_prefix) 54 | end 55 | 56 | def is_valid?(address, network_name, :p2sh) do 57 | network = apply(Bitcoinex.Network, network_name, []) 58 | is_valid_base58_check_address?(address, network.p2sh_version_decimal_prefix) 59 | end 60 | 61 | def is_valid?(address, network_name, :p2wpkh) do 62 | case Segwit.decode_address(address) do 63 | {:ok, {^network_name, witness_version, witness_program}} 64 | when witness_version == 0 and length(witness_program) == 20 -> 65 | true 66 | 67 | # network is not same as network set in config 68 | {:ok, {_network_name, _, _}} -> 69 | false 70 | 71 | {:error, _error} -> 72 | false 73 | end 74 | end 75 | 76 | def is_valid?(address, network_name, :p2wsh) do 77 | case Segwit.decode_address(address) do 78 | {:ok, {^network_name, witness_version, witness_program}} 79 | when witness_version == 0 and length(witness_program) == 32 -> 80 | true 81 | 82 | # network is not same as network set in config 83 | {:ok, {_network_name, _, _}} -> 84 | false 85 | 86 | {:error, _error} -> 87 | false 88 | end 89 | end 90 | 91 | def is_valid?(address, network_name, :p2tr) do 92 | case Segwit.decode_address(address) do 93 | {:ok, {^network_name, witness_version, witness_program}} 94 | when witness_version == 1 and length(witness_program) == 32 -> 95 | true 96 | 97 | # network is not same as network set in config 98 | {:ok, {_network_name, _, _}} -> 99 | false 100 | 101 | {:error, _error} -> 102 | false 103 | end 104 | end 105 | 106 | @doc """ 107 | Returns a list of supported address types. 108 | """ 109 | def supported_address_types() do 110 | @address_types 111 | end 112 | 113 | defp is_valid_base58_check_address?(address, valid_prefix) do 114 | case Base58.decode(address) do 115 | {:ok, <<^valid_prefix::8, _::binary>>} -> 116 | true 117 | 118 | _ -> 119 | false 120 | end 121 | end 122 | 123 | @doc """ 124 | Decodes an address and returns the address_type. 125 | """ 126 | @spec decode_type(String.t(), Bitcoinex.Network.network_name()) :: 127 | {:ok, address_type} | {:error, :decode_error} 128 | def decode_type(address, network_name) do 129 | case Enum.find(@address_types, &is_valid?(address, network_name, &1)) do 130 | nil -> {:error, :decode_error} 131 | type -> {:ok, type} 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/base58.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Base58 do 2 | @moduledoc """ 3 | Includes Base58 serialization and validation. 4 | 5 | Some code is inspired by: 6 | https://github.com/comboy/bitcoin-elixir/blob/develop/lib/bitcoin/base58_check.ex 7 | """ 8 | alias Bitcoinex.Utils 9 | 10 | @typedoc """ 11 | Base58 encoding is only supported for p2sh and p2pkh address types. 12 | """ 13 | @type address_type :: :p2sh | :p2pkh 14 | @base58_encode_list ~c(123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz) 15 | @base58_decode_map @base58_encode_list |> Enum.with_index() |> Enum.into(%{}) 16 | 17 | @base58_0 <> 18 | @type byte_list :: list(byte()) 19 | 20 | @doc """ 21 | Decodes a Base58 encoded string into a byte array and validates checksum. 22 | """ 23 | @spec decode(binary) :: {:ok, binary} | {:error, atom} 24 | def decode(binary) 25 | 26 | def decode(""), do: {:ok, ""} 27 | 28 | def decode(<>) do 29 | if valid_charset?(body_and_checksum) do 30 | body_and_checksum 31 | |> decode_base!() 32 | |> validate_checksum() 33 | else 34 | {:error, :invalid_characters} 35 | end 36 | end 37 | 38 | @doc """ 39 | Decodes a Base58 encoded string into a byte array. 40 | """ 41 | @spec decode_base!(binary) :: binary 42 | def decode_base!(binary) 43 | 44 | def decode_base!(@base58_0), do: <<0>> 45 | 46 | def decode_base!(@base58_0 <> body) when byte_size(body) > 0 do 47 | decode_base!(@base58_0) <> decode_base!(body) 48 | end 49 | 50 | def decode_base!(""), do: "" 51 | 52 | def decode_base!(bin) do 53 | bin 54 | |> :binary.bin_to_list() 55 | |> Enum.map(&Map.fetch!(@base58_decode_map, &1)) 56 | |> Integer.undigits(58) 57 | |> :binary.encode_unsigned() 58 | end 59 | 60 | @doc """ 61 | Validates a Base58 checksum. 62 | """ 63 | @spec validate_checksum(binary) :: {:ok, binary} | {:error, atom} 64 | def validate_checksum(data) do 65 | [decoded_body, checksum] = 66 | data 67 | |> :binary.bin_to_list() 68 | |> Enum.split(-4) 69 | |> Tuple.to_list() 70 | |> Enum.map(&:binary.list_to_bin(&1)) 71 | 72 | if checksum == checksum(decoded_body) do 73 | {:ok, decoded_body} 74 | else 75 | {:error, :invalid_checksum} 76 | end 77 | end 78 | 79 | defp valid_charset?(""), do: true 80 | 81 | defp valid_charset?(<> <> string), 82 | do: char in @base58_encode_list && valid_charset?(string) 83 | 84 | @doc """ 85 | Encodes binary into a Base58 encoded string. 86 | """ 87 | @spec encode(binary) :: String.t() 88 | def encode(bin) do 89 | bin 90 | |> append_checksum() 91 | |> encode_base() 92 | end 93 | 94 | @spec encode_base(binary) :: String.t() 95 | def encode_base(binary) 96 | 97 | def encode_base(""), do: "" 98 | 99 | def encode_base(<<0>> <> tail) do 100 | @base58_0 <> encode_base(tail) 101 | end 102 | 103 | @doc """ 104 | Encodes a binary into a Base58 encoded string. 105 | """ 106 | def encode_base(bin) do 107 | bin 108 | |> :binary.decode_unsigned() 109 | |> Integer.digits(58) 110 | |> Enum.map(&Enum.fetch!(@base58_encode_list, &1)) 111 | |> List.to_string() 112 | end 113 | 114 | @spec append_checksum(binary) :: binary 115 | def append_checksum(body) do 116 | body <> checksum(body) 117 | end 118 | 119 | @spec checksum(binary) :: binary 120 | defp checksum(body) do 121 | <> = Utils.double_sha256(body) 122 | 123 | checksum 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/bech32.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Bech32 do 2 | @moduledoc """ 3 | Includes Bech32 serialization and validation. 4 | 5 | Reference: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 6 | """ 7 | 8 | import Bitwise 9 | 10 | @gen [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] 11 | @data_charset_list ~c"qpzry9x8gf2tvdw0s3jn54khce6mua7l" 12 | @data_charset_map @data_charset_list 13 | |> Enum.zip(0..Enum.count(@data_charset_list)) 14 | |> Enum.into(%{}) 15 | @hrp_char_code_point_upper_limit 126 16 | @hrp_char_code_point_lower_limit 33 17 | @max_overall_encoded_length 90 18 | @separator "1" 19 | 20 | @encoding_constant_map %{ 21 | bech32: 1, 22 | bech32m: 0x2BC830A3 23 | } 24 | 25 | @type encoding_type :: :bech32 | :bech32m 26 | @type hrp :: String.t() 27 | @type data :: list(integer) 28 | 29 | @type witness_version :: Range.t(0, 16) 30 | @type witness_program :: list(integer) 31 | @type max_encoded_length :: pos_integer() | :infinity 32 | 33 | @type error :: atom() 34 | 35 | # Inspired by Ecto.Changeset. more descriptive than result tuple 36 | defmodule DecodeResult do 37 | @type t() :: %__MODULE__{ 38 | encoded_str: String.t(), 39 | encoding_type: Bitcoinex.Bech32.encoding_type() | nil, 40 | hrp: String.t() | nil, 41 | data: String.t() | nil, 42 | error: atom() | nil 43 | } 44 | defstruct [:encoded_str, :encoding_type, :hrp, :data, :error] 45 | 46 | @spec add_error(t(), atom()) :: t() 47 | def add_error(%DecodeResult{} = decode_result, error) do 48 | %{ 49 | decode_result 50 | | error: error 51 | } 52 | end 53 | 54 | @doc """ 55 | This naming is taken from Haskell. we will treat DecodeResult a bit like an Monad 56 | And bind function will take a function that take DecodeResult that's only without error and return DecodeResult 57 | And we can skip handling same error case for all function 58 | """ 59 | @spec bind(t(), (t -> t())) :: t() 60 | def bind(%DecodeResult{error: error} = decode_result, _fun) when not is_nil(error) do 61 | decode_result 62 | end 63 | 64 | def bind(%DecodeResult{} = decode_result, fun) do 65 | fun.(decode_result) 66 | end 67 | end 68 | 69 | @spec decode(String.t(), max_encoded_length()) :: 70 | {:ok, {encoding_type, hrp, data}} | {:error, error} 71 | def decode(bech32_str, max_encoded_length \\ @max_overall_encoded_length) 72 | when is_binary(bech32_str) do 73 | %DecodeResult{ 74 | encoded_str: bech32_str 75 | } 76 | |> DecodeResult.bind(&validate_bech32_length(&1, max_encoded_length)) 77 | |> DecodeResult.bind(&validate_bech32_case/1) 78 | |> DecodeResult.bind(&split_bech32_str/1) 79 | |> DecodeResult.bind(&validate_checksum_and_add_encoding_type/1) 80 | |> format_bech32_decoding_result 81 | end 82 | 83 | @spec encode(hrp, data | String.t(), encoding_type, max_encoded_length()) :: 84 | {:ok, String.t()} | {:error, error} 85 | def encode(hrp, data, encoding_type, max_encoded_length \\ @max_overall_encoded_length) 86 | 87 | def encode(hrp, data, encoding_type, max_encoded_length) when is_list(data) do 88 | hrp_charlist = hrp |> String.to_charlist() 89 | 90 | if is_valid_hrp?(hrp_charlist) do 91 | checksummed = data ++ create_checksum(hrp_charlist, data, encoding_type) 92 | dp = Enum.map(checksummed, &Enum.at(@data_charset_list, &1)) |> List.to_string() 93 | encoded_result = <> 94 | 95 | case validate_bech32_length(encoded_result, max_encoded_length) do 96 | :ok -> 97 | {:ok, String.downcase(encoded_result)} 98 | 99 | {:error, error} -> 100 | {:error, error} 101 | end 102 | else 103 | {:error, :hrp_char_out_opf_range} 104 | end 105 | end 106 | 107 | # Here we assume caller pass raw ASCII string 108 | def encode(hrp, data, encoding_type, max_encoded_length) when is_binary(data) do 109 | data_integers = data |> String.to_charlist() |> Enum.map(&Map.get(@data_charset_map, &1)) 110 | 111 | case check_data_charlist_validity(data_integers) do 112 | :ok -> 113 | encode(hrp, data_integers, encoding_type, max_encoded_length) 114 | 115 | {:error, error} -> 116 | {:error, error} 117 | end 118 | end 119 | 120 | # Big endian conversion of a list of integer from base 2^frombits to base 2^tobits. 121 | # ref https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py#L80 122 | 123 | @spec convert_bits(list(integer), integer(), integer(), boolean()) :: 124 | {:error, :invalid_data} | {:ok, list(integer)} 125 | def convert_bits(data, from_bits, to_bits, padding \\ true) when is_list(data) do 126 | max_v = (1 <<< to_bits) - 1 127 | max_acc = (1 <<< (from_bits + to_bits - 1)) - 1 128 | 129 | result = 130 | Enum.reduce_while(data, {0, 0, []}, fn val, {acc, bits, ret} -> 131 | if val < 0 or val >>> from_bits != 0 do 132 | {:halt, {:error, :invalid_data}} 133 | else 134 | acc = (acc <<< from_bits ||| val) &&& max_acc 135 | bits = bits + from_bits 136 | {bits, ret} = convert_bits_loop(to_bits, max_v, acc, bits, ret) 137 | {:cont, {acc, bits, ret}} 138 | end 139 | end) 140 | 141 | case result do 142 | {acc, bits, ret} -> 143 | if padding && bits > 0 do 144 | {:ok, ret ++ [acc <<< (to_bits - bits) &&& max_v]} 145 | else 146 | if bits >= from_bits || (acc <<< (to_bits - bits) &&& max_v) > 0 do 147 | {:error, :invalid_data} 148 | else 149 | {:ok, ret} 150 | end 151 | end 152 | 153 | {:error, :invalid_data} = e -> 154 | e 155 | end 156 | end 157 | 158 | defp convert_bits_loop(to, max_v, acc, bits, ret) do 159 | if bits >= to do 160 | bits = bits - to 161 | ret = ret ++ [acc >>> bits &&& max_v] 162 | convert_bits_loop(to, max_v, acc, bits, ret) 163 | else 164 | {bits, ret} 165 | end 166 | end 167 | 168 | defp validate_checksum_and_add_encoding_type( 169 | %DecodeResult{ 170 | data: data, 171 | hrp: hrp 172 | } = decode_result 173 | ) do 174 | case bech32_polymod(bech32_hrp_expand(hrp) ++ data) do 175 | unquote(@encoding_constant_map.bech32) -> 176 | %DecodeResult{decode_result | encoding_type: :bech32} 177 | 178 | unquote(@encoding_constant_map.bech32m) -> 179 | %DecodeResult{decode_result | encoding_type: :bech32m} 180 | 181 | _ -> 182 | DecodeResult.add_error(decode_result, :invalid_checksum) 183 | end 184 | end 185 | 186 | defp create_checksum(hrp, data, encoding_type) do 187 | values = bech32_hrp_expand(hrp) ++ data ++ [0, 0, 0, 0, 0, 0] 188 | mod = Bitwise.bxor(bech32_polymod(values), @encoding_constant_map[encoding_type]) 189 | for p <- 0..5, do: mod >>> (5 * (5 - p)) &&& 31 190 | end 191 | 192 | defp bech32_polymod(values) do 193 | Enum.reduce( 194 | values, 195 | 1, 196 | fn value, acc -> 197 | b = acc >>> 25 198 | acc = Bitwise.bxor((acc &&& 0x1FFFFFF) <<< 5, value) 199 | 200 | Enum.reduce(0..length(@gen), acc, fn i, in_acc -> 201 | right_side = 202 | if (b >>> i &&& 1) != 0 do 203 | Enum.at(@gen, i) 204 | else 205 | 0 206 | end 207 | 208 | Bitwise.bxor(in_acc, right_side) 209 | end) 210 | end 211 | ) 212 | end 213 | 214 | defp bech32_hrp_expand(chars) when is_list(chars) do 215 | Enum.map(chars, &(&1 >>> 5)) ++ [0 | Enum.map(chars, &(&1 &&& 31))] 216 | end 217 | 218 | defp format_bech32_decoding_result(%DecodeResult{ 219 | error: nil, 220 | hrp: hrp, 221 | data: data, 222 | encoding_type: encoding_type 223 | }) 224 | when not is_nil(hrp) and not is_nil(data) do 225 | {:ok, {encoding_type, to_string(hrp), Enum.drop(data, -6)}} 226 | end 227 | 228 | defp format_bech32_decoding_result(%DecodeResult{ 229 | error: error 230 | }) do 231 | {:error, error} 232 | end 233 | 234 | defp split_bech32_str( 235 | %DecodeResult{ 236 | encoded_str: encoded_str 237 | } = decode_result 238 | ) do 239 | # the bech 32 is at most 90 chars 240 | # so it's ok to do 3 time reverse here 241 | # otherwise we can use binary pattern matching with index for better performance 242 | downcase_encoded_str = encoded_str |> String.downcase() 243 | 244 | with {_, [data, hrp]} when hrp != "" and data != "" <- 245 | {:split_by_separator, 246 | downcase_encoded_str |> String.reverse() |> String.split(@separator, parts: 2)}, 247 | hrp = hrp |> String.reverse() |> String.to_charlist(), 248 | {_, true} <- {:check_hrp_validity, is_valid_hrp?(hrp)}, 249 | data <- 250 | data 251 | |> String.reverse() 252 | |> String.to_charlist() 253 | |> Enum.map(&Map.get(@data_charset_map, &1)), 254 | {_, :ok} <- {:check_data_validity, check_data_charlist_validity(data)} do 255 | %DecodeResult{ 256 | decode_result 257 | | hrp: hrp, 258 | data: data 259 | } 260 | else 261 | {:split_by_separator, [_]} -> 262 | DecodeResult.add_error(decode_result, :no_separator_character) 263 | 264 | {:split_by_separator, ["", _]} -> 265 | DecodeResult.add_error(decode_result, :empty_data) 266 | 267 | {:split_by_separator, [_, ""]} -> 268 | DecodeResult.add_error(decode_result, :empty_hrp) 269 | 270 | {:check_hrp_validity, false} -> 271 | DecodeResult.add_error(decode_result, :hrp_char_out_opf_range) 272 | 273 | {:check_data_validity, {:error, error}} -> 274 | DecodeResult.add_error(decode_result, error) 275 | end 276 | end 277 | 278 | defp validate_bech32_length( 279 | %DecodeResult{ 280 | encoded_str: encoded_str 281 | } = decode_result, 282 | max_length 283 | ) do 284 | case validate_bech32_length(encoded_str, max_length) do 285 | :ok -> 286 | decode_result 287 | 288 | {:error, error} -> 289 | DecodeResult.add_error(decode_result, error) 290 | end 291 | end 292 | 293 | defp validate_bech32_length(encoded_str, :infinity) when is_binary(encoded_str) do 294 | :ok 295 | end 296 | 297 | defp validate_bech32_length( 298 | encoded_str, 299 | max_length 300 | ) 301 | when is_binary(encoded_str) and byte_size(encoded_str) > max_length do 302 | {:error, :overall_max_length_exceeded} 303 | end 304 | 305 | defp validate_bech32_length( 306 | encoded_str, 307 | _max_length 308 | ) 309 | when is_binary(encoded_str) do 310 | :ok 311 | end 312 | 313 | defp validate_bech32_case( 314 | %DecodeResult{ 315 | encoded_str: encoded_str 316 | } = decode_result 317 | ) do 318 | case String.upcase(encoded_str) == encoded_str or String.downcase(encoded_str) == encoded_str do 319 | true -> 320 | decode_result 321 | 322 | false -> 323 | DecodeResult.add_error(decode_result, :mixed_case) 324 | end 325 | end 326 | 327 | defp check_data_charlist_validity(charlist) do 328 | if length(charlist) >= 6 do 329 | if Enum.all?(charlist, &(!is_nil(&1))) do 330 | :ok 331 | else 332 | {:error, :contain_invalid_data_char} 333 | end 334 | else 335 | {:error, :too_short_checksum} 336 | end 337 | end 338 | 339 | defp is_valid_hrp?(hrp) when is_list(hrp), do: Enum.all?(hrp, &is_valid_hrp_char?/1) 340 | 341 | defp is_valid_hrp_char?(char) do 342 | char <= @hrp_char_code_point_upper_limit and char >= @hrp_char_code_point_lower_limit 343 | end 344 | end 345 | -------------------------------------------------------------------------------- /lib/bitcoinex.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex do 2 | @moduledoc """ 3 | Documentation for Bitcoinex. 4 | 5 | Bitcoinex is an Elixir library supporting basic Bitcoin functionality. 6 | """ 7 | end 8 | -------------------------------------------------------------------------------- /lib/lightning_network/hop_hint.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.LightningNetwork.HopHint do 2 | @moduledoc """ 3 | A hop hint is used to help the payer route a payment to the receiver. 4 | 5 | A hint is included in BOLT#11 Invoices. 6 | """ 7 | 8 | @enforce_keys [ 9 | :node_id, 10 | :channel_id, 11 | :fee_base_m_sat, 12 | :fee_proportional_millionths, 13 | :cltv_expiry_delta 14 | ] 15 | defstruct [ 16 | :node_id, 17 | :channel_id, 18 | :fee_base_m_sat, 19 | :fee_proportional_millionths, 20 | :cltv_expiry_delta 21 | ] 22 | 23 | @type t() :: %__MODULE__{ 24 | node_id: String.t(), 25 | channel_id: non_neg_integer, 26 | fee_base_m_sat: non_neg_integer, 27 | fee_proportional_millionths: non_neg_integer, 28 | cltv_expiry_delta: non_neg_integer 29 | } 30 | end 31 | -------------------------------------------------------------------------------- /lib/lightning_network/lightning_network.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.LightningNetwork do 2 | @moduledoc """ 3 | Includes serialization and validation for Lightning Network BOLT#11 invoices. 4 | """ 5 | 6 | alias Bitcoinex.LightningNetwork.Invoice 7 | 8 | # defdelegate encode_invoice(invoice), to: Invoice, as: :encode 9 | defdelegate decode_invoice(invoice), to: Invoice, as: :decode 10 | end 11 | -------------------------------------------------------------------------------- /lib/network.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Network do 2 | @moduledoc """ 3 | Includes network-specific paramater options. 4 | 5 | Supported networks include mainnet, testnet3, and regtest. 6 | """ 7 | 8 | @enforce_keys [ 9 | :name, 10 | :hrp_segwit_prefix, 11 | :p2pkh_version_decimal_prefix, 12 | :p2sh_version_decimal_prefix 13 | ] 14 | defstruct [ 15 | :name, 16 | :hrp_segwit_prefix, 17 | :p2pkh_version_decimal_prefix, 18 | :p2sh_version_decimal_prefix 19 | ] 20 | 21 | @type t() :: %__MODULE__{ 22 | name: atom, 23 | hrp_segwit_prefix: String.t(), 24 | p2pkh_version_decimal_prefix: integer(), 25 | p2sh_version_decimal_prefix: integer() 26 | } 27 | 28 | @type network_name :: :mainnet | :testnet | :regtest 29 | 30 | @doc """ 31 | Returns a list of supported networks. 32 | """ 33 | def supported_networks() do 34 | [ 35 | mainnet(), 36 | testnet(), 37 | regtest() 38 | ] 39 | end 40 | 41 | def mainnet do 42 | %__MODULE__{ 43 | name: :mainnet, 44 | hrp_segwit_prefix: "bc", 45 | p2pkh_version_decimal_prefix: 0, 46 | p2sh_version_decimal_prefix: 5 47 | } 48 | end 49 | 50 | def testnet do 51 | %__MODULE__{ 52 | name: :testnet, 53 | hrp_segwit_prefix: "tb", 54 | p2pkh_version_decimal_prefix: 111, 55 | p2sh_version_decimal_prefix: 196 56 | } 57 | end 58 | 59 | def regtest do 60 | %__MODULE__{ 61 | name: :regtest, 62 | hrp_segwit_prefix: "bcrt", 63 | p2pkh_version_decimal_prefix: 111, 64 | p2sh_version_decimal_prefix: 196 65 | } 66 | end 67 | 68 | @spec get_network(network_name) :: t() 69 | def get_network(:mainnet) do 70 | mainnet() 71 | end 72 | 73 | def get_network(:testnet) do 74 | testnet() 75 | end 76 | 77 | def get_network(:regtest) do 78 | regtest() 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/opcode.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Opcode do 2 | @moduledoc """ 3 | a module for storing the list of opcodes by number and atom 4 | and defining the functions associated with each opcode 5 | """ 6 | 7 | def opcode_nums, 8 | do: %{ 9 | 0x00 => :op_0, 10 | 0x4C => :op_pushdata1, 11 | 0x4D => :op_pushdata2, 12 | 0x4E => :op_pushdata4, 13 | 0x4F => :op_1negate, 14 | 0x50 => :op_reserved, 15 | 0x51 => :op_1, 16 | 0x52 => :op_2, 17 | 0x53 => :op_3, 18 | 0x54 => :op_4, 19 | 0x55 => :op_5, 20 | 0x56 => :op_6, 21 | 0x57 => :op_7, 22 | 0x58 => :op_8, 23 | 0x59 => :op_9, 24 | 0x5A => :op_10, 25 | 0x5B => :op_11, 26 | 0x5C => :op_12, 27 | 0x5D => :op_13, 28 | 0x5E => :op_14, 29 | 0x5F => :op_15, 30 | 0x60 => :op_16, 31 | 0x61 => :op_nop, 32 | 0x62 => :op_ver, 33 | 0x63 => :op_if, 34 | 0x64 => :op_notif, 35 | 0x65 => :op_verif, 36 | 0x66 => :op_vernotif, 37 | 0x67 => :op_else, 38 | 0x68 => :op_endif, 39 | 0x69 => :op_verify, 40 | 0x6A => :op_return, 41 | 0x6B => :op_toaltstack, 42 | 0x6C => :op_fromaltstack, 43 | 0x6D => :op_2drop, 44 | 0x6E => :op_2dup, 45 | 0x6F => :op_3dup, 46 | 0x70 => :op_2over, 47 | 0x71 => :op_2rot, 48 | 0x72 => :op_2swap, 49 | 0x73 => :op_ifdup, 50 | 0x74 => :op_depth, 51 | 0x75 => :op_drop, 52 | 0x76 => :op_dup, 53 | 0x77 => :op_nip, 54 | 0x78 => :op_over, 55 | 0x79 => :op_pick, 56 | 0x7A => :op_roll, 57 | 0x7B => :op_rot, 58 | 0x7C => :op_swap, 59 | 0x7D => :op_tuck, 60 | 0x7E => :op_cat, 61 | 0x7F => :op_substr, 62 | 0x80 => :op_left, 63 | 0x81 => :op_right, 64 | 0x82 => :op_size, 65 | 0x83 => :op_invert, 66 | 0x84 => :op_and, 67 | 0x85 => :op_or, 68 | 0x86 => :op_xor, 69 | 0x87 => :op_equal, 70 | 0x88 => :op_equalverify, 71 | 0x89 => :op_reserved1, 72 | 0x8A => :op_reserved2, 73 | 0x8B => :op_1add, 74 | 0x8C => :op_1sub, 75 | 0x8D => :op_2mul, 76 | 0x8E => :op_2div, 77 | 0x8F => :op_negate, 78 | 0x90 => :op_abs, 79 | 0x91 => :op_not, 80 | 0x92 => :op_0notequal, 81 | 0x93 => :op_add, 82 | 0x94 => :op_sub, 83 | 0x95 => :op_mul, 84 | 0x96 => :op_div, 85 | 0x97 => :op_mod, 86 | 0x98 => :op_lshift, 87 | 0x99 => :op_rshift, 88 | 0x9A => :op_booland, 89 | 0x9B => :op_boolor, 90 | 0x9C => :op_numequal, 91 | 0x9D => :op_numequalverify, 92 | 0x9E => :op_numnotequal, 93 | 0x9F => :op_lessthan, 94 | 0xA0 => :op_greaterthan, 95 | 0xA1 => :op_lessthanorequal, 96 | 0xA2 => :op_greaterthanorequal, 97 | 0xA3 => :op_min, 98 | 0xA4 => :op_max, 99 | 0xA5 => :op_within, 100 | 0xA6 => :op_ripemd160, 101 | 0xA7 => :op_sha1, 102 | 0xA8 => :op_sha256, 103 | 0xA9 => :op_hash160, 104 | 0xAA => :op_hash256, 105 | 0xAB => :op_codeseparator, 106 | 0xAC => :op_checksig, 107 | 0xAD => :op_checksigverify, 108 | 0xAE => :op_checkmultisig, 109 | 0xAF => :op_checkmultisigverify, 110 | 0xB0 => :op_nop1, 111 | 0xB1 => :op_checklocktimeverify, 112 | 0xB2 => :op_nop3, 113 | 0xB3 => :op_nop4, 114 | 0xB4 => :op_nop5, 115 | 0xB5 => :op_nop6, 116 | 0xB6 => :op_nop7, 117 | 0xB7 => :op_nop8, 118 | 0xB8 => :op_nop9, 119 | 0xB9 => :op_nop10, 120 | 0xFA => :op_smallinteger, 121 | 0xFB => :op_pubkeys, 122 | 0xFD => :op_pubkeyhash, 123 | 0xFE => :op_pubkey, 124 | 0xFF => :op_invalidopcode 125 | } 126 | 127 | def opcode_atoms, 128 | do: %{ 129 | op_0: 0x00, 130 | op_false: 0x00, 131 | op_pushdata1: 0x4C, 132 | op_pushdata2: 0x4D, 133 | op_pushdata4: 0x4E, 134 | op_1negate: 0x4F, 135 | op_reserved: 0x50, 136 | op_1: 0x51, 137 | op_true: 0x51, 138 | op_2: 0x52, 139 | op_3: 0x53, 140 | op_4: 0x54, 141 | op_5: 0x55, 142 | op_6: 0x56, 143 | op_7: 0x57, 144 | op_8: 0x58, 145 | op_9: 0x59, 146 | op_10: 0x5A, 147 | op_11: 0x5B, 148 | op_12: 0x5C, 149 | op_13: 0x5D, 150 | op_14: 0x5E, 151 | op_15: 0x5F, 152 | op_16: 0x60, 153 | op_nop: 0x61, 154 | op_ver: 0x62, 155 | op_if: 0x63, 156 | op_notif: 0x64, 157 | op_verif: 0x65, 158 | op_vernotif: 0x66, 159 | op_else: 0x67, 160 | op_endif: 0x68, 161 | op_verify: 0x69, 162 | op_return: 0x6A, 163 | op_toaltstack: 0x6B, 164 | op_fromaltstack: 0x6C, 165 | op_2drop: 0x6D, 166 | op_2dup: 0x6E, 167 | op_3dup: 0x6F, 168 | op_2over: 0x70, 169 | op_2rot: 0x71, 170 | op_2swap: 0x72, 171 | op_ifdup: 0x73, 172 | op_depth: 0x74, 173 | op_drop: 0x75, 174 | op_dup: 0x76, 175 | op_nip: 0x77, 176 | op_over: 0x78, 177 | op_pick: 0x79, 178 | op_roll: 0x7A, 179 | op_rot: 0x7B, 180 | op_swap: 0x7C, 181 | op_tuck: 0x7D, 182 | op_cat: 0x7E, 183 | op_substr: 0x7F, 184 | op_left: 0x80, 185 | op_right: 0x81, 186 | op_size: 0x82, 187 | op_invert: 0x83, 188 | op_and: 0x84, 189 | op_or: 0x85, 190 | op_xor: 0x86, 191 | op_equal: 0x87, 192 | op_equalverify: 0x88, 193 | op_reserved1: 0x89, 194 | op_reserved2: 0x8A, 195 | op_1add: 0x8B, 196 | op_1sub: 0x8C, 197 | op_2mul: 0x8D, 198 | op_2div: 0x8E, 199 | op_negate: 0x8F, 200 | op_abs: 0x90, 201 | op_not: 0x91, 202 | op_0notequal: 0x92, 203 | op_add: 0x93, 204 | op_sub: 0x94, 205 | op_mul: 0x95, 206 | op_div: 0x96, 207 | op_mod: 0x97, 208 | op_lshift: 0x98, 209 | op_rshift: 0x99, 210 | op_booland: 0x9A, 211 | op_boolor: 0x9B, 212 | op_numequal: 0x9C, 213 | op_numequalverify: 0x9D, 214 | op_numnotequal: 0x9E, 215 | op_lessthan: 0x9F, 216 | op_greaterthan: 0xA0, 217 | op_lessthanorequal: 0xA1, 218 | op_greaterthanorequal: 0xA2, 219 | op_min: 0xA3, 220 | op_max: 0xA4, 221 | op_within: 0xA5, 222 | op_ripemd160: 0xA6, 223 | op_sha1: 0xA7, 224 | op_sha256: 0xA8, 225 | op_hash160: 0xA9, 226 | op_hash256: 0xAA, 227 | op_codeseparator: 0xAB, 228 | op_checksig: 0xAC, 229 | op_checksigverify: 0xAD, 230 | op_checkmultisig: 0xAE, 231 | op_checkmultisigverify: 0xAF, 232 | op_nop1: 0xB0, 233 | op_nop2: 0xB1, 234 | op_checklocktimeverify: 0xB1, 235 | op_nop3: 0xB2, 236 | op_nop4: 0xB3, 237 | op_nop5: 0xB4, 238 | op_nop6: 0xB5, 239 | op_nop7: 0xB6, 240 | op_nop8: 0xB7, 241 | op_nop9: 0xB8, 242 | op_nop10: 0xB9, 243 | op_smallinteger: 0xFA, 244 | op_pubkeys: 0xFB, 245 | op_pubkeyhash: 0xFD, 246 | op_pubkey: 0xFE, 247 | op_invalidopcode: 0xFF 248 | } 249 | end 250 | -------------------------------------------------------------------------------- /lib/secp256k1/ecdsa.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.Ecdsa do 2 | alias Bitcoinex.Secp256k1 3 | alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey, Signature} 4 | 5 | @n Params.curve().n 6 | 7 | @generator_point %Point{ 8 | x: Params.curve().g_x, 9 | y: Params.curve().g_y 10 | } 11 | 12 | @doc """ 13 | deterministic_k implements rfc6979 and returns 14 | a deterministic k value for signatures 15 | """ 16 | @spec deterministic_k(PrivateKey.t(), non_neg_integer()) :: PrivateKey.t() 17 | def deterministic_k(%PrivateKey{d: d}, raw_z) do 18 | # RFC 6979 https://tools.ietf.org/html/rfc6979#section-3.2 19 | k = :binary.list_to_bin(List.duplicate(<<0x00>>, 32)) 20 | v = :binary.list_to_bin(List.duplicate(<<0x01>>, 32)) 21 | n = @n 22 | z = lower_z(raw_z, n) 23 | # 3.2(d) - pad z and privkey for use 24 | sighash = Bitcoinex.Utils.pad(:binary.encode_unsigned(z), 32, :leading) 25 | secret = Bitcoinex.Utils.pad(:binary.encode_unsigned(d), 32, :leading) 26 | # 3.2(d) - hmac with key k 27 | k = :crypto.mac(:hmac, :sha256, k, v <> <<0x00>> <> secret <> sighash) 28 | # 3.2(e) - update v 29 | v = :crypto.mac(:hmac, :sha256, k, v) 30 | # 3.2(f) - update k 31 | k = :crypto.mac(:hmac, :sha256, k, v <> <<0x01>> <> secret <> sighash) 32 | # 3.2(g) - update v 33 | v = :crypto.mac(:hmac, :sha256, k, v) 34 | # 3.2(h) - algorithm 35 | final_k = find_candidate(k, v, n) 36 | %PrivateKey{d: final_k} 37 | end 38 | 39 | defp find_candidate(k, v, n) do 40 | # RFC 6979 https://tools.ietf.org/html/rfc6979#section-3.2 41 | v = :crypto.mac(:hmac, :sha256, k, v) 42 | candidate = :binary.decode_unsigned(v) 43 | # 3.2(h).3 - check candidate in [1,n-1] and r != 0 44 | if candidate >= 1 and candidate < n do 45 | case PrivateKey.to_point(%PrivateKey{d: candidate}) do 46 | {:error, _msg} -> 47 | find_next_candidate(k, v, n) 48 | 49 | pk -> 50 | if pk.x != 0 do 51 | candidate 52 | else 53 | find_next_candidate(k, v, n) 54 | end 55 | end 56 | else 57 | # if candidate is invalid 58 | find_next_candidate(k, v, n) 59 | end 60 | end 61 | 62 | defp find_next_candidate(k, v, n) do 63 | k = :crypto.mac(:hmac, :sha256, k, v <> <<0x00>>) 64 | v = :crypto.mac(:hmac, :sha256, k, v) 65 | find_candidate(k, v, n) 66 | end 67 | 68 | defp lower_z(z, n) do 69 | if z > n, do: z - n, else: z 70 | end 71 | 72 | @doc """ 73 | sign returns an ECDSA signature using the privkey and z 74 | where privkey is a PrivateKey object and z is an integer. 75 | The nonce is derived using RFC6979. 76 | """ 77 | @spec sign(PrivateKey.t(), integer) :: Signature.t() 78 | def sign(privkey, z) do 79 | case PrivateKey.validate(privkey) do 80 | {:error, msg} -> 81 | {:error, msg} 82 | 83 | {:ok, privkey} -> 84 | k = deterministic_k(privkey, z) 85 | 86 | case PrivateKey.to_point(k) do 87 | {:error, msg} -> 88 | {:error, msg} 89 | 90 | pk -> 91 | sig_r = pk.x 92 | inv_k = Math.inv(k.d, @n) 93 | sig_s = Math.modulo((z + sig_r * privkey.d) * inv_k, @n) 94 | 95 | if sig_s > @n / 2 do 96 | %Signature{r: sig_r, s: @n - sig_s} 97 | else 98 | %Signature{r: sig_r, s: sig_s} 99 | end 100 | end 101 | end 102 | end 103 | 104 | @doc """ 105 | sign_message returns an ECDSA signature using the privkey and "Bitcoin Signed Message: " 106 | where privkey is a PrivateKey object and msg is a binary message to be hashed. 107 | The message is hashed using hash256 (double SHA256) and the nonce is derived 108 | using RFC6979. 109 | """ 110 | @spec sign_message(PrivateKey.t(), binary) :: Signature.t() 111 | def sign_message(privkey, msg) do 112 | z = 113 | ("Bitcoin Signed Message:\n" <> msg) 114 | |> Bitcoinex.Utils.double_sha256() 115 | |> :binary.decode_unsigned() 116 | 117 | sign(privkey, z) 118 | end 119 | 120 | @doc """ 121 | verify whether the ecdsa signature is valid 122 | for the given message hash and public key 123 | """ 124 | @spec verify_signature(Point.t(), integer, Signature.t()) :: boolean 125 | def verify_signature(pubkey, sighash, %Signature{r: r, s: s}) do 126 | s_inv = Math.inv(s, @n) 127 | u = Math.modulo(sighash * s_inv, @n) 128 | v = Math.modulo(r * s_inv, @n) 129 | total = Math.add(Math.multiply(@generator_point, u), Math.multiply(pubkey, v)) 130 | total.x == r 131 | end 132 | 133 | @doc """ 134 | ecdsa_recover_compact does ECDSA public key recovery. 135 | """ 136 | @spec ecdsa_recover_compact(binary, binary, integer) :: 137 | {:ok, binary} | {:error, String.t()} 138 | def ecdsa_recover_compact(msg, compact_sig, recoveryId) do 139 | # Parse r and s from the signature. 140 | case Signature.parse_signature(compact_sig) do 141 | {:ok, sig} -> 142 | # Find the iteration. 143 | 144 | # R(x) = (n * i) + r 145 | # where n is the order of the curve and R is from the signature. 146 | r_x = @n * Integer.floor_div(recoveryId, 2) + sig.r 147 | 148 | # Check that R(x) is on the curve. 149 | if r_x > Params.curve().p do 150 | {:error, "R(x) is not on the curve"} 151 | else 152 | # Decompress to get R(y). 153 | case Secp256k1.get_y(r_x, rem(recoveryId, 2) == 1) do 154 | {:ok, r_y} -> 155 | # R(x,y) 156 | point_r = %Point{x: r_x, y: r_y} 157 | 158 | # Point Q is the recovered public key. 159 | # We satisfy this equation: Q = r^-1(sR-eG) 160 | inv_r = Math.inv(sig.r, @n) 161 | inv_r_s = (inv_r * sig.s) |> Math.modulo(@n) 162 | 163 | # R*s 164 | point_sr = Math.multiply(point_r, inv_r_s) 165 | 166 | # Find e using the message hash. 167 | e = 168 | :binary.decode_unsigned(msg) 169 | |> Kernel.*(-1) 170 | |> Math.modulo(@n) 171 | |> Kernel.*(inv_r |> Math.modulo(@n)) 172 | 173 | # G*e 174 | point_ge = Math.multiply(@generator_point, e) 175 | 176 | # R*e * G*e 177 | point_q = Math.add(point_sr, point_ge) 178 | 179 | # Returns serialized compressed public key. 180 | {:ok, Point.serialize_public_key(point_q)} 181 | 182 | {:error, error} -> 183 | {:error, error} 184 | end 185 | end 186 | 187 | {:error, e} -> 188 | {:error, e} 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/secp256k1/math.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.Math do 2 | @moduledoc """ 3 | Contains math utilities when dealing with secp256k1 curve points and scalars. 4 | 5 | All of the addition and multiplication uses the secp256k1 curve paramaters. 6 | 7 | Several of the jacobian multiplication and addition functions are borrowed heavily from https://github.com/starkbank/ecdsa-elixir/. 8 | """ 9 | alias Bitcoinex.Secp256k1.{Params, Point} 10 | import Bitcoinex.Secp256k1.Point 11 | 12 | @doc """ 13 | pow performs integer pow, 14 | where x is raised to the power of y. 15 | """ 16 | # Integer.pow/2 was added since 1.12.0. This function_exported? can be removed when we decide 17 | # to only support >= 1.12.0 in the future 18 | if function_exported?(Integer, :pow, 2) do 19 | defdelegate pow(base, exponent), to: Integer 20 | else 21 | # copy from https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/integer.ex#L104 22 | @spec pow(integer, non_neg_integer) :: integer 23 | def pow(base, exponent) when is_integer(base) and is_integer(exponent) and exponent >= 0 do 24 | guarded_pow(base, exponent) 25 | end 26 | 27 | # https://en.wikipedia.org/wiki/Exponentiation_by_squaring 28 | defp guarded_pow(_, 0), do: 1 29 | defp guarded_pow(b, 1), do: b 30 | defp guarded_pow(b, e) when (e &&& 1) == 0, do: guarded_pow(b * b, e >>> 1) 31 | defp guarded_pow(b, e), do: b * guarded_pow(b * b, e >>> 1) 32 | end 33 | 34 | @doc """ 35 | Inv performs the Extended Euclidean Algorithm to find 36 | the inverse of a number x mod n. 37 | """ 38 | @spec inv(integer, pos_integer) :: integer 39 | def inv(x, n) when is_integer(x) and is_integer(n) and n >= 1 do 40 | do_inv(x, n) 41 | end 42 | 43 | defp do_inv(x, _n) when x == 0, do: 0 44 | defp do_inv(x, n), do: do_inv(1, 0, modulo(x, n), n) |> modulo(n) 45 | 46 | defp do_inv(lm, hm, low, high) when low > 1 do 47 | r = div(high, low) 48 | 49 | do_inv( 50 | hm - lm * r, 51 | lm, 52 | high - low * r, 53 | low 54 | ) 55 | end 56 | 57 | defp do_inv(lm, _hm, _low, _high) do 58 | lm 59 | end 60 | 61 | @spec modulo(integer, integer) :: integer 62 | def modulo(x, n) when is_integer(x) and is_integer(n) do 63 | r = rem(x, n) 64 | if r < 0, do: r + n, else: r 65 | end 66 | 67 | @doc """ 68 | multiply accepts a point P and scalar n and, 69 | does jacobian multiplication to return resulting point. 70 | """ 71 | def multiply(p, n) when is_point(p) and is_integer(n) do 72 | p 73 | |> toJacobian() 74 | |> jacobianMultiply(n) 75 | |> fromJacobian() 76 | end 77 | 78 | @doc """ 79 | add accepts points p and q and, 80 | does jacobian addition to return resulting point. 81 | """ 82 | def add(p, q) when is_point(p) and is_point(q) do 83 | jacobianAdd(toJacobian(p), toJacobian(q)) 84 | |> fromJacobian() 85 | end 86 | 87 | # Convert our point P to jacobian coordinates. 88 | defp toJacobian(p) do 89 | %Point{x: p.x, y: p.y, z: 1} 90 | end 91 | 92 | # Convert our jacobian coordinates to a point P on secp256k1 curve. 93 | defp fromJacobian(p) do 94 | z = inv(p.z, Params.curve().p) 95 | 96 | %Point{ 97 | x: 98 | modulo( 99 | p.x * pow(z, 2), 100 | Params.curve().p 101 | ), 102 | y: 103 | modulo( 104 | p.y * pow(z, 3), 105 | Params.curve().p 106 | ) 107 | } 108 | end 109 | 110 | # double Point P to get point P + P 111 | # We use the dbl-1998-cmo-2 doubling formula. 112 | # For reference, http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html. 113 | defp jacobianDouble(p) do 114 | if p.y == 0 do 115 | %Point{x: 0, y: 0, z: 0} 116 | else 117 | # XX = X1^2 118 | xsq = 119 | pow(p.x, 2) 120 | |> modulo(Params.curve().p) 121 | 122 | # YY = Y1^2 123 | ysq = 124 | pow(p.y, 2) 125 | |> modulo(Params.curve().p) 126 | 127 | # S = 4 * X1 * YY 128 | s = 129 | (4 * p.x * ysq) 130 | |> modulo(Params.curve().p) 131 | 132 | # M = 3 * XX + a * Z1^4 133 | m = 134 | (3 * xsq + Params.curve().a * pow(p.z, 4)) 135 | |> modulo(Params.curve().p) 136 | 137 | # T = M^2 - 2 * S 138 | t = 139 | (pow(m, 2) - 2 * s) 140 | |> modulo(Params.curve().p) 141 | 142 | # X3 = T 143 | nx = t 144 | 145 | # Y3 = M * (S - T) - 8 * YY^2 146 | ny = 147 | (m * (s - t) - 8 * pow(ysq, 2)) 148 | |> modulo(Params.curve().p) 149 | 150 | # Z3 = 2 * Y1 * Z1 151 | nz = 152 | (2 * p.y * p.z) 153 | |> modulo(Params.curve().p) 154 | 155 | %Point{x: nx, y: ny, z: nz} 156 | end 157 | end 158 | 159 | # add points P and Q to get P + Q 160 | # We use the add-1998-cmo-2 addition formula. 161 | # For reference, http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html. 162 | defp jacobianAdd(p, q) do 163 | if p.y == 0 do 164 | q 165 | else 166 | if q.y == 0 do 167 | p 168 | else 169 | # U1 = X1 * Z2^2 170 | u1 = 171 | (p.x * pow(q.z, 2)) 172 | |> modulo(Params.curve().p) 173 | 174 | # U2 = X2 * Z2^2 175 | u2 = 176 | (q.x * pow(p.z, 2)) 177 | |> modulo(Params.curve().p) 178 | 179 | # S1 = Y1 * Z2^3 180 | s1 = 181 | (p.y * pow(q.z, 3)) 182 | |> modulo(Params.curve().p) 183 | 184 | # S2 = y2 * Z1^3 185 | s2 = 186 | (q.y * pow(p.z, 3)) 187 | |> modulo(Params.curve().p) 188 | 189 | if u1 == u2 do 190 | if s1 != s2 do 191 | %Point{x: 0, y: 0, z: 1} 192 | else 193 | jacobianDouble(p) 194 | end 195 | else 196 | # H = U2 - U1 197 | h = u2 - u1 198 | 199 | # r = S2 - S1 200 | r = s2 - s1 201 | 202 | # HH = H^2 203 | h2 = 204 | (h * h) 205 | |> modulo(Params.curve().p) 206 | 207 | # HHH = H * HH 208 | h3 = 209 | (h * h2) 210 | |> modulo(Params.curve().p) 211 | 212 | # V = U1 * HH 213 | v = 214 | (u1 * h2) 215 | |> modulo(Params.curve().p) 216 | 217 | # X3 = 42 - HHH - 2 * V 218 | nx = 219 | (pow(r, 2) - h3 - 2 * v) 220 | |> modulo(Params.curve().p) 221 | 222 | # Y3 = r * (V - X3) - S1 * HHH 223 | ny = 224 | (r * (v - nx) - s1 * h3) 225 | |> modulo(Params.curve().p) 226 | 227 | # Z3 = Z1 * Z2 * H 228 | nz = 229 | (h * p.z * q.z) 230 | |> modulo(Params.curve().p) 231 | 232 | %Point{x: nx, y: ny, z: nz} 233 | end 234 | end 235 | end 236 | end 237 | 238 | # multply point P with scalar n 239 | defp jacobianMultiply(_p, n) when n == 0 do 240 | %Point{x: 0, y: 0, z: 1} 241 | end 242 | 243 | defp jacobianMultiply(p, n) when n == 1 do 244 | if p.y == 0 do 245 | %Point{x: 0, y: 0, z: 1} 246 | else 247 | p 248 | end 249 | end 250 | 251 | defp jacobianMultiply(p, n) 252 | # This integer is n, the integer order of G for secp256k1. 253 | # Unfortunately cannot call Params.curve.n to get the curve order integer, 254 | # so instead, it is pasted it here. 255 | # In the future we should move it back to Params. 256 | when n < 0 or 257 | n > 258 | 115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337 do 259 | if p.y == 0 do 260 | %Point{x: 0, y: 0, z: 1} 261 | else 262 | jacobianMultiply(p, modulo(n, Params.curve().n)) 263 | end 264 | end 265 | 266 | defp jacobianMultiply(p, n) when rem(n, 2) == 0 do 267 | if p.y == 0 do 268 | %Point{x: 0, y: 0, z: 1} 269 | else 270 | jacobianMultiply(p, div(n, 2)) 271 | |> jacobianDouble() 272 | end 273 | end 274 | 275 | defp jacobianMultiply(p, n) do 276 | if p.y == 0 do 277 | %Point{x: 0, y: 0, z: 1} 278 | else 279 | jacobianMultiply(p, div(n, 2)) 280 | |> jacobianDouble() 281 | |> jacobianAdd(p) 282 | end 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /lib/secp256k1/params.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.Params do 2 | @doc """ 3 | Secp256k1 parameters. 4 | http://www.secg.org/sec2-v2.pdf 5 | """ 6 | @spec curve :: %{ 7 | p: non_neg_integer(), 8 | a: non_neg_integer(), 9 | b: non_neg_integer(), 10 | g_x: non_neg_integer(), 11 | g_y: non_neg_integer(), 12 | n: non_neg_integer(), 13 | h: non_neg_integer() 14 | } 15 | def curve do 16 | %{ 17 | p: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_FFFFFC2F, 18 | a: 0x00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000, 19 | b: 0x00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000007, 20 | g_x: 0x79BE667E_F9DCBBAC_55A06295_CE870B07_029BFCDB_2DCE28D9_59F2815B_16F81798, 21 | g_y: 0x483ADA77_26A3C465_5DA4FBFC_0E1108A8_FD17B448_A6855419_9C47D08F_FB10D4B8, 22 | n: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141, 23 | h: 0x01 24 | } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/secp256k1/point.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.Point do 2 | @moduledoc """ 3 | Contains the x, y, and z of an elliptic curve point. 4 | """ 5 | 6 | import Bitwise 7 | alias Bitcoinex.Utils 8 | alias Bitcoinex.Secp256k1 9 | alias Bitcoinex.Secp256k1.Params 10 | 11 | @p Params.curve().p 12 | 13 | @type t :: %__MODULE__{ 14 | x: integer(), 15 | y: integer(), 16 | z: integer() 17 | } 18 | 19 | @enforce_keys [ 20 | :x, 21 | :y 22 | ] 23 | defstruct [:x, :y, z: 0] 24 | 25 | defguard is_point(term) 26 | when is_map(term) and :erlang.map_get(:__struct__, term) == __MODULE__ and 27 | :erlang.is_map_key(:x, term) and :erlang.is_map_key(:y, term) and 28 | :erlang.is_map_key(:z, term) 29 | 30 | @doc """ 31 | is_inf returns whether or not point P is 32 | the point at infinity, ie. P.x == P.y == 0 33 | """ 34 | @spec is_inf(t()) :: boolean 35 | def is_inf(%__MODULE__{x: 0, y: 0}), do: true 36 | def is_inf(_), do: false 37 | 38 | @doc """ 39 | parse_public_key parses a public key 40 | """ 41 | @spec parse_public_key(binary) :: {:ok, t()} | {:error, String.t()} 42 | def parse_public_key(<<0x04, x::binary-size(32), y::binary-size(32)>>) do 43 | {:ok, %__MODULE__{x: :binary.decode_unsigned(x), y: :binary.decode_unsigned(y)}} 44 | end 45 | 46 | # Above matches with uncompressed keys. Below matches with compressed keys 47 | def parse_public_key(<>) do 48 | x = :binary.decode_unsigned(x_bytes) 49 | 50 | case :binary.decode_unsigned(prefix) do 51 | 2 -> 52 | case Bitcoinex.Secp256k1.get_y(x, false) do 53 | {:ok, y} -> {:ok, %__MODULE__{x: x, y: y}} 54 | _ -> {:error, "invalid public key"} 55 | end 56 | 57 | 3 -> 58 | case Bitcoinex.Secp256k1.get_y(x, true) do 59 | {:ok, y} -> {:ok, %__MODULE__{x: x, y: y}} 60 | _ -> {:error, "invalid public key"} 61 | end 62 | end 63 | end 64 | 65 | # Allow parse_public_key to parse SEC strings 66 | def parse_public_key(key) do 67 | key 68 | |> String.downcase() 69 | |> Base.decode16!(case: :lower) 70 | |> parse_public_key() 71 | end 72 | 73 | @doc """ 74 | lift_x returns the Point P where P.x = x 75 | and P.y is even. 76 | """ 77 | @spec lift_x(integer | binary) :: {:ok, t()} | {:error, String.t()} 78 | def lift_x(x) when is_integer(x) and x >= @p, do: {:error, "invalid x value (too large)"} 79 | 80 | def lift_x(x) when is_integer(x) do 81 | case Secp256k1.get_y(x, false) do 82 | {:ok, y} -> 83 | {:ok, %__MODULE__{x: x, y: y}} 84 | 85 | err -> 86 | err 87 | end 88 | end 89 | 90 | # parse 32-byte binary 91 | def lift_x(<>) do 92 | x 93 | |> :binary.decode_unsigned() 94 | |> lift_x 95 | end 96 | 97 | # attempt to parse x-only pubkey from hex 98 | def lift_x(x) when is_binary(x) do 99 | case Utils.hex_to_bin(x) do 100 | {:error, msg} -> 101 | {:error, msg} 102 | 103 | x_bytes -> 104 | lift_x(x_bytes) 105 | end 106 | end 107 | 108 | @doc """ 109 | sec serializes a compressed public key to binary 110 | """ 111 | @spec sec(t()) :: binary 112 | def sec(%__MODULE__{x: x, y: y}) do 113 | case rem(y, 2) do 114 | 0 -> 115 | <<0x02>> <> Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading) 116 | 117 | 1 -> 118 | <<0x03>> <> Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading) 119 | end 120 | end 121 | 122 | @doc """ 123 | x_bytes returns the binary encoding of the x value of the point 124 | """ 125 | @spec x_bytes(t()) :: binary 126 | def x_bytes(%__MODULE__{x: x}) do 127 | Bitcoinex.Utils.pad(:binary.encode_unsigned(x), 32, :leading) 128 | end 129 | 130 | @doc """ 131 | x_hex returns the hex-encoded x value of the point 132 | """ 133 | @spec x_hex(t()) :: String.t() 134 | def x_hex(p) do 135 | p 136 | |> x_bytes() 137 | |> Base.encode16(case: :lower) 138 | end 139 | 140 | @doc """ 141 | serialize_public_key serializes a compressed public key to string 142 | """ 143 | @spec serialize_public_key(t()) :: String.t() 144 | def serialize_public_key(pubkey) do 145 | pubkey 146 | |> sec() 147 | |> Base.encode16(case: :lower) 148 | end 149 | 150 | @doc """ 151 | has_even_y returns true if y is 152 | even and false if y is odd 153 | """ 154 | @spec has_even_y(t()) :: boolean 155 | def has_even_y(%__MODULE__{y: y}) do 156 | (y &&& 1) == 0 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/secp256k1/privatekey.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.PrivateKey do 2 | @moduledoc """ 3 | Contains an integer used to create a Point and sign. 4 | """ 5 | alias Bitcoinex.Secp256k1.{Params, Math, Point} 6 | alias Bitcoinex.Base58 7 | alias Bitcoinex.Utils 8 | 9 | @n Params.curve().n 10 | 11 | @max_privkey @n - 1 12 | 13 | @type t :: %__MODULE__{ 14 | d: non_neg_integer() 15 | } 16 | 17 | @enforce_keys [ 18 | :d 19 | ] 20 | defstruct [:d] 21 | 22 | def validate(%__MODULE__{d: d}) do 23 | if d > @max_privkey do 24 | {:error, "invalid private key out of range."} 25 | else 26 | {:ok, %__MODULE__{d: d}} 27 | end 28 | end 29 | 30 | @doc """ 31 | new creates a private key from an integer 32 | """ 33 | @spec new(non_neg_integer()) :: {:ok, t()} | {:error, String.t()} 34 | def new(d) do 35 | validate(%__MODULE__{d: d}) 36 | end 37 | 38 | @doc """ 39 | to_point calculate Point from private key or integer 40 | """ 41 | @spec to_point(t() | non_neg_integer()) :: Point.t() | {:error, String.t()} 42 | def to_point(prvkey = %__MODULE__{}) do 43 | case validate(prvkey) do 44 | {:error, msg} -> 45 | {:error, msg} 46 | 47 | {:ok, %__MODULE__{d: d}} -> 48 | g = %Point{x: Params.curve().g_x, y: Params.curve().g_y, z: 0} 49 | Math.multiply(g, d) 50 | end 51 | end 52 | 53 | def to_point(d) when is_integer(d) do 54 | case new(d) do 55 | {:ok, sk} -> 56 | to_point(sk) 57 | 58 | {:error, msg} -> 59 | {:error, msg} 60 | end 61 | end 62 | 63 | @doc """ 64 | serialize_private_key serializes a private key into hex 65 | """ 66 | @spec serialize_private_key(t()) :: String.t() 67 | def serialize_private_key(prvkey = %__MODULE__{}) do 68 | case validate(prvkey) do 69 | {:error, msg} -> 70 | {:error, msg} 71 | 72 | {:ok, %__MODULE__{d: d}} -> 73 | d 74 | |> :binary.encode_unsigned() 75 | |> Utils.pad(32, :leading) 76 | |> Base.encode16(case: :lower) 77 | end 78 | end 79 | 80 | @doc """ 81 | wif returns the base58check encoded private key as a string 82 | assumes all keys are compressed 83 | """ 84 | @spec wif!(t(), Bitcoinex.Network.network_name()) :: String.t() 85 | def wif!(prvkey = %__MODULE__{}, network_name) do 86 | case validate(prvkey) do 87 | {:error, msg} -> 88 | {:error, msg} 89 | 90 | {:ok, %__MODULE__{d: d}} -> 91 | d 92 | |> :binary.encode_unsigned() 93 | |> Utils.pad(32, :leading) 94 | |> wif_prefix(network_name) 95 | |> compressed_suffix() 96 | |> Base58.encode() 97 | end 98 | end 99 | 100 | def parse_wif!(wif_str) do 101 | {:ok, privkey, _, _} = parse_wif(wif_str) 102 | privkey 103 | end 104 | 105 | @doc """ 106 | returns the base58check encoded private key as a string 107 | assumes all keys are compressed 108 | """ 109 | @spec parse_wif(String.t()) :: {:ok, t(), atom, boolean} 110 | def parse_wif(wif_str) do 111 | {state, bin} = Base58.decode(wif_str) 112 | 113 | case state do 114 | :error -> {:error, bin} 115 | :ok -> parse_wif_bin(bin) 116 | end 117 | end 118 | 119 | # parse compressed 120 | @spec parse_wif_bin(binary) :: {:ok, t(), atom, boolean} 121 | def parse_wif_bin(<>) do 122 | {state, network_name} = wif_prefix(prefix) 123 | 124 | if state == :error do 125 | {:error, network_name} 126 | else 127 | secret = :binary.decode_unsigned(wif) 128 | 129 | case validate(%__MODULE__{d: secret}) do 130 | {:error, msg} -> 131 | {:error, msg} 132 | 133 | {:ok, %__MODULE__{d: d}} -> 134 | {:ok, %__MODULE__{d: d}, network_name, true} 135 | end 136 | end 137 | end 138 | 139 | # parse uncompressed 140 | def parse_wif_bin(<>) do 141 | {state, network_name} = wif_prefix(prefix) 142 | 143 | if state == :error do 144 | {:error, network_name} 145 | else 146 | secret = :binary.decode_unsigned(wif) 147 | 148 | case validate(%__MODULE__{d: secret}) do 149 | {:error, msg} -> 150 | {:error, msg} 151 | 152 | {:ok, %__MODULE__{d: d}} -> 153 | {:ok, %__MODULE__{d: d}, network_name, false} 154 | end 155 | end 156 | end 157 | 158 | defp compressed_suffix(binary), do: binary <> <<0x01>> 159 | # encoding 160 | defp wif_prefix(binary, :mainnet), do: <<0x80>> <> binary 161 | defp wif_prefix(binary, :testnet), do: <<0xEF>> <> binary 162 | defp wif_prefix(binary, :regtest), do: <<0xEF>> <> binary 163 | # decoding 164 | defp wif_prefix(<<0x80>>), do: {:ok, :mainnet} 165 | defp wif_prefix(<<0xEF>>), do: {:ok, :testnet} 166 | defp wif_prefix(_), do: {:error, "unrecognized network prefix for WIF"} 167 | end 168 | -------------------------------------------------------------------------------- /lib/secp256k1/schnorr.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.Schnorr do 2 | @moduledoc """ 3 | Schnorr-specific secp256k1 operations 4 | """ 5 | alias Bitcoinex.Secp256k1 6 | alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey, Signature} 7 | alias Bitcoinex.Utils 8 | 9 | @n Params.curve().n 10 | @p Params.curve().p 11 | 12 | @generator_point %Point{ 13 | x: Params.curve().g_x, 14 | y: Params.curve().g_y 15 | } 16 | 17 | @spec sign(PrivateKey.t(), non_neg_integer(), non_neg_integer()) :: 18 | {:ok, Signature.t()} | {:error, String.t()} 19 | def sign(privkey, z, aux) do 20 | case PrivateKey.validate(privkey) do 21 | {:error, msg} -> 22 | {:error, msg} 23 | 24 | {:ok, privkey} -> 25 | z_bytes = Utils.int_to_big(z, 32) 26 | aux_bytes = Utils.int_to_big(aux, 32) 27 | d_point = PrivateKey.to_point(privkey) 28 | 29 | case Secp256k1.force_even_y(privkey) do 30 | {:error, msg} -> 31 | {:error, msg} 32 | 33 | d -> 34 | d_bytes = Utils.int_to_big(d.d, 32) 35 | tagged_aux_hash = tagged_hash_aux(aux_bytes) 36 | t = Utils.xor_bytes(d_bytes, tagged_aux_hash) 37 | 38 | {:ok, k0} = 39 | tagged_hash_nonce(t <> Point.x_bytes(d_point) <> z_bytes) 40 | |> :binary.decode_unsigned() 41 | |> Math.modulo(@n) 42 | |> PrivateKey.new() 43 | 44 | if k0.d == 0 do 45 | {:error, "invalid aux randomness"} 46 | else 47 | r_point = PrivateKey.to_point(k0) 48 | 49 | case Secp256k1.force_even_y(k0) do 50 | {:error, msg} -> 51 | {:error, msg} 52 | 53 | k -> 54 | e = 55 | tagged_hash_challenge( 56 | Point.x_bytes(r_point) <> Point.x_bytes(d_point) <> z_bytes 57 | ) 58 | |> :binary.decode_unsigned() 59 | |> Math.modulo(@n) 60 | 61 | sig_s = 62 | (k.d + d.d * e) 63 | |> Math.modulo(@n) 64 | 65 | {:ok, %Signature{r: r_point.x, s: sig_s}} 66 | end 67 | end 68 | end 69 | end 70 | end 71 | 72 | defp tagged_hash_aux(aux), do: Utils.tagged_hash("BIP0340/aux", aux) 73 | defp tagged_hash_nonce(nonce), do: Utils.tagged_hash("BIP0340/nonce", nonce) 74 | defp tagged_hash_challenge(chal), do: Utils.tagged_hash("BIP0340/challenge", chal) 75 | 76 | @doc """ 77 | verify whether the schnorr signature is valid for the given message hash and public key 78 | """ 79 | @spec verify_signature(Point.t(), non_neg_integer, Signature.t()) :: 80 | boolean | {:error, String.t()} 81 | def verify_signature(_pubkey, _z, %Signature{r: r, s: s}) 82 | when r >= @p or s >= @n, 83 | do: {:error, "invalid signature"} 84 | 85 | def verify_signature(pubkey, z, %Signature{r: r, s: s}) do 86 | r_bytes = Utils.int_to_big(r, 32) 87 | z_bytes = Utils.int_to_big(z, 32) 88 | 89 | e = 90 | tagged_hash_challenge(r_bytes <> Point.x_bytes(pubkey) <> z_bytes) 91 | |> :binary.decode_unsigned() 92 | |> Math.modulo(@n) 93 | 94 | r_point = 95 | @generator_point 96 | |> Math.multiply(s) 97 | |> Math.add(Math.multiply(pubkey, @n - e)) 98 | 99 | !Point.is_inf(r_point) && Point.has_even_y(r_point) && r_point.x == r 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/secp256k1/secp256k1.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1 do 2 | @moduledoc """ 3 | General Secp256k1 curve operations. 4 | libsecp256k1: https://github.com/bitcoin-core/secp256k1 5 | 6 | Currently supports ECDSA public key recovery. 7 | 8 | In the future, we will NIF for critical operations. However, it is more portable to have a native elixir version. 9 | """ 10 | import Bitwise 11 | alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey} 12 | 13 | defmodule Signature do 14 | @moduledoc """ 15 | Contains r,s in signature. 16 | """ 17 | alias Bitcoinex.Utils 18 | 19 | @type t :: %__MODULE__{ 20 | r: pos_integer(), 21 | s: pos_integer() 22 | } 23 | 24 | @enforce_keys [ 25 | :r, 26 | :s 27 | ] 28 | defstruct [:r, :s] 29 | 30 | @spec parse_signature(binary) :: 31 | {:ok, t()} | {:error, String.t()} 32 | @doc """ 33 | accepts a compact signature and returns a Signature containing r,s 34 | """ 35 | def parse_signature(<>) do 36 | # Get r,s from signature. 37 | r = :binary.decode_unsigned(r) 38 | s = :binary.decode_unsigned(s) 39 | 40 | # Verify that r,s are integers in [1, n-1] where n is the integer order of G. 41 | cond do 42 | r < 1 -> 43 | {:error, "invalid signature"} 44 | 45 | r > Params.curve().n - 1 -> 46 | {:error, "invalid signature"} 47 | 48 | s < 1 -> 49 | {:error, "invalid signature"} 50 | 51 | s > Params.curve().n - 1 -> 52 | {:error, "invalid signature"} 53 | 54 | true -> 55 | {:ok, %Signature{r: r, s: s}} 56 | end 57 | end 58 | 59 | # attempt to parse 64-byte string 60 | def parse_signature(compact_sig) when is_binary(compact_sig) do 61 | case Utils.hex_to_bin(compact_sig) do 62 | {:error, msg} -> 63 | {:error, msg} 64 | 65 | sig_bytes -> 66 | parse_signature(sig_bytes) 67 | end 68 | end 69 | 70 | def parse_signature(_), do: {:error, "invalid signature size"} 71 | 72 | @doc """ 73 | der_parse_signature parses a DER binary to a Signature 74 | """ 75 | # @spec der_parse_signature(binary) :: {:ok, Signature.()} | {:error, String.t()} 76 | def der_parse_signature(<<0x30>> <> der_sig) when is_binary(der_sig) do 77 | sig_len = :binary.at(der_sig, 0) 78 | 79 | if sig_len + 1 != byte_size(der_sig) do 80 | {:error, "invalid signature length"} 81 | else 82 | case parse_sig_key(der_sig, 1) do 83 | {:error, err} -> 84 | {:error, err} 85 | 86 | {r, s_pos} -> 87 | case parse_sig_key(der_sig, s_pos) do 88 | {:error, err} -> 89 | {:error, err} 90 | 91 | {s, sig_len} -> 92 | if sig_len != byte_size(der_sig) do 93 | {:error, "invalid signature: signature is too long"} 94 | else 95 | {:ok, %Signature{r: r, s: s}} 96 | end 97 | end 98 | end 99 | end 100 | end 101 | 102 | def der_parse_signature(_), do: {:error, "invalid signature"} 103 | 104 | defp parse_sig_key(data, pos) do 105 | if :binary.at(data, pos) != 0x02 do 106 | {:error, "invalid signature key marker"} 107 | else 108 | k_len = :binary.at(data, pos + 1) 109 | len_k = :binary.part(data, pos + 2, k_len) 110 | {:binary.decode_unsigned(len_k), pos + 2 + k_len} 111 | end 112 | end 113 | 114 | @spec serialize_signature(t()) :: binary 115 | def serialize_signature(%__MODULE__{r: r, s: s}) do 116 | :binary.encode_unsigned(r) <> :binary.encode_unsigned(s) 117 | end 118 | 119 | @doc """ 120 | der_serialize_signature returns the DER serialization of an ecdsa signature 121 | """ 122 | @spec der_serialize_signature(Signature.t()) :: binary 123 | def der_serialize_signature(%Signature{r: r, s: s}) do 124 | r_bytes = serialize_sig_key(r) 125 | s_bytes = serialize_sig_key(s) 126 | <<0x30>> <> len_as_bytes(r_bytes <> s_bytes) <> r_bytes <> s_bytes 127 | end 128 | 129 | def der_serialize_signature(_), do: {:error, "Signature object required"} 130 | 131 | defp serialize_sig_key(k) do 132 | k 133 | |> :binary.encode_unsigned() 134 | |> lstrip(<<0x00>>) 135 | |> add_high_bit() 136 | |> prefix_key() 137 | end 138 | 139 | defp len_as_bytes(data), do: :binary.encode_unsigned(byte_size(data)) 140 | 141 | defp lstrip(<> <> tail, val) do 142 | if head == val, do: lstrip(tail, val), else: head <> tail 143 | end 144 | 145 | defp add_high_bit(k_bytes) do 146 | unless (:binary.at(k_bytes, 0) &&& 0x80) == 0 do 147 | <<0x00>> <> k_bytes 148 | else 149 | k_bytes 150 | end 151 | end 152 | 153 | defp prefix_key(k_bytes), do: <<0x02>> <> len_as_bytes(k_bytes) <> k_bytes 154 | end 155 | 156 | @doc """ 157 | Returns the y-coordinate of a secp256k1 curve point (P) using the x-coordinate. 158 | To get P(y), we solve for y in this equation: y^2 = x^3 + 7. 159 | """ 160 | @spec get_y(integer, boolean) :: {:ok, integer} | {:error, String.t()} 161 | def get_y(x, is_y_odd) do 162 | # x^3 + 7 163 | y_sq = 164 | :crypto.mod_pow(x, 3, Params.curve().p) 165 | |> :binary.decode_unsigned() 166 | |> Kernel.+(7 |> Math.modulo(Params.curve().p)) 167 | 168 | # Solve for y. 169 | y = 170 | :crypto.mod_pow(y_sq, Integer.floor_div(Params.curve().p + 1, 4), Params.curve().p) 171 | |> :binary.decode_unsigned() 172 | 173 | y = 174 | case rem(y, 2) == 1 do 175 | ^is_y_odd -> 176 | y 177 | 178 | _ -> 179 | Params.curve().p - y 180 | end 181 | 182 | # Check. 183 | if y_sq != :crypto.mod_pow(y, 2, Params.curve().p) |> :binary.decode_unsigned() do 184 | {:error, "invalid sq root"} 185 | else 186 | {:ok, y} 187 | end 188 | end 189 | 190 | @doc """ 191 | force_even_y returns the negated private key 192 | if the associated Point has an odd y. Otherwise 193 | it returns the private key 194 | """ 195 | @spec force_even_y(PrivateKey.t()) :: PrivateKey.t() | {:error, String.t()} 196 | def force_even_y(%PrivateKey{} = privkey) do 197 | case PrivateKey.to_point(privkey) do 198 | {:error, msg} -> 199 | {:error, msg} 200 | 201 | pubkey -> 202 | if Point.is_inf(pubkey) do 203 | {:error, "pubkey is infinity. bad luck"} 204 | else 205 | if Point.has_even_y(pubkey) do 206 | privkey 207 | else 208 | %PrivateKey{d: Params.curve().n - privkey.d} 209 | end 210 | end 211 | end 212 | end 213 | 214 | @doc """ 215 | verify_point verifies that a given point is on the secp256k1 216 | curve 217 | """ 218 | @spec verify_point(Point.t()) :: bool 219 | def verify_point(%Point{x: x, y: y}) do 220 | y_odd = rem(y, 2) == 1 221 | {:ok, new_y} = get_y(x, y_odd) 222 | y == new_y 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /lib/segwit.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Segwit do 2 | @moduledoc """ 3 | SegWit address serialization. 4 | """ 5 | 6 | alias Bitcoinex.Bech32 7 | 8 | @valid_witness_program_length_range 2..40 9 | @valid_witness_version 0..16 10 | @supported_network [:mainnet, :testnet, :regtest] 11 | 12 | @type hrp :: String.t() 13 | @type data :: list(integer) 14 | # seem no way to use list of atom module attribute in type spec 15 | @type network :: :testnet | :mainnet | :regtest 16 | 17 | @type witness_version :: 0..16 18 | @type witness_program :: list(integer) 19 | 20 | @type error :: atom() 21 | 22 | @doc """ 23 | Decodes an address and returns its network, witness version, and witness program. 24 | """ 25 | @spec decode_address(String.t()) :: 26 | {:ok, {network, witness_version, witness_program}} | {:error, error} 27 | def decode_address(address) when is_binary(address) do 28 | with {_, {:ok, {encoding_type, hrp, data}}} <- {:decode_bech32, Bech32.decode(address)}, 29 | {_, {:ok, network}} <- {:parse_network, parse_network(hrp |> String.to_charlist())}, 30 | {_, {:ok, {version, program}}} <- {:parse_segwit_data, parse_segwit_data(data)} do 31 | case witness_version_to_bech_encoding(version) do 32 | ^encoding_type -> 33 | {:ok, {network, version, program}} 34 | 35 | _ -> 36 | # encoding type derived from witness version (first byte of data) is different from the code derived from bech32 decoding 37 | {:error, :invalid_checksum} 38 | end 39 | else 40 | {_, {:error, error}} -> 41 | {:error, error} 42 | end 43 | end 44 | 45 | @doc """ 46 | Encodes an address string. 47 | """ 48 | @spec encode_address(network, witness_version, witness_program) :: 49 | {:ok, String.t()} | {:error, error} 50 | def encode_address(network, _, _) when network not in @supported_network do 51 | {:error, :invalid_network} 52 | end 53 | 54 | def encode_address(_, witness_version, _) 55 | when witness_version not in @valid_witness_version do 56 | {:error, :invalid_witness_version} 57 | end 58 | 59 | def encode_address(network, version, program) do 60 | with {:ok, converted_program} <- Bech32.convert_bits(program, 8, 5), 61 | {:is_program_length_valid, true} <- 62 | {:is_program_length_valid, is_program_length_valid?(version, program)} do 63 | hrp = 64 | case network do 65 | :mainnet -> 66 | "bc" 67 | 68 | :testnet -> 69 | "tb" 70 | 71 | :regtest -> 72 | "bcrt" 73 | end 74 | 75 | Bech32.encode(hrp, [version | converted_program], witness_version_to_bech_encoding(version)) 76 | else 77 | {:is_program_length_valid, false} -> 78 | {:error, :invalid_program_length} 79 | 80 | error -> 81 | error 82 | end 83 | end 84 | 85 | @doc """ 86 | Simpler Interface to check if address is valid 87 | """ 88 | @spec is_valid_segswit_address?(String.t()) :: boolean 89 | def is_valid_segswit_address?(address) when is_binary(address) do 90 | case decode_address(address) do 91 | {:ok, _} -> 92 | true 93 | 94 | _ -> 95 | false 96 | end 97 | end 98 | 99 | @spec get_segwit_script_pubkey(witness_version, witness_program) :: String.t() 100 | def get_segwit_script_pubkey(version, program) do 101 | # OP_0 is encoded as 0x00, but OP_1 through OP_16 are encoded as 0x51 though 0x60 102 | wit_version_adjusted = if(version == 0, do: 0, else: version + 0x50) 103 | 104 | [ 105 | wit_version_adjusted, 106 | Enum.count(program) | program 107 | ] 108 | |> :erlang.list_to_binary() 109 | # to hex and all lower case for better readability 110 | |> Base.encode16(case: :lower) 111 | end 112 | 113 | defp parse_segwit_data([]) do 114 | {:error, :empty_segwit_data} 115 | end 116 | 117 | defp parse_segwit_data([version | encoded]) when version in @valid_witness_version do 118 | case Bech32.convert_bits(encoded, 5, 8, false) do 119 | {:ok, program} -> 120 | if is_program_length_valid?(version, program) do 121 | {:ok, {version, program}} 122 | else 123 | {:error, :invalid_program_length} 124 | end 125 | 126 | {:error, error} -> 127 | {:error, error} 128 | end 129 | end 130 | 131 | defp parse_segwit_data(_), do: {:error, :invalid_witness_version} 132 | 133 | defp is_program_length_valid?(version, program) 134 | when length(program) in @valid_witness_program_length_range do 135 | case {version, length(program)} do 136 | # BIP141 specifies If the version byte is 0, but the witness program is neither 20 nor 32 bytes, the script must fail. 137 | {0, program_length} when program_length == 20 or program_length == 32 -> 138 | true 139 | 140 | {0, _} -> 141 | false 142 | 143 | _ -> 144 | true 145 | end 146 | end 147 | 148 | defp is_program_length_valid?(_, _), do: false 149 | 150 | defp parse_network(~c"bc"), do: {:ok, :mainnet} 151 | defp parse_network(~c"tb"), do: {:ok, :testnet} 152 | defp parse_network(~c"bcrt"), do: {:ok, :regtest} 153 | defp parse_network(_), do: {:error, :invalid_network} 154 | 155 | defp witness_version_to_bech_encoding(0), do: :bech32 156 | defp witness_version_to_bech_encoding(witver) when witver in 1..16, do: :bech32m 157 | end 158 | -------------------------------------------------------------------------------- /lib/transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Transaction do 2 | @moduledoc """ 3 | Bitcoin on-chain transaction structure. 4 | Supports serialization of transactions. 5 | """ 6 | alias Bitcoinex.Transaction 7 | alias Bitcoinex.Transaction.In 8 | alias Bitcoinex.Transaction.Out 9 | alias Bitcoinex.Transaction.Witness 10 | alias Bitcoinex.Utils 11 | alias Bitcoinex.Transaction.Utils, as: TxUtils 12 | 13 | @type t() :: %__MODULE__{ 14 | version: non_neg_integer(), 15 | inputs: list(In.t()), 16 | outputs: list(Out.t()), 17 | witnesses: list(Witness.t()), 18 | lock_time: non_neg_integer() 19 | } 20 | 21 | defstruct [ 22 | :version, 23 | :inputs, 24 | :outputs, 25 | :witnesses, 26 | :lock_time 27 | ] 28 | 29 | @doc """ 30 | Returns the TxID of the given tranasction. 31 | 32 | TxID is sha256(sha256(nVersion | txins | txouts | nLockTime)) 33 | """ 34 | def transaction_id(txn) do 35 | legacy_txn = TxUtils.serialize(%{txn | witnesses: []}) 36 | 37 | Base.encode16( 38 | <<:binary.decode_unsigned( 39 | Utils.double_sha256(legacy_txn), 40 | :big 41 | )::little-size(256)>>, 42 | case: :lower 43 | ) 44 | end 45 | 46 | @doc """ 47 | Decodes a transaction in a hex encoded string into binary. 48 | """ 49 | def decode(tx_hex) when is_binary(tx_hex) do 50 | case Base.decode16(tx_hex, case: :lower) do 51 | {:ok, tx_bytes} -> 52 | case parse(tx_bytes) do 53 | {:ok, txn} -> 54 | {:ok, txn} 55 | 56 | :error -> 57 | {:error, :parse_error} 58 | end 59 | 60 | :error -> 61 | {:error, :decode_error} 62 | end 63 | end 64 | 65 | # returns transaction 66 | defp parse(<>) do 67 | {is_segwit, remaining} = 68 | case remaining do 69 | <<1::size(16), segwit_remaining::binary>> -> 70 | {:segwit, segwit_remaining} 71 | 72 | _ -> 73 | {:not_segwit, remaining} 74 | end 75 | 76 | # Inputs. 77 | {in_counter, remaining} = TxUtils.get_counter(remaining) 78 | {inputs, remaining} = In.parse_inputs(in_counter, remaining) 79 | 80 | # Outputs. 81 | {out_counter, remaining} = TxUtils.get_counter(remaining) 82 | {outputs, remaining} = Out.parse_outputs(out_counter, remaining) 83 | 84 | # If flag 0001 is present, this indicates an attached segregated witness structure. 85 | {witnesses, remaining} = 86 | if is_segwit == :segwit do 87 | Witness.parse_witness(in_counter, remaining) 88 | else 89 | {nil, remaining} 90 | end 91 | 92 | <> = remaining 93 | 94 | if byte_size(remaining) != 0 do 95 | :error 96 | else 97 | {:ok, 98 | %Transaction{ 99 | version: version, 100 | inputs: inputs, 101 | outputs: outputs, 102 | witnesses: witnesses, 103 | lock_time: lock_time 104 | }} 105 | end 106 | end 107 | end 108 | 109 | defmodule Bitcoinex.Transaction.Utils do 110 | @moduledoc """ 111 | Utilities for when dealing with transaction objects. 112 | """ 113 | alias Bitcoinex.Transaction 114 | alias Bitcoinex.Transaction.In 115 | alias Bitcoinex.Transaction.Out 116 | alias Bitcoinex.Transaction.Witness 117 | 118 | @doc """ 119 | Returns the Variable Length Integer used in serialization. 120 | 121 | Reference: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer 122 | """ 123 | @spec get_counter(binary) :: {non_neg_integer(), binary()} 124 | def get_counter(<>) do 125 | case counter do 126 | # 0xFD followed by the length as uint16_t 127 | 0xFD -> 128 | <> = vec 129 | {len, vec} 130 | 131 | # 0xFE followed by the length as uint32_t 132 | 0xFE -> 133 | <> = vec 134 | {len, vec} 135 | 136 | # 0xFF followed by the length as uint64_t 137 | 0xFF -> 138 | <> = vec 139 | {len, vec} 140 | 141 | _ -> 142 | {counter, vec} 143 | end 144 | end 145 | 146 | @spec serialize(Transaction.t()) :: binary() 147 | def serialize(%Transaction{witnesses: witness} = txn) 148 | when is_list(witness) and length(witness) > 0 do 149 | version = <> 150 | marker = <<0x00::big-size(8)>> 151 | flag = <<0x01::big-size(8)>> 152 | tx_in_count = serialize_compact_size_unsigned_int(length(txn.inputs)) 153 | inputs = In.serialize_inputs(txn.inputs) |> :erlang.list_to_binary() 154 | tx_out_count = serialize_compact_size_unsigned_int(length(txn.outputs)) 155 | outputs = Out.serialize_outputs(txn.outputs) |> :erlang.list_to_binary() 156 | witness = Witness.serialize_witness(txn.witnesses) 157 | lock_time = <> 158 | 159 | version <> 160 | marker <> flag <> tx_in_count <> inputs <> tx_out_count <> outputs <> witness <> lock_time 161 | end 162 | 163 | def serialize(txn) do 164 | version = <> 165 | tx_in_count = serialize_compact_size_unsigned_int(length(txn.inputs)) 166 | inputs = In.serialize_inputs(txn.inputs) |> :erlang.list_to_binary() 167 | tx_out_count = serialize_compact_size_unsigned_int(length(txn.outputs)) 168 | outputs = Out.serialize_outputs(txn.outputs) |> :erlang.list_to_binary() 169 | lock_time = <> 170 | 171 | version <> tx_in_count <> inputs <> tx_out_count <> outputs <> lock_time 172 | end 173 | 174 | @doc """ 175 | Returns the serialized variable length integer. 176 | """ 177 | def serialize_compact_size_unsigned_int(compact_size) do 178 | cond do 179 | compact_size >= 0 and compact_size <= 0xFC -> 180 | <> 181 | 182 | compact_size <= 0xFFFF -> 183 | <<0xFD>> <> <> 184 | 185 | compact_size <= 0xFFFFFFFF -> 186 | <<0xFE>> <> <> 187 | 188 | compact_size <= 0xFF -> 189 | <<0xFF>> <> <> 190 | end 191 | end 192 | end 193 | 194 | defmodule Bitcoinex.Transaction.Witness do 195 | @moduledoc """ 196 | Witness structure part of an on-chain transaction. 197 | """ 198 | alias Bitcoinex.Transaction.Witness 199 | alias Bitcoinex.Transaction.Utils, as: TxUtils 200 | 201 | @type t :: %__MODULE__{ 202 | txinwitness: list(binary()) 203 | } 204 | defstruct [ 205 | :txinwitness 206 | ] 207 | 208 | @doc """ 209 | Wtiness accepts a binary and deserializes it. 210 | """ 211 | @spec witness(binary) :: t() 212 | def witness(witness_bytes) do 213 | {stack_size, witness_bytes} = TxUtils.get_counter(witness_bytes) 214 | 215 | {witness, _} = 216 | if stack_size == 0 do 217 | {%Witness{txinwitness: []}, witness_bytes} 218 | else 219 | {stack_items, witness_bytes} = parse_stack(witness_bytes, [], stack_size) 220 | {%Witness{txinwitness: stack_items}, witness_bytes} 221 | end 222 | 223 | witness 224 | end 225 | 226 | @spec serialize_witness(list(Witness.t())) :: binary 227 | def serialize_witness(witnesses) do 228 | serialize_witness(witnesses, <<>>) 229 | end 230 | 231 | defp serialize_witness([], serialized_witnesses), do: serialized_witnesses 232 | 233 | defp serialize_witness(witnesses, serialized_witnesses) do 234 | [witness | witnesses] = witnesses 235 | 236 | serialized_witness = 237 | if Enum.empty?(witness.txinwitness) do 238 | <<0x0::big-size(8)>> 239 | else 240 | stack_len = TxUtils.serialize_compact_size_unsigned_int(length(witness.txinwitness)) 241 | 242 | field = 243 | Enum.reduce(witness.txinwitness, <<>>, fn v, acc -> 244 | {:ok, item} = Base.decode16(v, case: :lower) 245 | item_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(item)) 246 | acc <> item_len <> item 247 | end) 248 | 249 | stack_len <> field 250 | end 251 | 252 | serialize_witness(witnesses, serialized_witnesses <> serialized_witness) 253 | end 254 | 255 | def parse_witness(0, remaining), do: {nil, remaining} 256 | 257 | def parse_witness(counter, witnesses) do 258 | parse(witnesses, [], counter) 259 | end 260 | 261 | defp parse(remaining, witnesses, 0), do: {Enum.reverse(witnesses), remaining} 262 | 263 | defp parse(remaining, witnesses, count) do 264 | {stack_size, remaining} = TxUtils.get_counter(remaining) 265 | 266 | {witness, remaining} = 267 | if stack_size == 0 do 268 | {%Witness{txinwitness: 0}, remaining} 269 | else 270 | {stack_items, remaining} = parse_stack(remaining, [], stack_size) 271 | {%Witness{txinwitness: stack_items}, remaining} 272 | end 273 | 274 | parse(remaining, [witness | witnesses], count - 1) 275 | end 276 | 277 | defp parse_stack(remaining, stack_items, 0), do: {Enum.reverse(stack_items), remaining} 278 | 279 | defp parse_stack(remaining, stack_items, stack_size) do 280 | {item_size, remaining} = TxUtils.get_counter(remaining) 281 | 282 | <> = remaining 283 | 284 | parse_stack( 285 | remaining, 286 | [Base.encode16(stack_item, case: :lower) | stack_items], 287 | stack_size - 1 288 | ) 289 | end 290 | end 291 | 292 | defmodule Bitcoinex.Transaction.In do 293 | @moduledoc """ 294 | Transaction Input part of an on-chain transaction. 295 | """ 296 | alias Bitcoinex.Transaction.In 297 | alias Bitcoinex.Transaction.Utils, as: TxUtils 298 | 299 | @type t :: %__MODULE__{ 300 | prev_txid: binary(), 301 | prev_vout: non_neg_integer(), 302 | script_sig: binary(), 303 | sequence_no: non_neg_integer() 304 | } 305 | 306 | defstruct [ 307 | :prev_txid, 308 | :prev_vout, 309 | :script_sig, 310 | :sequence_no 311 | ] 312 | 313 | @spec serialize_inputs(list(In.t())) :: iolist() 314 | def serialize_inputs(inputs) do 315 | serialize_input(inputs, []) 316 | end 317 | 318 | defp serialize_input([], serialized_inputs), do: serialized_inputs 319 | 320 | defp serialize_input(inputs, serialized_inputs) do 321 | [input | inputs] = inputs 322 | 323 | {:ok, prev_txid} = Base.decode16(input.prev_txid, case: :lower) 324 | 325 | prev_txid = 326 | prev_txid 327 | |> :binary.decode_unsigned(:big) 328 | |> :binary.encode_unsigned(:little) 329 | |> Bitcoinex.Utils.pad(32, :trailing) 330 | 331 | {:ok, script_sig} = Base.decode16(input.script_sig, case: :lower) 332 | 333 | script_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(script_sig)) 334 | 335 | serialized_input = [ 336 | prev_txid, 337 | <>, 338 | script_len, 339 | script_sig, 340 | <> 341 | ] 342 | 343 | serialize_input(inputs, [serialized_inputs, serialized_input]) 344 | end 345 | 346 | def parse_inputs(counter, inputs) do 347 | parse(inputs, [], counter) 348 | end 349 | 350 | defp parse(remaining, inputs, 0), do: {Enum.reverse(inputs), remaining} 351 | 352 | defp parse( 353 | <>, 354 | inputs, 355 | count 356 | ) do 357 | {script_len, remaining} = TxUtils.get_counter(remaining) 358 | 359 | <> = 360 | remaining 361 | 362 | input = %In{ 363 | prev_txid: 364 | Base.encode16(<<:binary.decode_unsigned(prev_txid, :big)::little-size(256)>>, 365 | case: :lower 366 | ), 367 | prev_vout: prev_vout, 368 | script_sig: Base.encode16(script_sig, case: :lower), 369 | sequence_no: sequence_no 370 | } 371 | 372 | parse(remaining, [input | inputs], count - 1) 373 | end 374 | end 375 | 376 | defmodule Bitcoinex.Transaction.Out do 377 | @moduledoc """ 378 | Transaction Output part of an on-chain transaction. 379 | """ 380 | alias Bitcoinex.Transaction.Out 381 | alias Bitcoinex.Transaction.Utils, as: TxUtils 382 | 383 | @type t :: %__MODULE__{ 384 | value: non_neg_integer(), 385 | script_pub_key: binary() 386 | } 387 | 388 | defstruct [ 389 | :value, 390 | :script_pub_key 391 | ] 392 | 393 | @spec serialize_outputs(list(Out.t())) :: iolist() 394 | def serialize_outputs(outputs) do 395 | serialize_output(outputs, []) 396 | end 397 | 398 | defp serialize_output([], serialized_outputs), do: serialized_outputs 399 | 400 | defp serialize_output(outputs, serialized_outputs) do 401 | [output | outputs] = outputs 402 | 403 | {:ok, script_pub_key} = Base.decode16(output.script_pub_key, case: :lower) 404 | 405 | script_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(script_pub_key)) 406 | 407 | serialized_output = [<>, script_len, script_pub_key] 408 | serialize_output(outputs, [serialized_outputs, serialized_output]) 409 | end 410 | 411 | def output(out_bytes) do 412 | <> = out_bytes 413 | {script_len, out_bytes} = TxUtils.get_counter(out_bytes) 414 | <> = out_bytes 415 | %Out{value: value, script_pub_key: Base.encode16(script_pub_key, case: :lower)} 416 | end 417 | 418 | def parse_outputs(counter, outputs) do 419 | parse(outputs, [], counter) 420 | end 421 | 422 | defp parse(remaining, outputs, 0), do: {Enum.reverse(outputs), remaining} 423 | 424 | defp parse(<>, outputs, count) do 425 | {script_len, remaining} = TxUtils.get_counter(remaining) 426 | 427 | <> = remaining 428 | 429 | output = %Out{ 430 | value: value, 431 | script_pub_key: Base.encode16(script_pub_key, case: :lower) 432 | } 433 | 434 | parse(remaining, [output | outputs], count - 1) 435 | end 436 | end 437 | -------------------------------------------------------------------------------- /lib/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Utils do 2 | @moduledoc """ 3 | Contains useful utility functions used in Bitcoinex. 4 | """ 5 | 6 | @spec sha256(iodata()) :: binary 7 | def sha256(str) do 8 | :crypto.hash(:sha256, str) 9 | end 10 | 11 | def tagged_hash(tag, str) do 12 | tag_hash = sha256(tag) 13 | sha256(tag_hash <> tag_hash <> str) 14 | end 15 | 16 | @spec replicate(term(), integer()) :: list(term()) 17 | def replicate(_num, 0) do 18 | [] 19 | end 20 | 21 | def replicate(x, num) when x > 0 do 22 | for _ <- 1..num, do: x 23 | end 24 | 25 | @spec double_sha256(iodata()) :: binary 26 | def double_sha256(preimage) do 27 | :crypto.hash( 28 | :sha256, 29 | :crypto.hash(:sha256, preimage) 30 | ) 31 | end 32 | 33 | @spec hash160(iodata()) :: binary 34 | def hash160(preimage) do 35 | :crypto.hash( 36 | :ripemd160, 37 | :crypto.hash(:sha256, preimage) 38 | ) 39 | end 40 | 41 | @typedoc """ 42 | The pad_type describes the padding to use. 43 | """ 44 | @type pad_type :: :leading | :trailing 45 | 46 | @doc """ 47 | pads binary according to the byte length and the padding type. A binary can be padded with leading or trailing zeros. 48 | """ 49 | @spec pad(bin :: binary, byte_len :: integer, pad_type :: pad_type) :: binary 50 | def pad(bin, byte_len, _pad_type) when is_binary(bin) and byte_size(bin) == byte_len do 51 | bin 52 | end 53 | 54 | def pad(bin, byte_len, pad_type) when is_binary(bin) and pad_type == :leading do 55 | pad_len = 8 * byte_len - byte_size(bin) * 8 56 | <<0::size(pad_len)>> <> bin 57 | end 58 | 59 | def pad(bin, byte_len, pad_type) when is_binary(bin) and pad_type == :trailing do 60 | pad_len = 8 * byte_len - byte_size(bin) * 8 61 | bin <> <<0::size(pad_len)>> 62 | end 63 | 64 | @spec int_to_big(non_neg_integer(), non_neg_integer()) :: binary 65 | def int_to_big(i, p) do 66 | i 67 | |> :binary.encode_unsigned() 68 | |> pad(p, :leading) 69 | end 70 | 71 | def int_to_little(i, p) do 72 | i 73 | |> :binary.encode_unsigned(:little) 74 | |> pad(p, :trailing) 75 | end 76 | 77 | def little_to_int(i), do: :binary.decode_unsigned(i, :little) 78 | 79 | def encode_int(i) when i > 0 do 80 | cond do 81 | i < 0xFD -> :binary.encode_unsigned(i) 82 | i <= 0xFFFF -> <<0xFD>> <> int_to_little(i, 2) 83 | i <= 0xFFFFFFFF -> <<0xFE>> <> int_to_little(i, 4) 84 | i <= 0xFFFFFFFFFFFFFFFF -> <<0xFF>> <> int_to_little(i, 8) 85 | true -> {:error, "invalid integer size"} 86 | end 87 | end 88 | 89 | def hex_to_bin(str) do 90 | str 91 | |> String.downcase() 92 | |> Base.decode16(case: :lower) 93 | |> case do 94 | # In case of error, its already binary or its invalid 95 | :error -> {:error, "invalid string"} 96 | # valid binary 97 | {:ok, bin} -> bin 98 | end 99 | end 100 | 101 | # todo: better to just convert to ints and XOR them? 102 | @spec xor_bytes(binary, binary) :: binary 103 | def xor_bytes(bin0, bin1) do 104 | {bl0, bl1} = {:binary.bin_to_list(bin0), :binary.bin_to_list(bin1)} 105 | 106 | Enum.zip(bl0, bl1) 107 | |> Enum.map(fn {b0, b1} -> Bitwise.bxor(b0, b1) end) 108 | |> :binary.list_to_bin() 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bitcoinex, 7 | version: "0.1.8", 8 | elixir: "~> 1.11", 9 | package: package(), 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | aliases: aliases(), 13 | description: description(), 14 | source_url: "https://github.com/RiverFinancial/bitcoinex" 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 29 | {:excoveralls, "~> 0.10", only: :test}, 30 | {:mix_test_watch, "~> 1.1", only: :dev, runtime: false}, 31 | {:stream_data, "~> 0.1", only: :test}, 32 | {:decimal, "~> 1.0 or ~> 2.0"}, 33 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 34 | {:benchee, ">= 1.0.0", only: :dev} 35 | ] 36 | end 37 | 38 | defp aliases do 39 | [ 40 | "lint.all": [ 41 | "format --check-formatted", 42 | "credo --strict --only warning" 43 | ], 44 | compile: ["compile --warnings-as-errors"] 45 | ] 46 | end 47 | 48 | defp package do 49 | [ 50 | files: ~w(lib test .formatter.exs mix.exs README.md UNLICENSE CHANGELOG.md SECURITY.md), 51 | licenses: ["Unlicense"], 52 | links: %{"GitHub" => "https://github.com/RiverFinancial/bitcoinex"} 53 | ] 54 | end 55 | 56 | defp description() do 57 | "Bitcoinex is a Bitcoin Library for Elixir." 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, 3 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 4 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 5 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, 6 | "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, 7 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 8 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 9 | "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, 10 | "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, 11 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 12 | "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, 13 | "excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"}, 14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 15 | "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"}, 16 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 17 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 18 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 19 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 20 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 21 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 22 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 23 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 24 | "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, 25 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 26 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 27 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 28 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, 29 | "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, 30 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 31 | } 32 | -------------------------------------------------------------------------------- /scripts/decode_psbt.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule DecodePSBT do 3 | @moduledoc """ 4 | Decode a PSBT file and print the relevant details to the console. 5 | This is useful for getting the address and value of the inputs and outputs of a PSBT. 6 | """ 7 | alias Bitcoinex.{PSBT, Script, Transaction} 8 | 9 | @doc """ 10 | Run the script with the first (and only) argument as the path to the PSBT file. 11 | """ 12 | @spec run(list(String.t())) :: :ok 13 | def run(args) do 14 | psbt_path = List.first(args) 15 | hash = calculate_sha256(psbt_path) 16 | data = decode_psbt(psbt_path) 17 | 18 | print_results(hash, data) 19 | 20 | :ok 21 | end 22 | 23 | @doc """ 24 | Print the results to the console. 25 | """ 26 | @spec print_results(String.t(), map()) :: :ok 27 | def print_results(hash, %{inputs: inputs, outputs: outputs, fee: fee}) do 28 | IO.puts("SHA256: #{hash}") 29 | 30 | IO.puts("\nInputs:") 31 | Enum.each(inputs, fn input -> 32 | if input.note != nil do 33 | IO.puts(input.note) 34 | end 35 | IO.puts(" #{input.address}: #{sats_to_btc_str(input.value)} BTC") 36 | end) 37 | 38 | IO.puts("\nOutputs:") 39 | Enum.each(outputs, fn output -> 40 | IO.puts(" #{output.address}: #{sats_to_btc_str(output.value)} BTC") 41 | end) 42 | 43 | IO.puts("\nFee: #{sats_to_btc_str(fee)} BTC") 44 | 45 | :ok 46 | end 47 | 48 | # Convert sats to btc. 49 | @spec sats_to_btc_str(non_neg_integer()) :: String.t() 50 | defp sats_to_btc_str(sats) do 51 | :erlang.float_to_binary(sats / 100_000_000, decimals: 8) 52 | end 53 | 54 | @doc """ 55 | Calculate the SHA256 hash of the PSBT file. 56 | """ 57 | @spec calculate_sha256(String.t()) :: String.t() 58 | def calculate_sha256(filename) do 59 | {:ok, file_content} = File.read(filename) 60 | :crypto.hash(:sha256, file_content) |> Base.encode16(case: :lower) 61 | end 62 | 63 | @doc """ 64 | Decode the PSBT file. 65 | """ 66 | @spec decode_psbt(String.t()) :: %{inputs: list(map()), outputs: list(map()), fee: non_neg_integer()} 67 | def decode_psbt(psbt_path) do 68 | {:ok, psbt} = PSBT.from_file(psbt_path) 69 | %PSBT{global: %PSBT.Global{unsigned_tx: tx}, inputs: inputs} = psbt 70 | %Bitcoinex.Transaction{outputs: outputs} = tx 71 | 72 | inputs = parse_inputs(inputs) 73 | outputs = parse_outputs(outputs) 74 | 75 | fee = sum_values(inputs) - sum_values(outputs) 76 | 77 | %{ 78 | inputs: inputs, 79 | outputs: outputs, 80 | fee: fee 81 | } 82 | end 83 | 84 | @spec sum_values(list(%{value: non_neg_integer()})) :: non_neg_integer() 85 | defp sum_values(entries) do 86 | Enum.reduce(entries, 0, fn %{value: value}, sum -> sum + value end) 87 | end 88 | 89 | @doc """ 90 | Parse the inputs of the PSBT. 91 | """ 92 | @spec parse_inputs(list(PSBT.In.t())) :: list(map()) 93 | def parse_inputs(inputs) do 94 | Enum.map(inputs, fn input -> 95 | %PSBT.In{witness_utxo: %Transaction.Out{ 96 | value: value, 97 | script_pub_key: script_pub_key 98 | }, sighash_type: sighash_type} = input 99 | 100 | {:ok, script} = Script.parse_script(script_pub_key) 101 | {:ok, address} = Script.to_address(script, :mainnet) 102 | 103 | sighash_type = 104 | if sighash_type != nil do 105 | Bitcoinex.Utils.little_to_int(sighash_type) 106 | else 107 | nil 108 | end 109 | 110 | note = 111 | if sighash_type != nil and sighash_type != 0x01 do 112 | "🚨🚨🚨 WARNING: NON-STANDARD SIGHASH TYPE: #{sighash_name(sighash_type)} 🚨🚨🚨" 113 | else 114 | nil 115 | end 116 | 117 | %{ 118 | address: address, 119 | value: value, 120 | note: note 121 | } 122 | end) 123 | end 124 | 125 | # map between a sighash's int and a name 126 | @spec sighash_name(non_neg_integer()) :: String.t() 127 | defp sighash_name(n) do 128 | case n do 129 | 0x00 -> "SIGHASH_DEFAULT" # for Segwit v1 (taproot) inputs only 130 | 0x01 -> "SIGHASH_ALL" 131 | 0x02 -> "SIGHASH_NONE" 132 | 0x03 -> "SIGHASH_SINGLE" 133 | 0x81 -> "SIGHASH_ALL/ANYONECANPAY" 134 | 0x82 -> "SIGHASH_NONE/ANYONECANPAY" 135 | 0x83 -> "SIGHASH_SINGLE/ANYONECANPAY" 136 | _ -> "UNKNOWN" 137 | end 138 | end 139 | 140 | @doc """ 141 | Parse the outputs of the PSBT. 142 | """ 143 | @spec parse_outputs(list(Transaction.Out.t())) :: list(map()) 144 | def parse_outputs(outputs) do 145 | Enum.map(outputs, fn output -> 146 | %Transaction.Out{ 147 | value: value, 148 | script_pub_key: script_pub_key 149 | } = output 150 | 151 | {:ok, script} = Script.parse_script(script_pub_key) 152 | {:ok, address} = Script.to_address(script, :mainnet) 153 | 154 | %{ 155 | address: address, 156 | value: value 157 | } 158 | end) 159 | end 160 | end 161 | 162 | DecodePSBT.run(System.argv()) 163 | -------------------------------------------------------------------------------- /test/address_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.AddressTest do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Address 4 | 5 | alias Bitcoinex.Address 6 | 7 | describe "is_valid?/1" do 8 | setup do 9 | valid_mainnet_p2pkh_addresses = [ 10 | "12KYrjTdVGjFMtaxERSk3gphreJ5US8aUP", 11 | "12QeMLzSrB8XH8FvEzPMVoRxVAzTr5XM2y", 12 | "17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem", 13 | "1oNLrsHnBcR6dpaBpwz3LSwutbUNkNSjs" 14 | ] 15 | 16 | valid_testnet_p2pkh_addresses = [ 17 | "mzBc4XEFSdzCDcTxAgf6EZXgsZWpztRhef", 18 | "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn" 19 | ] 20 | 21 | valid_mainnet_p2sh_addresses = [ 22 | "3NJZLcZEEYBpxYEUGewU4knsQRn1WM5Fkt" 23 | ] 24 | 25 | valid_testnet_p2sh_addresses = [ 26 | "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc" 27 | ] 28 | 29 | valid_mainnet_segwit_addresses = [ 30 | "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4" 31 | ] 32 | 33 | valid_testnet_segwit_addresses = [ 34 | "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", 35 | "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy" 36 | ] 37 | 38 | valid_regtest_segwit_addresses = [ 39 | "bcrt1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qzf4jry", 40 | "bcrt1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvseswlauz7" 41 | ] 42 | 43 | valid_mainnet_p2wpkh_addresses = [ 44 | "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" 45 | ] 46 | 47 | valid_testnet_p2wpkh_addresses = [ 48 | "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx" 49 | ] 50 | 51 | valid_mainnet_p2wsh_addresses = [ 52 | "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3" 53 | ] 54 | 55 | valid_testnet_p2wsh_addresses = [ 56 | "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7" 57 | ] 58 | 59 | valid_mainnet_p2tr_addresses = [ 60 | "bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e", 61 | "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0" 62 | ] 63 | 64 | valid_testnet_p2tr_addresses = [ 65 | "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c" 66 | ] 67 | 68 | valid_mainnet_addresses = 69 | valid_mainnet_p2pkh_addresses ++ 70 | valid_mainnet_p2sh_addresses ++ 71 | valid_mainnet_segwit_addresses ++ 72 | valid_mainnet_p2wpkh_addresses ++ 73 | valid_mainnet_p2wsh_addresses ++ 74 | valid_mainnet_p2tr_addresses 75 | 76 | valid_testnet_addresses = 77 | valid_testnet_p2pkh_addresses ++ 78 | valid_testnet_p2sh_addresses ++ 79 | valid_testnet_segwit_addresses ++ 80 | valid_testnet_p2wpkh_addresses ++ 81 | valid_testnet_p2wsh_addresses ++ 82 | valid_testnet_p2tr_addresses 83 | 84 | valid_regtest_addresses = 85 | valid_testnet_p2pkh_addresses ++ 86 | valid_testnet_p2sh_addresses ++ valid_regtest_segwit_addresses 87 | 88 | invalid_addresses = [ 89 | # witness v1 address using bech32 (not bech32m) encoding 90 | "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", 91 | "BC1SW50QA3JX3S", 92 | "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", 93 | "", 94 | "rrRmhfXzGBKbV4YHtbpxfA1ftEcry8AJaX", 95 | "LSxNsEQekEpXMS4B7tUYstMEdMyH321ZQ1", 96 | "rrRmhfXzGBKbV4YHtbpxfA1ftEcry8AJaX", 97 | "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", 98 | "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", 99 | "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", 100 | "bc1rw5uspcuh", 101 | "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", 102 | "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", 103 | "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", 104 | "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", 105 | "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", 106 | "bc1gmk9yu", 107 | # p2tr addresses 108 | "bc1pqyqszqgpqyqszqgpqyqszqppgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e", 109 | "bc1p0xlxvlhemja6c4dqv22uapctqupfpphlxm9h8z3k2e72q4k9hcz7vqzk5jj0", 110 | "bc1pqyqszqgpqyqszqgpqyqszgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e", 111 | "bc1p0xlxvlhemja6c4dqv22uapctquphlxm9h8z3k2e72q4k9hcz7vqzk5jj0", 112 | "bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9f", 113 | "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj1" 114 | ] 115 | 116 | {:ok, 117 | valid_mainnet_addresses: valid_mainnet_addresses, 118 | valid_testnet_addresses: valid_testnet_addresses, 119 | valid_regtest_addresses: valid_regtest_addresses, 120 | valid_mainnet_p2pkh_addresses: valid_mainnet_p2pkh_addresses, 121 | valid_testnet_p2pkh_addresses: valid_testnet_p2pkh_addresses, 122 | valid_mainnet_p2sh_addresses: valid_mainnet_p2sh_addresses, 123 | valid_testnet_p2sh_addresses: valid_testnet_p2sh_addresses, 124 | valid_mainnet_segwit_addresses: valid_mainnet_segwit_addresses, 125 | valid_testnet_segwit_addresses: valid_testnet_segwit_addresses, 126 | valid_regtest_segwit_addresses: valid_regtest_segwit_addresses, 127 | valid_mainnet_p2wpkh_addresses: valid_mainnet_p2wpkh_addresses, 128 | valid_testnet_p2wpkh_addresses: valid_testnet_p2wpkh_addresses, 129 | valid_mainnet_p2wsh_addresses: valid_mainnet_p2wsh_addresses, 130 | valid_testnet_p2wsh_addresses: valid_testnet_p2wsh_addresses, 131 | valid_mainnet_p2tr_addresses: valid_mainnet_p2tr_addresses, 132 | valid_testnet_p2tr_addresses: valid_testnet_p2tr_addresses, 133 | invalid_addresses: invalid_addresses} 134 | end 135 | 136 | test "return true when the address is valid address either p2sh, p2pkh, pwsh, p2wpkh, p2tr", 137 | %{ 138 | valid_mainnet_p2pkh_addresses: valid_mainnet_p2pkh_addresses, 139 | valid_mainnet_p2sh_addresses: valid_mainnet_p2sh_addresses, 140 | valid_mainnet_segwit_addresses: valid_mainnet_segwit_addresses, 141 | valid_mainnet_p2tr_addresses: valid_mainnet_p2tr_addresses 142 | } do 143 | all_valid_addresses = 144 | valid_mainnet_p2sh_addresses ++ 145 | valid_mainnet_p2pkh_addresses ++ 146 | valid_mainnet_segwit_addresses ++ valid_mainnet_p2tr_addresses 147 | 148 | for valid_address <- all_valid_addresses do 149 | assert Address.is_valid?(valid_address, :mainnet) 150 | end 151 | end 152 | 153 | test "return false when the address is valid address either p2sh, p2pkh, pwsh, p2wpkh but not in correct network", 154 | %{ 155 | valid_testnet_segwit_addresses: valid_testnet_segwit_addresses, 156 | valid_testnet_p2pkh_addresses: valid_testnet_p2pkh_addresses, 157 | valid_testnet_p2sh_addresses: valid_testnet_p2sh_addresses, 158 | valid_regtest_segwit_addresses: valid_regtest_segwit_addresses, 159 | valid_testnet_p2tr_addresses: valid_testnet_p2tr_addresses 160 | } do 161 | all_valid_testnet_addresses = 162 | valid_testnet_segwit_addresses ++ 163 | valid_testnet_p2pkh_addresses ++ 164 | valid_testnet_p2sh_addresses ++ 165 | valid_regtest_segwit_addresses ++ 166 | valid_testnet_p2tr_addresses 167 | 168 | for valid_testnet_address <- all_valid_testnet_addresses do 169 | refute Address.is_valid?(valid_testnet_address, :mainnet) 170 | end 171 | end 172 | 173 | test "return false when the address is not valid address either p2sh, p2pkh, pwsh, p2wpkh, p2tr", 174 | %{ 175 | invalid_addresses: invalid_addresses 176 | } do 177 | all_invalid_addresses = invalid_addresses 178 | 179 | for invalid_address <- all_invalid_addresses do 180 | for %{name: network_name} <- Bitcoinex.Network.supported_networks() do 181 | for address_type <- Bitcoinex.Address.supported_address_types() do 182 | refute Address.is_valid?(invalid_address, network_name, address_type) 183 | end 184 | end 185 | end 186 | end 187 | 188 | test "check that the address decodes to the correct address type", %{ 189 | valid_mainnet_p2pkh_addresses: valid_mainnet_p2pkh_addresses, 190 | valid_testnet_p2pkh_addresses: valid_testnet_p2pkh_addresses, 191 | valid_mainnet_p2sh_addresses: valid_mainnet_p2sh_addresses, 192 | valid_testnet_p2sh_addresses: valid_testnet_p2sh_addresses, 193 | valid_mainnet_p2wpkh_addresses: valid_mainnet_p2wpkh_addresses, 194 | valid_testnet_p2wpkh_addresses: valid_testnet_p2wpkh_addresses, 195 | valid_mainnet_p2wsh_addresses: valid_mainnet_p2wsh_addresses, 196 | valid_testnet_p2wsh_addresses: valid_testnet_p2wsh_addresses, 197 | valid_mainnet_p2tr_addresses: valid_mainnet_p2tr_addresses, 198 | valid_testnet_p2tr_addresses: valid_testnet_p2tr_addresses 199 | } do 200 | for mainnet_p2pkh <- valid_mainnet_p2pkh_addresses do 201 | assert Address.decode_type(mainnet_p2pkh, :mainnet) == {:ok, :p2pkh} 202 | end 203 | 204 | for testnet_p2pkh <- valid_testnet_p2pkh_addresses do 205 | assert Address.decode_type(testnet_p2pkh, :testnet) == {:ok, :p2pkh} 206 | end 207 | 208 | for mainnet_p2sh <- valid_mainnet_p2sh_addresses do 209 | assert Address.decode_type(mainnet_p2sh, :mainnet) == {:ok, :p2sh} 210 | end 211 | 212 | for testnet_p2sh <- valid_testnet_p2sh_addresses do 213 | assert Address.decode_type(testnet_p2sh, :testnet) == {:ok, :p2sh} 214 | end 215 | 216 | for mainnet_p2wpkh <- valid_mainnet_p2wpkh_addresses do 217 | assert Address.decode_type(mainnet_p2wpkh, :mainnet) == {:ok, :p2wpkh} 218 | end 219 | 220 | for testnet_p2wpkh <- valid_testnet_p2wpkh_addresses do 221 | assert Address.decode_type(testnet_p2wpkh, :testnet) == {:ok, :p2wpkh} 222 | end 223 | 224 | for mainnet_p2wsh <- valid_mainnet_p2wsh_addresses do 225 | assert Address.decode_type(mainnet_p2wsh, :mainnet) == {:ok, :p2wsh} 226 | end 227 | 228 | for testnet_p2wsh <- valid_testnet_p2wsh_addresses do 229 | assert Address.decode_type(testnet_p2wsh, :testnet) == {:ok, :p2wsh} 230 | end 231 | 232 | for mainnet_p2tr <- valid_mainnet_p2tr_addresses do 233 | assert Address.decode_type(mainnet_p2tr, :mainnet) == {:ok, :p2tr} 234 | end 235 | 236 | for testnet_p2tr <- valid_testnet_p2tr_addresses do 237 | assert Address.decode_type(testnet_p2tr, :testnet) == {:ok, :p2tr} 238 | end 239 | end 240 | end 241 | 242 | describe "encode/3" do 243 | test "return true for encoding p2pkh" do 244 | pubkey_hash = Base.decode16!("6dcd022b3c5e6439238eb333ec1d6ddd1973b5ba", case: :lower) 245 | assert "1B1aF9aUzxqgEviiCSe9u339hpUWLVWfxu" == Address.encode(pubkey_hash, :mainnet, :p2pkh) 246 | end 247 | 248 | test "return true for encoding p2sh" do 249 | script_hash = Base.decode16!("6d77fa9de297e9c536c6b23cfda1a8450bb5f765", case: :lower) 250 | assert "3BfqJjn7H2jsbKd2NVHGP4sQWQ2bQWBRLv" == Address.encode(script_hash, :mainnet, :p2sh) 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /test/base58_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Base58Test do 2 | use ExUnit.Case 3 | use ExUnitProperties 4 | doctest Bitcoinex.Base58 5 | 6 | alias Bitcoinex.Base58 7 | 8 | # From 9 | @base58_encode_decode [ 10 | ["", ""], 11 | ["61", "2g"], 12 | ["626262", "a3gV"], 13 | ["636363", "aPEr"], 14 | ["73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"], 15 | ["00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"], 16 | ["516b6fcd0f", "ABnLTmg"], 17 | ["bf4f89001e670274dd", "3SEo3LWLoPntC"], 18 | ["572e4794", "3EFU7m"], 19 | ["ecac89cad93923c02321", "EJDM8drfXA6uyA"], 20 | ["10c8511e", "Rt5zm"], 21 | ["00000000000000000000", "1111111111"], 22 | [ 23 | "000111d38e5fc9071ffcd20b4a763cc9ae4f252bb4e48fd66a835e252ada93ff480d6dd43dc62a641155a5", 24 | "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 25 | ], 26 | [ 27 | "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", 28 | "1cWB5HCBdLjAuqGGReWE3R3CguuwSjw6RHn39s2yuDRTS5NsBgNiFpWgAnEx6VQi8csexkgYw3mdYrMHr8x9i7aEwP8kZ7vccXWqKDvGv3u1GxFKPuAkn8JCPPGDMf3vMMnbzm6Nh9zh1gcNsMvH3ZNLmP5fSG6DGbbi2tuwMWPthr4boWwCxf7ewSgNQeacyozhKDDQQ1qL5fQFUW52QKUZDZ5fw3KXNQJMcNTcaB723LchjeKun7MuGW5qyCBZYzA1KjofN1gYBV3NqyhQJ3Ns746GNuf9N2pQPmHz4xpnSrrfCvy6TVVz5d4PdrjeshsWQwpZsZGzvbdAdN8MKV5QsBDY" 29 | ] 30 | ] 31 | 32 | # From https://github.com/bitcoinjs/bs58check/blob/master/test/fixtures.json 33 | @valid_base58_strings [ 34 | ["1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i", "0065a16059864a2fdbc7c99a4723a8395bc6f188eb"], 35 | ["3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou", "0574f209f6ea907e2ea48f74fae05782ae8a665257"], 36 | ["mo9ncXisMeAoXwqcV5EWuyncbmCcQN4rVs", "6f53c0307d6851aa0ce7825ba883c6bd9ad242b486"], 37 | ["2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br", "c46349a418fc4578d10a372b54b45c280cc8c4382f"], 38 | [ 39 | "5Kd3NBUAdUnhyzenEwVLy9pBKxSwXvE9FMPyR4UKZvpe6E3AgLr", 40 | "80eddbdc1168f1daeadbd3e44c1e3f8f5a284c2029f78ad26af98583a499de5b19" 41 | ], 42 | [ 43 | "Kz6UJmQACJmLtaQj5A3JAge4kVTNQ8gbvXuwbmCj7bsaabudb3RD", 44 | "8055c9bccb9ed68446d1b75273bbce89d7fe013a8acd1625514420fb2aca1a21c401" 45 | ], 46 | [ 47 | "9213qJab2HNEpMpYNBa7wHGFKKbkDn24jpANDs2huN3yi4J11ko", 48 | "ef36cb93b9ab1bdabf7fb9f2c04f1b9cc879933530ae7842398eef5a63a56800c2" 49 | ], 50 | [ 51 | "cTpB4YiyKiBcPxnefsDpbnDxFDffjqJob8wGCEDXxgQ7zQoMXJdH", 52 | "efb9f4892c9e8282028fea1d2667c4dc5213564d41fc5783896a0d843fc15089f301" 53 | ], 54 | ["1Ax4gZtb7gAit2TivwejZHYtNNLT18PUXJ", "006d23156cbbdcc82a5a47eee4c2c7c583c18b6bf4"], 55 | ["3QjYXhTkvuj8qPaXHTTWb5wjXhdsLAAWVy", "05fcc5460dd6e2487c7d75b1963625da0e8f4c5975"], 56 | ["n3ZddxzLvAY9o7184TB4c6FJasAybsw4HZ", "6ff1d470f9b02370fdec2e6b708b08ac431bf7a5f7"], 57 | ["2NBFNJTktNa7GZusGbDbGKRZTxdK9VVez3n", "c4c579342c2c4c9220205e2cdc285617040c924a0a"], 58 | [ 59 | "5K494XZwps2bGyeL71pWid4noiSNA2cfCibrvRWqcHSptoFn7rc", 60 | "80a326b95ebae30164217d7a7f57d72ab2b54e3be64928a19da0210b9568d4015e" 61 | ], 62 | [ 63 | "L1RrrnXkcKut5DEMwtDthjwRcTTwED36thyL1DebVrKuwvohjMNi", 64 | "807d998b45c219a1e38e99e7cbd312ef67f77a455a9b50c730c27f02c6f730dfb401" 65 | ], 66 | [ 67 | "93DVKyFYwSN6wEo3E2fCrFPUp17FtrtNi2Lf7n4G3garFb16CRj", 68 | "efd6bca256b5abc5602ec2e1c121a08b0da2556587430bcf7e1898af2224885203" 69 | ], 70 | [ 71 | "cTDVKtMGVYWTHCb1AFjmVbEbWjvKpKqKgMaR3QJxToMSQAhmCeTN", 72 | "efa81ca4e8f90181ec4b61b6a7eb998af17b2cb04de8a03b504b9e34c4c61db7d901" 73 | ], 74 | ["1C5bSj1iEGUgSTbziymG7Cn18ENQuT36vv", "007987ccaa53d02c8873487ef919677cd3db7a6912"], 75 | ["3AnNxabYGoTxYiTEZwFEnerUoeFXK2Zoks", "0563bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb"], 76 | ["n3LnJXCqbPjghuVs8ph9CYsAe4Sh4j97wk", "6fef66444b5b17f14e8fae6e7e19b045a78c54fd79"], 77 | ["2NB72XtkjpnATMggui83aEtPawyyKvnbX2o", "c4c3e55fceceaa4391ed2a9677f4a4d34eacd021a0"], 78 | [ 79 | "5KaBW9vNtWNhc3ZEDyNCiXLPdVPHCikRxSBWwV9NrpLLa4LsXi9", 80 | "80e75d936d56377f432f404aabb406601f892fd49da90eb6ac558a733c93b47252" 81 | ], 82 | [ 83 | "L1axzbSyynNYA8mCAhzxkipKkfHtAXYF4YQnhSKcLV8YXA874fgT", 84 | "808248bd0375f2f75d7e274ae544fb920f51784480866b102384190b1addfbaa5c01" 85 | ], 86 | [ 87 | "927CnUkUbasYtDwYwVn2j8GdTuACNnKkjZ1rpZd2yBB1CLcnXpo", 88 | "ef44c4f6a096eac5238291a94cc24c01e3b19b8d8cef72874a079e00a242237a52" 89 | ], 90 | [ 91 | "cUcfCMRjiQf85YMzzQEk9d1s5A4K7xL5SmBCLrezqXFuTVefyhY7", 92 | "efd1de707020a9059d6d3abaf85e17967c6555151143db13dbb06db78df0f15c6901" 93 | ], 94 | ["1Gqk4Tv79P91Cc1STQtU3s1W6277M2CVWu", "00adc1cc2081a27206fae25792f28bbc55b831549d"], 95 | ["33vt8ViH5jsr115AGkW6cEmEz9MpvJSwDk", "05188f91a931947eddd7432d6e614387e32b244709"], 96 | ["mhaMcBxNh5cqXm4aTQ6EcVbKtfL6LGyK2H", "6f1694f5bc1a7295b600f40018a618a6ea48eeb498"], 97 | ["2MxgPqX1iThW3oZVk9KoFcE5M4JpiETssVN", "c43b9b3fd7a50d4f08d1a5b0f62f644fa7115ae2f3"], 98 | [ 99 | "5HtH6GdcwCJA4ggWEL1B3jzBBUB8HPiBi9SBc5h9i4Wk4PSeApR", 100 | "80091035445ef105fa1bb125eccfb1882f3fe69592265956ade751fd095033d8d0" 101 | ], 102 | [ 103 | "L2xSYmMeVo3Zek3ZTsv9xUrXVAmrWxJ8Ua4cw8pkfbQhcEFhkXT8", 104 | "80ab2b4bcdfc91d34dee0ae2a8c6b6668dadaeb3a88b9859743156f462325187af01" 105 | ], 106 | [ 107 | "92xFEve1Z9N8Z641KQQS7ByCSb8kGjsDzw6fAmjHN1LZGKQXyMq", 108 | "efb4204389cef18bbe2b353623cbf93e8678fbc92a475b664ae98ed594e6cf0856" 109 | ], 110 | [ 111 | "cVM65tdYu1YK37tNoAyGoJTR13VBYFva1vg9FLuPAsJijGvG6NEA", 112 | "efe7b230133f1b5489843260236b06edca25f66adb1be455fbd38d4010d48faeef01" 113 | ], 114 | ["1JwMWBVLtiqtscbaRHai4pqHokhFCbtoB4", "00c4c1b72491ede1eedaca00618407ee0b772cad0d"], 115 | ["3QCzvfL4ZRvmJFiWWBVwxfdaNBT8EtxB5y", "05f6fe69bcb548a829cce4c57bf6fff8af3a5981f9"], 116 | ["mizXiucXRCsEriQCHUkCqef9ph9qtPbZZ6", "6f261f83568a098a8638844bd7aeca039d5f2352c0"], 117 | ["2NEWDzHWwY5ZZp8CQWbB7ouNMLqCia6YRda", "c4e930e1834a4d234702773951d627cce82fbb5d2e"], 118 | [ 119 | "5KQmDryMNDcisTzRp3zEq9e4awRmJrEVU1j5vFRTKpRNYPqYrMg", 120 | "80d1fab7ab7385ad26872237f1eb9789aa25cc986bacc695e07ac571d6cdac8bc0" 121 | ], 122 | [ 123 | "L39Fy7AC2Hhj95gh3Yb2AU5YHh1mQSAHgpNixvm27poizcJyLtUi", 124 | "80b0bbede33ef254e8376aceb1510253fc3550efd0fcf84dcd0c9998b288f166b301" 125 | ], 126 | [ 127 | "91cTVUcgydqyZLgaANpf1fvL55FH53QMm4BsnCADVNYuWuqdVys", 128 | "ef037f4192c630f399d9271e26c575269b1d15be553ea1a7217f0cb8513cef41cb" 129 | ], 130 | [ 131 | "cQspfSzsgLeiJGB2u8vrAiWpCU4MxUT6JseWo2SjXy4Qbzn2fwDw", 132 | "ef6251e205e8ad508bab5596bee086ef16cd4b239e0cc0c5d7c4e6035441e7d5de01" 133 | ], 134 | ["19dcawoKcZdQz365WpXWMhX6QCUpR9SY4r", "005eadaf9bb7121f0f192561a5a62f5e5f54210292"], 135 | ["37Sp6Rv3y4kVd1nQ1JV5pfqXccHNyZm1x3", "053f210e7277c899c3a155cc1c90f4106cbddeec6e"], 136 | ["myoqcgYiehufrsnnkqdqbp69dddVDMopJu", "6fc8a3c2a09a298592c3e180f02487cd91ba3400b5"], 137 | ["2N7FuwuUuoTBrDFdrAZ9KxBmtqMLxce9i1C", "c499b31df7c9068d1481b596578ddbb4d3bd90baeb"], 138 | [ 139 | "5KL6zEaMtPRXZKo1bbMq7JDjjo1bJuQcsgL33je3oY8uSJCR5b4", 140 | "80c7666842503db6dc6ea061f092cfb9c388448629a6fe868d068c42a488b478ae" 141 | ], 142 | [ 143 | "KwV9KAfwbwt51veZWNscRTeZs9CKpojyu1MsPnaKTF5kz69H1UN2", 144 | "8007f0803fc5399e773555ab1e8939907e9badacc17ca129e67a2f5f2ff84351dd01" 145 | ], 146 | [ 147 | "93N87D6uxSBzwXvpokpzg8FFmfQPmvX4xHoWQe3pLdYpbiwT5YV", 148 | "efea577acfb5d1d14d3b7b195c321566f12f87d2b77ea3a53f68df7ebf8604a801" 149 | ], 150 | [ 151 | "cMxXusSihaX58wpJ3tNuuUcZEQGt6DKJ1wEpxys88FFaQCYjku9h", 152 | "ef0b3b34f0958d8a268193a9814da92c3e8b58b4a4378a542863e34ac289cd830c01" 153 | ], 154 | ["13p1ijLwsnrcuyqcTvJXkq2ASdXqcnEBLE", "001ed467017f043e91ed4c44b4e8dd674db211c4e6"], 155 | ["3ALJH9Y951VCGcVZYAdpA3KchoP9McEj1G", "055ece0cadddc415b1980f001785947120acdb36fc"] 156 | ] 157 | 158 | @invalid_base58_strings [ 159 | ["Z9inZq4e2HGQRZQezDjFMmqgUE8NwMRok", "Invalid checksum"], 160 | ["3HK7MezAm6qEZQUMPRf8jX7wDv6zig6Ky8", "Invalid checksum"], 161 | ["3AW8j12DUk8mgA7kkfZ1BrrzCVFuH1LsXS", "Invalid checksum"] 162 | # ["#####", "Non-base58 character"] # TODO: handle gracefully 163 | ] 164 | 165 | describe "decode_base!/1" do 166 | test "decode_base! properly decodes base58 encoded strings" do 167 | for pair <- @base58_encode_decode do 168 | [base16_str, base58_str] = pair 169 | base16_bin = Base.decode16!(base16_str, case: :lower) 170 | assert base16_bin == Base58.decode_base!(base58_str) 171 | end 172 | end 173 | end 174 | 175 | describe "encode_base!/1" do 176 | test "properly encodes to base58" do 177 | for pair <- @base58_encode_decode do 178 | [base16_str, base58_str] = pair 179 | base16_bin = Base.decode16!(base16_str, case: :lower) 180 | assert base58_str == Base58.encode_base(base16_bin) 181 | end 182 | end 183 | end 184 | 185 | describe "encode/1" do 186 | test "properly encodes Base58" do 187 | for pair <- @valid_base58_strings do 188 | [base58_str, base16_str] = pair 189 | 190 | base16_bin = Base.decode16!(base16_str, case: :lower) 191 | assert base58_str == Base58.encode(base16_bin) 192 | 193 | # double check 194 | {:ok, _decoded} = Base58.decode(base58_str) 195 | end 196 | end 197 | end 198 | 199 | describe "decode/1" do 200 | test "properly decodes Base58" do 201 | for pair <- @valid_base58_strings do 202 | [base58_str, base16_str] = pair 203 | base16_bin = Base.decode16!(base16_str, case: :lower) 204 | {:ok, decoded} = Base58.decode(base58_str) 205 | assert base16_bin == decoded 206 | end 207 | end 208 | 209 | test "catches invalid checksums" do 210 | for pair <- @invalid_base58_strings do 211 | [base58_str, _base16_str] = pair 212 | assert {:error, :invalid_checksum} = Base58.decode(base58_str) 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /test/bech32_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Bech32Test do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Bech32 4 | 5 | alias Bitcoinex.Bech32 6 | 7 | # Bech32 8 | @valid_bech32 [ 9 | "A12UEL5L", 10 | "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", 11 | "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", 12 | "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", 13 | "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w" 14 | ] 15 | 16 | @invalid_bech32_hrp_char_out_of_range [ 17 | <<0x20, "1nwldj5">>, 18 | <<0x7F, "1axkwrx">>, 19 | <<0x90::utf8, "1eym55h">> 20 | ] 21 | 22 | @invalid_bech32_max_length_exceeded [ 23 | "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx" 24 | ] 25 | 26 | @invalid_bech32_no_separator_character [ 27 | "pzry9x0s0muk" 28 | ] 29 | 30 | @invalid_bech32_empty_hrp [ 31 | "1pzry9x0s0muk", 32 | "10a06t8", 33 | "1qzzfhee" 34 | ] 35 | 36 | @invalid_bech32_checksum [ 37 | "A12UEL5A" 38 | ] 39 | 40 | @invalid_bech32_invalid_data_character [ 41 | "x1b4n0q5v" 42 | ] 43 | 44 | @invalid_bech32_too_short_checksum [ 45 | "li1dgmt3" 46 | ] 47 | 48 | @invalid_bech32_invalid_character_in_checksum [ 49 | <<"de1lg7wt", 0xFF::utf8>> 50 | ] 51 | 52 | # Bech32m 53 | @valid_bech32m [ 54 | "A1LQFN3A", 55 | "a1lqfn3a", 56 | "an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6", 57 | "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx", 58 | "11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8", 59 | "split1checkupstagehandshakeupstreamerranterredcaperredlc445v", 60 | "?1v759aa" 61 | ] 62 | 63 | @invalid_bech32m_hrp_char_out_of_range [ 64 | <<0x20, "1xj0phk">>, 65 | <<0x7F, "1g6xzxy">>, 66 | <<0x90::utf8, "1vctc34">> 67 | ] 68 | 69 | @invalid_bech32m_max_length_exceeded [ 70 | "an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4" 71 | ] 72 | 73 | @invalid_bech32m_no_separator_character [ 74 | "qyrz8wqd2c9m" 75 | ] 76 | 77 | @invalid_bech32m_empty_hrp [ 78 | "1qyrz8wqd2c9m", 79 | "16plkw9", 80 | "1p2gdwpf" 81 | ] 82 | 83 | @invalid_bech32m_checksum [ 84 | "M1VUXWEZ" 85 | ] 86 | 87 | @invalid_bech32m_invalid_data_character [ 88 | "y1b0jsk6g", 89 | "lt1igcx5c0" 90 | ] 91 | 92 | @invalid_bech32m_too_short_checksum [ 93 | "in1muywd" 94 | ] 95 | 96 | @invalid_bech32m_invalid_character_in_checksum [ 97 | "mm1crxm3i", 98 | "au1s5cgom" 99 | ] 100 | 101 | describe "decode/1 for bech32" do 102 | test "successfully decode with valid bech32" do 103 | for bech <- @valid_bech32 do 104 | assert {:ok, {:bech32, hrp, data}} = Bech32.decode(bech) 105 | assert hrp != nil 106 | 107 | # encode after decode should be the same(after downcase) as before 108 | {:ok, new_bech} = Bech32.encode(hrp, data, :bech32) 109 | assert new_bech == String.downcase(bech) 110 | end 111 | end 112 | 113 | test "fail to decode with invalid bech32 out of ranges" do 114 | for bech <- @invalid_bech32_hrp_char_out_of_range do 115 | assert {:error, msg} = Bech32.decode(bech) 116 | assert msg == :hrp_char_out_opf_range 117 | end 118 | end 119 | 120 | test "fail to decode with invalid bech32 overall max length exceeded" do 121 | for bech <- @invalid_bech32_max_length_exceeded do 122 | assert {:error, msg} = Bech32.decode(bech) 123 | assert msg == :overall_max_length_exceeded 124 | end 125 | end 126 | 127 | test "fail to decode with invalid bech32 no separator character" do 128 | for bech <- @invalid_bech32_no_separator_character do 129 | assert {:error, msg} = Bech32.decode(bech) 130 | assert msg == :no_separator_character 131 | end 132 | end 133 | 134 | test "fail to decode with invalid bech32 empty hrp" do 135 | for bech <- @invalid_bech32_empty_hrp do 136 | assert {:error, msg} = Bech32.decode(bech) 137 | assert msg == :empty_hrp 138 | end 139 | end 140 | 141 | test "fail to decode with invalid data character" do 142 | for bech <- @invalid_bech32_invalid_data_character do 143 | assert {:error, msg} = Bech32.decode(bech) 144 | assert msg == :contain_invalid_data_char 145 | end 146 | end 147 | 148 | test "fail to decode with too short checksum" do 149 | for bech <- @invalid_bech32_too_short_checksum do 150 | assert {:error, msg} = Bech32.decode(bech) 151 | assert msg == :too_short_checksum 152 | end 153 | end 154 | 155 | test "fail to decode with invalid character in checksum" do 156 | for bech <- @invalid_bech32_invalid_character_in_checksum do 157 | assert {:error, msg} = Bech32.decode(bech) 158 | assert msg == :contain_invalid_data_char 159 | end 160 | end 161 | 162 | test "fail to decode with invalid checksum" do 163 | for bech <- @invalid_bech32_checksum do 164 | assert {:error, msg} = Bech32.decode(bech) 165 | assert msg == :invalid_checksum 166 | end 167 | end 168 | end 169 | 170 | describe "decode/1 for bech32m" do 171 | test "successfully decode with valid bech32" do 172 | for bech <- @valid_bech32m do 173 | assert {:ok, {:bech32m, hrp, data}} = Bech32.decode(bech) 174 | assert hrp != nil 175 | 176 | # encode after decode should be the same(after downcase) as before 177 | {:ok, new_bech} = Bech32.encode(hrp, data, :bech32m) 178 | assert new_bech == String.downcase(bech) 179 | end 180 | end 181 | 182 | test "fail to decode with invalid bech32m out of ranges" do 183 | for bech <- @invalid_bech32m_hrp_char_out_of_range do 184 | assert {:error, msg} = Bech32.decode(bech) 185 | assert msg == :hrp_char_out_opf_range 186 | end 187 | end 188 | 189 | test "fail to decode with invalid bech32m overall max length exceeded" do 190 | for bech <- @invalid_bech32m_max_length_exceeded do 191 | assert {:error, msg} = Bech32.decode(bech) 192 | assert msg == :overall_max_length_exceeded 193 | end 194 | end 195 | 196 | test "fail to decode with invalid bech32m no separator character" do 197 | for bech <- @invalid_bech32m_no_separator_character do 198 | assert {:error, msg} = Bech32.decode(bech) 199 | assert msg == :no_separator_character 200 | end 201 | end 202 | 203 | test "fail to decode with invalid bech32m empty hrp" do 204 | for bech <- @invalid_bech32m_empty_hrp do 205 | assert {:error, msg} = Bech32.decode(bech) 206 | assert msg == :empty_hrp 207 | end 208 | end 209 | 210 | test "fail to decode with invalid data character" do 211 | for bech <- @invalid_bech32m_invalid_data_character do 212 | assert {:error, msg} = Bech32.decode(bech) 213 | assert msg == :contain_invalid_data_char 214 | end 215 | end 216 | 217 | test "fail to decode with too short checksum" do 218 | for bech <- @invalid_bech32m_too_short_checksum do 219 | assert {:error, msg} = Bech32.decode(bech) 220 | assert msg == :too_short_checksum 221 | end 222 | end 223 | 224 | test "fail to decode with invalid character in checksum" do 225 | for bech <- @invalid_bech32m_invalid_character_in_checksum do 226 | assert {:error, msg} = Bech32.decode(bech) 227 | assert msg == :contain_invalid_data_char 228 | end 229 | end 230 | 231 | test "fail to decode with invalid checksum" do 232 | for bech <- @invalid_bech32m_checksum do 233 | assert {:error, msg} = Bech32.decode(bech) 234 | assert msg == :invalid_checksum 235 | end 236 | end 237 | end 238 | 239 | describe "encode/2 for bech32" do 240 | test "successfully encode with valid hrp and empty data" do 241 | assert {:ok, _bech} = Bech32.encode("bc", [], :bech32) 242 | end 243 | 244 | test "successfully encode with valid hrp and non empty valid data" do 245 | assert {:ok, _bech} = Bech32.encode("bc", [1, 2], :bech32) 246 | end 247 | 248 | test "successfully encode with invalid string data" do 249 | assert {:ok, _bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7l", :bech32) 250 | end 251 | 252 | test "fail to encode with valid hrp and non empty valid string data" do 253 | assert {:error, _bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7lo", :bech32) 254 | end 255 | 256 | test "fail to encode with overall encoded length is over 90" do 257 | data = for _ <- 1..82, do: 1 258 | assert {:error, :overall_max_length_exceeded} = Bech32.encode("bc", data, :bech32) 259 | end 260 | 261 | test "fail to encode with hrp contain invalid char(out of 1 to 83 US-ASCII)" do 262 | data = [1] 263 | assert {:error, :hrp_char_out_opf_range} = Bech32.encode(" ", data, :bech32) 264 | assert {:error, :hrp_char_out_opf_range} = Bech32.encode("\ł", data, :bech32) 265 | assert {:error, :hrp_char_out_opf_range} = Bech32.encode("中文", data, :bech32) 266 | end 267 | end 268 | 269 | describe "encode/2 for bech32m" do 270 | test "successfully encode with valid hrp and empty data" do 271 | assert {:ok, _bech} = Bech32.encode("bc", [], :bech32m) 272 | end 273 | 274 | test "successfully encode with valid hrp and non empty valid data" do 275 | assert {:ok, _bech} = Bech32.encode("bc", [1, 2], :bech32m) 276 | end 277 | 278 | test "successfully encode with invalid string data" do 279 | assert {:ok, _bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7l", :bech32m) 280 | end 281 | 282 | test "fail to encode with valid hrp and non empty valid string data" do 283 | assert {:error, _bech} = Bech32.encode("bc", "qpzry9x8gf2tvdw0s3jn54khce6mua7lo", :bech32m) 284 | end 285 | 286 | test "fail to encode with overall encoded length is over 90" do 287 | data = for _ <- 1..82, do: 1 288 | assert {:error, :overall_max_length_exceeded} = Bech32.encode("bc", data, :bech32m) 289 | end 290 | 291 | test "fail to encode with hrp contain invalid char(out of 1 to 83 US-ASCII)" do 292 | data = [1] 293 | assert {:error, :hrp_char_out_opf_range} = Bech32.encode(" ", data, :bech32m) 294 | assert {:error, :hrp_char_out_opf_range} = Bech32.encode("\ł", data, :bech32m) 295 | assert {:error, :hrp_char_out_opf_range} = Bech32.encode("中文", data, :bech32m) 296 | end 297 | end 298 | end 299 | -------------------------------------------------------------------------------- /test/network.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Network do 2 | @enforce_keys [ 3 | :name, 4 | :hrp_segwit_prefix, 5 | :p2pkh_version_decimal_prefix, 6 | :p2sh_version_decimal_prefix 7 | ] 8 | defstruct [ 9 | :name, 10 | :hrp_segwit_prefix, 11 | :p2pkh_version_decimal_prefix, 12 | :p2sh_version_decimal_prefix 13 | ] 14 | 15 | @type t() :: %__MODULE__{ 16 | name: atom, 17 | hrp_segwit_prefix: String.t(), 18 | p2pkh_version_decimal_prefix: integer(), 19 | p2sh_version_decimal_prefix: integer() 20 | } 21 | 22 | @type network_name :: :mainnet | :testnet | :regtest 23 | 24 | def supported_networks() do 25 | [ 26 | mainnet(), 27 | testnet(), 28 | regtest() 29 | ] 30 | end 31 | 32 | def mainnet do 33 | %__MODULE__{ 34 | name: :mainnet, 35 | hrp_segwit_prefix: "bc", 36 | p2pkh_version_decimal_prefix: 0, 37 | p2sh_version_decimal_prefix: 5 38 | } 39 | end 40 | 41 | def testnet do 42 | %__MODULE__{ 43 | name: :testnet, 44 | hrp_segwit_prefix: "tb", 45 | p2pkh_version_decimal_prefix: 111, 46 | p2sh_version_decimal_prefix: 196 47 | } 48 | end 49 | 50 | def regtest do 51 | %__MODULE__{ 52 | name: :regtest, 53 | hrp_segwit_prefix: "bcrt", 54 | p2pkh_version_decimal_prefix: 111, 55 | p2sh_version_decimal_prefix: 196 56 | } 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/secp256k1/ecdsa_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.EcdsaTest do 2 | use ExUnit.Case 3 | 4 | doctest Bitcoinex.Secp256k1.Ecdsa 5 | 6 | alias Bitcoinex.Secp256k1.{Ecdsa, Point, PrivateKey, Signature} 7 | 8 | @valid_signatures_for_public_key_recovery [ 9 | %{ 10 | message_hash: 11 | Base.decode16!( 12 | "CE0677BB30BAA8CF067C88DB9811F4333D131BF8BCF12FE7065D211DCE971008", 13 | case: :upper 14 | ), 15 | signature: 16 | Base.decode16!( 17 | "90F27B8B488DB00B00606796D2987F6A5F59AE62EA05EFFE84FEF5B8B0E549984A691139AD57A3F0B906637673AA2F63D1F55CB1A69199D4009EEA23CEADDC93", 18 | case: :upper 19 | ), 20 | recovery_id: 1, 21 | pubkey: "02e32df42865e97135acfb65f3bae71bdc86f4d49150ad6a440b6f15878109880a" 22 | }, 23 | %{ 24 | message_hash: 25 | Base.decode16!( 26 | "5555555555555555555555555555555555555555555555555555555555555555", 27 | case: :upper 28 | ), 29 | signature: 30 | Base.decode16!( 31 | "01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", 32 | case: :upper 33 | ), 34 | recovery_id: 0, 35 | pubkey: "02c1ab1d7b32c1adcdab9d378c2ae75ee27822541c6875beed3255f981f0dea378" 36 | } 37 | ] 38 | 39 | @invalid_signatures_for_public_key_recovery [ 40 | %{ 41 | # invalid curve point 42 | message_hash: 43 | Base.decode16!( 44 | "00C547E4F7B0F325AD1E56F57E26C745B09A3E503D86E00E5255FF7F715D3D1C", 45 | case: :upper 46 | ), 47 | signature: 48 | Base.decode16!( 49 | "00B1693892219D736CABA55BDB67216E485557EA6B6AF75F37096C9AA6A5A75F00B940B1D03B21E36B0E47E79769F095FE2AB855BD91E3A38756B7D75A9C4549", 50 | case: :upper 51 | ), 52 | recovery_id: 0 53 | }, 54 | %{ 55 | # Low r and s. 56 | message_hash: 57 | Base.decode16!( 58 | "BA09EDC1275A285FB27BFE82C4EEA240A907A0DBAF9E55764B8F318C37D5974F", 59 | case: :upper 60 | ), 61 | signature: 62 | Base.decode16!( 63 | "00000000000000000000000000000000000000000000000000000000000000002C0000000000000000000000000000000000000000000000000000000000000004", 64 | case: :upper 65 | ), 66 | recovery_id: 1 67 | }, 68 | %{ 69 | # invalid signature 70 | message_hash: 71 | Base.decode16!( 72 | "5555555555555555555555555555555555555555555555555555555555555555", 73 | case: :upper 74 | ), 75 | signature: 76 | Base.decode16!( 77 | "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 78 | case: :upper 79 | ), 80 | recovery_id: 0 81 | } 82 | ] 83 | 84 | @valid_signature_pubkey_sighash_sets [ 85 | %{ 86 | # valid signature from private_key used in privatekey_test.exs and msg "hello world" 87 | privkey: %PrivateKey{d: 123_414_253_234_542_345_423_623}, 88 | # 3044022071223e8822fafbc0b09336d3f2a92fd7970a354d40185d69a297e0500e6c91e602202697b97c52da81a9328fd65a0ad883545f162cc3e5e2c70ea226c0d1cd4ae392 89 | signature: %Signature{ 90 | r: 91 | 51_171_856_268_621_681_203_379_064_931_680_562_348_117_352_680_621_396_833_116_333_722_055_478_120_934, 92 | s: 93 | 17_455_962_327_778_698_045_206_777_017_096_967_323_286_973_535_288_379_967_544_467_291_763_458_630_546 94 | }, 95 | # "033b15e1b8c51bb947a134d17addc3eb6abbda551ad02137699636f907ad7e0f1a" 96 | pubkey: %Point{ 97 | x: 98 | 26_725_119_729_089_203_965_150_132_282_997_341_343_516_273_140_835_737_223_575_952_640_907_021_258_522, 99 | y: 100 | 35_176_335_436_138_229_778_595_179_837_068_778_482_032_382_451_813_967_420_917_290_469_529_927_283_651 101 | }, 102 | msg: "hello world" 103 | } 104 | ] 105 | 106 | @rfc6979_test_cases [ 107 | # From https://bitcointalk.org/index.php?topic=285142.msg3150733 108 | %{ 109 | d: 0x1, 110 | m: "Satoshi Nakamoto", 111 | k: 0x8F8A276C19F4149656B280621E358CCE24F5F52542772691EE69063B74F15D15, 112 | sig: 113 | "934b1ea10a4b3c1757e2b0c017d0b6143ce3c9a7e6a4a49860d7a6ab210ee3d8dbbd3162d46e9f9bef7feb87c16dc13b4f6568a87f4e83f728e2443ba586675c" 114 | }, 115 | %{ 116 | d: 0x1, 117 | m: "All those moments will be lost in time, like tears in rain. Time to die...", 118 | k: 0x38AA22D72376B4DBC472E06C3BA403EE0A394DA63FC58D88686C611ABA98D6B3, 119 | sig: 120 | "8600dbd41e348fe5c9465ab92d23e3db8b98b873beecd930736488696438cb6bab8019bbd8b6924cc4099fe625340ffb1eaac34bf4477daa39d0835429094520" 121 | }, 122 | %{ 123 | d: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140, 124 | m: "Satoshi Nakamoto", 125 | k: 0x33A19B60E25FB6F4435AF53A3D42D493644827367E6453928554F43E49AA6F90, 126 | sig: 127 | "fd567d121db66e382991534ada77a6bd3106f0a1098c231e47993447cd6af2d094c632f14e4379fc1ea610a3df5a375152549736425ee17cebe10abbc2a2826c" 128 | }, 129 | %{ 130 | d: 0xF8B8AF8CE3C7CCA5E300D33939540C10D45CE001B8F252BFBC57BA0342904181, 131 | m: "Alan Turing", 132 | k: 0x525A82B70E67874398067543FD84C83D30C175FDC45FDEEE082FE13B1D7CFDF1, 133 | sig: 134 | "7063ae83e7f62bbb171798131b4a0564b956930092b33b07b395615d9ec7e15ca72033e1ff5ca1ea8d0c99001cb45f0272d3be7525d3049c0d9e98dc7582b857" 135 | }, 136 | # from https://bitcointalk.org/index.php?topic=285142.40 137 | %{ 138 | d: 0xE91671C46231F833A6406CCBEA0E3E392C76C167BAC1CB013F6F1013980455C2, 139 | m: 140 | "There is a computer disease that anybody who works with computers knows about. It's a very serious disease and it interferes completely with the work. The trouble with computers is that you 'play' with them!", 141 | k: 0x1F4B84C23A86A221D233F2521BE018D9318639D5B8BBD6374A8A59232D16AD3D, 142 | sig: 143 | "b552edd27580141f3b2a5463048cb7cd3e047b97c9f98076c32dbdf85a68718b279fa72dd19bfae05577e06c7c0c1900c371fcd5893f7e1d56a37d30174671f6" 144 | } 145 | ] 146 | 147 | describe "test deterministic k calculation" do 148 | test "successfully derive correct k value" do 149 | for t <- @rfc6979_test_cases do 150 | p = %PrivateKey{d: t.d} 151 | z = :binary.decode_unsigned(:crypto.hash(:sha256, t.m)) 152 | assert Ecdsa.deterministic_k(p, z) == %PrivateKey{d: t.k} 153 | end 154 | end 155 | end 156 | 157 | describe "ecdsa_recover_compact/3" do 158 | test "successfully recover a public key from a signature" do 159 | for t <- @valid_signatures_for_public_key_recovery do 160 | assert {:ok, recovered_pubkey} = 161 | Ecdsa.ecdsa_recover_compact(t.message_hash, t.signature, t.recovery_id) 162 | 163 | assert recovered_pubkey == t.pubkey 164 | end 165 | end 166 | 167 | test "unsuccessfully recover a public key from a signature" do 168 | for t <- @invalid_signatures_for_public_key_recovery do 169 | assert {:error, _error} = 170 | Ecdsa.ecdsa_recover_compact(t.message_hash, t.signature, t.recovery_id) 171 | end 172 | end 173 | end 174 | 175 | describe "sign/2" do 176 | test "successfully sign message with private key" do 177 | sk = %PrivateKey{d: 123_414_253_234_542_345_423_623} 178 | msg = "hello world" 179 | 180 | correct_sig = %Signature{ 181 | r: 182 | 51_171_856_268_621_681_203_379_064_931_680_562_348_117_352_680_621_396_833_116_333_722_055_478_120_934, 183 | s: 184 | 17_455_962_327_778_698_045_206_777_017_096_967_323_286_973_535_288_379_967_544_467_291_763_458_630_546 185 | } 186 | 187 | correct_der = 188 | "3044022071223e8822fafbc0b09336d3f2a92fd7970a354d40185d69a297e0500e6c91e602202697b97c52da81a9328fd65a0ad883545f162cc3e5e2c70ea226c0d1cd4ae392" 189 | 190 | z = :binary.decode_unsigned(Bitcoinex.Utils.double_sha256(msg)) 191 | sig = Ecdsa.sign(sk, z) 192 | assert sig == correct_sig 193 | der = Signature.der_serialize_signature(sig) 194 | assert Base.encode16(der, case: :lower) == correct_der 195 | end 196 | end 197 | 198 | describe "fuzz test signing" do 199 | setup do 200 | privkey = %PrivateKey{d: 123_414_253_234_542_345_423_623} 201 | pubkey = PrivateKey.to_point(privkey) 202 | {:ok, privkey: privkey, pubkey: pubkey} 203 | end 204 | 205 | test "successfully sign a large number of random sighashes", %{ 206 | privkey: privkey, 207 | pubkey: pubkey 208 | } do 209 | for _ <- 1..1000 do 210 | z = 211 | 32 212 | |> :crypto.strong_rand_bytes() 213 | |> :binary.decode_unsigned() 214 | 215 | sig = Ecdsa.sign(privkey, z) 216 | assert Ecdsa.verify_signature(pubkey, z, sig) 217 | end 218 | end 219 | 220 | test "successfully sign a sighash with a large number of keys" do 221 | z = 222 | 32 223 | |> :crypto.strong_rand_bytes() 224 | |> :binary.decode_unsigned() 225 | 226 | for _ <- 1..1000 do 227 | secret = 228 | 32 229 | |> :crypto.strong_rand_bytes() 230 | |> :binary.decode_unsigned() 231 | 232 | privkey = %PrivateKey{d: secret} 233 | pubkey = PrivateKey.to_point(privkey) 234 | sig = Ecdsa.sign(privkey, z) 235 | assert Ecdsa.verify_signature(pubkey, z, sig) 236 | end 237 | end 238 | end 239 | 240 | describe "verify_signature/3" do 241 | test "successfully verify signature with pubkey and message hash" do 242 | for t <- @valid_signature_pubkey_sighash_sets do 243 | z = :binary.decode_unsigned(Bitcoinex.Utils.double_sha256(t.msg)) 244 | sig = Ecdsa.sign(t.privkey, z) 245 | assert sig == t.signature 246 | assert Ecdsa.verify_signature(t.pubkey, z, sig) 247 | end 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /test/secp256k1/point_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.PointTest do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Secp256k1.Point 4 | 5 | alias Bitcoinex.Secp256k1 6 | alias Bitcoinex.Secp256k1.Point 7 | 8 | @x_only_pubkeys [ 9 | "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", 10 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 11 | "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", 12 | "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", 13 | "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9" 14 | ] 15 | 16 | describe "serialize_public_key/1" do 17 | test "successfully pad public key" do 18 | assert "020003b94aecea4d0a57a6c87cf43c50c8b3736f33ab7fd34f02441b6e94477689" == 19 | Point.serialize_public_key(%Point{ 20 | x: 21 | 6_579_384_254_631_425_969_190_483_614_785_133_746_155_874_651_439_631_590_927_590_192_220_436_105, 22 | y: 23 | 71_870_263_570_581_286_056_939_190_487_148_011_225_641_308_782_404_760_504_903_461_107_415_970_265_024 24 | }) 25 | end 26 | end 27 | 28 | describe "parse_public_key/1" do 29 | test "successfully parse public key from sec" do 30 | sec = 31 | Base.decode16!("033b15e1b8c51bb947a134d17addc3eb6abbda551ad02137699636f907ad7e0f1a", 32 | case: :lower 33 | ) 34 | 35 | assert Point.parse_public_key(sec) == 36 | {:ok, 37 | %Point{ 38 | x: 39 | 26_725_119_729_089_203_965_150_132_282_997_341_343_516_273_140_835_737_223_575_952_640_907_021_258_522, 40 | y: 41 | 35_176_335_436_138_229_778_595_179_837_068_778_482_032_382_451_813_967_420_917_290_469_529_927_283_651 42 | }} 43 | end 44 | 45 | test "successfully parse uncompressed key from sec" do 46 | sec = 47 | Base.decode16!( 48 | "048fdc3d8944cc8d8fe6c666c41a8ed42e60aa399861a756707e127a80b383d178edfbf94dda0487f7910d130f2a37a0647be9335eab5b8d3aa5242445e1604024", 49 | case: :lower 50 | ) 51 | 52 | assert Point.parse_public_key(sec) == 53 | {:ok, 54 | %Point{ 55 | x: 0x8FDC3D8944CC8D8FE6C666C41A8ED42E60AA399861A756707E127A80B383D178, 56 | y: 0xEDFBF94DDA0487F7910D130F2A37A0647BE9335EAB5B8D3AA5242445E1604024 57 | }} 58 | end 59 | 60 | test "successfully parse compressed key from sec hex" do 61 | sec = "0299d7ff3d96c731e54e75637798cab801fe80827191e280f53427bc8915323e8b" 62 | 63 | pk = %Point{ 64 | x: 65 | 69_585_499_557_921_076_123_288_400_932_281_161_043_766_220_600_235_811_505_715_105_664_976_077_078_155, 66 | y: 67 | 102_549_807_389_226_195_103_316_638_704_859_105_787_106_440_500_810_433_784_118_696_258_589_643_376_818, 68 | z: 0 69 | } 70 | 71 | assert Point.parse_public_key(sec) == {:ok, pk} 72 | assert Point.serialize_public_key(pk) == sec 73 | end 74 | end 75 | 76 | describe "sec/1" do 77 | test "successfully calculate SEC encoding and hash160 of public key" do 78 | correct_hash160 = "d1914384b57de2944ce1b6a90adf2f7b72cfe61e" 79 | 80 | hash160 = 81 | %Point{ 82 | x: 83 | 26_725_119_729_089_203_965_150_132_282_997_341_343_516_273_140_835_737_223_575_952_640_907_021_258_522, 84 | y: 85 | 35_176_335_436_138_229_778_595_179_837_068_778_482_032_382_451_813_967_420_917_290_469_529_927_283_651 86 | } 87 | |> Point.sec() 88 | |> Bitcoinex.Utils.hash160() 89 | |> Base.encode16(case: :lower) 90 | 91 | assert correct_hash160 == hash160 92 | end 93 | end 94 | 95 | describe "lift_x/1" do 96 | test "lift_x on 32-byte x-only pubkeys" do 97 | for t <- @x_only_pubkeys do 98 | {:ok, pubkey} = Point.lift_x(t) 99 | assert Point.has_even_y(pubkey) 100 | assert Secp256k1.verify_point(pubkey) 101 | end 102 | end 103 | 104 | test "return error when x is equal or greater than Secp256k1 p" do 105 | for i <- 0..2 do 106 | assert {:error, error} = Point.lift_x(Bitcoinex.Secp256k1.Params.curve().p + i) 107 | assert String.match?(error, ~r/(too large)/) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /test/secp256k1/privatekey_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.PrivateKeyTest do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Secp256k1.PrivateKey 4 | 5 | alias Bitcoinex.Secp256k1.PrivateKey 6 | 7 | describe "serialize_private_key/1" do 8 | test "successfully pad private key" do 9 | sk = %PrivateKey{d: 123_414_253_234_542_345_423_623} 10 | 11 | assert "000000000000000000000000000000000000000000001a224cd1a01427f38b07" == 12 | PrivateKey.serialize_private_key(sk) 13 | end 14 | end 15 | 16 | describe "wif/2" do 17 | test "successfully return private key wif encoding" do 18 | sk = %PrivateKey{d: 123_414_253_234_542_345_423_623} 19 | 20 | assert "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E" == 21 | PrivateKey.wif!(sk, :mainnet) 22 | 23 | assert "cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm" == 24 | PrivateKey.wif!(sk, :testnet) 25 | end 26 | end 27 | 28 | describe "parse_wif/1" do 29 | test "successfully return private key from wif str" do 30 | sk = %PrivateKey{d: 123_414_253_234_542_345_423_623} 31 | wif = "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi4ZxKRdkhWeLbjoGkhRF5E" 32 | assert {:ok, sk, :mainnet, true} == PrivateKey.parse_wif(wif) 33 | twif = "cMahea7zqjxrtgAbB7LSGbcQUr1uX1okdzTtkBA29TFk41r74ddm" 34 | assert {:ok, sk, :testnet, true} == PrivateKey.parse_wif(twif) 35 | end 36 | end 37 | 38 | describe "parse & serialize wif" do 39 | test "successfully return private key from wif str" do 40 | sk = %PrivateKey{d: 123_414_253_234_542_345_423_623} 41 | wif = PrivateKey.wif!(sk, :mainnet) 42 | assert {:ok, sk, :mainnet, true} == PrivateKey.parse_wif(wif) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/secp256k1/schnorr_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.SchnorrTest do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Secp256k1.Schnorr 4 | 5 | alias Bitcoinex.Utils 6 | alias Bitcoinex.Secp256k1 7 | alias Bitcoinex.Secp256k1.{Point, PrivateKey, Schnorr, Signature} 8 | 9 | # BIP340 official test vectors: 10 | # https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv 11 | @schnorr_signatures_with_secrets [ 12 | %{ 13 | secret: 3, 14 | pubkey: "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", 15 | aux_rand: 0x0000000000000000000000000000000000000000000000000000000000000000, 16 | message: 0x0000000000000000000000000000000000000000000000000000000000000000, 17 | signature: 18 | "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0", 19 | result: true 20 | }, 21 | %{ 22 | secret: 0xB7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF, 23 | pubkey: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 24 | aux_rand: 0x0000000000000000000000000000000000000000000000000000000000000001, 25 | message: 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 26 | signature: 27 | "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A", 28 | result: true 29 | }, 30 | %{ 31 | secret: 0xC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9, 32 | pubkey: "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", 33 | aux_rand: 0xC87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906, 34 | message: 0x7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C, 35 | signature: 36 | "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7", 37 | result: true 38 | }, 39 | # test fails if msg is reduced mod p or n 40 | %{ 41 | secret: 0x0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710, 42 | pubkey: "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", 43 | aux_rand: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 44 | message: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 45 | signature: 46 | "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3", 47 | result: true 48 | } 49 | ] 50 | 51 | # BIP340 official test vectors: 52 | # https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv 53 | @schnorr_signatures_no_secrets @schnorr_signatures_with_secrets ++ 54 | [ 55 | %{ 56 | pubkey: 57 | "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9", 58 | message: 59 | 0x4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703, 60 | signature: 61 | "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4", 62 | result: true 63 | }, 64 | %{ 65 | pubkey: 66 | "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", 67 | message: 68 | 0x4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703, 69 | signature: 70 | "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4", 71 | result: false 72 | }, 73 | %{ 74 | pubkey: 75 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 76 | message: 77 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 78 | signature: 79 | "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2", 80 | result: false 81 | }, 82 | %{ 83 | pubkey: 84 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 85 | message: 86 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 87 | signature: 88 | "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD", 89 | result: false 90 | }, 91 | %{ 92 | pubkey: 93 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 94 | message: 95 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 96 | signature: 97 | "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6", 98 | result: false 99 | }, 100 | %{ 101 | pubkey: 102 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 103 | message: 104 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 105 | signature: 106 | "0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051", 107 | result: false 108 | }, 109 | %{ 110 | pubkey: 111 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 112 | message: 113 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 114 | signature: 115 | "00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197", 116 | result: false 117 | }, 118 | %{ 119 | pubkey: 120 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 121 | message: 122 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 123 | signature: 124 | "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", 125 | result: false 126 | }, 127 | %{ 128 | pubkey: 129 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 130 | message: 131 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 132 | signature: 133 | "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", 134 | result: false 135 | }, 136 | %{ 137 | pubkey: 138 | "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", 139 | message: 140 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 141 | signature: 142 | "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 143 | result: false 144 | }, 145 | %{ 146 | pubkey: 147 | "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", 148 | message: 149 | 0x243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89, 150 | signature: 151 | "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", 152 | result: false 153 | } 154 | ] 155 | 156 | describe "sign/3" do 157 | test "sign" do 158 | for t <- @schnorr_signatures_with_secrets do 159 | {:ok, sk} = PrivateKey.new(t.secret) 160 | 161 | {:ok, sig} = Schnorr.sign(sk, t.message, t.aux_rand) 162 | result = Signature.serialize_signature(sig) |> Base.encode16(case: :upper) == t.signature 163 | assert result == t.result 164 | end 165 | end 166 | 167 | test "sign & verify" do 168 | for t <- @schnorr_signatures_with_secrets do 169 | {:ok, sk} = PrivateKey.new(t.secret) 170 | {:ok, pubkey} = Point.lift_x(t.pubkey) 171 | 172 | {:ok, sig} = Schnorr.sign(sk, t.message, t.aux_rand) 173 | result = Schnorr.verify_signature(pubkey, t.message, sig) 174 | assert result == t.result 175 | end 176 | end 177 | end 178 | 179 | describe "verify_signature/3" do 180 | test "verify_signature" do 181 | for t <- @schnorr_signatures_no_secrets do 182 | pk_res = 183 | t.pubkey 184 | |> Utils.hex_to_bin() 185 | |> Point.lift_x() 186 | 187 | sig_res = 188 | t.signature 189 | |> Base.decode16!(case: :upper) 190 | |> Signature.parse_signature() 191 | 192 | case {pk_res, sig_res} do 193 | {{:ok, pubkey}, {:ok, sig}} -> 194 | assert Schnorr.verify_signature(pubkey, t.message, sig) == t.result 195 | 196 | _ -> 197 | assert !t.result 198 | end 199 | end 200 | end 201 | end 202 | 203 | describe "fuzz test signing" do 204 | setup do 205 | privkey = Secp256k1.force_even_y(%PrivateKey{d: 123_414_253_234_542_345_423_623}) 206 | pubkey = PrivateKey.to_point(privkey) 207 | {:ok, privkey: privkey, pubkey: pubkey} 208 | end 209 | 210 | test "successfully sign a large number of random messages", %{ 211 | privkey: privkey, 212 | pubkey: pubkey 213 | } do 214 | aux = 215 | 32 216 | |> :crypto.strong_rand_bytes() 217 | |> :binary.decode_unsigned() 218 | 219 | for _ <- 1..1000 do 220 | z = 221 | 32 222 | |> :crypto.strong_rand_bytes() 223 | |> :binary.decode_unsigned() 224 | 225 | {:ok, sig} = Schnorr.sign(privkey, z, aux) 226 | assert Schnorr.verify_signature(pubkey, z, sig) 227 | end 228 | end 229 | 230 | test "successfully sign a message with a large number of aux inputs", %{ 231 | privkey: privkey, 232 | pubkey: pubkey 233 | } do 234 | z = 235 | 32 236 | |> :crypto.strong_rand_bytes() 237 | |> :binary.decode_unsigned() 238 | 239 | for _ <- 1..1000 do 240 | aux = 241 | 32 242 | |> :crypto.strong_rand_bytes() 243 | |> :binary.decode_unsigned() 244 | 245 | {:ok, sig} = Schnorr.sign(privkey, z, aux) 246 | assert Schnorr.verify_signature(pubkey, z, sig) 247 | end 248 | end 249 | 250 | test "successfully sign a message with a large number of keys" do 251 | z = 252 | 32 253 | |> :crypto.strong_rand_bytes() 254 | |> :binary.decode_unsigned() 255 | 256 | aux = 257 | 32 258 | |> :crypto.strong_rand_bytes() 259 | |> :binary.decode_unsigned() 260 | 261 | for _ <- 1..1000 do 262 | secret = 263 | 32 264 | |> :crypto.strong_rand_bytes() 265 | |> :binary.decode_unsigned() 266 | 267 | privkey = Secp256k1.force_even_y(%PrivateKey{d: secret}) 268 | pubkey = PrivateKey.to_point(privkey) 269 | {:ok, sig} = Schnorr.sign(privkey, z, aux) 270 | assert Schnorr.verify_signature(pubkey, z, sig) 271 | end 272 | end 273 | end 274 | end 275 | -------------------------------------------------------------------------------- /test/secp256k1/secp256k1_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.Secp256k1.Secp256k1Test do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Secp256k1 4 | 5 | alias Bitcoinex.Secp256k1 6 | alias Bitcoinex.Secp256k1.{Signature} 7 | 8 | @valid_der_signatures [ 9 | %{ 10 | # valid signature from 3ea1a64c550ff91c6faba076aa776faa60aa524b48a54801d458d1c927333c8f:0 11 | der_signature: 12 | Base.decode16!( 13 | "3044022006d29e78c6698c6338b2c216aa15a455b50833c6d850078e1a29292df8a38d8902206bf6335e3eee06df655933a31653e9d8e2ef39abb71c12065bc4f9a98e8473df", 14 | case: :lower 15 | ), 16 | obj_signature: %Secp256k1.Signature{ 17 | r: 18 | 3_086_008_707_114_705_845_761_137_809_128_827_774_006_369_836_063_216_572_143_683_251_326_398_205_321, 19 | s: 20 | 48_832_473_706_270_939_780_454_642_696_934_734_127_348_929_209_136_601_810_472_271_862_324_755_985_375 21 | } 22 | # pubkey: 027246ae6ffc3e4be3a2b3ee7392dc484a1d285f190a0532a514db5be823bcdd81 23 | }, 24 | %{ 25 | # valid signature from 40352adf6fba255e083c60a21f9f85774ce7c97017f542bf22c63be2ef9f366b:0 26 | # no high bits 27 | der_signature: 28 | Base.decode16!( 29 | "30440220363d2376abd4d166ee712210a8b92fc8713b93140103a618eeafd41d1497dca20220175894aee64dbfb35199a351b8fe2742aea529092e0473de3227f848eee162f6", 30 | case: :lower 31 | ), 32 | obj_signature: %Secp256k1.Signature{ 33 | r: 34 | 24_532_916_254_939_660_922_795_650_783_597_793_726_391_618_675_384_527_964_949_105_336_796_168_314_018, 35 | s: 36 | 10_559_704_232_859_480_938_506_730_553_108_837_258_684_636_748_731_694_899_240_401_738_284_146_975_478 37 | } 38 | # pubkey: 027246ae6ffc3e4be3a2b3ee7392dc484a1d285f190a0532a514db5be823bcdd81 39 | }, 40 | %{ 41 | # valid signature from f8f6704f1e80da23d1865627046eaec1f3d1a3288937bf3d12b9a3327aaa91de:0 42 | # r high bit 43 | der_signature: 44 | Base.decode16!( 45 | "3045022100974eb42bbc729f95f537cc41d52b6029731a2149cbce8dfb9e335f76a0e8b024022056dbeffa20d7b4231708f110b1789ad5b021fcb235e75099d50b502eabf5cae9", 46 | case: :lower 47 | ), 48 | obj_signature: %Secp256k1.Signature{ 49 | r: 50 | 68_438_297_700_591_931_769_061_022_939_284_422_764_636_608_635_142_803_529_940_449_930_283_506_511_908, 51 | s: 52 | 39_287_500_746_169_653_973_150_952_458_317_583_883_135_951_896_192_490_367_558_600_116_141_508_905_705 53 | } 54 | }, 55 | %{ 56 | # valid signature from private_key used in privatekey_test.exs and msg "hello world" 57 | der_signature: 58 | Base.decode16!( 59 | "3044022071223e8822fafbc0b09336d3f2a92fd7970a354d40185d69a297e0500e6c91e602202697b97c52da81a9328fd65a0ad883545f162cc3e5e2c70ea226c0d1cd4ae392", 60 | case: :lower 61 | ), 62 | obj_signature: %Secp256k1.Signature{ 63 | r: 64 | 51_171_856_268_621_681_203_379_064_931_680_562_348_117_352_680_621_396_833_116_333_722_055_478_120_934, 65 | s: 66 | 17_455_962_327_778_698_045_206_777_017_096_967_323_286_973_535_288_379_967_544_467_291_763_458_630_546 67 | } 68 | } 69 | ] 70 | 71 | @invalid_der_signatures [ 72 | %{ 73 | # invalid signature - incorrect prefix 74 | der_signature: 75 | Base.decode16!( 76 | "4044022006d29e78c6698c6338b2c216aa15a455b50833c6d850078e1a29292df8a38d8902206bf6335e3eee06df655933a31653e9d8e2ef39abb71c12065bc4f9a98e8473df", 77 | case: :lower 78 | ) 79 | }, 80 | %{ 81 | # invalid signature - sighash appended 82 | der_signature: 83 | Base.decode16!( 84 | "3044022006d29e78c6698c6338b2c216aa15a455b50833c6d850078e1a29292df8a38d8902206bf6335e3eee06df655933a31653e9d8e2ef39abb71c12065bc4f9a98e8473df01", 85 | case: :lower 86 | ) 87 | }, 88 | %{ 89 | # invalid signature - missing key marker 90 | der_signature: 91 | Base.decode16!( 92 | "30442006d29e78c6698c6338b2c216aa15a455b50833c6d850078e1a29292df8a38d8902206bf6335e3eee06df655933a31653e9d8e2ef39abb71c12065bc4f9a98e8473df", 93 | case: :lower 94 | ) 95 | }, 96 | %{ 97 | # invalid signature - incorrect length byte 98 | der_signature: 99 | Base.decode16!( 100 | "3043022006d29e78c6698c6338b2c216aa15a455b50833c6d850078e1a29292df8a38d8902206bf6335e3eee06df655933a31653e9d8e2ef39abb71c12065bc4f9a98e8473df", 101 | case: :lower 102 | ) 103 | } 104 | ] 105 | 106 | @valid_schnorr_signatures [ 107 | "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0", 108 | "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A", 109 | "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7", 110 | "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3", 111 | "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4" 112 | ] 113 | 114 | describe "der_parse_signature/1" do 115 | test "successfully parse valid signature from DER binary" do 116 | for t <- @valid_der_signatures do 117 | {state, parsed_sig} = Secp256k1.Signature.der_parse_signature(t.der_signature) 118 | assert state == :ok 119 | assert parsed_sig == t.obj_signature 120 | assert Secp256k1.Signature.der_serialize_signature(t.obj_signature) == t.der_signature 121 | end 122 | end 123 | 124 | test "unsuccessfully parse signature from DER binary" do 125 | for t <- @invalid_der_signatures do 126 | assert {:error, _error} = Secp256k1.Signature.der_parse_signature(t.der_signature) 127 | end 128 | end 129 | end 130 | 131 | describe "parse_signature/1" do 132 | test "parse 64-byte schnorr signatures from binary" do 133 | for t <- @valid_schnorr_signatures do 134 | {res, _sig} = 135 | t 136 | |> Base.decode16!(case: :upper) 137 | |> Signature.parse_signature() 138 | 139 | assert res == :ok 140 | end 141 | end 142 | 143 | test "parse 64-byte schnorr signatures from string" do 144 | for t <- @valid_schnorr_signatures do 145 | {res, _sig} = 146 | t 147 | |> Signature.parse_signature() 148 | 149 | assert res == :ok 150 | end 151 | end 152 | 153 | test "ensure equavalent sigs parsed from string and binary" do 154 | for t <- @valid_schnorr_signatures do 155 | {:ok, sig1} = 156 | t 157 | |> Base.decode16!(case: :upper) 158 | |> Signature.parse_signature() 159 | 160 | {:ok, sig2} = 161 | t 162 | |> Signature.parse_signature() 163 | 164 | assert sig1 == sig2 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /test/segwit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.SegwitTest do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Segwit 4 | 5 | alias Bitcoinex.Segwit 6 | import Bitcoinex.Utils, only: [replicate: 2] 7 | 8 | @valid_segwit_address_hexscript_pairs_mainnet [ 9 | {"BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", 10 | "0014751e76e8199196d454941c45d1b3a323f1433bd6"}, 11 | {"bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", 12 | "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"}, 13 | {"BC1SW50QGDZ25J", "6002751e"}, 14 | {"bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", "5210751e76e8199196d454941c45d1b3a323"}, 15 | {"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", 16 | "512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"} 17 | ] 18 | 19 | @valid_segwit_address_hexscript_pairs_testnet [ 20 | {"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", 21 | "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"}, 22 | {"tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", 23 | "5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"}, 24 | {"tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", 25 | "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"} 26 | ] 27 | 28 | @valid_segwit_address_hexscript_pairs_regtest [ 29 | {"bcrt1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qzf4jry", 30 | "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"}, 31 | {"bcrt1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvseswlauz7", 32 | "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"} 33 | ] 34 | 35 | @invalid_segwit_addresses [ 36 | "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut", 37 | "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd", 38 | "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf", 39 | "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL", 40 | "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh", 41 | "tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47", 42 | "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4", 43 | "BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R", 44 | "bc1pw5dgrnzv", 45 | "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav", 46 | "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", 47 | "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", 48 | "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", 49 | "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq", 50 | "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf", 51 | "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j", 52 | "bc1rw5uspcuh", 53 | "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", 54 | "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", 55 | "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", 56 | "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", 57 | "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", 58 | "bc1gmk9yu" 59 | ] 60 | 61 | describe "decode/1" do 62 | test "successfully decode with valid segwit addresses in mainnet" do 63 | for {address, hexscript} <- @valid_segwit_address_hexscript_pairs_mainnet do 64 | assert_valid_segwit_address(address, hexscript, :mainnet) 65 | end 66 | end 67 | 68 | test "successfully decode with valid segwit addresses in testnet" do 69 | for {address, hexscript} <- @valid_segwit_address_hexscript_pairs_testnet do 70 | assert_valid_segwit_address(address, hexscript, :testnet) 71 | end 72 | end 73 | 74 | test "successfully decode with valid segwit addresses in regtest" do 75 | for {address, hexscript} <- @valid_segwit_address_hexscript_pairs_regtest do 76 | assert_valid_segwit_address(address, hexscript, :regtest) 77 | end 78 | end 79 | 80 | test "fail to decode with invalid address" do 81 | for address <- @invalid_segwit_addresses do 82 | assert {:error, _error} = Segwit.decode_address(address) 83 | end 84 | end 85 | end 86 | 87 | describe "encode_address/1" do 88 | test "successfully encode with valid netwrok, version and program " do 89 | version = 1 90 | program = replicate(1, 10) 91 | assert {:ok, mainnet_address} = Segwit.encode_address(:mainnet, version, program) 92 | assert {:ok, testnet_address} = Segwit.encode_address(:testnet, version, program) 93 | assert {:ok, regtest_address} = Segwit.encode_address(:regtest, version, program) 94 | all_addresses = [mainnet_address, testnet_address, regtest_address] 95 | # make sure they are different 96 | assert Enum.uniq(all_addresses) == all_addresses 97 | end 98 | 99 | test "fail to encode with program length > 40 " do 100 | assert {:error, _error} = Segwit.encode_address(:mainnet, 1, replicate(1, 41)) 101 | end 102 | 103 | test "fail to encode with version 0 but program length not equalt to 20 or 32 " do 104 | assert {:ok, _address} = Segwit.encode_address(:mainnet, 0, replicate(1, 20)) 105 | assert {:ok, _address} = Segwit.encode_address(:mainnet, 0, replicate(1, 32)) 106 | assert {:error, _error} = Segwit.encode_address(:mainnet, 0, replicate(1, 21)) 107 | assert {:error, _error} = Segwit.encode_address(:mainnet, 0, replicate(1, 33)) 108 | end 109 | end 110 | 111 | describe "is_valid_segswit_address?/1" do 112 | test "return true given valid address" do 113 | for {address, _hexscript} <- 114 | @valid_segwit_address_hexscript_pairs_mainnet ++ 115 | @valid_segwit_address_hexscript_pairs_testnet ++ 116 | @valid_segwit_address_hexscript_pairs_regtest do 117 | assert Segwit.is_valid_segswit_address?(address) 118 | end 119 | end 120 | 121 | test "return false given invalid address" do 122 | for address <- @invalid_segwit_addresses do 123 | refute Segwit.is_valid_segswit_address?(address) 124 | end 125 | end 126 | end 127 | 128 | # local private test helper 129 | defp assert_valid_segwit_address(address, hexscript, network) do 130 | assert {:ok, {hrp, version, program}} = Segwit.decode_address(address) 131 | assert hrp == network 132 | assert version in 0..16 133 | assert Segwit.get_segwit_script_pubkey(version, program) == hexscript 134 | 135 | # encode after decode should be the same(after downcase) as before 136 | {:ok, new_address} = Segwit.encode_address(hrp, version, program) 137 | assert new_address == String.downcase(address) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/transaction_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bitcoinex.TransactionTest do 2 | use ExUnit.Case 3 | doctest Bitcoinex.Transaction 4 | 5 | alias Bitcoinex.Transaction 6 | 7 | @txn_serialization_1 %{ 8 | tx_hex: 9 | "01000000010470c3139dc0f0882f98d75ae5bf957e68dadd32c5f81261c0b13e85f592ff7b0000000000ffffffff02b286a61e000000001976a9140f39a0043cf7bdbe429c17e8b514599e9ec53dea88ac01000000000000001976a9148a8c9fd79173f90cf76410615d2a52d12d27d21288ac00000000" 10 | } 11 | 12 | @txn_segwit_serialization_1 %{ 13 | tx_hex: 14 | "01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000" 15 | } 16 | 17 | @txn_segwit_serialization_2 %{ 18 | tx_hex: 19 | "01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000" 20 | } 21 | 22 | @txn_segwit_serialization_3 %{ 23 | tx_hex: 24 | "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000" 25 | } 26 | 27 | @txn_segwit_serialization_4 %{ 28 | tx_hex: 29 | "0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000" 30 | } 31 | 32 | describe "decode/1" do 33 | test "decodes legacy bitcoin transaction" do 34 | txn_test = @txn_serialization_1 35 | {:ok, txn} = Transaction.decode(txn_test.tx_hex) 36 | assert 1 == length(txn.inputs) 37 | assert 2 == length(txn.outputs) 38 | assert 1 == txn.version 39 | assert nil == txn.witnesses 40 | assert 0 == txn.lock_time 41 | 42 | assert "b020bdec4e92cb69db93557dcbbfcc73076fc01f6828e41eb3ef5f628414ee62" == 43 | Transaction.transaction_id(txn) 44 | 45 | in_1 = Enum.at(txn.inputs, 0) 46 | 47 | assert "7bff92f5853eb1c06112f8c532ddda687e95bfe55ad7982f88f0c09d13c37004" == in_1.prev_txid 48 | assert 0 == in_1.prev_vout 49 | assert "" == in_1.script_sig 50 | assert 4_294_967_295 == in_1.sequence_no 51 | 52 | out_0 = Enum.at(txn.outputs, 0) 53 | assert 514_229_938 == out_0.value 54 | assert "76a9140f39a0043cf7bdbe429c17e8b514599e9ec53dea88ac" == out_0.script_pub_key 55 | 56 | out_1 = Enum.at(txn.outputs, 1) 57 | assert 1 == out_1.value 58 | assert "76a9148a8c9fd79173f90cf76410615d2a52d12d27d21288ac" == out_1.script_pub_key 59 | end 60 | 61 | test "decodes native segwit p2wpkh bitcoin transaction" do 62 | txn_test = @txn_segwit_serialization_1 63 | {:ok, txn} = Transaction.decode(txn_test.tx_hex) 64 | assert 2 == length(txn.inputs) 65 | assert 2 == length(txn.outputs) 66 | assert 1 == txn.version 67 | assert 2 == length(txn.witnesses) 68 | assert 17 == txn.lock_time 69 | 70 | assert "e8151a2af31c368a35053ddd4bdb285a8595c769a3ad83e0fa02314a602d4609" == 71 | Transaction.transaction_id(txn) 72 | 73 | in_1 = Enum.at(txn.inputs, 0) 74 | 75 | assert "9f96ade4b41d5433f4eda31e1738ec2b36f6e7d1420d94a6af99801a88f7f7ff" == in_1.prev_txid 76 | assert 0 == in_1.prev_vout 77 | 78 | assert "4830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01" == 79 | in_1.script_sig 80 | 81 | assert 4_294_967_278 == in_1.sequence_no 82 | 83 | in_2 = Enum.at(txn.inputs, 1) 84 | 85 | assert "8ac60eb9575db5b2d987e29f301b5b819ea83a5c6579d282d189cc04b8e151ef" == in_2.prev_txid 86 | assert 1 == in_2.prev_vout 87 | assert "" == in_2.script_sig 88 | assert 4_294_967_295 == in_2.sequence_no 89 | 90 | witness_in_0 = Enum.at(txn.witnesses, 0) 91 | assert 0 == witness_in_0.txinwitness 92 | 93 | witness_in_1 = Enum.at(txn.witnesses, 1) 94 | 95 | assert [ 96 | "304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee01", 97 | "025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357" 98 | ] == witness_in_1.txinwitness 99 | 100 | out_0 = Enum.at(txn.outputs, 0) 101 | assert 112_340_000 == out_0.value 102 | assert "76a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac" == out_0.script_pub_key 103 | 104 | out_1 = Enum.at(txn.outputs, 1) 105 | assert 223_450_000 == out_1.value 106 | assert "76a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac" == out_1.script_pub_key 107 | end 108 | 109 | test "decodes native segwit p2wsh bitcoin transaction" do 110 | txn_test = @txn_segwit_serialization_2 111 | {:ok, txn} = Transaction.decode(txn_test.tx_hex) 112 | assert 2 == length(txn.inputs) 113 | assert 1 == length(txn.outputs) 114 | assert 1 == txn.version 115 | assert 2 == length(txn.witnesses) 116 | assert 0 == txn.lock_time 117 | 118 | assert "570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab" == 119 | Transaction.transaction_id(txn) 120 | 121 | in_0 = Enum.at(txn.inputs, 0) 122 | 123 | assert "6eb316926b1c5d567cd6f5e6a84fec606fc53d7b474526d1fff3948020c93dfe" == in_0.prev_txid 124 | assert 0 == in_0.prev_vout 125 | 126 | assert "47304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201" == 127 | in_0.script_sig 128 | 129 | assert 4_294_967_295 == in_0.sequence_no 130 | 131 | in_1 = Enum.at(txn.inputs, 1) 132 | 133 | assert "f825690aee1b3dc247da796cacb12687a5e802429fd291cfd63e010f02cf1508" == in_1.prev_txid 134 | assert 0 == in_1.prev_vout 135 | assert "" == in_1.script_sig 136 | assert 4_294_967_295 == in_1.sequence_no 137 | 138 | witness_in_0 = Enum.at(txn.witnesses, 0) 139 | assert 0 == witness_in_0.txinwitness 140 | 141 | witness_in_1 = Enum.at(txn.witnesses, 1) 142 | 143 | assert [ 144 | "304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503", 145 | "3044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e2703", 146 | "21026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac" 147 | ] == witness_in_1.txinwitness 148 | 149 | out_1 = Enum.at(txn.outputs, 0) 150 | assert 5_000_000_000 == out_1.value 151 | assert "76a914a30741f8145e5acadf23f751864167f32e0963f788ac" == out_1.script_pub_key 152 | end 153 | 154 | test "decodes segwit p2sh-pw2pkh bitcoin transaction" do 155 | txn_test = @txn_segwit_serialization_3 156 | {:ok, txn} = Transaction.decode(txn_test.tx_hex) 157 | assert 1 == length(txn.inputs) 158 | assert 2 == length(txn.outputs) 159 | assert 1 == txn.version 160 | assert 1 == length(txn.witnesses) 161 | assert 1170 == txn.lock_time 162 | 163 | assert "ef48d9d0f595052e0f8cdcf825f7a5e50b6a388a81f206f3f4846e5ecd7a0c23" == 164 | Transaction.transaction_id(txn) 165 | 166 | in_0 = Enum.at(txn.inputs, 0) 167 | 168 | assert "77541aeb3c4dac9260b68f74f44c973081a9d4cb2ebe8038b2d70faa201b6bdb" == in_0.prev_txid 169 | assert 1 == in_0.prev_vout 170 | 171 | assert "16001479091972186c449eb1ded22b78e40d009bdf0089" == 172 | in_0.script_sig 173 | 174 | assert 4_294_967_294 == in_0.sequence_no 175 | 176 | witness_in_0 = Enum.at(txn.witnesses, 0) 177 | 178 | assert [ 179 | "3044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb01", 180 | "03ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a26873" 181 | ] == witness_in_0.txinwitness 182 | 183 | out_0 = Enum.at(txn.outputs, 0) 184 | assert 199_996_600 == out_0.value 185 | assert "76a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac" == out_0.script_pub_key 186 | 187 | out_1 = Enum.at(txn.outputs, 1) 188 | assert 800_000_000 == out_1.value 189 | assert "76a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac" == out_1.script_pub_key 190 | end 191 | 192 | test "decodes segwit p2sh-p2wsh bitcoin transaction" do 193 | txn_test = @txn_segwit_serialization_4 194 | {:ok, txn} = Transaction.decode(txn_test.tx_hex) 195 | assert 1 == length(txn.inputs) 196 | assert 2 == length(txn.outputs) 197 | assert 1 == txn.version 198 | assert 1 == length(txn.witnesses) 199 | assert 0 == txn.lock_time 200 | 201 | assert "27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac" == 202 | Transaction.transaction_id(txn) 203 | 204 | in_0 = Enum.at(txn.inputs, 0) 205 | 206 | assert "6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436" == in_0.prev_txid 207 | assert 1 == in_0.prev_vout 208 | 209 | assert "220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54" == 210 | in_0.script_sig 211 | 212 | assert 4_294_967_295 == in_0.sequence_no 213 | 214 | witness_in_0 = Enum.at(txn.witnesses, 0) 215 | 216 | assert [ 217 | "", 218 | "304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01", 219 | "3044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502", 220 | "3044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403", 221 | "3045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381", 222 | "3045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a0882", 223 | "30440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783", 224 | "56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae" 225 | ] == witness_in_0.txinwitness 226 | 227 | out_0 = Enum.at(txn.outputs, 0) 228 | assert 900_000_000 == out_0.value 229 | assert "76a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688ac" == out_0.script_pub_key 230 | 231 | out_1 = Enum.at(txn.outputs, 1) 232 | assert 87_000_000 == out_1.value 233 | assert "76a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac" == out_1.script_pub_key 234 | end 235 | end 236 | end 237 | --------------------------------------------------------------------------------