Skip to content

Commit

Permalink
ongoing refactoring/implementation, bugfixes on transaction hashing
Browse files Browse the repository at this point in the history
  • Loading branch information
izelnakri committed Sep 3, 2017
1 parent 181008a commit e0aacf5
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 12 deletions.
29 changes: 29 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
- check toChecksumAddress + isValidChecksumAddress
- fromRPCtoSig, toRPCSig

- rlp exports.toBuffer(x) behavior might be different that the one I use

.toBuffer(x) is:
if (exports.isHexString(v)) {
v = Buffer.from(exports.padToEven(exports.stripHexPrefix(v)), 'hex')
elseif (isNumber) {
intToBuffer
} else if (isNull || isUndefined) {
allocUnsafe(0)
}

function intToHex (i) {
var hex = i.toString(16)
if (hex.length % 2) {
Expand Down Expand Up @@ -51,3 +62,21 @@ describe('message sig', function () {
})
})
})


function setter (v) {
v = exports.toBuffer(v)

if (v.toString('hex') === '00' && !field.allowZero) {
v = Buffer.allocUnsafe(0)
}

if (field.allowLess && field.length) {
v = exports.stripZeros(v)
assert(field.length >= v.length, 'The field ' + field.name + ' must not have more ' + field.length + ' bytes')
} else if (!(field.allowZero && v.length === 0) && field.length) {
assert(field.length === v.length, 'The field ' + field.name + ' must have byte length of ' + field.length)
}

self.raw[i] = v
}
68 changes: 58 additions & 10 deletions lib/eth/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule ETH.Transaction do
] |> Enum.map(fn(x) ->
{key, value} = x
{key, Base.encode16(value, case: :lower)}
end) |> Enum.into(%{chain_id: chain_id})
end) |> Enum.into(%{chain_id: chain_id}) # NOTE: this is probably wrong
end

def sign(transaction = %{
Expand Down Expand Up @@ -54,23 +54,56 @@ defmodule ETH.Transaction do
def hash_transaction(transaction = %{
to: _to, value: _value, data: _data, gas_price: _gas_price, gas_limit: _gas_limit,
nonce: _nonce, chain_id: chain_id
}) do # EIP155 spec:
# when computing the hash of a transaction for purposes of signing or recovering,
# instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
# hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0
}) do
transaction
|> Map.merge(%{v: encode16(<<chain_id>>), r: <<>>, s: <<>>})
|> to_list
|> Enum.map(fn(x) -> Base.decode16!(x, case: :mixed) end)
|> hash
end

def hash(transaction_list = [_nonce, _gas_price, _gas_limit, _to, _value, _data, _v, _r, _s]) do
transaction_list
def hash(transaction_list, include_signature \\ true) do
target_list = case include_signature do
true -> transaction_list
false ->
# EIP155 spec:
# when computing the hash of a transaction for purposes of signing or recovering,
# instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
# hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0
list = transaction_list |> Enum.take(6)
v = transaction_list |> Enum.at(6)
if get_chain_id(v) > 0, do: list ++ ["0x#{chain_id}", "0x", "0x"], else: list # NOTE: this part is dangerous: in JS we change state(v: chainId, r: 0, s: 0)
end
IO.puts("target_list is:")
IO.inspect(target_list)

target_list
|> Enum.map(fn(value) -> # NOTE: maybe move this should be moved somewhere else (probably .set() sets these or transaction list)
cond do
is_number(value) ->
IO.puts("value is")
IO.puts(value)
string_value = to_string(value)
result = add_0_for_uneven_encoding(string_value)
IO.puts("result is:")
IO.puts(result)
result
String.slice(value, 0..1) == "0x" ->
"0x" <> stripped_value = value
encoded_value = add_0_for_uneven_encoding(stripped_value)
Base.decode16!(encoded_value, case: :mixed)
true -> value
end
# NOTE: else if (v === null || v === undefined) { v = Buffer.allocUnsafe(0) }
end)
|> ExRLP.encode
|> keccak256
end

def verify_signature(transaction) do

end

