├── .github └── workflows │ ├── build.yml │ ├── codeql.yml │ └── docs.yml ├── .gitignore ├── .gitmodules ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── eth.gemspec ├── lib ├── eth.rb └── eth │ ├── address.rb │ ├── gas.rb │ ├── key.rb │ ├── key │ ├── decrypter.rb │ └── encrypter.rb │ ├── open_ssl.rb │ ├── secp256k1.rb │ ├── sedes.rb │ ├── tx.rb │ ├── utils.rb │ └── version.rb └── spec ├── eth ├── address_spec.rb ├── eip155_spec.rb ├── eth_spec.rb ├── key │ ├── decrypter_spec.rb │ └── encrypter_spec.rb ├── key_spec.rb ├── tx_spec.rb └── utils_spec.rb ├── ethereum_tests_spec.rb ├── fixtures ├── 66734e70ea28eaa28eb1bace4ca87573c48f52cca7590459ad20dc58bae1a819.hex ├── 7151f5b0d229c62a5076de4133ba06fffc033e25bf99691c3e0a0a99c5a64538.hex └── keys │ ├── testingtesting.json │ ├── testpassword.json │ └── testunknownkdf.json └── spec_helper.rb /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - develop 8 | push: 9 | branches: 10 | - develop 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest, macos-latest] 19 | ruby: ['2.7', '3.0'] 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Brew Automake 27 | run: | 28 | brew install automake 29 | if: startsWith(matrix.os, 'macOS') 30 | - name: Install Dependencies 31 | run: | 32 | git submodule update --init 33 | bundle install 34 | - name: Run Tests 35 | run: | 36 | bundle exec rspec 37 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CodeQL 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - develop 8 | push: 9 | branches: 10 | - develop 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: 24 | - ruby 25 | steps: 26 | - name: "Checkout repository" 27 | uses: actions/checkout@v2 28 | - name: "Initialize CodeQL" 29 | uses: github/codeql-action/init@v1 30 | with: 31 | languages: "${{ matrix.language }}" 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v1 34 | - name: "Perform CodeQL Analysis" 35 | uses: github/codeql-action/analyze@v1 36 | - uses: ruby/setup-ruby@v1 37 | with: 38 | ruby-version: '2.7' 39 | bundler-cache: true 40 | - name: "Run rufo code formatting checks" 41 | run: | 42 | gem install rufo 43 | rufo --check ./lib 44 | rufo --check ./spec 45 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs 3 | 4 | on: 5 | push: 6 | branches: 7 | - develop 8 | 9 | jobs: 10 | docs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: '2.7' 17 | bundler-cache: true 18 | - name: Run Yard Doc 19 | run: | 20 | gem install yard 21 | yard doc 22 | - name: Deploy GH Pages 23 | uses: JamesIves/github-pages-deploy-action@4.1.7 24 | with: 25 | branch: gh-pages 26 | folder: doc/ 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | 3 | /test/test_vectors.rb 4 | /ext/digest/Makefile 5 | /ext/digest/keccak.so 6 | /ext/digest/mkmf.log 7 | 8 | *.o 9 | *.so 10 | *.gem 11 | *.log 12 | *.rbc 13 | /.config 14 | /.rake_tasks~ 15 | /coverage/ 16 | /InstalledFiles 17 | /pkg/ 18 | /tmp/ 19 | 20 | # RSpec configuration and generated files: 21 | /.rspec 22 | /spec/examples.txt 23 | 24 | # Documentation cache and generated files: 25 | /.yardoc/ 26 | /_yardoc/ 27 | /doc/ 28 | /rdoc/ 29 | 30 | # Environment normalization: 31 | /.bundle/ 32 | /vendor/bundle/* 33 | !/vendor/bundle/.keep 34 | /lib/bundler/man/ 35 | 36 | # For a library or gem, you might want to ignore these files since the code is 37 | # intended to run in multiple environments; otherwise, check them in: 38 | /Gemfile.lock 39 | /.ruby-version 40 | /.ruby-gemset 41 | 42 | # Unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 43 | .rvmrc 44 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/fixtures/ethereum_tests"] 2 | path = spec/fixtures/ethereum_tests 3 | url = https://github.com/ethereum/tests 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require pry 2 | --require spec_helper 3 | --format documentation 4 | --color 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ### Unreleased 8 | 9 | ## [0.4.17] 10 | ### Changed 11 | - Gems: bump version to 0.4.17 [#70](https://github.com/se3000/ruby-eth/pull/70) 12 | - Gems: bump keccak to 1.3.0 [#69](https://github.com/se3000/ruby-eth/pull/69) 13 | 14 | ## [0.4.16] 15 | ### Changed 16 | - Docs: update changelog [#65](https://github.com/se3000/ruby-eth/pull/65) 17 | - Gems: bump version to 0.4.16 [#65](https://github.com/se3000/ruby-eth/pull/65) 18 | - License: update copyright notice [#64](https://github.com/se3000/ruby-eth/pull/64) 19 | - Docs: add badges to readme [#64](https://github.com/se3000/ruby-eth/pull/64) 20 | - Git: deprecating master [#63](https://github.com/se3000/ruby-eth/pull/63) 21 | - CI: replace travis with github actions [#62](https://github.com/se3000/ruby-eth/pull/62) 22 | - Gems: replace digest-sha3-patched with keccak [#58](https://github.com/se3000/ruby-eth/pull/58) 23 | 24 | ## [0.4.13], [0.4.14], [0.4.15] 25 | _Released as [`eth-patched`](https://github.com/q9f/ruby-eth) from a different source tree._ 26 | 27 | ## [0.4.12] 28 | ### Changed 29 | - Bump rake version because of security vulnerability 30 | 31 | ## [0.4.11] 32 | ### Added 33 | - Support for recovering signatures with a V value below 27 (like from Ledger hardware wallets) 34 | 35 | ## [0.4.10] 36 | ### Changed 37 | - Use updated sha3 dependency 38 | - Improved OpenSSL support 39 | 40 | ### Changed 41 | - Changed Eth::Configuration.default_chain_id back to .chain_id for dependent libraries. 42 | 43 | ## [0.4.9] 44 | ### Changed 45 | - [escoffon](https://github.com/escoffon) added support for chain IDs larger than 120. 46 | 47 | ## [0.4.8] 48 | ### Added 49 | - [@buhrmi](https://github.com/buhrmi) added Eth::Key#personal_sign. 50 | - [@buhrmi](https://github.com/buhrmi) added Eth::Key#personal_recover. 51 | 52 | ## [0.4.7] 53 | ### Changed 54 | - Updated MoneyTree dependency. 55 | 56 | ## [0.4.6] 57 | ### Added 58 | - Support scrypt private key decryption 59 | 60 | ## [0.4.5] 61 | ### Changed 62 | - Further improve Open SSL configurability 63 | 64 | ## [0.4.4] 65 | ### Changed 66 | - Support old versions of SSL to help avoid preious breaking changes 67 | 68 | ## [0.4.3] 69 | ### Added 70 | - Eth::Key::Encrypter class to handle encrypting keys. 71 | - Eth::Key.encrypt as a nice wrapper around Encrypter class. 72 | - Eth::Key::Decrypter class to handle encrypting keys. 73 | - Eth::Key.decrypt as a nice wrapper around Decrypter class. 74 | 75 | ## [0.4.2] 76 | ### Added 77 | - Address#valid? to validate EIP55 checksums. 78 | - Address#checksummed to generate EIP55 checksums. 79 | - Utils.valid_address? to easily validate EIP55 checksums. 80 | - Utils.format_address to easily convert an address to EIP55 checksummed. 81 | 82 | ### Changed 83 | - Dependencies no longer include Ethereum::Base. Eth now implements those helpers directly and includes ffi, digest-sha3, and rlp directly. 84 | 85 | 86 | ## [0.4.1] 87 | ### Changed 88 | - Tx#hash includes the '0x' hex prefix. 89 | 90 | ## [0.4.0] 91 | ### Added 92 | - Tx#data_bin returns the data field of a transaction in binary. 93 | - Tx#data_hex returns the data field of a transaction as a hexadecimal string. 94 | - Tx#id is an alias of Tx#hash 95 | 96 | ### Changed 97 | - Tx#data is configurable to return either hex or binary: `config.tx_data_hex = true`. 98 | - Tx#hex includes the '0x' hex prefix. 99 | - Key#address getter is prepended by '0x'. 100 | - Extract public key to address method into Utils.public_key_to_address. 101 | - Tx#from returns an address instead of a public key. 102 | - Chain ID is updated to the later version of the spec. 103 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # ref: https://github.com/GemHQ/money-tree/issues/50 4 | gem "money-tree", git: "https://github.com/GemHQ/money-tree.git" 5 | 6 | source "https://rubygems.org" 7 | 8 | # Specify your gem's dependencies in eth.gemspec 9 | gemspec 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021 Steve Ellis 4 | Copyright (c) 2021-2022 Afri Schoedon 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum for Ruby 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/q9f/eth.rb/Spec)](https://github.com/q9f/eth.rb/actions) 4 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/q9f/eth.rb)](https://github.com/q9f/eth.rb/releases) 5 | [![Gem](https://img.shields.io/gem/v/eth)](https://rubygems.org/gems/eth) 6 | [![Gem](https://img.shields.io/gem/dt/eth)](https://rubygems.org/gems/eth) 7 | [![Visitors](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fq9f%2Feth.rb&count_bg=%2379C83D&title_bg=%23555555&icon=rubygems.svg&icon_color=%23FF0000&title=visitors&edge_flat=false)](https://hits.seeyoufarm.com) 8 | [![codecov](https://codecov.io/gh/q9f/eth.rb/branch/main/graph/badge.svg?token=IK7USBPBZY)](https://codecov.io/gh/q9f/eth.rb) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/469e6f66425198ad7614/maintainability)](https://codeclimate.com/github/q9f/eth.rb/maintainability) 10 | [![Top Language](https://img.shields.io/github/languages/top/q9f/eth.rb?color=red)](https://github.com/q9f/eth.rb/pulse) 11 | [![Yard Doc API](https://img.shields.io/badge/documentation-API-blue)](https://q9f.github.io/eth.rb) 12 | [![Usage Wiki](https://img.shields.io/badge/usage-WIKI-blue)](https://github.com/q9f/eth.rb/wiki) 13 | [![Open-Source License](https://img.shields.io/github/license/q9f/eth.rb)](LICENSE) 14 | [![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/q9f/eth.rb/issues) 15 | 16 | A straightforward library to build, sign, and broadcast Ethereum transactions. It allows the separation of key and node management. Sign transactions and handle keys anywhere you can run Ruby and broadcast transactions through any local or remote node. Sign messages and recover signatures for authentication. 17 | 18 | **Note,** this repository is just a long-term support branch of the minimally maintained `eth` gem version `~> 0.4`. For the partial rewrite of version `~> 0.5` see [q9f/eth.rb](https://github.com/q9f/eth.rb/). 19 | 20 | ## Installation `~> 0.4` 21 | 22 | Add this line to your application's Gemfile: 23 | 24 | ```ruby 25 | gem 'eth' 26 | ``` 27 | 28 | And then execute: 29 | 30 | $ bundle install 31 | 32 | Or install it yourself as: 33 | 34 | $ gem install eth 35 | 36 | ## Usage `~> 0.4` 37 | 38 | ### Keys 39 | 40 | Create a new public/private key and get its address: 41 | 42 | ```ruby 43 | key = Eth::Key.new 44 | key.private_hex 45 | key.public_hex 46 | key.address # EIP55 checksummed address 47 | ``` 48 | 49 | Import an existing key: 50 | 51 | ```ruby 52 | old_key = Eth::Key.new priv: private_key 53 | ``` 54 | 55 | Or decrypt an [encrypted key](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition): 56 | 57 | ```ruby 58 | decrypted_key = Eth::Key.decrypt File.read('./some/path.json'), 'p455w0rD' 59 | ``` 60 | 61 | You can also encrypt your keys for use with other ethereum libraries: 62 | 63 | ```ruby 64 | encrypted_key_info = Eth::Key.encrypt key, 'p455w0rD' 65 | ``` 66 | 67 | ### Transactions `~> 0.4` 68 | 69 | Build a transaction from scratch: 70 | 71 | ```ruby 72 | tx = Eth::Tx.new({ 73 | data: hex_data, 74 | gas_limit: 21_000, 75 | gas_price: 3_141_592, 76 | nonce: 1, 77 | to: key2.address, 78 | value: 1_000_000_000_000, 79 | }) 80 | ``` 81 | 82 | Or decode an encoded raw transaction: 83 | 84 | ```ruby 85 | tx = Eth::Tx.decode hex 86 | ``` 87 | 88 | Then sign the transaction: 89 | 90 | ```ruby 91 | tx.sign key 92 | ``` 93 | 94 | Get the raw transaction with `tx.hex`, and broadcast it through any Ethereum node. Or, just get the TXID with `tx.hash`. 95 | 96 | ### Utils 97 | 98 | Validate an [EIP55](https://github.com/ethereum/EIPs/issues/55) checksummed address: 99 | 100 | ```ruby 101 | Eth::Utils.valid_address? address 102 | ``` 103 | 104 | Or add a checksum to an existing address: 105 | 106 | ```ruby 107 | Eth::Utils.format_address "0x4bc787699093f11316e819b5692be04a712c4e69" # => "0x4bc787699093f11316e819B5692be04A712C4E69" 108 | ``` 109 | 110 | ### Personal Signatures 111 | 112 | You can recover public keys and generate web3/metamask-compatible signatures: 113 | 114 | ```ruby 115 | # Generate signature 116 | key.personal_sign('hello world') 117 | 118 | # Recover signature 119 | message = 'test' 120 | signature = '0x3eb24bd327df8c2b614c3f652ec86efe13aa721daf203820241c44861a26d37f2bffc6e03e68fc4c3d8d967054c9cb230ed34339b12ef89d512b42ae5bf8c2ae1c' 121 | Eth::Key.personal_recover(message, signature) # => 043e5b33f0080491e21f9f5f7566de59a08faabf53edbc3c32aaacc438552b25fdde531f8d1053ced090e9879cbf2b0d1c054e4b25941dab9254d2070f39418afc 122 | ``` 123 | 124 | ### Configure 125 | 126 | In order to prevent replay attacks, you must specify which Ethereum chain your transactions are created for. See [EIP 155](https://github.com/ethereum/EIPs/issues/155) for more detail. 127 | 128 | ```ruby 129 | Eth.configure do |config| 130 | config.chain_id = 1 # nil by default, meaning valid on any chain 131 | end 132 | ``` 133 | 134 | ## Contributing 135 | 136 | Bug reports and pull requests are welcome on GitHub at [github.com/q9f/eth.rb](https://github.com/q9f/eth.rb/). Tests are encouraged. 137 | 138 | ### Tests 139 | 140 | First install the [Ethereum common tests](https://github.com/ethereum/tests): 141 | 142 | ```shell 143 | git submodule update --init 144 | ``` 145 | 146 | Then run the associated tests: 147 | 148 | ```shell 149 | rspec 150 | ``` 151 | 152 | ## License 153 | 154 | The gem version `~> 0.4` is available as open-source software under the terms of the [MIT License](http://opensource.org/licenses/MIT). 155 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "eth" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | require "pry" 10 | Pry.start 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /eth.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # coding: utf-8 3 | 4 | lib = File.expand_path("lib", __dir__).freeze 5 | $LOAD_PATH.unshift lib unless $LOAD_PATH.include? lib 6 | 7 | require "eth/version" 8 | 9 | Gem::Specification.new do |spec| 10 | spec.name = "eth" 11 | spec.version = Eth::VERSION 12 | spec.authors = ["Steve Ellis", "Afri Schoedon"] 13 | spec.email = ["email@steveell.is", "ruby@q9f.cc"] 14 | 15 | spec.summary = %q{Simple API to sign Ethereum transactions.} 16 | spec.description = %q{Library to build, parse, and sign Ethereum transactions.} 17 | spec.homepage = "https://github.com/se3000/ruby-eth" 18 | spec.license = "MIT" 19 | 20 | spec.metadata = { 21 | "homepage_uri" => "https://github.com/se3000/ruby-eth", 22 | "source_code_uri" => "https://github.com/se3000/ruby-eth", 23 | "github_repo" => "https://github.com/se3000/ruby-eth", 24 | "bug_tracker_uri" => "https://github.com/se3000/ruby-eth/issues", 25 | }.freeze 26 | 27 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | spec.test_files = spec.files.grep %r{^(test|spec|features)/} 32 | 33 | spec.add_dependency "keccak", "~> 1.3" 34 | spec.add_dependency "ffi", "~> 1.15" 35 | spec.add_dependency "money-tree", "~> 0.11" 36 | spec.add_dependency "openssl", "~> 3.0" 37 | spec.add_dependency "rlp", "~> 0.7" 38 | spec.add_dependency "scrypt", "~> 3.0" 39 | 40 | spec.platform = Gem::Platform::RUBY 41 | spec.required_ruby_version = ">= 2.6", "< 4.0" 42 | 43 | spec.add_development_dependency "bundler", "~> 2.2" 44 | spec.add_development_dependency "pry", "~> 0.14" 45 | spec.add_development_dependency "rake", "~> 13.0" 46 | spec.add_development_dependency "rspec", "~> 3.10" 47 | end 48 | -------------------------------------------------------------------------------- /lib/eth.rb: -------------------------------------------------------------------------------- 1 | require "digest/keccak" 2 | require "ffi" 3 | require "money-tree" 4 | require "rlp" 5 | 6 | module Eth 7 | BYTE_ZERO = "\x00".freeze 8 | UINT_MAX = 2 ** 256 - 1 9 | 10 | autoload :Address, "eth/address" 11 | autoload :Gas, "eth/gas" 12 | autoload :Key, "eth/key" 13 | autoload :OpenSsl, "eth/open_ssl" 14 | autoload :Secp256k1, "eth/secp256k1" 15 | autoload :Sedes, "eth/sedes" 16 | autoload :Tx, "eth/tx" 17 | autoload :Utils, "eth/utils" 18 | 19 | class << self 20 | def configure 21 | yield(configuration) 22 | end 23 | 24 | def replayable_chain_id 25 | 27 26 | end 27 | 28 | def chain_id 29 | configuration.chain_id 30 | end 31 | 32 | def v_base 33 | replayable_chain_id 34 | end 35 | 36 | def replayable_v?(v) 37 | [replayable_chain_id, replayable_chain_id + 1].include? v 38 | end 39 | 40 | def tx_data_hex? 41 | !!configuration.tx_data_hex 42 | end 43 | 44 | def chain_id_from_signature(signature) 45 | return nil if Eth.replayable_v?(signature[:v]) 46 | 47 | cid = (signature[:v] - 35) / 2 48 | (cid < 1) ? nil : cid 49 | end 50 | 51 | private 52 | 53 | def configuration 54 | @configuration ||= Configuration.new 55 | end 56 | end 57 | 58 | class Configuration 59 | attr_accessor :chain_id, :tx_data_hex 60 | 61 | def initialize 62 | self.chain_id = nil 63 | self.tx_data_hex = true 64 | end 65 | end 66 | 67 | class ValidationError < StandardError; end 68 | class InvalidTransaction < ValidationError; end 69 | end 70 | -------------------------------------------------------------------------------- /lib/eth/address.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | class Address 3 | def initialize(address) 4 | @address = Utils.prefix_hex(address) 5 | end 6 | 7 | def valid? 8 | if !matches_any_format? 9 | false 10 | elsif not_checksummed? 11 | true 12 | else 13 | checksum_matches? 14 | end 15 | end 16 | 17 | def checksummed 18 | raise "Invalid address: #{address}" unless matches_any_format? 19 | 20 | cased = unprefixed.chars.zip(checksum.chars).map do |char, check| 21 | check.match(/[0-7]/) ? char.downcase : char.upcase 22 | end 23 | 24 | Utils.prefix_hex(cased.join) 25 | end 26 | 27 | private 28 | 29 | attr_reader :address 30 | 31 | def checksum_matches? 32 | address == checksummed 33 | end 34 | 35 | def not_checksummed? 36 | all_uppercase? || all_lowercase? 37 | end 38 | 39 | def all_uppercase? 40 | address.match(/(?:0[xX])[A-F0-9]{40}/) 41 | end 42 | 43 | def all_lowercase? 44 | address.match(/(?:0[xX])[a-f0-9]{40}/) 45 | end 46 | 47 | def matches_any_format? 48 | address.match(/\A(?:0[xX])[a-fA-F0-9]{40}\z/) 49 | end 50 | 51 | def checksum 52 | Utils.bin_to_hex(Utils.keccak256 unprefixed.downcase) 53 | end 54 | 55 | def unprefixed 56 | Utils.remove_hex_prefix address 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/eth/gas.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | class Gas 3 | GTXCOST = 21000 # TX BASE GAS COST 4 | GTXDATANONZERO = 68 # TX DATA NON ZERO BYTE GAS COST 5 | GTXDATAZERO = 4 # TX DATA ZERO BYTE GAS COST 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/eth/key.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | class Key 3 | autoload :Decrypter, "eth/key/decrypter" 4 | autoload :Encrypter, "eth/key/encrypter" 5 | 6 | attr_reader :private_key, :public_key 7 | 8 | def self.encrypt(key, password) 9 | key = new(priv: key) unless key.is_a?(Key) 10 | 11 | Encrypter.perform key.private_hex, password 12 | end 13 | 14 | def self.decrypt(data, password) 15 | priv = Decrypter.perform data, password 16 | new priv: priv 17 | end 18 | 19 | def self.personal_recover(message, signature) 20 | bin_signature = Utils.hex_to_bin(signature).bytes.rotate(-1).pack("c*") 21 | OpenSsl.recover_compact(Utils.keccak256(Utils.prefix_message(message)), bin_signature) 22 | end 23 | 24 | def initialize(priv: nil) 25 | @private_key = MoneyTree::PrivateKey.new key: priv 26 | @public_key = MoneyTree::PublicKey.new private_key, compressed: false 27 | end 28 | 29 | def private_hex 30 | private_key.to_hex 31 | end 32 | 33 | def public_bytes 34 | public_key.to_bytes 35 | end 36 | 37 | def public_hex 38 | public_key.to_hex 39 | end 40 | 41 | def address 42 | Utils.public_key_to_address public_hex 43 | end 44 | 45 | alias_method :to_address, :address 46 | 47 | def sign(message) 48 | sign_hash message_hash(message) 49 | end 50 | 51 | def sign_hash(hash) 52 | loop do 53 | signature = OpenSsl.sign_compact hash, private_hex, public_hex 54 | return signature if valid_s? signature 55 | end 56 | end 57 | 58 | def verify_signature(message, signature) 59 | hash = message_hash(message) 60 | public_hex == OpenSsl.recover_compact(hash, signature) 61 | end 62 | 63 | def personal_sign(message) 64 | Utils.bin_to_hex(sign(Utils.prefix_message(message)).bytes.rotate(1).pack("c*")) 65 | end 66 | 67 | private 68 | 69 | def message_hash(message) 70 | Utils.keccak256 message 71 | end 72 | 73 | def valid_s?(signature) 74 | s_value = Utils.v_r_s_for(signature).last 75 | s_value <= Secp256k1::N / 2 && s_value != 0 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/eth/key/decrypter.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "scrypt" 3 | 4 | class Eth::Key::Decrypter 5 | include Eth::Utils 6 | 7 | def self.perform(data, password) 8 | new(data, password).perform 9 | end 10 | 11 | def initialize(data, password) 12 | @data = JSON.parse(data) 13 | @password = password 14 | end 15 | 16 | def perform 17 | derive_key password 18 | check_macs 19 | bin_to_hex decrypted_data 20 | end 21 | 22 | private 23 | 24 | attr_reader :data, :key, :password 25 | 26 | def derive_key(password) 27 | case kdf 28 | when "pbkdf2" 29 | @key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest) 30 | when "scrypt" 31 | # OpenSSL 1.1 inclues OpenSSL::KDF.scrypt, but it is not available usually, otherwise we could do: OpenSSL::KDF.scrypt(password, salt: salt, N: n, r: r, p: p, length: key_length) 32 | @key = SCrypt::Engine.scrypt(password, salt, n, r, p, key_length) 33 | else 34 | raise "Unsupported key derivation function: #{kdf}!" 35 | end 36 | end 37 | 38 | def check_macs 39 | mac1 = keccak256(key[(key_length / 2), key_length] + ciphertext) 40 | mac2 = hex_to_bin crypto_data["mac"] 41 | 42 | if mac1 != mac2 43 | raise "Message Authentications Codes do not match!" 44 | end 45 | end 46 | 47 | def decrypted_data 48 | @decrypted_data ||= cipher.update(ciphertext) + cipher.final 49 | end 50 | 51 | def crypto_data 52 | @crypto_data ||= data["crypto"] || data["Crypto"] 53 | end 54 | 55 | def ciphertext 56 | hex_to_bin crypto_data["ciphertext"] 57 | end 58 | 59 | def cipher_name 60 | "aes-128-ctr" 61 | end 62 | 63 | def cipher 64 | @cipher ||= OpenSSL::Cipher.new(cipher_name).tap do |cipher| 65 | cipher.decrypt 66 | cipher.key = key[0, (key_length / 2)] 67 | cipher.iv = iv 68 | end 69 | end 70 | 71 | def iv 72 | hex_to_bin crypto_data["cipherparams"]["iv"] 73 | end 74 | 75 | def salt 76 | hex_to_bin crypto_data["kdfparams"]["salt"] 77 | end 78 | 79 | def iterations 80 | crypto_data["kdfparams"]["c"].to_i 81 | end 82 | 83 | def kdf 84 | crypto_data["kdf"] 85 | end 86 | 87 | def key_length 88 | crypto_data["kdfparams"]["dklen"].to_i 89 | end 90 | 91 | def n 92 | crypto_data["kdfparams"]["n"].to_i 93 | end 94 | 95 | def r 96 | crypto_data["kdfparams"]["r"].to_i 97 | end 98 | 99 | def p 100 | crypto_data["kdfparams"]["p"].to_i 101 | end 102 | 103 | def digest 104 | OpenSSL::Digest.new digest_name 105 | end 106 | 107 | def digest_name 108 | "sha256" 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/eth/key/encrypter.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "securerandom" 3 | 4 | class Eth::Key::Encrypter 5 | include Eth::Utils 6 | 7 | def self.perform(key, password, options = {}) 8 | new(key, options).perform(password) 9 | end 10 | 11 | def initialize(key, options = {}) 12 | @key = key 13 | @options = options 14 | end 15 | 16 | def perform(password) 17 | derive_key password 18 | encrypt 19 | 20 | data.to_json 21 | end 22 | 23 | def data 24 | { 25 | crypto: { 26 | cipher: cipher_name, 27 | cipherparams: { 28 | iv: bin_to_hex(iv), 29 | }, 30 | ciphertext: bin_to_hex(encrypted_key), 31 | kdf: "pbkdf2", 32 | kdfparams: { 33 | c: iterations, 34 | dklen: 32, 35 | prf: prf, 36 | salt: bin_to_hex(salt), 37 | }, 38 | mac: bin_to_hex(mac), 39 | }, 40 | id: id, 41 | version: 3, 42 | }.tap do |data| 43 | data[:address] = address unless options[:skip_address] 44 | end 45 | end 46 | 47 | def id 48 | @id ||= options[:id] || SecureRandom.uuid 49 | end 50 | 51 | private 52 | 53 | attr_reader :derived_key, :encrypted_key, :key, :options 54 | 55 | def cipher 56 | @cipher ||= OpenSSL::Cipher.new(cipher_name).tap do |cipher| 57 | cipher.encrypt 58 | cipher.iv = iv 59 | cipher.key = derived_key[0, (key_length / 2)] 60 | end 61 | end 62 | 63 | def digest 64 | @digest ||= OpenSSL::Digest.new digest_name 65 | end 66 | 67 | def derive_key(password) 68 | @derived_key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest) 69 | end 70 | 71 | def encrypt 72 | @encrypted_key = cipher.update(hex_to_bin key) + cipher.final 73 | end 74 | 75 | def mac 76 | keccak256(derived_key[(key_length / 2), key_length] + encrypted_key) 77 | end 78 | 79 | def cipher_name 80 | "aes-128-ctr" 81 | end 82 | 83 | def digest_name 84 | "sha256" 85 | end 86 | 87 | def prf 88 | "hmac-#{digest_name}" 89 | end 90 | 91 | def key_length 92 | 32 93 | end 94 | 95 | def salt_length 96 | 32 97 | end 98 | 99 | def iv_length 100 | 16 101 | end 102 | 103 | def iterations 104 | options[:iterations] || 262_144 105 | end 106 | 107 | def salt 108 | @salt ||= if options[:salt] 109 | hex_to_bin options[:salt] 110 | else 111 | SecureRandom.random_bytes(salt_length) 112 | end 113 | end 114 | 115 | def iv 116 | @iv ||= if options[:iv] 117 | hex_to_bin options[:iv] 118 | else 119 | SecureRandom.random_bytes(iv_length) 120 | end 121 | end 122 | 123 | def address 124 | Eth::Key.new(priv: key).address 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/eth/open_ssl.rb: -------------------------------------------------------------------------------- 1 | # originally lifted from https://github.com/lian/bitcoin-ruby 2 | # thanks to everyone there for figuring this out 3 | 4 | # encoding: ascii-8bit 5 | 6 | require "openssl" 7 | require "ffi" 8 | 9 | module Eth 10 | class OpenSsl 11 | extend FFI::Library 12 | 13 | # Use the library loaded by the extension require above. 14 | ffi_lib FFI::CURRENT_PROCESS 15 | 16 | NID_secp256k1 = 714 17 | POINT_CONVERSION_COMPRESSED = 2 18 | POINT_CONVERSION_UNCOMPRESSED = 4 19 | 20 | # OpenSSL 1.1.0 engine constants, taken from: 21 | # https://github.com/openssl/openssl/blob/2be8c56a39b0ec2ec5af6ceaf729df154d784a43/include/openssl/crypto.h 22 | OPENSSL_INIT_ENGINE_RDRAND = 0x00000200 23 | OPENSSL_INIT_ENGINE_DYNAMIC = 0x00000400 24 | OPENSSL_INIT_ENGINE_CRYPTODEV = 0x00001000 25 | OPENSSL_INIT_ENGINE_CAPI = 0x00002000 26 | OPENSSL_INIT_ENGINE_PADLOCK = 0x00004000 27 | OPENSSL_INIT_ENGINE_ALL_BUILTIN = (OPENSSL_INIT_ENGINE_RDRAND | 28 | OPENSSL_INIT_ENGINE_DYNAMIC | 29 | OPENSSL_INIT_ENGINE_CRYPTODEV | 30 | OPENSSL_INIT_ENGINE_CAPI | 31 | OPENSSL_INIT_ENGINE_PADLOCK) 32 | 33 | # OpenSSL 1.1.0 load strings constant, taken from: 34 | # https://github.com/openssl/openssl/blob/c162c126be342b8cd97996346598ecf7db56130f/include/openssl/ssl.h 35 | OPENSSL_INIT_LOAD_SSL_STRINGS = 0x00200000 36 | 37 | # This is the very first function we need to use to determine what version 38 | # of OpenSSL we are interacting with. 39 | begin 40 | attach_function :OpenSSL_version_num, [], :ulong 41 | rescue FFI::NotFoundError 42 | attach_function :SSLeay, [], :long 43 | end 44 | 45 | begin 46 | # Initialization procedure for the library was changed in OpenSSL 1.1.0 47 | attach_function :OPENSSL_init_ssl, [:uint64, :pointer], :int 48 | rescue FFI::NotFoundError 49 | attach_function :SSL_library_init, [], :int 50 | attach_function :ERR_load_crypto_strings, [], :void 51 | attach_function :SSL_load_error_strings, [], :void 52 | end 53 | 54 | attach_function :RAND_poll, [], :int 55 | 56 | attach_function :BN_CTX_free, [:pointer], :int 57 | attach_function :BN_CTX_new, [], :pointer 58 | attach_function :BN_add, %i[pointer pointer pointer], :int 59 | attach_function :BN_bin2bn, %i[pointer int pointer], :pointer 60 | attach_function :BN_bn2bin, %i[pointer pointer], :int 61 | attach_function :BN_cmp, %i[pointer pointer], :int 62 | attach_function :BN_dup, [:pointer], :pointer 63 | attach_function :BN_free, [:pointer], :int 64 | attach_function :BN_mod_inverse, %i[pointer pointer pointer pointer], :pointer 65 | attach_function :BN_mod_mul, %i[pointer pointer pointer pointer pointer], :int 66 | attach_function :BN_mod_sub, %i[pointer pointer pointer pointer pointer], :int 67 | attach_function :BN_mul_word, %i[pointer int], :int 68 | attach_function :BN_new, [], :pointer 69 | attach_function :BN_rshift, %i[pointer pointer int], :int 70 | attach_function :BN_rshift1, %i[pointer pointer], :int 71 | attach_function :BN_set_word, %i[pointer int], :int 72 | attach_function :BN_sub, %i[pointer pointer pointer], :int 73 | attach_function :EC_GROUP_get_curve_GFp, %i[pointer pointer pointer pointer pointer], :int 74 | attach_function :EC_GROUP_get_degree, [:pointer], :int 75 | attach_function :EC_GROUP_get_order, %i[pointer pointer pointer], :int 76 | attach_function :EC_KEY_free, [:pointer], :int 77 | attach_function :EC_KEY_get0_group, [:pointer], :pointer 78 | attach_function :EC_KEY_get0_private_key, [:pointer], :pointer 79 | attach_function :EC_KEY_new_by_curve_name, [:int], :pointer 80 | attach_function :EC_KEY_set_conv_form, %i[pointer int], :void 81 | attach_function :EC_KEY_set_private_key, %i[pointer pointer], :int 82 | attach_function :EC_KEY_set_public_key, %i[pointer pointer], :int 83 | attach_function :EC_POINT_free, [:pointer], :int 84 | attach_function :EC_POINT_mul, %i[pointer pointer pointer pointer pointer pointer], :int 85 | attach_function :EC_POINT_new, [:pointer], :pointer 86 | attach_function :EC_POINT_set_compressed_coordinates_GFp, 87 | %i[pointer pointer pointer int pointer], :int 88 | attach_function :i2o_ECPublicKey, %i[pointer pointer], :uint 89 | attach_function :ECDSA_do_sign, %i[pointer uint pointer], :pointer 90 | attach_function :BN_num_bits, [:pointer], :int 91 | attach_function :ECDSA_SIG_free, [:pointer], :void 92 | attach_function :EC_POINT_add, %i[pointer pointer pointer pointer pointer], :int 93 | attach_function :EC_POINT_point2hex, %i[pointer pointer int pointer], :string 94 | attach_function :EC_POINT_hex2point, %i[pointer string pointer pointer], :pointer 95 | attach_function :d2i_ECDSA_SIG, %i[pointer pointer long], :pointer 96 | attach_function :i2d_ECDSA_SIG, %i[pointer pointer], :int 97 | attach_function :OPENSSL_free, :CRYPTO_free, [:pointer], :void 98 | 99 | def self.BN_num_bytes(ptr) # rubocop:disable Naming/MethodName 100 | (BN_num_bits(ptr) + 7) / 8 101 | end 102 | 103 | # resolve public from private key, using ffi and libssl.so 104 | # example: 105 | # keypair = Bitcoin.generate_key; Bitcoin::OpenSSL_EC.regenerate_key(keypair.first) == keypair 106 | def self.regenerate_key(private_key) 107 | private_key = [private_key].pack("H*") if private_key.bytesize >= (32 * 2) 108 | private_key_hex = private_key.unpack("H*")[0] 109 | 110 | group = OpenSSL::PKey::EC::Group.new("secp256k1") 111 | key = OpenSSL::PKey::EC.new(group) 112 | key.private_key = OpenSSL::BN.new(private_key_hex, 16) 113 | key.public_key = group.generator.mul(key.private_key) 114 | 115 | priv_hex = key.private_key.to_bn.to_s(16).downcase.rjust(64, "0") 116 | if priv_hex != private_key_hex 117 | raise "regenerated wrong private_key, raise here before generating a faulty public_key too!" 118 | end 119 | 120 | [priv_hex, key.public_key.to_bn.to_s(16).downcase] 121 | end 122 | 123 | # Given the components of a signature and a selector value, recover and 124 | # return the public key that generated the signature according to the 125 | # algorithm in SEC1v2 section 4.1.6. 126 | # 127 | # rec_id is an index from 0 to 3 that indicates which of the 4 possible 128 | # keys is the correct one. Because the key recovery operation yields 129 | # multiple potential keys, the correct key must either be stored alongside 130 | # the signature, or you must be willing to try each rec_id in turn until 131 | # you find one that outputs the key you are expecting. 132 | # 133 | # If this method returns nil, it means recovery was not possible and rec_id 134 | # should be iterated. 135 | # 136 | # Given the above two points, a correct usage of this method is inside a 137 | # for loop from 0 to 3, and if the output is nil OR a key that is not the 138 | # one you expect, you try again with the next rec_id. 139 | # 140 | # message_hash = hash of the signed message. 141 | # signature = the R and S components of the signature, wrapped. 142 | # rec_id = which possible key to recover. 143 | # is_compressed = whether or not the original pubkey was compressed. 144 | def self.recover_public_key_from_signature(message_hash, signature, rec_id, is_compressed) 145 | return nil if rec_id < 0 || signature.bytesize != 65 146 | init_ffi_ssl 147 | 148 | signature = FFI::MemoryPointer.from_string(signature) 149 | # signature_bn = BN_bin2bn(signature, 65, BN_new()) 150 | r = BN_bin2bn(signature[1], 32, BN_new()) 151 | s = BN_bin2bn(signature[33], 32, BN_new()) 152 | 153 | i = rec_id / 2 154 | eckey = EC_KEY_new_by_curve_name(NID_secp256k1) 155 | 156 | EC_KEY_set_conv_form(eckey, POINT_CONVERSION_COMPRESSED) if is_compressed 157 | 158 | group = EC_KEY_get0_group(eckey) 159 | order = BN_new() 160 | EC_GROUP_get_order(group, order, nil) 161 | x = BN_dup(order) 162 | BN_mul_word(x, i) 163 | BN_add(x, x, r) 164 | 165 | field = BN_new() 166 | EC_GROUP_get_curve_GFp(group, field, nil, nil, nil) 167 | 168 | if BN_cmp(x, field) >= 0 169 | [r, s, order, x, field].each { |item| BN_free(item) } 170 | EC_KEY_free(eckey) 171 | return nil 172 | end 173 | 174 | big_r = EC_POINT_new(group) 175 | EC_POINT_set_compressed_coordinates_GFp(group, big_r, x, rec_id % 2, nil) 176 | 177 | big_q = EC_POINT_new(group) 178 | n = EC_GROUP_get_degree(group) 179 | e = BN_bin2bn(message_hash, message_hash.bytesize, BN_new()) 180 | BN_rshift(e, e, 8 - (n & 7)) if 8 * message_hash.bytesize > n 181 | 182 | ctx = BN_CTX_new() 183 | zero = BN_new() 184 | rr = BN_new() 185 | sor = BN_new() 186 | eor = BN_new() 187 | BN_set_word(zero, 0) 188 | BN_mod_sub(e, zero, e, order, ctx) 189 | BN_mod_inverse(rr, r, order, ctx) 190 | BN_mod_mul(sor, s, rr, order, ctx) 191 | BN_mod_mul(eor, e, rr, order, ctx) 192 | EC_POINT_mul(group, big_q, eor, big_r, sor, ctx) 193 | EC_KEY_set_public_key(eckey, big_q) 194 | BN_CTX_free(ctx) 195 | 196 | [r, s, order, x, field, e, zero, rr, sor, eor].each { |item| BN_free(item) } 197 | [big_r, big_q].each { |item| EC_POINT_free(item) } 198 | 199 | length = i2o_ECPublicKey(eckey, nil) 200 | buf = FFI::MemoryPointer.new(:uint8, length) 201 | ptr = FFI::MemoryPointer.new(:pointer).put_pointer(0, buf) 202 | pub_hex = buf.read_string(length).unpack("H*")[0] if i2o_ECPublicKey(eckey, ptr) == length 203 | 204 | EC_KEY_free(eckey) 205 | 206 | pub_hex 207 | end 208 | 209 | # Regenerate a DER-encoded signature such that the S-value complies with the BIP62 210 | # specification. 211 | # 212 | def self.signature_to_low_s(signature) 213 | init_ffi_ssl 214 | 215 | buf = FFI::MemoryPointer.new(:uint8, 34) 216 | temp = signature.unpack("C*") 217 | length_r = temp[3] 218 | length_s = temp[5 + length_r] 219 | sig = FFI::MemoryPointer.from_string(signature) 220 | 221 | # Calculate the lower s value 222 | s = BN_bin2bn(sig[6 + length_r], length_s, BN_new()) 223 | eckey = EC_KEY_new_by_curve_name(NID_secp256k1) 224 | group = EC_KEY_get0_group(eckey) 225 | order = BN_new() 226 | halforder = BN_new() 227 | ctx = BN_CTX_new() 228 | 229 | EC_GROUP_get_order(group, order, ctx) 230 | BN_rshift1(halforder, order) 231 | BN_sub(s, order, s) if BN_cmp(s, halforder) > 0 232 | 233 | BN_free(halforder) 234 | BN_free(order) 235 | BN_CTX_free(ctx) 236 | 237 | length_s = BN_bn2bin(s, buf) 238 | # p buf.read_string(length_s).unpack("H*") 239 | 240 | # Re-encode the signature in DER format 241 | sig = [0x30, 0, 0x02, length_r] 242 | sig.concat(temp.slice(4, length_r)) 243 | sig << 0x02 244 | sig << length_s 245 | sig.concat(buf.read_string(length_s).unpack("C*")) 246 | sig[1] = sig.size - 2 247 | 248 | BN_free(s) 249 | EC_KEY_free(eckey) 250 | 251 | sig.pack("C*") 252 | end 253 | 254 | def self.sign_compact(hash, private_key, public_key_hex = nil, pubkey_compressed = nil) 255 | msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hash) 256 | 257 | private_key = [private_key].pack("H*") if private_key.bytesize >= 64 258 | private_key_hex = private_key.unpack("H*")[0] 259 | 260 | public_key_hex ||= regenerate_key(private_key_hex).last 261 | pubkey_compressed ||= public_key_hex[0..1] != "04" 262 | 263 | init_ffi_ssl 264 | eckey = EC_KEY_new_by_curve_name(NID_secp256k1) 265 | priv_key = BN_bin2bn(private_key, private_key.bytesize, BN_new()) 266 | 267 | group = EC_KEY_get0_group(eckey) 268 | order = BN_new() 269 | ctx = BN_CTX_new() 270 | EC_GROUP_get_order(group, order, ctx) 271 | 272 | pub_key = EC_POINT_new(group) 273 | EC_POINT_mul(group, pub_key, priv_key, nil, nil, ctx) 274 | EC_KEY_set_private_key(eckey, priv_key) 275 | EC_KEY_set_public_key(eckey, pub_key) 276 | 277 | signature = ECDSA_do_sign(msg32, msg32.size, eckey) 278 | 279 | BN_free(order) 280 | BN_CTX_free(ctx) 281 | EC_POINT_free(pub_key) 282 | BN_free(priv_key) 283 | EC_KEY_free(eckey) 284 | 285 | buf = FFI::MemoryPointer.new(:uint8, 32) 286 | head = nil 287 | r, s = signature.get_array_of_pointer(0, 2).map do |i| 288 | BN_bn2bin(i, buf) 289 | buf.read_string(BN_num_bytes(i)).rjust(32, "\x00") 290 | end 291 | 292 | rec_id = nil 293 | if signature.get_array_of_pointer(0, 2).all? { |i| BN_num_bits(i) <= 256 } 294 | 4.times do |i| 295 | head = [27 + i + (pubkey_compressed ? 4 : 0)].pack("C") 296 | recovered_key = recover_public_key_from_signature( 297 | msg32.read_string(32), [head, r, s].join, i, pubkey_compressed 298 | ) 299 | if public_key_hex == recovered_key 300 | rec_id = i 301 | break 302 | end 303 | end 304 | end 305 | 306 | ECDSA_SIG_free(signature) 307 | 308 | [head, [r, s]].join if rec_id 309 | end 310 | 311 | def self.recover_compact(hash, signature) 312 | return false if signature.bytesize != 65 313 | msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hash) 314 | 315 | version = signature.unpack("C")[0] 316 | 317 | # Version of signature should be 27 or 28, but 0 and 1 are also possible versions 318 | # which can show up in Ledger hardwallet signings 319 | if version < 27 320 | version += 27 321 | end 322 | 323 | return false if version < 27 || version > 34 324 | 325 | compressed = version >= 31 326 | version -= 4 if compressed 327 | 328 | recover_public_key_from_signature(msg32.read_string(32), signature, version - 27, compressed) 329 | end 330 | 331 | # lifted from https://github.com/GemHQ/money-tree 332 | def self.ec_add(point0, point1) 333 | init_ffi_ssl 334 | 335 | eckey = EC_KEY_new_by_curve_name(NID_secp256k1) 336 | group = EC_KEY_get0_group(eckey) 337 | 338 | point_0_hex = point0.to_bn.to_s(16) 339 | point_0_pt = EC_POINT_hex2point(group, point_0_hex, nil, nil) 340 | point_1_hex = point1.to_bn.to_s(16) 341 | point_1_pt = EC_POINT_hex2point(group, point_1_hex, nil, nil) 342 | 343 | sum_point = EC_POINT_new(group) 344 | EC_POINT_add(group, sum_point, point_0_pt, point_1_pt, nil) 345 | hex = EC_POINT_point2hex(group, sum_point, POINT_CONVERSION_UNCOMPRESSED, nil) 346 | EC_KEY_free(eckey) 347 | EC_POINT_free(sum_point) 348 | hex 349 | end 350 | 351 | # repack signature for OpenSSL 1.0.1k handling of DER signatures 352 | # https://github.com/bitcoin/bitcoin/pull/5634/files 353 | def self.repack_der_signature(signature) 354 | init_ffi_ssl 355 | 356 | return false if signature.empty? 357 | 358 | # New versions of OpenSSL will reject non-canonical DER signatures. de/re-serialize first. 359 | norm_der = FFI::MemoryPointer.new(:pointer) 360 | sig_ptr = FFI::MemoryPointer.new(:pointer).put_pointer( 361 | 0, FFI::MemoryPointer.from_string(signature) 362 | ) 363 | 364 | norm_sig = d2i_ECDSA_SIG(nil, sig_ptr, signature.bytesize) 365 | 366 | derlen = i2d_ECDSA_SIG(norm_sig, norm_der) 367 | ECDSA_SIG_free(norm_sig) 368 | return false if derlen <= 0 369 | 370 | ret = norm_der.read_pointer.read_string(derlen) 371 | OPENSSL_free(norm_der.read_pointer) 372 | 373 | ret 374 | end 375 | 376 | def self.init_ffi_ssl 377 | @ssl_loaded ||= false 378 | return if @ssl_loaded 379 | 380 | if self.method_defined?(:OPENSSL_init_ssl) 381 | OPENSSL_init_ssl( 382 | OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ENGINE_ALL_BUILTIN, 383 | nil 384 | ) 385 | else 386 | SSL_library_init() 387 | ERR_load_crypto_strings() 388 | SSL_load_error_strings() 389 | end 390 | 391 | RAND_poll() 392 | @ssl_loaded = true 393 | end 394 | end 395 | end 396 | -------------------------------------------------------------------------------- /lib/eth/secp256k1.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | class Secp256k1 3 | N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/eth/sedes.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | module Sedes 3 | include RLP::Sedes 4 | 5 | extend self 6 | 7 | def address 8 | Binary.fixed_length(20, allow_empty: true) 9 | end 10 | 11 | def int20 12 | BigEndianInt.new(20) 13 | end 14 | 15 | def int32 16 | BigEndianInt.new(32) 17 | end 18 | 19 | def int256 20 | BigEndianInt.new(256) 21 | end 22 | 23 | def hash32 24 | Binary.fixed_length(32) 25 | end 26 | 27 | def trie_root 28 | Binary.fixed_length(32, allow_empty: true) 29 | end 30 | 31 | def big_endian_int 32 | RLP::Sedes.big_endian_int 33 | end 34 | 35 | def binary 36 | RLP::Sedes.binary 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/eth/tx.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | class Tx 3 | include RLP::Sedes::Serializable 4 | extend Sedes 5 | 6 | set_serializable_fields({ 7 | nonce: big_endian_int, 8 | gas_price: big_endian_int, 9 | gas_limit: big_endian_int, 10 | to: address, 11 | value: big_endian_int, 12 | data_bin: binary, 13 | v: big_endian_int, 14 | r: big_endian_int, 15 | s: big_endian_int, 16 | }) 17 | 18 | attr_writer :signature 19 | 20 | def self.decode(data) 21 | data = Utils.hex_to_bin(data) if data.match(/\A(?:0x)?\h+\Z/) 22 | txh = deserialize(RLP.decode data).to_h 23 | 24 | txh[:chain_id] = Eth.chain_id_from_signature(txh) 25 | 26 | self.new txh 27 | end 28 | 29 | def initialize(params) 30 | fields = { v: 0, r: 0, s: 0 }.merge params 31 | fields[:to] = Utils.normalize_address(fields[:to]) 32 | 33 | self.chain_id = (params[:chain_id]) ? params.delete(:chain_id) : Eth.chain_id 34 | 35 | if params[:data] 36 | self.data = params.delete(:data) 37 | fields[:data_bin] = data_bin 38 | end 39 | serializable_initialize fields 40 | 41 | check_transaction_validity 42 | end 43 | 44 | def unsigned_encoded 45 | us = unsigned 46 | RLP.encode(us, sedes: us.sedes) 47 | end 48 | 49 | def signing_data 50 | Utils.bin_to_prefixed_hex unsigned_encoded 51 | end 52 | 53 | def encoded 54 | RLP.encode self 55 | end 56 | 57 | def hex 58 | Utils.bin_to_prefixed_hex encoded 59 | end 60 | 61 | def sign(key) 62 | sig = key.sign(unsigned_encoded) 63 | vrs = Utils.v_r_s_for sig 64 | self.v = (self.chain_id) ? ((self.chain_id * 2) + vrs[0] + 8) : vrs[0] 65 | self.r = vrs[1] 66 | self.s = vrs[2] 67 | 68 | clear_signature 69 | self 70 | end 71 | 72 | def to_h 73 | hash_keys.inject({}) do |hash, field| 74 | hash[field] = send field 75 | hash 76 | end 77 | end 78 | 79 | def from 80 | if ecdsa_signature 81 | public_key = OpenSsl.recover_compact(signature_hash, ecdsa_signature) 82 | Utils.public_key_to_address(public_key) if public_key 83 | end 84 | end 85 | 86 | def signature 87 | return @signature if @signature 88 | @signature = { v: v, r: r, s: s } if [v, r, s].all? && (v > 0) 89 | end 90 | 91 | def ecdsa_signature 92 | return @ecdsa_signature if @ecdsa_signature 93 | 94 | if [v, r, s].all? && (v > 0) 95 | s_v = (self.chain_id) ? (v - (self.chain_id * 2) - 8) : v 96 | @ecdsa_signature = [ 97 | Utils.int_to_base256(s_v), 98 | Utils.zpad_int(r), 99 | Utils.zpad_int(s), 100 | ].join 101 | end 102 | end 103 | 104 | def hash 105 | "0x#{Utils.bin_to_hex Utils.keccak256_rlp(self)}" 106 | end 107 | 108 | alias_method :id, :hash 109 | 110 | def data_hex 111 | Utils.bin_to_prefixed_hex data_bin 112 | end 113 | 114 | def data_hex=(hex) 115 | self.data_bin = Utils.hex_to_bin(hex) 116 | end 117 | 118 | def data 119 | Eth.tx_data_hex? ? data_hex : data_bin 120 | end 121 | 122 | def data=(string) 123 | Eth.tx_data_hex? ? self.data_hex = (string) : self.data_bin = (string) 124 | end 125 | 126 | def chain_id 127 | @chain_id 128 | end 129 | 130 | def chain_id=(cid) 131 | if cid != @chain_id 132 | self.v = 0 133 | self.r = 0 134 | self.s = 0 135 | 136 | clear_signature 137 | end 138 | 139 | @chain_id = (cid == 0) ? nil : cid 140 | end 141 | 142 | def prevent_replays? 143 | !self.chain_id.nil? 144 | end 145 | 146 | private 147 | 148 | def clear_signature 149 | @signature = nil 150 | @ecdsa_signature = nil 151 | end 152 | 153 | def hash_keys 154 | keys = self.class.serializable_fields.keys 155 | keys.delete(:data_bin) 156 | keys + [:data, :chain_id] 157 | end 158 | 159 | def check_transaction_validity 160 | if [gas_price, gas_limit, value, nonce].max > UINT_MAX 161 | raise InvalidTransaction, "Values way too high!" 162 | elsif gas_limit < intrinsic_gas_used 163 | raise InvalidTransaction, "Gas limit too low" 164 | end 165 | end 166 | 167 | def intrinsic_gas_used 168 | num_zero_bytes = data_bin.count(BYTE_ZERO) 169 | num_non_zero_bytes = data_bin.size - num_zero_bytes 170 | 171 | Gas::GTXCOST + 172 | Gas::GTXDATAZERO * num_zero_bytes + 173 | Gas::GTXDATANONZERO * num_non_zero_bytes 174 | end 175 | 176 | def signature_hash 177 | Utils.keccak256 unsigned_encoded 178 | end 179 | 180 | def unsigned 181 | Tx.new to_h.merge(v: (self.chain_id) ? self.chain_id : 0, r: 0, s: 0) 182 | end 183 | 184 | protected 185 | 186 | def sedes 187 | if self.prevent_replays? && !(Eth.replayable_v? v) 188 | self.class 189 | else 190 | UnsignedTx 191 | end 192 | end 193 | end 194 | 195 | UnsignedTx = Tx.exclude([:v, :r, :s]) 196 | end 197 | -------------------------------------------------------------------------------- /lib/eth/utils.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | module Utils 3 | extend self 4 | 5 | def normalize_address(address) 6 | if address.nil? || address == "" 7 | "" 8 | elsif address.size == 40 9 | hex_to_bin address 10 | elsif address.size == 42 && address[0..1] == "0x" 11 | hex_to_bin address[2..-1] 12 | else 13 | address 14 | end 15 | end 16 | 17 | def bin_to_hex(string) 18 | RLP::Utils.encode_hex string 19 | end 20 | 21 | def hex_to_bin(string) 22 | RLP::Utils.decode_hex remove_hex_prefix(string) 23 | end 24 | 25 | def base256_to_int(str) 26 | RLP::Sedes.big_endian_int.deserialize str.sub(/\A(\x00)+/, "") 27 | end 28 | 29 | def int_to_base256(int) 30 | RLP::Sedes.big_endian_int.serialize int 31 | end 32 | 33 | def v_r_s_for(signature) 34 | [ 35 | signature[0].bytes[0], 36 | Utils.base256_to_int(signature[1..32]), 37 | Utils.base256_to_int(signature[33..65]), 38 | ] 39 | end 40 | 41 | def prefix_hex(hex) 42 | hex.match(/\A0x/) ? hex : "0x#{hex}" 43 | end 44 | 45 | def remove_hex_prefix(s) 46 | s[0, 2] == "0x" ? s[2..-1] : s 47 | end 48 | 49 | def bin_to_prefixed_hex(binary) 50 | prefix_hex bin_to_hex(binary) 51 | end 52 | 53 | def prefix_message(message) 54 | "\x19Ethereum Signed Message:\n#{message.length}#{message}" 55 | end 56 | 57 | def public_key_to_address(hex) 58 | bytes = hex_to_bin(hex) 59 | address_bytes = Utils.keccak256(bytes[1..-1])[-20..-1] 60 | format_address bin_to_prefixed_hex(address_bytes) 61 | end 62 | 63 | def sha256(x) 64 | Digest::SHA256.digest x 65 | end 66 | 67 | def keccak256(x) 68 | Digest::Keccak.new(256).digest(x) 69 | end 70 | 71 | def keccak512(x) 72 | Digest::Keccak.new(512).digest(x) 73 | end 74 | 75 | def keccak256_rlp(x) 76 | keccak256 RLP.encode(x) 77 | end 78 | 79 | def ripemd160(x) 80 | Digest::RMD160.digest x 81 | end 82 | 83 | def hash160(x) 84 | ripemd160 sha256(x) 85 | end 86 | 87 | def zpad(x, l) 88 | lpad x, BYTE_ZERO, l 89 | end 90 | 91 | def zunpad(x) 92 | x.sub(/\A\x00+/, "") 93 | end 94 | 95 | def zpad_int(n, l = 32) 96 | zpad encode_int(n), l 97 | end 98 | 99 | def zpad_hex(s, l = 32) 100 | zpad decode_hex(s), l 101 | end 102 | 103 | def valid_address?(address) 104 | Address.new(address).valid? 105 | end 106 | 107 | def format_address(address) 108 | Address.new(address).checksummed 109 | end 110 | 111 | private 112 | 113 | def lpad(x, symbol, l) 114 | return x if x.size >= l 115 | symbol * (l - x.size) + x 116 | end 117 | 118 | def encode_int(n) 119 | unless n.is_a?(Integer) && n >= 0 && n <= UINT_MAX 120 | raise ArgumentError, "Integer invalid or out of range: #{n}" 121 | end 122 | 123 | int_to_base256 n 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/eth/version.rb: -------------------------------------------------------------------------------- 1 | module Eth 2 | VERSION = "0.4.18" 3 | end 4 | -------------------------------------------------------------------------------- /spec/eth/address_spec.rb: -------------------------------------------------------------------------------- 1 | describe Eth::Address do 2 | describe "#valid?" do 3 | context "given an address with a valid checksum" do 4 | let(:addresses) do 5 | [ 6 | "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", 7 | "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", 8 | "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", 9 | "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", 10 | ] 11 | end 12 | 13 | it "returns true" do 14 | addresses.each do |address| 15 | expect(Eth::Address.new address).to be_valid 16 | end 17 | end 18 | end 19 | 20 | context "given an address with an invalid checksum" do 21 | let(:addresses) do 22 | [ 23 | "0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", 24 | "0xFB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", 25 | "0xDbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", 26 | "0xd1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", 27 | ] 28 | end 29 | 30 | it "returns false" do 31 | addresses.each do |address| 32 | expect(Eth::Address.new address).not_to be_valid 33 | end 34 | end 35 | end 36 | 37 | context "given an address with all uppercase letters" do 38 | let(:addresses) do 39 | [ 40 | "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED", 41 | "0xFB6916095CA1DF60BB79CE92CE3EA74C37C5D359", 42 | "0xDBF03B407C01E7CD3CBEA99509D93F8DDDC8C6FB", 43 | "0xD1220A0CF47C7B9BE7A2E6BA89F429762E7B9ADB", 44 | # common EIP55 examples 45 | "0x52908400098527886E0F7030069857D2E4169EE7", 46 | "0x8617E340B3D01FA5F11F306F4090FD50E238070D", 47 | ] 48 | end 49 | 50 | it "returns true" do 51 | addresses.each do |address| 52 | expect(Eth::Address.new address).to be_valid 53 | end 54 | end 55 | end 56 | 57 | context "given an address with all lowercase letters" do 58 | let(:addresses) do 59 | [ 60 | "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed", 61 | "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", 62 | "0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb", 63 | "0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb", 64 | # common EIP55 examples 65 | "0xde709f2102306220921060314715629080e2fb77", 66 | "0x27b1fdb04752bbc536007a920d24acb045561c26", 67 | ] 68 | end 69 | 70 | it "returns true" do 71 | addresses.each do |address| 72 | expect(Eth::Address.new address).to be_valid 73 | end 74 | end 75 | end 76 | 77 | context "given an invalid address" do 78 | let(:addresses) do 79 | [ 80 | "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beae", 81 | "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359d", 82 | "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAE", 83 | "0xFB6916095CA1DF60BB79CE92CE3EA74C37C5D359D", 84 | ] 85 | end 86 | 87 | it "returns true" do 88 | addresses.each do |address| 89 | expect(Eth::Address.new address).not_to be_valid 90 | end 91 | end 92 | end 93 | end 94 | 95 | describe "#checksummed" do 96 | let(:addresses) do 97 | [ 98 | # downcased 99 | ["0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed", "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"], 100 | ["0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"], 101 | ["0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb", "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"], 102 | ["0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb", "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"], 103 | # upcased 104 | ["0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED", "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"], 105 | ["0xFB6916095CA1DF60BB79CE92CE3EA74C37C5D359", "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"], 106 | ["0xDBF03B407C01E7CD3CBEA99509D93F8DDDC8C6FB", "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"], 107 | ["0xD1220A0CF47C7B9BE7A2E6BA89F429762E7B9ADB", "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"], 108 | # checksummed 109 | ["0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"], 110 | ["0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"], 111 | ["0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"], 112 | ["0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"], 113 | ] 114 | end 115 | 116 | it "follows EIP55 standard" do 117 | addresses.each do |plain, checksummed| 118 | address = Eth::Address.new(plain) 119 | expect(address.checksummed).to eq checksummed 120 | end 121 | end 122 | 123 | context "given an invalid address" do 124 | let(:bad) { "0x#{SecureRandom.hex(21)[0..40]}" } 125 | 126 | it "raises an error" do 127 | expect { 128 | Eth::Address.new(bad).checksummed 129 | }.to raise_error "Invalid address: #{bad}" 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /spec/eth/eip155_spec.rb: -------------------------------------------------------------------------------- 1 | describe "EIP 155 and replay protection" do 2 | let(:key) { Eth::Key.new priv: "4646464646464646464646464646464646464646464646464646464646464646" } 3 | 4 | context "EIP155 example" do 5 | #via https://github.com/ethereum/EIPs/issues/155#issue-183002027 6 | 7 | let(:hex) { "0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a006f3c7fb391722beb8ae599a899fe6fd6f6eae4b0f3df4bbc54bc3c673aa92cda0423bb70e7f851514a73a14cee940ec0acab1bab6fb274fa7b922adbdcbf08611" } 8 | let(:expected_signing_data) { "ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080" } 9 | let(:tx) { Eth::Tx.decode hex } 10 | let(:signing_data) { tx.unsigned_encoded } 11 | 12 | it "decodes the transaction and recognizes the signer" do 13 | sig = tx.signature 14 | 15 | expect(tx.chain_id).to eq 1 16 | 17 | expect(sig[:v]).to eq 37 18 | expect(sig[:r]).to eq 3144601148722688608716531623347812328676993065715946531989621665587339629261 19 | expect(sig[:s]).to eq 29958155393767630265145914935642492209353907522463775796041782419660953650705 20 | 21 | expect(bin_to_hex signing_data).to eq(expected_signing_data) 22 | expect(key.verify_signature signing_data, tx.ecdsa_signature).to be true 23 | expect(key.address).to eq(tx.from) 24 | end 25 | end 26 | 27 | context "pre-EIP155 fork" do 28 | let(:hex) do 29 | Eth::Tx.new({ 30 | chain_id: nil, 31 | nonce: 9, 32 | gas_price: (20 * 10 ** 9), 33 | gas_limit: 21000, 34 | to: "0x3535353535353535353535353535353535353535", 35 | value: (10 ** 18), 36 | data: "", 37 | }).sign(key).hex 38 | end 39 | let(:expected_signing_data) { "e9098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080" } 40 | let(:tx) { Eth::Tx.decode hex } 41 | 42 | it "decodes the transaction and recognizes the signer" do 43 | sig = tx.signature 44 | signing_data = tx.unsigned_encoded 45 | 46 | expect([Eth.v_base, (Eth.v_base + 1)]).to include sig[:v] 47 | 48 | expect(bin_to_hex signing_data).to eq(expected_signing_data) 49 | expect(key.verify_signature signing_data, tx.ecdsa_signature).to be true 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/eth/eth_spec.rb: -------------------------------------------------------------------------------- 1 | describe Eth do 2 | describe ".configure" do 3 | end 4 | 5 | describe "#v_base" do 6 | it "is set to 27 by default" do 7 | expect(Eth.v_base).to eq(27) 8 | end 9 | end 10 | 11 | describe "#replayable_v?" do 12 | it "returns true for anything other than 27 and 28" do 13 | expect(Eth.replayable_v? 0).to be false 14 | expect(Eth.replayable_v? nil).to be false 15 | expect(Eth.replayable_v? 26).to be false 16 | expect(Eth.replayable_v? 27).to be true 17 | expect(Eth.replayable_v? 28).to be true 18 | expect(Eth.replayable_v? 28.1).to be false 19 | expect(Eth.replayable_v? 29).to be false 20 | expect(Eth.replayable_v? Float::INFINITY).to be false 21 | end 22 | end 23 | 24 | describe "#chain_id_from_signature" do 25 | it "converts v to the correct chain ID" do 26 | expect(Eth.chain_id_from_signature({ v: 27, r: 0, s: 0 })).to be_nil 27 | expect(Eth.chain_id_from_signature({ v: 28, r: 0, s: 0 })).to be_nil 28 | expect(Eth.chain_id_from_signature({ v: 29, r: 0, s: 0 })).to be_nil 29 | expect(Eth.chain_id_from_signature({ v: 30, r: 0, s: 0 })).to be_nil 30 | expect(Eth.chain_id_from_signature({ v: 36, r: 0, s: 0 })).to be_nil 31 | expect(Eth.chain_id_from_signature({ v: 37, r: 0, s: 0 })).to eq(1) 32 | expect(Eth.chain_id_from_signature({ v: 38, r: 0, s: 0 })).to eq(1) 33 | expect(Eth.chain_id_from_signature({ v: 119, r: 0, s: 0 })).to eq(42) 34 | expect(Eth.chain_id_from_signature({ v: 120, r: 0, s: 0 })).to eq(42) 35 | expect(Eth.chain_id_from_signature({ v: 867, r: 0, s: 0 })).to eq(416) 36 | expect(Eth.chain_id_from_signature({ v: 868, r: 0, s: 0 })).to eq(416) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/eth/key/decrypter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Eth::Key::Decrypter do 2 | describe ".perform pbkdf2 key" do 3 | let(:password) { "testpassword" } 4 | let(:key_data) { read_key_fixture password } 5 | 6 | it "recovers the example pbkdf2 key" do 7 | result = Eth::Key::Decrypter.perform key_data, password 8 | expect(result).to eq("7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d") 9 | end 10 | end 11 | 12 | describe ".perform scrypt key" do 13 | let(:password) { "testingtesting" } 14 | let(:key_data) { read_key_fixture password } 15 | 16 | it "recovers the example scrypt key" do 17 | result = Eth::Key::Decrypter.perform key_data, password 18 | expect(result).to eq("61a59f570abf5145971648acec6edc5f61487a9b570ca9c4e4c9f2d8e356b9af") 19 | end 20 | end 21 | 22 | describe "detects unknown key derivation functions" do 23 | let(:password) { "testunknownkdf" } 24 | let(:key_data) { read_key_fixture password } 25 | 26 | it "detects unknown key derivation functions" do 27 | expect { Eth::Key::Decrypter.perform key_data, password }.to raise_error(RuntimeError) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/eth/key/encrypter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Eth::Key::Encrypter do 2 | describe ".perform" do 3 | let(:password) { "testpassword" } 4 | let(:key) { "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" } 5 | let(:uuid) { "3198bc9c-6672-5ab3-d995-4942343ae5b6" } 6 | let(:iv) { "6087dab2f9fdbbfaddc31a909735c1e6" } 7 | let(:salt) { "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" } 8 | let(:options) do 9 | { 10 | iv: iv, 11 | salt: salt, 12 | id: uuid, 13 | } 14 | end 15 | 16 | it "recovers the key" do 17 | result = Eth::Key::Encrypter.perform key, password, options 18 | json = JSON.parse(result) 19 | 20 | expect(json["address"]).to eq("0x008AeEda4D805471dF9b2A5B0f38A0C3bCBA786b") 21 | expect(json["crypto"]["cipher"]).to eq("aes-128-ctr") 22 | expect(json["crypto"]["cipherparams"]["iv"]).to eq(iv) 23 | expect(json["crypto"]["ciphertext"]).to eq("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46") 24 | expect(json["crypto"]["kdf"]).to eq("pbkdf2") 25 | expect(json["crypto"]["kdfparams"]["c"]).to eq(262_144) 26 | expect(json["crypto"]["kdfparams"]["dklen"]).to eq(32) 27 | expect(json["crypto"]["kdfparams"]["prf"]).to eq("hmac-sha256") 28 | expect(json["crypto"]["kdfparams"]["salt"]).to eq(salt) 29 | expect(json["crypto"]["mac"]).to eq("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2") 30 | expect(json["id"]).to eq(uuid) 31 | expect(json["version"]).to eq(3) 32 | end 33 | 34 | context "when specifying not to include the address" do 35 | let(:options) do 36 | { 37 | skip_address: true, 38 | } 39 | end 40 | 41 | it "recovers the key" do 42 | result = Eth::Key::Encrypter.perform key, password, options 43 | json = JSON.parse(result) 44 | expect(json["address"]).to be_nil 45 | expect(json.keys).not_to include "address" 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/eth/key_spec.rb: -------------------------------------------------------------------------------- 1 | describe Eth::Key, type: :model do 2 | let(:priv) { nil } 3 | subject(:key) { Eth::Key.new priv: priv } 4 | 5 | describe "#initialize" do 6 | it "returns a key with a new private key" do 7 | key1 = Eth::Key.new 8 | key2 = Eth::Key.new 9 | 10 | expect(key1.private_hex).not_to eq(key2.private_hex) 11 | expect(key1.public_hex).not_to eq(key2.public_hex) 12 | end 13 | 14 | it "regenerates an old private key" do 15 | key1 = Eth::Key.new 16 | key2 = Eth::Key.new priv: key1.private_hex 17 | 18 | expect(key1.private_hex).to eq(key2.private_hex) 19 | expect(key1.public_hex).to eq(key2.public_hex) 20 | end 21 | end 22 | 23 | describe "#sign" do 24 | let(:message) { "Hi Mom!" } 25 | 26 | it "signs a message so that the public key is recoverable" do 27 | 10.times do 28 | signature = key.sign message 29 | expect(key.verify_signature message, signature).to be_truthy 30 | s_value = Eth::Utils.v_r_s_for(signature).last 31 | expect(s_value).to be < (Eth::Secp256k1::N / 2) 32 | end 33 | end 34 | end 35 | 36 | describe "#personal_sign" do 37 | let(:message) { "Hi Mom!" } 38 | 39 | it "signs a message so that the public key can be recovered with personal_recover" do 40 | 10.times do 41 | signature = key.personal_sign message 42 | expect(Eth::Key.personal_recover message, signature).to eq(key.public_hex) 43 | end 44 | end 45 | end 46 | 47 | describe ".personal_recover" do 48 | let(:message) { "test" } 49 | let(:signature) { "3eb24bd327df8c2b614c3f652ec86efe13aa721daf203820241c44861a26d37f2bffc6e03e68fc4c3d8d967054c9cb230ed34339b12ef89d512b42ae5bf8c2ae1c" } 50 | let(:public_hex) { "043e5b33f0080491e21f9f5f7566de59a08faabf53edbc3c32aaacc438552b25fdde531f8d1053ced090e9879cbf2b0d1c054e4b25941dab9254d2070f39418afc" } 51 | 52 | it "it can recover a public key from a signature generated with web3/metamask" do 53 | 10.times do 54 | expect(Eth::Key.personal_recover message, signature).to eq(public_hex) 55 | end 56 | end 57 | 58 | context "when signing with ledger that uses signature with v = 0" do 59 | let(:message) { "test" } 60 | let(:signature) { "0x5c433983b23738940ce256c59d5bc6a3d5fd12c5bc9bdbf0ffdffb7be1a09d1815ca1db167c61a10945837f3fb4821086d6656b4fa6ede9c4d1aeaf07e2b0adf01" } 61 | let(:public_hex) { "04e51ff5abc511f2fda0f893c10054123e92527b5e69e24cca538e74edbd604508259e1b265b54628bc8024fb791e459f67adb770b20962eb38fabe8b86f2aebaa" } 62 | 63 | it "it can recover a public key from a signature generated with ledger/metamask" do 64 | expect(Eth::Key.personal_recover message, signature).to eq(public_hex) 65 | end 66 | end 67 | end 68 | 69 | describe "#verify_signature" do 70 | let(:priv) { "5a37533acfa3ff9386aed01e16c0e7a79038ce05cc383e290d360b8ce9cd6fdf" } 71 | let(:signature) { hex_to_bin "1ce2f13b4123a23a4a280ac4adcba1ffa3f3848f494dc1de440af43f677e0e01260fb4667ed117d555659b249702c8215162b3f0ee09628813a4ef83616f99f180" } 72 | let(:message) { "Hi Mom!" } 73 | 74 | it "signs a message so that the public key is recoverable" do 75 | expect(key.verify_signature message, signature).to be_truthy 76 | end 77 | 78 | context "when the signature matches another public key" do 79 | let(:other_priv) { "fd7f87d1f8c6cdfeb36caa491864519e89b405850c9e2e070839e74966d810cf" } 80 | let(:signature) { hex_to_bin "1b21a66b55af07a2b0981e3a0ba1768382c5bdbed3d16bcc58a8011425b3bbc090f881cc13d16792af55438637fbe9a2a81d85d6bb18b87b6c08aa9c20ce1341f4" } 81 | 82 | it "does not verify the signature" do 83 | expect(key.verify_signature message, signature).to be_falsy 84 | end 85 | end 86 | 87 | context "when the signature does not match any public key" do 88 | let(:signature) { hex_to_bin "1b21a66b" } 89 | 90 | it "signs a message so that the public key is recoverable" do 91 | expect(key.verify_signature message, signature).to be_falsy 92 | end 93 | end 94 | end 95 | 96 | describe "#address" do 97 | subject { key.address } 98 | let(:priv) { "c3a4349f6e57cfd2cbba275e3b3d15a2e4cf00c89e067f6e05bfee25208f9cbb" } 99 | it { is_expected.to eq("0x759b427456623a33030bbC2195439C22A8a51d25") } 100 | it { is_expected.to eq(key.to_address) } 101 | end 102 | 103 | describe ".encrypt/.decrypt" do 104 | # see: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition 105 | 106 | let(:password) { SecureRandom.base64 } 107 | let(:key) { Eth::Key.new } 108 | 109 | it "reads and writes keys in the Ethereum Secret Storage definition" do 110 | encrypted = Eth::Key.encrypt key, password 111 | decrypted = Eth::Key.decrypt encrypted, password 112 | 113 | expect(key.address).to eq(decrypted.address) 114 | expect(key.public_hex).to eq(decrypted.public_hex) 115 | expect(key.private_hex).to eq(decrypted.private_hex) 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/eth/tx_spec.rb: -------------------------------------------------------------------------------- 1 | describe Eth::Tx, type: :model do 2 | let(:nonce) { rand 1_000_000 } 3 | let(:gas_price) { 10_000 } 4 | let(:gas_limit) { 100_000 } 5 | let(:recipient) { SecureRandom.hex 20 } 6 | let(:value) { 10 ** 11 } 7 | let(:data) { SecureRandom.hex } 8 | let(:v) { 27 } 9 | let(:r) { rand(1_000_000_000) } 10 | let(:s) { rand(1_000_000_000) } 11 | let(:options) { {} } 12 | let(:tx) do 13 | Eth::Tx.new({ 14 | nonce: nonce, 15 | gas_price: gas_price, 16 | gas_limit: gas_limit, 17 | to: recipient, 18 | value: value, 19 | data: data, 20 | v: v, 21 | r: r, 22 | s: s, 23 | }) 24 | end 25 | let(:tx_fields_42) do 26 | { 27 | chain_id: 42, 28 | nonce: nonce, 29 | gas_price: gas_price, 30 | gas_limit: gas_limit, 31 | to: recipient, 32 | value: value, 33 | data: data, 34 | v: v, 35 | r: r, 36 | s: s, 37 | } 38 | end 39 | let(:tx_fields_416) do 40 | { 41 | chain_id: 416, 42 | nonce: nonce, 43 | gas_price: gas_price, 44 | gas_limit: gas_limit, 45 | to: recipient, 46 | value: value, 47 | data: data, 48 | v: v, 49 | r: r, 50 | s: s, 51 | } 52 | end 53 | let(:tx_fields_encoded_416) do 54 | { 55 | data: "0xc950f8f0000000000000000000000000762a441605c438742754bb357dd241c8326c25e0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000028", 56 | to: "0x69351bffb36f3d1a6fa4374da03562a1935e7047", 57 | gas_price: 0x3b9aca00, 58 | value: 0x0, 59 | nonce: 99, 60 | chain_id: 416, 61 | from: "0x22441c383a1e27acbf99663f1861e4936ac86049", 62 | gas_limit: 71115, 63 | } 64 | end 65 | let(:tx_encoded_416) do 66 | "0xf8cb63843b9aca00830115cb9469351bffb36f3d1a6fa4374da03562a1935e704780b864c950f8f0000000000000000000000000762a441605c438742754bb357dd241c8326c25e0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000028820364a03944cee478f84e7186b726858a03b7212281dfd0045df6c22bfb101042a8115ca07676eec5290861869cbd4e029cb8aecb17571dfd2de2499809e462595a956e39" 67 | end 68 | 69 | describe "#initialize" do 70 | it "sets the arguments in the order of serializable fields" do 71 | expect(tx.nonce).to eq(nonce) 72 | expect(tx.gas_price).to eq(gas_price) 73 | expect(tx.gas_limit).to eq(gas_limit) 74 | expect(tx.to).to eq(hex_to_bin recipient) 75 | expect(tx.value).to eq(value) 76 | expect(tx.data).to eq("0x#{data}") 77 | expect(tx.v).to eq(v) 78 | expect(tx.r).to eq(r) 79 | expect(tx.s).to eq(s) 80 | end 81 | 82 | context "when the gas limit is too low" do 83 | let(:gas_limit) { 20_000 } 84 | 85 | it "raises an InvalidTransaction error" do 86 | expect { tx }.to raise_error(Eth::InvalidTransaction, "Gas limit too low") 87 | end 88 | end 89 | 90 | context "there are values beyond the unsigned integer max" do 91 | let(:nonce) { Eth::UINT_MAX + 1 } 92 | 93 | it "raises an InvalidTransaction error" do 94 | expect { tx }.to raise_error(Eth::InvalidTransaction, "Values way too high!") 95 | end 96 | end 97 | 98 | context "when configured to take data as binary" do 99 | before { configure_tx_data_hex false } 100 | let(:data) { hex_to_bin SecureRandom.hex } 101 | 102 | it "still propperly sets the data field" do 103 | expect(tx.data).to eq(data) 104 | end 105 | end 106 | 107 | context "when chain_id is not in the parameters" do 108 | it "uses the default chain ID" do 109 | cid = Eth.chain_id 110 | configure_chain_id 42 111 | tx = Eth::Tx.new({ 112 | nonce: nonce, 113 | gas_price: gas_price, 114 | gas_limit: gas_limit, 115 | to: recipient, 116 | value: value, 117 | data: data, 118 | v: v, 119 | r: r, 120 | s: s, 121 | }) 122 | configure_chain_id cid 123 | expect(tx.chain_id).to eq 42 124 | end 125 | end 126 | end 127 | 128 | describe ".decode" do 129 | let(:key) { Eth::Key.new } 130 | let(:tx1) { tx.sign key } 131 | 132 | it "returns an instance that matches the original encoded one" do 133 | tx2 = Eth::Tx.decode tx1.encoded 134 | expect(tx2).to eq(tx1) 135 | end 136 | 137 | it "also accepts hex" do 138 | tx2 = Eth::Tx.decode(tx1.hex) 139 | expect(tx2).to eq(tx1) 140 | end 141 | 142 | it "decodes Web3.js generated data correctly" do 143 | tx_416 = Eth::Tx.new tx_fields_encoded_416 144 | tx2 = Eth::Tx.decode tx_encoded_416 145 | fields = tx_fields_encoded_416.keys 146 | tx2_fields = tx2.to_h.select { |k, v| tx_fields_encoded_416.include?(k) } 147 | tx_416_fields = tx_416.to_h.select { |k, v| tx_fields_encoded_416.include?(k) } 148 | expect(tx2_fields).to eq(tx_416_fields) 149 | end 150 | 151 | it "initializes chain_id correctly" do 152 | tx_416 = Eth::Tx.new tx_fields_encoded_416 153 | expect(tx_416.chain_id).to eq 416 154 | end 155 | 156 | it "ignores the default chain ID" do 157 | configure_chain_id 42 158 | tx_416 = Eth::Tx.new tx_fields_encoded_416 159 | expect(tx_416.chain_id).to eq 416 160 | end 161 | end 162 | 163 | describe "#sign" do 164 | let(:v) { nil } 165 | let(:r) { nil } 166 | let(:s) { nil } 167 | let(:key) { Eth::Key.new } 168 | 169 | context "creates a recoverable signature for the transaction" do 170 | it "with undefined chain ID" do 171 | tx.sign key 172 | verified = key.verify_signature tx.unsigned_encoded, tx.ecdsa_signature 173 | expect(verified).to be_truthy 174 | end 175 | 176 | it "with small chain ID" do 177 | tx_42 = Eth::Tx.new(tx_fields_42) 178 | expect(tx_42.chain_id).to equal(42) 179 | tx_42.sign key 180 | verified = key.verify_signature tx_42.unsigned_encoded, tx_42.ecdsa_signature 181 | expect(verified).to be_truthy 182 | end 183 | 184 | it "with large chain ID" do 185 | tx_416 = Eth::Tx.new(tx_fields_416) 186 | expect(tx_416.chain_id).to equal(416) 187 | tx_416.sign key 188 | verified = key.verify_signature tx_416.unsigned_encoded, tx_416.ecdsa_signature 189 | expect(verified).to be_truthy 190 | end 191 | 192 | it "with nil chain ID" do 193 | nil_fields = tx_fields_416 194 | nil_fields[:chain_id] = nil 195 | tx_nil = Eth::Tx.new(nil_fields) 196 | expect(tx_nil.chain_id).to be_nil 197 | tx_nil.sign key 198 | verified = key.verify_signature tx_nil.unsigned_encoded, tx_nil.ecdsa_signature 199 | expect(verified).to be_truthy 200 | end 201 | 202 | it "after chain ID is modified" do 203 | tx_42 = Eth::Tx.new(tx_fields_42) 204 | expect(tx_42.chain_id).to equal(42) 205 | tx_42.sign key 206 | verified = key.verify_signature tx_42.unsigned_encoded, tx_42.ecdsa_signature 207 | expect(verified).to be_truthy 208 | 209 | tx_42.chain_id = 616 210 | tx_42.sign key 211 | verified = key.verify_signature tx_42.unsigned_encoded, tx_42.ecdsa_signature 212 | expect(verified).to be_truthy 213 | end 214 | end 215 | 216 | context "generates the same signer for the transaction" do 217 | it "after chain ID is modified" do 218 | tx_42 = Eth::Tx.new(tx_fields_42) 219 | expect(tx_42.chain_id).to equal(42) 220 | tx_42.sign key 221 | expect(tx_42.from).to eq(key.address) 222 | tx_42.chain_id = 616 223 | expect(tx_42.from).to be_nil 224 | tx_42.sign key 225 | expect(tx_42.from).to eq(key.address) 226 | end 227 | end 228 | end 229 | 230 | describe "#to_h" do 231 | let(:key) { Eth::Key.new } 232 | 233 | before { tx.sign key } 234 | 235 | it "returns all the same values" do 236 | hash = tx.to_h 237 | 238 | expect(hash[:nonce]).to eq(tx.nonce) 239 | expect(hash[:gas_price]).to eq(tx.gas_price) 240 | expect(hash[:gas_limit]).to eq(tx.gas_limit) 241 | expect(hash[:to]).to eq(tx.to) 242 | expect(hash[:data]).to eq(tx.data) 243 | expect(hash[:v]).to eq(tx.v) 244 | expect(hash[:r]).to eq(tx.r) 245 | expect(hash[:s]).to eq(tx.s) 246 | end 247 | 248 | it "does not set the binary data field" do 249 | hash = tx.to_h 250 | expect(hash[:data_bin]).to be_nil 251 | end 252 | 253 | it "can be converted back into a transaction" do 254 | tx2 = Eth::Tx.new(tx.to_h) 255 | expect(tx2.data).to eq tx.data 256 | expect(tx2).to eq tx 257 | end 258 | end 259 | 260 | describe "#hex" do 261 | let(:key) { Eth::Key.new } 262 | 263 | it "creates a hex representation" do 264 | tx = Eth::Tx.new({ 265 | data: "abcdef", 266 | gas_limit: 3_141_592, 267 | gas_price: 20_000_000_000, 268 | nonce: 0, 269 | to: key.address, 270 | value: 1_000_000_000_000, 271 | }) 272 | 273 | expect(tx.hex).not_to be_nil 274 | end 275 | end 276 | 277 | describe "#from" do 278 | let(:key) { Eth::Key.new } 279 | subject { tx.from } 280 | 281 | context "when the signature is present" do 282 | before do 283 | tx.sign key 284 | end 285 | 286 | it { is_expected.to eq(key.address) } 287 | end 288 | 289 | context "when the signature does NOT match" do 290 | before do 291 | tx.sign key 292 | tx.signature = nil 293 | tx.r = tx.r + 1 294 | end 295 | 296 | it { is_expected.not_to eq(key.address) } 297 | end 298 | 299 | context "when the signature is NOT present" do 300 | let(:v) { nil } 301 | let(:r) { nil } 302 | let(:s) { nil } 303 | 304 | it { is_expected.to be_nil } 305 | end 306 | 307 | context "from a decoded transaction" do 308 | it "returns the correct sender" do 309 | tx2 = Eth::Tx.decode tx_encoded_416 310 | expect(tx2.from.upcase).to eq(tx_fields_encoded_416[:from].upcase) 311 | end 312 | end 313 | 314 | context "when the chain ID is changed" do 315 | let(:key) { Eth::Key.new priv: "4bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200" } 316 | 317 | it "returns a nil sender if the chain ID did change" do 318 | tx42 = Eth::Tx.new tx_fields_42 319 | tx42.sign key 320 | expect(tx42.from).to eq(key.address) 321 | tx42.chain_id = 616 322 | expect(tx42.from).to be_nil 323 | end 324 | 325 | it "returns a nil sender if the chain ID is nulled" do 326 | tx42 = Eth::Tx.new tx_fields_42 327 | tx42.sign key 328 | expect(tx42.from).to eq(key.address) 329 | tx42.chain_id = nil 330 | expect(tx42.from).to be_nil 331 | end 332 | 333 | it "returns the same sender if the chain ID did not change" do 334 | tx42 = Eth::Tx.new tx_fields_42 335 | tx42.sign key 336 | expect(tx42.from).to eq(key.address) 337 | tx42.chain_id = tx_fields_42[:chain_id] 338 | expect(tx42.from).to be_nil 339 | end 340 | end 341 | end 342 | 343 | describe "#hash" do 344 | let(:txid1) { "0x66734e70ea28eaa28eb1bace4ca87573c48f52cca7590459ad20dc58bae1a819" } 345 | let(:txid2) { "0x7151f5b0d229c62a5076de4133ba06fffc033e25bf99691c3e0a0a99c5a64538" } 346 | let(:txids) { [txid1, txid2] } 347 | 348 | it "hashes the serialized full transaction" do 349 | txids.each do |txid| 350 | tx = Eth::Tx.decode read_hex_fixture(txid) 351 | expect(tx.hash).to eq(txid) 352 | expect(tx.id).to eq(txid) 353 | end 354 | end 355 | end 356 | 357 | describe "#data_hex" do 358 | it "converts the hex to binary and persists it" do 359 | hex = "0123456789abcdef" 360 | binary = Eth::Utils.hex_to_bin hex 361 | 362 | expect { 363 | tx.data_hex = hex 364 | }.to change { 365 | tx.data_bin 366 | }.to(binary).and change { 367 | tx.data_hex 368 | }.to("0x#{hex}") 369 | end 370 | end 371 | 372 | describe "#data_bin" do 373 | it "returns the data in a binary format" do 374 | hex = "0123456789abcdef" 375 | binary = Eth::Utils.hex_to_bin hex 376 | 377 | expect { 378 | tx.data_bin = binary 379 | }.to change { 380 | tx.data_bin 381 | }.to(binary).and change { 382 | tx.data 383 | }.to("0x#{hex}") 384 | end 385 | end 386 | 387 | describe "#data" do 388 | after { configure_tx_data_hex } 389 | 390 | let(:hex) { "0123456789abcdef" } 391 | let(:binary) { Eth::Utils.hex_to_bin hex } 392 | 393 | context "when configured to use hex" do 394 | before { configure_tx_data_hex true } 395 | 396 | it "accepts hex" do 397 | expect { 398 | tx.data = hex 399 | }.to change { 400 | tx.data_bin 401 | }.to(binary).and change { 402 | tx.data_hex 403 | }.to("0x#{hex}") 404 | end 405 | end 406 | 407 | context "when configured to use binary" do 408 | before { configure_tx_data_hex false } 409 | 410 | it "converts the hex to binary and persists it" do 411 | expect { 412 | tx.data = binary 413 | }.to change { 414 | tx.data_bin 415 | }.to(binary).and change { 416 | tx.data_hex 417 | }.to("0x#{hex}") 418 | end 419 | end 420 | end 421 | 422 | describe "#chain_id" do 423 | context "when transaction was signed explicitly" do 424 | let(:key) { Eth::Key.new } 425 | let(:tx42) { Eth::Tx.new tx_fields_42 } 426 | 427 | it "nulls the signature on a chain ID change" do 428 | tx42.sign key 429 | 430 | expect(tx42.chain_id).to equal(42) 431 | expect(tx42.signature).to be_a(Hash) 432 | expect(tx42.signature).to include(:v, :r, :s) 433 | tx42.chain_id = 416 434 | expect(tx42.chain_id).to equal(416) 435 | expect(tx42.signature).to be_nil 436 | end 437 | 438 | it "nulls the signature when chain ID is nulled" do 439 | tx42.sign key 440 | 441 | expect(tx42.chain_id).to equal(42) 442 | expect(tx42.signature).to be_a(Hash) 443 | expect(tx42.signature).to include(:v, :r, :s) 444 | tx42.chain_id = nil 445 | expect(tx42.chain_id).to be_nil 446 | expect(tx42.signature).to be_nil 447 | end 448 | 449 | it "keeps the signature if chain ID does not change" do 450 | tx42.sign key 451 | 452 | expect(tx42.chain_id).to equal(42) 453 | expect(tx42.signature).to be_a(Hash) 454 | expect(tx42.signature).to include(:v, :r, :s) 455 | osig = {}.merge(tx42.signature) 456 | tx42.chain_id = 42 457 | expect(tx42.chain_id).to equal(42) 458 | expect(tx42.signature).to eq(osig) 459 | end 460 | end 461 | 462 | context "when transaction was decoded" do 463 | let(:tx416) { Eth::Tx.decode tx_encoded_416 } 464 | 465 | it "nulls the signature on a chain ID change" do 466 | tx416 = Eth::Tx.decode tx_encoded_416 467 | expect(tx416.chain_id).to equal(416) 468 | expect(tx416.signature).to be_a(Hash) 469 | expect(tx416.signature).to include(:v, :r, :s) 470 | tx416.chain_id = 42 471 | expect(tx416.chain_id).to equal(42) 472 | expect(tx416.signature).to be_nil 473 | end 474 | 475 | it "nulls the signature when chain ID is nulled" do 476 | tx416 = Eth::Tx.decode tx_encoded_416 477 | expect(tx416.chain_id).to equal(416) 478 | expect(tx416.signature).to be_a(Hash) 479 | expect(tx416.signature).to include(:v, :r, :s) 480 | tx416.chain_id = nil 481 | expect(tx416.chain_id).to be_nil 482 | expect(tx416.signature).to be_nil 483 | end 484 | 485 | it "keeps the signature if chain ID does not change" do 486 | tx416 = Eth::Tx.decode tx_encoded_416 487 | expect(tx416.chain_id).to equal(416) 488 | expect(tx416.signature).to be_a(Hash) 489 | expect(tx416.signature).to include(:v, :r, :s) 490 | osig = {}.merge(tx416.signature) 491 | tx416.chain_id = 416 492 | expect(tx416.chain_id).to equal(416) 493 | expect(tx416.signature).to eq(osig) 494 | end 495 | end 496 | end 497 | end 498 | -------------------------------------------------------------------------------- /spec/eth/utils_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : ascii-8bit -*- 2 | 3 | describe Eth::Utils, type: :model do 4 | describe ".int_to_base256" do 5 | let(:hex) { "1c18f80381f0ef01e63617fc8eeda646bcef8dea61b34cf0aa079b48ec64e6e55d64d0398818a61bfdcf938e9aa175d16661ffa696629a6abc367a49fad3df90b8" } 6 | let(:bin) { Eth::Utils.hex_to_bin hex } 7 | let(:int) { Eth::Utils.base256_to_int bin } 8 | 9 | it "gets the same result back" do 10 | base256 = Eth::Utils.int_to_base256 int 11 | expect(base256).to eq(bin) 12 | end 13 | end 14 | 15 | describe ".base256_to_int" do 16 | it "properly converts binary to integers" do 17 | expect(Eth::Utils.base256_to_int("\xff")).to eq(255) 18 | expect(Eth::Utils.base256_to_int("\x00\x00\xff")).to eq(255) 19 | end 20 | end 21 | 22 | describe ".prefix_hex" do 23 | it "ensures that a hex value has 0x at the beginning" do 24 | expect(Eth::Utils.prefix_hex("abc")).to eq("0xabc") 25 | expect(Eth::Utils.prefix_hex("0xabc")).to eq("0xabc") 26 | end 27 | 28 | it "does not reformat the hex or remove leading zeros" do 29 | expect(Eth::Utils.prefix_hex("0123")).to eq("0x0123") 30 | end 31 | end 32 | 33 | describe ".public_key_to_addres" do 34 | let(:address) { "0x8ABC566c5198bc6993526DB697FFe58ce4e2425A" } 35 | let(:pub) { "0463a1ad6824c03f81ad6c9c224384172c67f6bfd2dbde8c4747a033629b531ae3284db3045e4e40c2b865e22a806ae7dff9264299ea8696321f689d6e134d937e" } 36 | 37 | it "turns a hex public key into a hex address" do 38 | expect(Eth::Utils.public_key_to_address(pub)).to eq(address) 39 | end 40 | end 41 | 42 | describe ".keccak256" do 43 | it "properly hashes using" do 44 | value = "\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p" 45 | 46 | expect(value).to eq(Eth::Utils.keccak256("")) 47 | end 48 | end 49 | 50 | describe ".keccak256_rlp" do 51 | it "properly serializes and hashes" do 52 | value1 = "V\xe8\x1f\x17\x1b\xccU\xa6\xff\x83E\xe6\x92\xc0\xf8n[H\xe0\x1b\x99l\xad\xc0\x01b/\xb5\xe3c\xb4!" 53 | value2 = "_\xe7\xf9w\xe7\x1d\xba.\xa1\xa6\x8e!\x05{\xee\xbb\x9b\xe2\xac0\xc6A\n\xa3\x8dO?\xbeA\xdc\xff\xd2" 54 | value3 = "\x1d\xccM\xe8\xde\xc7]z\xab\x85\xb5g\xb6\xcc\xd4\x1a\xd3\x12E\x1b\x94\x8at\x13\xf0\xa1B\xfd@\xd4\x93G" 55 | value4 = "YZ\xef\x85BA8\x89\x08?\x83\x13\x88\xcfv\x10\x0f\xd8a:\x97\xaf\xb8T\xdb#z#PF89" 56 | 57 | expect(value1).to eq Eth::Utils.keccak256_rlp("") 58 | expect(value2).to eq Eth::Utils.keccak256_rlp(1) 59 | expect(value3).to eq Eth::Utils.keccak256_rlp([]) 60 | expect(value4).to eq Eth::Utils.keccak256_rlp([1, [2, 3], "4", ["5", [6]]]) 61 | end 62 | end 63 | 64 | describe ".hex_to_bin" do 65 | it "raises an error when given invalid hex" do 66 | expect { 67 | Eth::Utils.hex_to_bin("xxxx") 68 | }.to raise_error(TypeError) 69 | 70 | expect { 71 | Eth::Utils.hex_to_bin("\x00\x00") 72 | }.to raise_error(TypeError) 73 | end 74 | end 75 | 76 | describe ".ripemd160" do 77 | it "properly hashes with RIPEMD-160" do 78 | value = "\xc8\x1b\x94\x934 \"\x1az\xc0\x04\xa9\x02B\xd8\xb1\xd3\xe5\x07\r" 79 | 80 | expect(value).to eq Eth::Utils.ripemd160("\x00") 81 | end 82 | end 83 | 84 | describe ".format_address" do 85 | let(:address) { "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED" } 86 | subject { Eth::Utils.format_address address } 87 | 88 | it "returns checksummed addresses" do 89 | expect(subject).to eq("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") 90 | end 91 | end 92 | 93 | describe ".zunpad" do 94 | subject { Eth::Utils.zunpad(address) } 95 | 96 | context "with single leading null byte" do 97 | let(:address) { "\0\xc8\x1b\x94\x934 \"\x1az\xc0\x04\xa9\x02B\xd8\xb1\xd3\xe5\x07\r" } 98 | 99 | it "returns address without leading null byte" do 100 | expect(subject).to eq("\xc8\x1b\x94\x934 \"\x1az\xc0\x04\xa9\x02B\xd8\xb1\xd3\xe5\x07\r") 101 | end 102 | end 103 | 104 | context "with multiple leading null bytes" do 105 | let(:address) { "\0\0\xc8\x1b\x94\x934 \"\x1az\xc0\x04\xa9\x02B\xd8\xb1\xd3\xe5\x07\r" } 106 | 107 | it "returns address without leading null bytes" do 108 | expect(subject).to eq("\xc8\x1b\x94\x934 \"\x1az\xc0\x04\xa9\x02B\xd8\xb1\xd3\xe5\x07\r") 109 | end 110 | end 111 | 112 | context "without leading null byte" do 113 | let(:address) { "\xc8\x1b\x94\x934 \"\x1az\xc0\x04\xa9\x02B\xd8\xb1\xd3\xe5\x07\r" } 114 | 115 | it "returns unchanged address" do 116 | expect(subject).to eq("\xc8\x1b\x94\x934 \"\x1az\xc0\x04\xa9\x02B\xd8\xb1\xd3\xe5\x07\r") 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/ethereum_tests_spec.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | describe "Ethereum common tests" do 4 | before { configure_chain_id 1 } 5 | 6 | it "passes all the transaction tests" do 7 | [ 8 | "spec/fixtures/ethereum_tests/TransactionTests/ttTransactionTest.json", 9 | "spec/fixtures/ethereum_tests/TransactionTests/EIP155/ttTransactionTestVRule.json", 10 | "spec/fixtures/ethereum_tests/TransactionTests/EIP155/ttTransactionTest.json", 11 | "spec/fixtures/ethereum_tests/TransactionTests/EIP155/ttTransactionTestEip155VitaliksTests.json", 12 | ].each do |file_path| 13 | JSON.parse(File.read file_path).each do |name, json| 14 | next unless json_tx = json["transaction"] 15 | 16 | tx = Eth::Tx.decode json["rlp"] 17 | 18 | expect(tx.from.downcase).to eq "0x#{json["sender"]}" 19 | expect(tx.v).to eq json_tx["v"].to_i(16) 20 | expect(tx.r).to eq json_tx["r"].to_i(16) 21 | expect(tx.s).to eq json_tx["s"].to_i(16) 22 | expect(tx.value).to eq json_tx["value"].to_i(16) 23 | expect(tx.nonce).to eq json_tx["nonce"].to_i(16) 24 | expect(tx.gas_price).to eq json_tx["gasPrice"].to_i(16) 25 | expect(tx.gas_limit).to eq json_tx["gasLimit"].to_i(16) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/fixtures/66734e70ea28eaa28eb1bace4ca87573c48f52cca7590459ad20dc58bae1a819.hex: -------------------------------------------------------------------------------- 1 | f871831007e28505d21dba0082c350949757b0f208aebeb22b9f7a73c152654b2de1acc0808a8b1472453633352e36351ca066bbc9b2b4d5621de654df7391a29b884e89eda63c57bd903fffee0f8d2bbbc0a068b91d3ac9e74772f49cb53052df9d6038838959bb33a30248589dbe12895403 2 | -------------------------------------------------------------------------------- /spec/fixtures/7151f5b0d229c62a5076de4133ba06fffc033e25bf99691c3e0a0a99c5a64538.hex: -------------------------------------------------------------------------------- 1 | f871831007e08505d21dba0082c3509491d21117ce2056b62e42a422186e098ceab56f86808a8b1472453633372e35301ca0b0fa8b20828178ce382a1029f264065bcc8fcff36f28bff5430e7d2d6e5a78baa01156a157cb4bad439cbbd1e0fe74e7d028b280b124dd572f8035a9ec69736bc7 2 | -------------------------------------------------------------------------------- /spec/fixtures/keys/testingtesting.json: -------------------------------------------------------------------------------- 1 | {"version":3,"id":"a8c1108e-033d-4d41-b5ea-e26f620b9eda","address":"65bcb68d4c73e163c69eea056d63bb09faacdd8e","Crypto":{"ciphertext":"3f0fcedfa488f23bd83e465bca44650eee604400a24272f18b38aca82e64d624","cipherparams":{"iv":"7c0860a90c41061f6cdfefc4904b83d0"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"866a4095d90a5d0abbad1e246004f8ecfec2c485a32e4c65b998409dc40d485d","n":1024,"r":8,"p":1},"mac":"0a180238e0865bc26b97bd5dfc1b8e9dbfbe0963a2f3a506bd64d86e7955df7e"}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/keys/testpassword.json: -------------------------------------------------------------------------------- 1 | { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" }, "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", "kdf" : "pbkdf2", "kdfparams" : { "c" : 262144, "dklen" : 32, "prf" : "hmac-sha256", "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" }, "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 } 2 | -------------------------------------------------------------------------------- /spec/fixtures/keys/testunknownkdf.json: -------------------------------------------------------------------------------- 1 | {"version":3,"id":"a8c1108e-033d-4d41-b5ea-e26f620b9eda","address":"65bcb68d4c73e163c69eea056d63bb09faacdd8e","Crypto":{"ciphertext":"3f0fcedfa488f23bd83e465bca44650eee604400a24272f18b38aca82e64d624","cipherparams":{"iv":"7c0860a90c41061f6cdfefc4904b83d0"},"cipher":"aes-128-ctr","kdf":"nosuchalgorithm","kdfparams":{"dklen":32,"salt":"866a4095d90a5d0abbad1e246004f8ecfec2c485a32e4c65b998409dc40d485d","n":1024,"r":8,"p":1},"mac":"0a180238e0865bc26b97bd5dfc1b8e9dbfbe0963a2f3a506bd64d86e7955df7e"}} 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "eth" 3 | require "securerandom" 4 | 5 | module Helpers 6 | def bin_to_hex(string) 7 | string.unpack("H*")[0] 8 | end 9 | 10 | def hex_to_bin(string) 11 | [string].pack("H*") 12 | end 13 | 14 | def read_hex_fixture(name) 15 | File.read("./spec/fixtures/#{name.gsub(/\A0x/, "")}.hex").strip 16 | end 17 | 18 | def configure_defaults 19 | Eth.configure do |config| 20 | config.chain_id = nil 21 | config.tx_data_hex = true 22 | end 23 | end 24 | 25 | def configure_chain_id(id) 26 | Eth.configure do |config| 27 | config.chain_id = id 28 | end 29 | end 30 | 31 | def configure_tx_data_hex(using_hex = true) 32 | Eth.configure do |config| 33 | config.tx_data_hex = using_hex 34 | end 35 | end 36 | 37 | def read_key_fixture(path) 38 | File.read "./spec/fixtures/keys/#{path}.json" 39 | end 40 | end 41 | 42 | RSpec.configure do |config| 43 | config.include Helpers 44 | 45 | config.before(:example, :chain_id) do |example| 46 | configure_chain_id example.metadata[:chain_id] 47 | end 48 | 49 | config.after do 50 | # always make sure tests are reset to default ID 51 | # in case they were changed in a test 52 | configure_defaults 53 | end 54 | end 55 | --------------------------------------------------------------------------------