From 61c1646b439699e4782f30679c0419c13d4350c6 Mon Sep 17 00:00:00 2001 From: Seun LanLege Date: Tue, 30 Oct 2018 22:12:42 +0100 Subject: [PATCH] EIP-712 implementation (#9631) * EIP-712 impl * added more tests * removed size parsing unwrap * corrected TYPE_REGEX to disallow zero sized fixed length arrays, replaced LinkedHashSet with IndexSet, added API spec to docs, fixed Type::Byte encoding branch * use Option instead of u64 for Type::Array::Length * replace `.iter()` with `.values()` Co-Authored-By: seunlanlege * tabify eip712.rs * use proper comments for docs * Cargo.lock: revert unrelated changes * tabify encode.rs --- Cargo.lock | 136 ++++++++++- rpc/Cargo.toml | 1 + rpc/src/lib.rs | 1 + rpc/src/v1/helpers/errors.rs | 8 + rpc/src/v1/impls/personal.rs | 24 ++ rpc/src/v1/traits/personal.rs | 7 +- util/EIP-712/Cargo.toml | 23 ++ util/EIP-712/src/eip712.rs | 177 +++++++++++++++ util/EIP-712/src/encode.rs | 415 ++++++++++++++++++++++++++++++++++ util/EIP-712/src/error.rs | 125 ++++++++++ util/EIP-712/src/lib.rs | 192 ++++++++++++++++ util/EIP-712/src/parser.rs | 172 ++++++++++++++ 12 files changed, 1277 insertions(+), 4 deletions(-) create mode 100644 util/EIP-712/Cargo.toml create mode 100644 util/EIP-712/src/eip712.rs create mode 100644 util/EIP-712/src/encode.rs create mode 100644 util/EIP-712/src/error.rs create mode 100644 util/EIP-712/src/lib.rs create mode 100644 util/EIP-712/src/parser.rs diff --git a/Cargo.lock b/Cargo.lock index 99edb6de0..52af9dcbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,29 @@ name = "edit-distance" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "eip712" +version = "0.1.0" +dependencies = [ + "ethabi 6.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ethereum-types 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", + "keccak-hash 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lunarity-lexer 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-hex 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", + "toolshed 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "validator 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "validator_derive 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "either" version = "1.5.0" @@ -1061,6 +1084,26 @@ dependencies = [ "vm 0.1.0", ] +[[package]] +name = "failure" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "fake-fetch" version = "0.0.1" @@ -1173,6 +1216,14 @@ dependencies = [ "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gcc" version = "0.3.55" @@ -1216,7 +1267,7 @@ dependencies = [ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "http 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "string 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1389,6 +1440,11 @@ dependencies = [ "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "if_chain" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "igd" version = "0.7.0" @@ -1407,7 +1463,7 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1442,6 +1498,14 @@ dependencies = [ "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "itertools" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "itoa" version = "0.4.3" @@ -1751,6 +1815,14 @@ dependencies = [ "linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lunarity-lexer" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "toolshed 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "macros" version = "0.1.0" @@ -2261,6 +2333,7 @@ version = "1.12.0" dependencies = [ "ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "cid 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "eip712 0.1.0", "ethash 1.12.0", "ethcore 1.12.0", "ethcore-devtools 1.12.0", @@ -3176,6 +3249,17 @@ dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "synstructure" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.11 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "target_info" version = "0.1.0" @@ -3515,6 +3599,14 @@ dependencies = [ "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "toolshed" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "trace-time" version = "0.1.1" @@ -3687,6 +3779,34 @@ name = "utf8-ranges" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "validator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "validator_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.11 (registry+https://github.com/rust-lang/crates.io-index)", + "validator 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vec_map" version = "0.8.1" @@ -3972,6 +4092,8 @@ dependencies = [ "checksum ethbloom 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a93a43ce2e9f09071449da36bfa7a1b20b950ee344b6904ff23de493b03b386" "checksum ethereum-types 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "35b3c5a18bc5e73a32a110ac743ec04b02bbbcd3b71d3118d40a6113d509378a" "checksum ethereum-types-serialize 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ac59a21a9ce98e188f3dace9eb67a6c4a3c67ec7fbc7218cb827852679dc002" +"checksum failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6dd377bcc1b1b7ce911967e3ec24fa19c3224394ec05b54aa7b083d498341ac7" +"checksum failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "64c2d913fe8ed3b6c6518eedf4538255b989945c14c2a7d5cbff62a5e2120596" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa" "checksum fixed-hash 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d5ec8112f00ea8a483e04748a85522184418fd1cf02890b626d8fc28683f7de" @@ -3982,6 +4104,7 @@ dependencies = [ "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b" "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +"checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" "checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" "checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" "checksum getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0a7292d30132fb5424b354f5dc02512a86e4c516fe544bb7a25e7f266951b797" @@ -4001,13 +4124,15 @@ dependencies = [ "checksum hyper 0.12.11 (registry+https://github.com/rust-lang/crates.io-index)" = "78d50abbd1790e0f4c74cb1d4a2211b439bac661d54107ad5564c55e77906762" "checksum hyper-rustls 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "68f2aa6b1681795bf4da8063f718cd23145aa0c9a5143d9787b345aa60d38ee4" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +"checksum if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4bac95d9aa0624e7b78187d6fb8ab012b41d9f6f54b1bcb61e61c4845f8357ec" "checksum igd 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8a254e265e8810deb357a9de757f784787ec415d056ededf410c0aa460afee9e" -"checksum indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08173ba1e906efb6538785a8844dd496f5d34f0a2d88038e95195172fc667220" +"checksum indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7e81a7c05f79578dbc15793d8b619db9ba32b4577003ef3af1a91c416798c58d" "checksum integer-encoding 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "26746cbc2e680af687e88d717f20ff90079bd10fc984ad57d277cd0e37309fa5" "checksum interleaved-ordered 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "141340095b15ed7491bd3d4ced9d20cebfb826174b6bb03386381f62b01e3d77" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum ipnetwork 0.12.8 (registry+https://github.com/rust-lang/crates.io-index)" = "70783119ac90828aaba91eae39db32c6c1b8838deea3637e5238efa0130801ab" "checksum itertools 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4833d6978da405305126af4ac88569b5d71ff758581ce5a987dbfa3755f694fc" +"checksum itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)" = "f58856976b776fedd95533137617a02fb25719f40e7d9b01c7043cd65474f450" "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" "checksum jni 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1ecfa3b81afc64d9a6539c4eece96ac9a93c551c713a313800dade8e33d7b5c1" "checksum jni-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" @@ -4038,6 +4163,7 @@ dependencies = [ "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" "checksum lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d06ff7ff06f729ce5f4e227876cb88d10bc59cd4ae1e09fbb2bde15c850dc21" +"checksum lunarity-lexer 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8a1670671f305792567116d4660e6e5bd785d6fa973e817c3445c0a7a54cecb6" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum memchr 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4b3629fe9fdbff6daa6c33b90f7c08355c1aca05a3d01fa8063b822fcf185f3b" "checksum memmap 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e2ffa2c986de11a9df78620c01eeaaf27d94d3ff02bf81bfcca953102dd0c6ff" @@ -4157,6 +4283,7 @@ dependencies = [ "checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum syn 0.15.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b036b7b35e846707c0e55c2c9441fa47867c0f87fca416921db3261b1d8c741a" +"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" "checksum target_info 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" "checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327" @@ -4189,6 +4316,7 @@ dependencies = [ "checksum tokio-udp 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "da941144b816d0dcda4db3a1ba87596e4df5e860a72b70783fe435891f80601c" "checksum tokio-uds 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "22e3aa6d1fcc19e635418dc0a30ab5bd65d347973d6f43f1a37bf8d9d1335fc9" "checksum toml 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "4a2ecc31b0351ea18b3fe11274b8db6e4d82bce861bbb22e6dbed40417902c65" +"checksum toolshed 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "450441e131c7663af72e63a33c02a6a1fbaaa8601dc652ed6757813bb55aeec7" "checksum trace-time 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe82f2f0bf1991e163e757baf044282823155dd326e70f44ce2186c3c320cc9" "checksum transaction-pool 1.13.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e5866e5126b14358f1d7af4bf51a0be677a363799b90e655edcec8254edef1d2" "checksum transient-hashmap 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aeb4b191d033a35edfce392a38cdcf9790b6cebcb30fa690c312c29da4dc433e" @@ -4211,6 +4339,8 @@ dependencies = [ "checksum untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" "checksum url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a321979c09843d272956e73700d12c4e7d3d92b2ee112b31548aef0d4efc5a6" "checksum utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd70f467df6810094968e2fce0ee1bd0e87157aceb026a8c083bcf5e25b9efe4" +"checksum validator 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "236a5eda3df2c877872e98dbc55d497d943792e6405d8fc65bd4f8a5e3b53c99" +"checksum validator_derive 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d360d6f5754972c0c1da14fb3d5580daa31aee566e1e45e2f8d3bf5950ecd3e9" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum vergen 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c3365f36c57e5df714a34be40902b27a992eeddb9996eca52d0584611cf885d" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index fa48e7495..dd1dbed07 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -59,6 +59,7 @@ parity-updater = { path = "../updater" } parity-version = { path = "../util/version" } patricia-trie = "0.3.0" rlp = { version = "0.3.0", features = ["ethereum"] } +eip712 = { path = "../util/EIP-712" } stats = { path = "../util/stats" } vm = { path = "../ethcore/vm" } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index cb14c42c4..e1f5423ca 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -63,6 +63,7 @@ extern crate parity_runtime; extern crate parity_updater as updater; extern crate parity_version as version; extern crate patricia_trie as trie; +extern crate eip712; extern crate rlp; extern crate stats; extern crate vm; diff --git a/rpc/src/v1/helpers/errors.rs b/rpc/src/v1/helpers/errors.rs index e7526e8cd..f177e2515 100644 --- a/rpc/src/v1/helpers/errors.rs +++ b/rpc/src/v1/helpers/errors.rs @@ -286,6 +286,14 @@ pub fn signing(error: AccountError) -> Error { } } +pub fn invalid_call_data(error: T) -> Error { + Error { + code: ErrorCode::ServerError(codes::ENCODING_ERROR), + message: format!("{}", error), + data: None + } +} + pub fn password(error: AccountError) -> Error { Error { code: ErrorCode::ServerError(codes::PASSWORD_INVALID), diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index a83fff926..7c5463071 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -38,6 +38,7 @@ use v1::types::{ RichRawTransaction as RpcRichRawTransaction, }; use v1::metadata::Metadata; +use eip712::{EIP712, hash_structured_data}; /// Account management (personal) rpc implementation. pub struct PersonalClient { @@ -150,6 +151,29 @@ impl Personal for PersonalClient { })) } + fn sign_typed_data(&self, typed_data: EIP712, account: RpcH160, password: String) -> BoxFuture { + let data = match hash_structured_data(typed_data) { + Ok(d) => d, + Err(err) => return Box::new(future::done(Err(errors::invalid_call_data(err.kind())))), + }; + let dispatcher = self.dispatcher.clone(); + let accounts = self.accounts.clone(); + + let payload = RpcConfirmationPayload::EthSignMessage((account.clone(), RpcBytes(data)).into()); + + Box::new(dispatch::from_rpc(payload, account.into(), &dispatcher) + .and_then(|payload| { + dispatch::execute(dispatcher, accounts, payload, dispatch::SignWith::Password(password.into())) + }) + .map(|v| v.into_value()) + .then(|res| match res { + Ok(RpcConfirmationResponse::Signature(signature)) => Ok(signature), + Err(e) => Err(e), + e => Err(errors::internal("Unexpected result", e)), + }) + ) + } + fn ec_recover(&self, data: RpcBytes, signature: RpcH520) -> BoxFuture { let signature: H520 = signature.into(); let signature = Signature::from_electrum(&signature); diff --git a/rpc/src/v1/traits/personal.rs b/rpc/src/v1/traits/personal.rs index 718721910..4798b0e2d 100644 --- a/rpc/src/v1/traits/personal.rs +++ b/rpc/src/v1/traits/personal.rs @@ -16,7 +16,7 @@ //! Personal rpc interface. use jsonrpc_core::{BoxFuture, Result}; - +use eip712::EIP712; use v1::types::{Bytes, U128, H160, H256, H520, TransactionRequest, RichRawTransaction as RpcRichRawTransaction}; build_rpc_trait! { @@ -42,6 +42,11 @@ build_rpc_trait! { #[rpc(name = "personal_sign")] fn sign(&self, Bytes, H160, String) -> BoxFuture; + /// Produces an EIP-712 compliant signature with given account using the given password to unlock the + /// account during the request. + #[rpc(name = "personal_signTypedData")] + fn sign_typed_data(&self, EIP712, H160, String) -> BoxFuture; + /// Returns the account associated with the private key that was used to calculate the signature in /// `personal_sign`. #[rpc(name = "personal_ecRecover")] diff --git a/util/EIP-712/Cargo.toml b/util/EIP-712/Cargo.toml new file mode 100644 index 000000000..308df98d8 --- /dev/null +++ b/util/EIP-712/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "eip712" +version = "0.1.0" +authors = ["Parity Technologies "] + +[dependencies] +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +ethabi = "6.0" +keccak-hash = "0.1" +ethereum-types = "0.4" +failure = "0.1" +itertools = "0.7" +failure_derive = "0.1" +lazy_static = "1.1" +toolshed = "0.4" +regex = "1.0" +validator = "0.8" +validator_derive = "0.8" +lunarity-lexer = "0.1" +rustc-hex = "2.0" +indexmap = "1.0.2" diff --git a/util/EIP-712/src/eip712.rs b/util/EIP-712/src/eip712.rs new file mode 100644 index 000000000..fea97a988 --- /dev/null +++ b/util/EIP-712/src/eip712.rs @@ -0,0 +1,177 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! EIP712 structs +use serde_json::{Value}; +use std::collections::HashMap; +use ethereum_types::{U256, H256, Address}; +use regex::Regex; +use validator::Validate; +use validator::ValidationErrors; + +pub(crate) type MessageTypes = HashMap>; + +lazy_static! { + // match solidity identifier with the addition of '[(\d)*]*' + static ref TYPE_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9]*(\[([1-9]\d*)*\])*$").unwrap(); + static ref IDENT_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9]*$").unwrap(); +} + +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[derive(Deserialize, Serialize, Validate, Debug, Clone)] +pub(crate) struct EIP712Domain { + pub(crate) name: String, + pub(crate) version: String, + pub(crate) chain_id: U256, + pub(crate) verifying_contract: Address, + #[serde(skip_serializing_if="Option::is_none")] + pub(crate) salt: Option, +} +/// EIP-712 struct +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[derive(Deserialize, Debug, Clone)] +pub struct EIP712 { + pub(crate) types: MessageTypes, + pub(crate) primary_type: String, + pub(crate) message: Value, + pub(crate) domain: EIP712Domain, +} + +impl Validate for EIP712 { + fn validate(&self) -> Result<(), ValidationErrors> { + for field_types in self.types.values() { + for field_type in field_types { + field_type.validate()?; + } + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Validate, Debug, Clone)] +pub(crate) struct FieldType { + #[validate(regex = "IDENT_REGEX")] + pub name: String, + #[serde(rename = "type")] + #[validate(regex = "TYPE_REGEX")] + pub type_: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::from_str; + + #[test] + fn test_regex() { + let test_cases = vec!["unint bytes32", "Seun\\[]", "byte[]uint", "byte[7[]uint][]", "Person[0]"]; + for case in test_cases { + assert_eq!(TYPE_REGEX.is_match(case), false) + } + + let test_cases = vec!["bytes32", "Foo[]", "bytes1", "bytes32[][]", "byte[9]", "contents"]; + for case in test_cases { + assert_eq!(TYPE_REGEX.is_match(case), true) + } + } + + #[test] + fn test_deserialization() { + let string = r#"{ + "primaryType": "Mail", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": "0x1", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents": "Hello, Bob!" + }, + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ] + } + }"#; + let _ = from_str::(string).unwrap(); + } + + #[test] + fn test_failing_deserialization() { + let string = r#"{ + "primaryType": "Mail", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": "0x1", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents": "Hello, Bob!" + }, + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "7uint256[x] Seun" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet amen", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ] + } + }"#; + let data = from_str::(string).unwrap(); + assert_eq!(data.validate().is_err(), true); + } +} diff --git a/util/EIP-712/src/encode.rs b/util/EIP-712/src/encode.rs new file mode 100644 index 000000000..dee4a38aa --- /dev/null +++ b/util/EIP-712/src/encode.rs @@ -0,0 +1,415 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! EIP712 Encoder +use ethabi::{encode, Token as EthAbiToken}; +use ethereum_types::{Address as EthAddress, U256, H256}; +use keccak_hash::keccak; +use serde_json::Value; +use std::str::FromStr; +use itertools::Itertools; +use indexmap::IndexSet; +use serde_json::to_value; +use parser::{Parser, Type}; +use error::{Result, ErrorKind, serde_error}; +use eip712::{EIP712, MessageTypes}; +use rustc_hex::FromHex; +use validator::Validate; +use std::collections::HashSet; + + +fn check_hex(string: &str) -> Result<()> { + if string.len() >= 2 && &string[..2] == "0x" { + return Ok(()) + } + + return Err(ErrorKind::HexParseError( + format!("Expected a 0x-prefixed string of even length, found {} length string", string.len())) + )? +} +/// given a type and HashMap> +/// returns a HashSet of dependent types of the given type +fn build_dependencies<'a>(message_type: &'a str, message_types: &'a MessageTypes) -> Option<(HashSet<&'a str>)> +{ + if message_types.get(message_type).is_none() { + return None; + } + + let mut types = IndexSet::new(); + types.insert(message_type); + let mut deps = HashSet::new(); + + while let Some(item) = types.pop() { + if let Some(fields) = message_types.get(item) { + deps.insert(item); + + for field in fields { + // seen this type before? or not a custom type skip + if deps.contains(&*field.type_) || !message_types.contains_key(&*field.type_) { + continue; + } + types.insert(&*field.type_); + } + } + }; + + return Some(deps) +} + +fn encode_type(message_type: &str, message_types: &MessageTypes) -> Result { + let deps = { + let mut temp = build_dependencies(message_type, message_types).ok_or_else(|| ErrorKind::NonExistentType)?; + temp.remove(message_type); + let mut temp = temp.into_iter().collect::>(); + (&mut temp[..]).sort_unstable(); + temp.insert(0, message_type); + temp + }; + + let encoded = deps + .into_iter() + .filter_map(|dep| { + message_types.get(dep).map(|field_types| { + let types = field_types + .iter() + .map(|value| format!("{} {}", value.type_, value.name)) + .join(","); + return format!("{}({})", dep, types); + }) + }) + .collect::>() + .concat(); + Ok(encoded) +} + +fn type_hash(message_type: &str, typed_data: &MessageTypes) -> Result { + Ok(keccak(encode_type(message_type, typed_data)?)) +} + +fn encode_data( + parser: &Parser, + message_type: &Type, + message_types: &MessageTypes, + value: &Value, + field_name: Option<&str> +) -> Result> +{ + let encoded = match message_type { + Type::Array { + inner, + length + } => { + let mut items = vec![]; + let values = value.as_array().ok_or_else(|| serde_error("array", field_name))?; + + // check if the type definition actually matches + // the length of items to be encoded + if length.is_some() && Some(values.len() as u64) != *length { + let array_type = format!("{}[{}]", *inner, length.unwrap()); + return Err(ErrorKind::UnequalArrayItems(length.unwrap(), array_type, values.len() as u64))? + } + + for item in values { + let mut encoded = encode_data(parser, &*inner, &message_types, item, field_name)?; + items.append(&mut encoded); + } + + keccak(items).to_vec() + } + + Type::Custom(ref ident) if message_types.get(&*ident).is_some() => { + let type_hash = (&type_hash(ident, &message_types)?).to_vec(); + let mut tokens = encode(&[EthAbiToken::FixedBytes(type_hash)]); + + for field in message_types.get(ident).expect("Already checked in match guard; qed") { + let value = &value[&field.name]; + let type_ = parser.parse_type(&*field.type_)?; + let mut encoded = encode_data(parser, &type_, &message_types, &value, Some(&*field.name))?; + tokens.append(&mut encoded); + } + + keccak(tokens).to_vec() + } + + Type::Bytes => { + let string = value.as_str().ok_or_else(|| serde_error("string", field_name))?; + + check_hex(&string)?; + + let bytes = (&string[2..]) + .from_hex::>() + .map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?; + let bytes = keccak(&bytes).to_vec(); + + encode(&[EthAbiToken::FixedBytes(bytes)]) + } + + Type::Byte(_) => { + let string = value.as_str().ok_or_else(|| serde_error("string", field_name))?; + + check_hex(&string)?; + + let mut bytes = (&string[2..]) + .from_hex::>() + .map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?; + + encode(&[EthAbiToken::FixedBytes(bytes)]) + } + + Type::String => { + let value = value.as_str().ok_or_else(|| serde_error("string", field_name))?; + let hash = keccak(value).to_vec(); + encode(&[EthAbiToken::FixedBytes(hash)]) + } + + Type::Bool => encode(&[EthAbiToken::Bool(value.as_bool().ok_or_else(|| serde_error("bool", field_name))?)]), + + Type::Address => { + let addr = value.as_str().ok_or_else(|| serde_error("string", field_name))?; + if addr.len() != 42 { + return Err(ErrorKind::InvalidAddressLength(addr.len()))?; + } + let address = EthAddress::from_str(&addr[2..]).map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?; + encode(&[EthAbiToken::Address(address)]) + } + + Type::Uint | Type::Int => { + let string = value.as_str().ok_or_else(|| serde_error("int/uint", field_name))?; + + check_hex(&string)?; + + let uint = U256::from_str(&string[2..]).map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?; + + let token = if *message_type == Type::Uint { + EthAbiToken::Uint(uint) + } else { + EthAbiToken::Int(uint) + }; + encode(&[token]) + } + + _ => return Err(ErrorKind::UnknownType(format!("{}", field_name.unwrap_or("")), format!("{}", *message_type)))? + }; + + Ok(encoded) +} + +/// encodes and hashes the given EIP712 struct +pub fn hash_structured_data(typed_data: EIP712) -> Result> { + // validate input + typed_data.validate()?; + // EIP-191 compliant + let prefix = (b"\x19\x01").to_vec(); + let domain = to_value(&typed_data.domain).unwrap(); + let parser = Parser::new(); + let (domain_hash, data_hash) = ( + encode_data(&parser, &Type::Custom("EIP712Domain".into()), &typed_data.types, &domain, None)?, + encode_data(&parser, &Type::Custom(typed_data.primary_type), &typed_data.types, &typed_data.message, None)? + ); + let concat = [&prefix[..], &domain_hash[..], &data_hash[..]].concat(); + Ok(keccak(concat).to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::from_str; + use rustc_hex::ToHex; + + const JSON: &'static str = r#"{ + "primaryType": "Mail", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": "0x1", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents": "Hello, Bob!" + }, + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ] + } + }"#; + + #[test] + fn test_build_dependencies() { + let string = r#"{ + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ] + }"#; + + let value = from_str::(string).expect("alas error!"); + let mail = "Mail"; + let person = "Person"; + + let hashset = { + let mut temp = HashSet::new(); + temp.insert(mail); + temp.insert(person); + temp + }; + assert_eq!(build_dependencies(mail, &value), Some(hashset)); + } + + #[test] + fn test_encode_type() { + let string = r#"{ + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ] + }"#; + + let value = from_str::(string).expect("alas error!"); + let mail = &String::from("Mail"); + assert_eq!( + "Mail(Person from,Person to,string contents)Person(string name,address wallet)", + encode_type(&mail, &value).expect("alas error!") + ) + } + + #[test] + fn test_encode_type_hash() { + let string = r#"{ + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ] + }"#; + + let value = from_str::(string).expect("alas error!"); + let mail = &String::from("Mail"); + let hash = (type_hash(&mail, &value).expect("alas error!").0).to_hex::(); + assert_eq!( + hash, + "a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2" + ); + } + + #[test] + fn test_hash_data() { + let typed_data = from_str::(JSON).expect("alas error!"); + assert_eq!( + hash_structured_data(typed_data).expect("alas error!").to_hex::(), + "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2" + ) + } + + #[test] + fn test_unequal_array_lengths() { + const TEST: &'static str = r#"{ + "primaryType": "Mail", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": "0x1", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": [{ + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }], + "contents": "Hello, Bob!" + }, + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person[2]" }, + { "name": "contents", "type": "string" } + ] + } + }"#; + + let typed_data = from_str::(TEST).expect("alas error!"); + assert_eq!( + hash_structured_data(typed_data).unwrap_err().kind(), + ErrorKind::UnequalArrayItems(2, "Person[2]".into(), 1) + ) + } +} diff --git a/util/EIP-712/src/error.rs b/util/EIP-712/src/error.rs new file mode 100644 index 000000000..e6b5634e5 --- /dev/null +++ b/util/EIP-712/src/error.rs @@ -0,0 +1,125 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use std::fmt::{self, Display}; +use failure::{Fail, Context, Backtrace}; +use validator::ValidationErrors; +use validator::ValidationErrorsKind; + +pub(crate) type Result = ::std::result::Result; +/// Error type +#[derive(Debug)] +pub struct Error { + inner: Context, +} +/// Possible errors encountered while hashing/encoding an EIP-712 compliant data structure +#[derive(Clone, Fail, Debug, PartialEq)] +pub enum ErrorKind { + /// if we fail to deserialize from a serde::Value as a type specified in message types + /// fail with this error. + #[fail(display = "Expected type '{}' for field '{}'", _0, _1)] + UnexpectedType(String, String), + /// the primary type supplied doesn't exist in the MessageTypes + #[fail(display = "The given primaryType wasn't found in the types field")] + NonExistentType, + /// an invalid address was encountered during encoding + #[fail(display = "Address string should be a 0x-prefixed 40 character string, got '{}'", _0)] + InvalidAddressLength(usize), + /// a hex parse error occured + #[fail(display = "Failed to parse hex '{}'", _0)] + HexParseError(String), + /// the field was declared with a unknown type + #[fail(display = "The field '{}' has an unknown type '{}'", _0, _1)] + UnknownType(String, String), + /// Unexpected token + #[fail(display = "Unexpected token '{}' while parsing typename '{}'", _0, _1)] + UnexpectedToken(String, String), + /// the user has attempted to define a typed array with a depth > 10 + #[fail(display = "Maximum depth for nested arrays is 10")] + UnsupportedArrayDepth, + /// FieldType validation error + #[fail(display = "{}", _0)] + ValidationError(String), + /// the typed array defined in message types was declared with a fixed length + /// that is of unequal length with the items to be encoded + #[fail(display = "Expected {} items for array type {}, got {} items", _0, _1, _2)] + UnequalArrayItems(u64, String, u64), + /// Typed array length doesn't fit into a u64 + #[fail(display = "Attempted to declare fixed size with length {}", _0)] + InvalidArraySize(String) +} + +pub(crate) fn serde_error(expected: &str, field: Option<&str>) -> ErrorKind { + ErrorKind::UnexpectedType(expected.to_owned(), field.unwrap_or("").to_owned()) +} + +impl Fail for Error { + fn cause(&self) -> Option<&Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.inner, f) + } +} + +impl Error { + /// extract the error kind + pub fn kind(&self) -> ErrorKind { + self.inner.get_context().clone() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error { inner: Context::new(kind) } + } +} + +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner } + } +} + +impl From for Error { + fn from(error: ValidationErrors) -> Self { + let mut string: String = "".into(); + for (field_name, error_kind) in error.errors() { + match error_kind { + ValidationErrorsKind::Field(validation_errors) => { + for error in validation_errors { + let str_error = format!( + "the field '{}', has an invalid value {}", + field_name, + error.params["value"] + ); + string.push_str(&str_error); + } + }, + _ => unreachable!("#[validate] is only used on fields for regex;\ + its impossible to get any other ErrorKind; qed") + } + } + ErrorKind::ValidationError(string).into() + } +} diff --git a/util/EIP-712/src/lib.rs b/util/EIP-712/src/lib.rs new file mode 100644 index 000000000..206b68510 --- /dev/null +++ b/util/EIP-712/src/lib.rs @@ -0,0 +1,192 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! EIP-712 encoding utilities +//! +//! # Specification +//! +//! `encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)` +//! - data adheres to 𝕊, a structure defined in the rigorous eip-712 +//! - `\x01` is needed to comply with EIP-191 +//! - `domainSeparator` and `hashStruct` are defined below +//! +//! ## A) domainSeparator +//! +//! `domainSeparator = hashStruct(eip712Domain)` +//!
+//!
+//! Struct named `EIP712Domain` with the following fields +//! +//! - `name: String` +//! - `version: String` +//! - `chain_id: U256`, +//! - `verifying_contract: H160` +//! - `salt: Option` +//! +//! ## C) hashStruct +//! +//! `hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))` +//!
+//! `typeHash = keccak256(encodeType(typeOf(s)))` +//! +//! ### i) encodeType +//! +//! - `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"` +//! - each member is written as `type ‖ " " ‖ name` +//! - encodings cascade down and are sorted by name +//! +//! ### ii) encodeData +//! +//! - `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)` +//! - each encoded member is 32-byte long +//! +//! #### a) atomic +//! +//! - `boolean` => `U256` +//! - `address` => `H160` +//! - `uint` => sign-extended `U256` in big endian order +//! - `bytes1:31` => `H@256` +//! +//! #### b) dynamic +//! +//! - `bytes` => `keccak256(bytes)` +//! - `string` => `keccak256(string)` +//! +//! #### c) referenced +//! +//! - `array` => `keccak256(encodeData(array))` +//! - `struct` => `rec(keccak256(hashStruct(struct)))` +//! +//! ## D) Example +//! ### Query +//! ```json +//! { +//! "jsonrpc": "2.0", +//! "method": "eth_signTypedData", +//! "params": [ +//! "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", +//! { +//! "types": { +//! "EIP712Domain": [ +//! { +//! "name": "name", +//! "type": "string" +//! }, +//! { +//! "name": "version", +//! "type": "string" +//! }, +//! { +//! "name": "chainId", +//! "type": "uint256" +//! }, +//! { +//! "name": "verifyingContract", +//! "type": "address" +//! } +//! ], +//! "Person": [ +//! { +//! "name": "name", +//! "type": "string" +//! }, +//! { +//! "name": "wallet", +//! "type": "address" +//! } +//! ], +//! "Mail": [ +//! { +//! "name": "from", +//! "type": "Person" +//! }, +//! { +//! "name": "to", +//! "type": "Person" +//! }, +//! { +//! "name": "contents", +//! "type": "string" +//! } +//! ] +//! }, +//! "primaryType": "Mail", +//! "domain": { +//! "name": "Ether Mail", +//! "version": "1", +//! "chainId": 1, +//! "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" +//! }, +//! "message": { +//! "from": { +//! "name": "Cow", +//! "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" +//! }, +//! "to": { +//! "name": "Bob", +//! "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" +//! }, +//! "contents": "Hello, Bob!" +//! } +//! } +//! ], +//! "id": 1 +//! } +//! ``` +// +//! ### Response +//! ```json +//! { +//! "id":1, +//! "jsonrpc": "2.0", +//! "result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" +//! } +//! ``` + +#![warn(missing_docs, unused_extern_crates)] + +extern crate serde_json; +extern crate ethabi; +extern crate ethereum_types; +extern crate keccak_hash; +extern crate itertools; +extern crate failure; +extern crate indexmap; +extern crate lunarity_lexer; +extern crate toolshed; +extern crate regex; +extern crate validator; +#[macro_use] +extern crate validator_derive; +#[macro_use] +extern crate failure_derive; +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate lazy_static; +extern crate rustc_hex; + +mod eip712; +mod error; +mod parser; +mod encode; + +/// the EIP-712 encoding function +pub use encode::hash_structured_data; +/// encoding Error types +pub use error::{ErrorKind, Error}; +/// EIP712 struct +pub use eip712::EIP712; diff --git a/util/EIP-712/src/parser.rs b/util/EIP-712/src/parser.rs new file mode 100644 index 000000000..7f515f7c9 --- /dev/null +++ b/util/EIP-712/src/parser.rs @@ -0,0 +1,172 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! Solidity type-name parsing +use lunarity_lexer::{Lexer, Token}; +use error::*; +use toolshed::Arena; +use std::{fmt, result}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Type { + Address, + Uint, + Int, + String, + Bool, + Bytes, + Byte(u8), + Custom(String), + Array { + length: Option, + inner: Box + } +} + +impl From for String { + fn from(field_type: Type) -> String { + match field_type { + Type::Address => "address".into(), + Type::Uint => "uint".into(), + Type::Int => "int".into(), + Type::String => "string".into(), + Type::Bool => "bool".into(), + Type::Bytes => "bytes".into(), + Type::Byte(len) => format!("bytes{}", len), + Type::Custom(custom) => custom, + Type::Array { + inner, + length + } => { + let inner: String = (*inner).into(); + match length { + None => format!("{}[]", inner), + Some(length) => format!("{}[{}]", inner, length) + } + } + } + } +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { + let item: String = self.clone().into(); + write!(f, "{}", item) + } +} + +pub struct Parser { + arena: Arena, +} + +impl Parser { + pub fn new() -> Self { + Parser { + arena: Arena::new() + } + } + + /// the type string is being validated before it's parsed. + pub fn parse_type(&self, field_type: &str) -> Result { + #[derive(PartialEq)] + enum State { Open, Close } + + let mut lexer = Lexer::new(&self.arena, field_type); + let mut token = None; + let mut state = State::Close; + let mut array_depth = 0; + let mut current_array_length: Option = None; + + while lexer.token != Token::EndOfProgram { + let type_ = match lexer.token { + Token::Identifier => Type::Custom(lexer.token_as_str().to_owned()), + Token::TypeByte => Type::Byte(lexer.type_size.0), + Token::TypeBytes => Type::Bytes, + Token::TypeBool => Type::Bool, + Token::TypeUint => Type::Uint, + Token::TypeInt => Type::Int, + Token::TypeString => Type::String, + Token::TypeAddress => Type::Address, + Token::LiteralInteger => { + let length = lexer.token_as_str(); + current_array_length = Some(length + .parse() + .map_err(|_| + ErrorKind::InvalidArraySize(length.into()) + )? + ); + lexer.consume(); + continue; + }, + Token::BracketOpen if token.is_some() && state == State::Close => { + state = State::Open; + lexer.consume(); + continue + } + Token::BracketClose if array_depth < 10 => { + if state == State::Open && token.is_some() { + let length = current_array_length.take(); + state = State::Close; + token = Some(Type::Array { + inner: Box::new(token.expect("if statement checks for some; qed")), + length + }); + lexer.consume(); + array_depth += 1; + continue + } else { + return Err(ErrorKind::UnexpectedToken(lexer.token_as_str().to_owned(), field_type.to_owned()))? + } + } + Token::BracketClose if array_depth == 10 => { + return Err(ErrorKind::UnsupportedArrayDepth)? + } + _ => return Err(ErrorKind::UnexpectedToken(lexer.token_as_str().to_owned(), field_type.to_owned()))? + }; + + token = Some(type_); + lexer.consume(); + } + + Ok(token.ok_or_else(|| ErrorKind::NonExistentType)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser() { + let parser = Parser::new(); + let source = "byte[][][7][][][][][][][]"; + parser.parse_type(source).unwrap(); + } + + #[test] + fn test_nested_array() { + let parser = Parser::new(); + let source = "byte[][][7][][][][][][][][]"; + assert_eq!(parser.parse_type(source).is_err(), true); + } + + #[test] + fn test_malformed_array_type() { + let parser = Parser::new(); + let source = "byte[7[]uint][]"; + assert_eq!(parser.parse_type(source).is_err(), true) + } +}