defp sign_transaction(transaction = %{
to: _to, value: _value, data: _data, gas_price: _gas_price, gas_limit: _gas_limit,
nonce: _nonce, chain_id: _chain_id
Expand All @@ -88,7 +121,7 @@ defmodule ETH.Transaction do
|> ExRLP.encode
end

defp adjust_v_for_chain_id(transaction = %{
defp adjust_v_for_chain_id(transaction = %{ # NOTE: this is probably not correct
to: _to, value: _value, data: _data, gas_price: _gas_price, gas_limit: _gas_limit,
nonce: _nonce, chain_id: chain_id, v: v, r: r, s: s
}) do
Expand All @@ -107,8 +140,23 @@ defmodule ETH.Transaction do
v = Map.get(transaction, :v, Base.encode16(<<28>>, case: :lower))
r = Map.get(transaction, :r, "")
s = Map.get(transaction, :s, "")
[nonce, gas_price, gas_limit, to, value, data, v, r, s]

[nonce, gas_price, gas_limit, to, value, data, v, r, s] # NOTE: maybe this should turn things toBuffer
end

defp add_0_for_uneven_encoding(value) do
case rem(String.length(value), 2) == 1 do
true -> "0" <> value
false -> value
end
end

defp get_chain_id(v) do
"0x" <> v_string = v
{sig_v, _} = Integer.parse(v_string, 16)
chain_id = Float.floor((sig_v - 35) / 2)
if chain_id < 0, do: 0, else: Kernel.trunc(chain_id)
end

def to_hex(value), do: Hexate.encode(value)
def to_hex(value), do: HexPrefix.encode(value)
end
1 change: 1 addition & 0 deletions lib/eth/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ defmodule ETH.Utils do

def keccak256(data), do: :keccakf1600.hash(:sha3_256, data)
def encode16(value), do: Base.encode16(value, case: :lower)
def decode16(value), do: Base.decode16!(value, case: :mixed)
end

# NOTE: old version that is error-prone:
Expand Down
2 changes: 2 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ defmodule Eth.Mixfile do
{:libsecp256k1, [github: "mbrix/libsecp256k1", manager: :rebar]},
{:keccakf1600, git: "https://github.com/jur0/erlang-keccakf1600", branch: "original-keccak"},
{:ex_rlp, "~> 0.2.1"},
{:hex_prefix, "~> 0.1.0"},
{:hexate, "~> 0.6.1"},
{:ethereumex, "~> 0.1.0"},
{:poison, "~> 3.1"},
{:ex_doc, ">= 0.0.0", only: :dev}
]
end
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"ex_doc": {:hex, :ex_doc, "0.16.3", "cd2a4cfe5d26e37502d3ec776702c72efa1adfa24ed9ce723bb565f4c30bd31a", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]},
"ex_rlp": {:hex, :ex_rlp, "0.2.1", "bd320900d6316cdfe01d365d4bda22eb2f39b359798daeeffd3bd1ca7ba958ec", [:mix], []},
"hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, optional: false]}, {:idna, "5.0.2", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]},
"hex_prefix": {:hex, :hex_prefix, "0.1.0", "e96b5cbb6ad8493196ce193726240023f5ce0ae0753118a19a5b43e2db0267ca", [:mix], []},
"hexate": {:hex, :hexate, "0.6.1", "1cea42e462c1daa32223127d4752e71016c3d933d492b9bb7fa4709a4a0fd50d", [:mix], []},
"httpoison": {:hex, :httpoison, "0.11.2", "9e59f17a473ef6948f63c51db07320477bad8ba88cf1df60a3eee01150306665", [:mix], [{:hackney, "~> 1.8.0", [hex: :hackney, optional: false]}]},
"idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, optional: false]}]},
Expand Down
99 changes: 99 additions & 0 deletions test/eth/eth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
require IEx
# elliptic curve cryptography library for signing transactions in Ethereum
defmodule ETH do
@moduledoc """
Documentation for Eth.
"""

@doc """
Hello world.
## Examples
iex> Eth.hello
:world
"""

# def sign_transaction(
# source_eth_address, value, target_eth_address,
# options \\ [gas_price: 100, gas_limit: 1000, data: "", chain_id: 3]
# ) do
# gas_price = options[:gas_price] |> Hexate.encode
# gas_limit = options[:gas_limit] |> Hexate.encode
# data = options[:data] |> Hexate.encode
#
# nonce = case options[:nonce] do
# nil -> ETH.Query.get_transaction_count(source_eth_address)
# _ -> options[:nonce]
# end
#
# # NOTE: calc nonce
# %{
# to: target_eth_address, value: Hexate.encode(value), gas_price: gas_price,
# gas_limit: gas_limit, data: data, chain_id: 3
# }
# # get nonce and make a transaction map -> sign_transaction -> send it to client
# end


# def sign_transaction(transaction, private_key) do # must have chain_id
# hash = hash_transaction(transaction)
# decoded_private_key = Base.decode16!(private_key, case: :lower)
# [signature: signature, recovery: recovery] = secp256k1_signature(hash, decoded_private_key)
#
# << r :: binary-size(32) >> <> << s :: binary-size(32) >> = signature
#
# transaction
# |> Map.merge(%{r: encode16(r), s: encode16(s), v: encode16(<<recovery + 27>>)})
# |> adjust_v_for_chain_id
# |> transaction_list
# |> Enum.map(fn(x) -> Base.decode16!(x, case: :lower) end)
# |> ExRLP.encode
# end
#
# def adjust_v_for_chain_id(transaction) do
# if transaction.chain_id > 0 do
# current_v_bytes = Base.decode16!(transaction.v, case: :lower) |> :binary.decode_unsigned
# target_v_bytes = current_v_bytes + (transaction.chain_id * 2 + 8)
# transaction |> Map.merge(%{v: encode16(<< target_v_bytes >>) })
# else
# transaction
# end
# end
#
# def secp256k1_signature(hash, private_key) do
# {:ok, signature, recovery} = :libsecp256k1.ecdsa_sign_compact(hash, private_key, :default, <<>>)
# [signature: signature, recovery: recovery]
# end
#
# # must have [nonce, gasPrice, gasLimit, to, value, data] # and chainId inside the transaction?
# def hash_transaction(transaction) do
# # NOTE: if transaction is decoded no need to encode
# # EIP155 spec:
# # when computing the hash of a transaction for purposes of signing or recovering,
# # instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
# # hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0
# transaction |> Map.merge(%{v: encode16(<<transaction.chain_id>>), r: <<>>, s: <<>> })
# |> transaction_list
# |> Enum.map(fn(x) -> Base.decode16!(x, case: :lower) end)
# |> hash
# end
#
# def hash(transaction_list) do
# transaction_list
# |> ExRLP.encode
# |> keccak256
# end
#
# def transaction_list(transaction \\ %{}) do
# %{
# nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data
# } = transaction
#
# v = if Map.get(transaction, :v), do: transaction.v, else: Base.encode16(<<28>>, case: :lower)
# r = Map.get(transaction, :r, "")
# s = Map.get(transaction, :s, "")
# [nonce, gas_price, gas_limit, to, value, data, v, r, s]
# end
end
40 changes: 38 additions & 2 deletions test/eth/transaction_test.exs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
defmodule ETH.TransactionTest do
use ExUnit.Case
import ETH.Utils
# TODO: "it should decode transactions"

@first_transaction_list [
"", "09184e72a000", "2710", "0000000000000000000000000000000000000000", "",
"7f7465737432000000000000000000000000000000000000000000000000000000600057", "29",
"f2d54d3399c9bcd3ac3482a5ffaeddfe68e9a805375f626b4f2f8cf530c2d95a",
"5b3bb54e6e8db52083a9b674e578c843a87c292f0383ddba168573808d36dc8e"
] |> Enum.map(fn(x) -> Base.decode16!(x, case: :lower) end)
] |> Enum.map(fn(x) -> decode16(x) end)

@first_example_wallet %{
private_key: "e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109"
Expand All @@ -20,11 +22,24 @@ defmodule ETH.TransactionTest do
data: "7f7465737432000000000000000000000000000000000000000000000000000000600057",
chain_id: 3 # EIP 155 chainId - mainnet: 1, ropsten: 3
}

@transactions File.read!("test/fixtures/transactions.json") |> Poison.decode!
@eip155_transactions File.read!("test/fixtures/eip155_vitalik_tests.json") |> Poison.decode!

test "hash/1 works" do
target_hash = "5C207A650B59A8C2D1271F5CBDA78A658CB411A87271D68062E61AB1A3F85CF9"
assert ETH.Transaction.hash(@first_transaction_list) |> Base.encode16 == target_hash

first_transaction_list = @transactions |> Enum.at(2) |> Map.get("raw")
second_transaction_list = @transactions |> Enum.at(3) |> Map.get("raw")
IO.inspect(first_transaction_list)

assert ETH.Transaction.hash(first_transaction_list) == decode16("375a8983c9fc56d7cfd118254a80a8d7403d590a6c9e105532b67aca1efb97aa")
assert ETH.Transaction.hash(first_transaction_list, false) == decode16("61e1ec33764304dddb55348e7883d4437426f44ab3ef65e6da1e025734c03ff0")
assert ETH.Transaction.hash(first_transaction_list, true) == decode16("375a8983c9fc56d7cfd118254a80a8d7403d590a6c9e105532b67aca1efb97aa")

assert ETH.Transaction.hash(second_transaction_list) == decode16("0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4")
assert ETH.Transaction.hash(second_transaction_list, true) == decode16("0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4")
assert ETH.Transaction.hash(second_transaction_list, false) == decode16("f97c73fdca079da7652dbc61a46cd5aeef804008e057be3e712c43eac389aaf0")
end

test "secp256k1_signature/2 works" do
Expand All @@ -50,4 +65,25 @@ defmodule ETH.TransactionTest do
|> Base.encode16(case: :lower)
assert signature == "f889808609184e72a00082271094000000000000000000000000000000000000000080a47f746573743200000000000000000000000000000000000000000000000000000060005729a0f2d54d3399c9bcd3ac3482a5ffaeddfe68e9a805375f626b4f2f8cf530c2d95aa05b3bb54e6e8db52083a9b674e578c843a87c292f0383ddba168573808d36dc8e"
end

# def decode16(value), do: Base.decode16!(value, case: :mixed)
end


# this happens on each setting
# function setter (v) {
# v = exports.toBuffer(v)
#
# if (v.toString('hex') === '00' && !field.allowZero) {
# v = Buffer.allocUnsafe(0)
# }
#
# if (field.allowLess && field.length) {
# v = exports.stripZeros(v)
# assert(field.length >= v.length, 'The field ' + field.name + ' must not have more ' + field.length + ' bytes')
# } else if (!(field.allowZero && v.length === 0) && field.length) {
# assert(field.length === v.length, 'The field ' + field.name + ' must have byte length of ' + field.length)
# }
#
# self.raw[i] = v
# }
39 changes: 39 additions & 0 deletions test/eth/transactions_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule ETH.TransactionsTest do
use ExUnit.Case

@transactions File.read!("test/fixtures/transactions.json") |> Poison.decode!
@eip155_transactions File.read!("test/fixtures/eip155_vitalik_tests.json") |> Poison.decode!

# test "verify signature" do
# @transactions |> Enum.each(fn(transaction) ->
# hash = # ETH.Transaction.hash()
# # transaction
#
# # ETH.Transaction.verify_signature([v: v, r: r, s: s])
#
# # signed_transaction
# # assert ExRLP.encode(transaction.raw) == signed_transaction
#
# # NOTE: t
# end)
#
# end

# test "can get transactions sender address after signing it" do
#
# end

# test "can get transactions sender public kye after signing it" do
#
# end

# NOTE: maybe verify/check_gas function and tests
# NOTE: maybe roundtrip a transaction encoding/decoding
# there are upfront gas/costs tests that we probably dont need

# test "verify EIP155 Signature based on Vitalik\'s tests" do
#
# end
end

# TODO: also implement transaction-runner tests

0 comments on commit e0aacf5

Please # to comment.