aboutsummaryrefslogtreecommitdiffstats
path: root/signer
diff options
context:
space:
mode:
authorMartin Holst Swende <martin@swende.se>2018-04-16 20:04:32 +0800
committerPéter Szilágyi <peterke@gmail.com>2018-04-16 20:04:32 +0800
commitec3db0f56c779387132dcf2049ed32bf4ed34a4f (patch)
treed509c580e02053fd133b0402c0838940d4b871d2 /signer
parentde2a7bb764c82dbaa80d37939c5862358174bc6e (diff)
downloadgo-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar.gz
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar.bz2
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar.lz
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar.xz
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar.zst
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.zip
cmd/clef, signer: initial poc of the standalone signer (#16154)
* signer: introduce external signer command * cmd/signer, rpc: Implement new signer. Add info about remote user to Context * signer: refactored request/response, made use of urfave.cli * cmd/signer: Use common flags * cmd/signer: methods to validate calldata against abi * cmd/signer: work on abi parser * signer: add mutex around UI * cmd/signer: add json 4byte directory, remove passwords from api * cmd/signer: minor changes * cmd/signer: Use ErrRequestDenied, enable lightkdf * cmd/signer: implement tests * cmd/signer: made possible for UI to modify tx parameters * cmd/signer: refactors, removed channels in ui comms, added UI-api via stdin/out * cmd/signer: Made lowercase json-definitions, added UI-signer test functionality * cmd/signer: update documentation * cmd/signer: fix bugs, improve abi detection, abi argument display * cmd/signer: minor change in json format * cmd/signer: rework json communication * cmd/signer: implement mixcase addresses in API, fix json id bug * cmd/signer: rename fromaccount, update pythonpoc with new json encoding format * cmd/signer: make use of new abi interface * signer: documentation * signer/main: remove redundant option * signer: implement audit logging * signer: create package 'signer', minor changes * common: add 0x-prefix to mixcaseaddress in json marshalling + validation * signer, rules, storage: implement rules + ephemeral storage for signer rules * signer: implement OnApprovedTx, change signing response (API BREAKAGE) * signer: refactoring + documentation * signer/rules: implement dispatching to next handler * signer: docs * signer/rules: hide json-conversion from users, ensure context is cleaned * signer: docs * signer: implement validation rules, change signature of call_info * signer: fix log flaw with string pointer * signer: implement custom 4byte databsae that saves submitted signatures * signer/storage: implement aes-gcm-backed credential storage * accounts: implement json unmarshalling of url * signer: fix listresponse, fix gas->uint64 * node: make http/ipc start methods public * signer: add ipc capability+review concerns * accounts: correct docstring * signer: address review concerns * rpc: go fmt -s * signer: review concerns+ baptize Clef * signer,node: move Start-functions to separate file * signer: formatting
Diffstat (limited to 'signer')
-rw-r--r--signer/core/abihelper.go256
-rw-r--r--signer/core/abihelper_test.go247
-rw-r--r--signer/core/api.go500
-rw-r--r--signer/core/api_test.go386
-rw-r--r--signer/core/auditlog.go110
-rw-r--r--signer/core/cliui.go247
-rw-r--r--signer/core/stdioui.go113
-rw-r--r--signer/core/types.go95
-rw-r--r--signer/core/validation.go163
-rw-r--r--signer/core/validation_test.go139
-rw-r--r--signer/rules/deps/bignumber.js4
-rw-r--r--signer/rules/deps/bindata.go235
-rw-r--r--signer/rules/deps/deps.go21
-rw-r--r--signer/rules/rules.go248
-rw-r--r--signer/rules/rules_test.go631
-rw-r--r--signer/storage/aes_gcm_storage.go164
-rw-r--r--signer/storage/aes_gcm_storage_test.go115
-rw-r--r--signer/storage/storage.go62
18 files changed, 3736 insertions, 0 deletions
diff --git a/signer/core/abihelper.go b/signer/core/abihelper.go
new file mode 100644
index 000000000..2674c7346
--- /dev/null
+++ b/signer/core/abihelper.go
@@ -0,0 +1,256 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common"
+
+ "bytes"
+ "os"
+ "regexp"
+)
+
+type decodedArgument struct {
+ soltype abi.Argument
+ value interface{}
+}
+type decodedCallData struct {
+ signature string
+ name string
+ inputs []decodedArgument
+}
+
+// String implements stringer interface, tries to use the underlying value-type
+func (arg decodedArgument) String() string {
+ var value string
+ switch arg.value.(type) {
+ case fmt.Stringer:
+ value = arg.value.(fmt.Stringer).String()
+ default:
+ value = fmt.Sprintf("%v", arg.value)
+ }
+ return fmt.Sprintf("%v: %v", arg.soltype.Type.String(), value)
+}
+
+// String implements stringer interface for decodedCallData
+func (cd decodedCallData) String() string {
+ args := make([]string, len(cd.inputs))
+ for i, arg := range cd.inputs {
+ args[i] = arg.String()
+ }
+ return fmt.Sprintf("%s(%s)", cd.name, strings.Join(args, ","))
+}
+
+// parseCallData matches the provided call data against the abi definition,
+// and returns a struct containing the actual go-typed values
+func parseCallData(calldata []byte, abidata string) (*decodedCallData, error) {
+
+ if len(calldata) < 4 {
+ return nil, fmt.Errorf("Invalid ABI-data, incomplete method signature of (%d bytes)", len(calldata))
+ }
+
+ sigdata, argdata := calldata[:4], calldata[4:]
+ if len(argdata)%32 != 0 {
+ return nil, fmt.Errorf("Not ABI-encoded data; length should be a multiple of 32 (was %d)", len(argdata))
+ }
+
+ abispec, err := abi.JSON(strings.NewReader(abidata))
+ if err != nil {
+ return nil, fmt.Errorf("Failed parsing JSON ABI: %v, abidata: %v", err, abidata)
+ }
+
+ method, err := abispec.MethodById(sigdata)
+ if err != nil {
+ return nil, err
+ }
+
+ v, err := method.Inputs.UnpackValues(argdata)
+ if err != nil {
+ return nil, err
+ }
+
+ decoded := decodedCallData{signature: method.Sig(), name: method.Name}
+
+ for n, argument := range method.Inputs {
+ if err != nil {
+ return nil, fmt.Errorf("Failed to decode argument %d (signature %v): %v", n, method.Sig(), err)
+ } else {
+ decodedArg := decodedArgument{
+ soltype: argument,
+ value: v[n],
+ }
+ decoded.inputs = append(decoded.inputs, decodedArg)
+ }
+ }
+
+ // We're finished decoding the data. At this point, we encode the decoded data to see if it matches with the
+ // original data. If we didn't do that, it would e.g. be possible to stuff extra data into the arguments, which
+ // is not detected by merely decoding the data.
+
+ var (
+ encoded []byte
+ )
+ encoded, err = method.Inputs.PackValues(v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if !bytes.Equal(encoded, argdata) {
+ was := common.Bytes2Hex(encoded)
+ exp := common.Bytes2Hex(argdata)
+ return nil, fmt.Errorf("WARNING: Supplied data is stuffed with extra data. \nWant %s\nHave %s\nfor method %v", exp, was, method.Sig())
+ }
+ return &decoded, nil
+}
+
+// MethodSelectorToAbi converts a method selector into an ABI struct. The returned data is a valid json string
+// which can be consumed by the standard abi package.
+func MethodSelectorToAbi(selector string) ([]byte, error) {
+
+ re := regexp.MustCompile(`^([^\)]+)\(([a-z0-9,\[\]]*)\)`)
+
+ type fakeArg struct {
+ Type string `json:"type"`
+ }
+ type fakeABI struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Inputs []fakeArg `json:"inputs"`
+ }
+ groups := re.FindStringSubmatch(selector)
+ if len(groups) != 3 {
+ return nil, fmt.Errorf("Did not match: %v (%v matches)", selector, len(groups))
+ }
+ name := groups[1]
+ args := groups[2]
+ arguments := make([]fakeArg, 0)
+ if len(args) > 0 {
+ for _, arg := range strings.Split(args, ",") {
+ arguments = append(arguments, fakeArg{arg})
+ }
+ }
+ abicheat := fakeABI{
+ name, "function", arguments,
+ }
+ return json.Marshal([]fakeABI{abicheat})
+
+}
+
+type AbiDb struct {
+ db map[string]string
+ customdb map[string]string
+ customdbPath string
+}
+
+// NewEmptyAbiDB exists for test purposes
+func NewEmptyAbiDB() (*AbiDb, error) {
+ return &AbiDb{make(map[string]string), make(map[string]string), ""}, nil
+}
+
+// NewAbiDBFromFile loads signature database from file, and
+// errors if the file is not valid json. Does no other validation of contents
+func NewAbiDBFromFile(path string) (*AbiDb, error) {
+ raw, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ db, err := NewEmptyAbiDB()
+ if err != nil {
+ return nil, err
+ }
+ json.Unmarshal(raw, &db.db)
+ return db, nil
+}
+
+// NewAbiDBFromFiles loads both the standard signature database and a custom database. The latter will be used
+// to write new values into if they are submitted via the API
+func NewAbiDBFromFiles(standard, custom string) (*AbiDb, error) {
+
+ db := &AbiDb{make(map[string]string), make(map[string]string), custom}
+ db.customdbPath = custom
+
+ raw, err := ioutil.ReadFile(standard)
+ if err != nil {
+ return nil, err
+ }
+ json.Unmarshal(raw, &db.db)
+ // Custom file may not exist. Will be created during save, if needed
+ if _, err := os.Stat(custom); err == nil {
+ raw, err = ioutil.ReadFile(custom)
+ if err != nil {
+ return nil, err
+ }
+ json.Unmarshal(raw, &db.customdb)
+ }
+
+ return db, nil
+}
+
+// LookupMethodSelector checks the given 4byte-sequence against the known ABI methods.
+// OBS: This method does not validate the match, it's assumed the caller will do so
+func (db *AbiDb) LookupMethodSelector(id []byte) (string, error) {
+ if len(id) < 4 {
+ return "", fmt.Errorf("Expected 4-byte id, got %d", len(id))
+ }
+ sig := common.ToHex(id[:4])
+ if key, exists := db.db[sig]; exists {
+ return key, nil
+ }
+ if key, exists := db.customdb[sig]; exists {
+ return key, nil
+ }
+ return "", fmt.Errorf("Signature %v not found", sig)
+}
+func (db *AbiDb) Size() int {
+ return len(db.db)
+}
+
+// saveCustomAbi saves a signature ephemerally. If custom file is used, also saves to disk
+func (db *AbiDb) saveCustomAbi(selector, signature string) error {
+ db.customdb[signature] = selector
+ if db.customdbPath == "" {
+ return nil //Not an error per se, just not used
+ }
+ d, err := json.Marshal(db.customdb)
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(db.customdbPath, d, 0600)
+ return err
+}
+
+// Adds a signature to the database, if custom database saving is enabled.
+// OBS: This method does _not_ validate the correctness of the data,
+// it is assumed that the caller has already done so
+func (db *AbiDb) AddSignature(selector string, data []byte) error {
+ if len(data) < 4 {
+ return nil
+ }
+ _, err := db.LookupMethodSelector(data[:4])
+ if err == nil {
+ return nil
+ }
+ sig := common.ToHex(data[:4])
+ return db.saveCustomAbi(selector, sig)
+}
diff --git a/signer/core/abihelper_test.go b/signer/core/abihelper_test.go
new file mode 100644
index 000000000..8bb577669
--- /dev/null
+++ b/signer/core/abihelper_test.go
@@ -0,0 +1,247 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "io/ioutil"
+ "math/big"
+ "reflect"
+
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common"
+)
+
+func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {
+
+ abispec, err := abi.JSON(strings.NewReader(jsondata))
+ if err != nil {
+ t.Fatal(err)
+ }
+ cd := common.Hex2Bytes(calldata)
+ sigdata, argdata := cd[:4], cd[4:]
+ method, err := abispec.MethodById(sigdata)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data, err := method.Inputs.UnpackValues(argdata)
+
+ if len(data) != len(exp) {
+ t.Fatalf("Mismatched length, expected %d, got %d", len(exp), len(data))
+ }
+ for i, elem := range data {
+ if !reflect.DeepEqual(elem, exp[i]) {
+ t.Fatalf("Unpack error, arg %d, got %v, want %v", i, elem, exp[i])
+ }
+ }
+}
+func TestNewUnpacker(t *testing.T) {
+ type unpackTest struct {
+ jsondata string
+ calldata string
+ exp []interface{}
+ }
+ testcases := []unpackTest{
+ { // https://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types
+ `[{"type":"function","name":"f", "inputs":[{"type":"uint256"},{"type":"uint32[]"},{"type":"bytes10"},{"type":"bytes"}]}]`,
+ // 0x123, [0x456, 0x789], "1234567890", "Hello, world!"
+ "8be65246" + "00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000",
+ []interface{}{
+ big.NewInt(0x123),
+ []uint32{0x456, 0x789},
+ [10]byte{49, 50, 51, 52, 53, 54, 55, 56, 57, 48},
+ common.Hex2Bytes("48656c6c6f2c20776f726c6421"),
+ },
+ }, { // https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#examples
+ `[{"type":"function","name":"sam","inputs":[{"type":"bytes"},{"type":"bool"},{"type":"uint256[]"}]}]`,
+ // "dave", true and [1,2,3]
+ "a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
+ []interface{}{
+ []byte{0x64, 0x61, 0x76, 0x65},
+ true,
+ []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)},
+ },
+ }, {
+ `[{"type":"function","name":"send","inputs":[{"type":"uint256"}]}]`,
+ "a52c101e0000000000000000000000000000000000000000000000000000000000000012",
+ []interface{}{big.NewInt(0x12)},
+ }, {
+ `[{"type":"function","name":"compareAndApprove","inputs":[{"type":"address"},{"type":"uint256"},{"type":"uint256"}]}]`,
+ "751e107900000000000000000000000000000133700000deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
+ []interface{}{
+ common.HexToAddress("0x00000133700000deadbeef000000000000000000"),
+ new(big.Int).SetBytes([]byte{0x00}),
+ big.NewInt(0x1),
+ },
+ },
+ }
+ for _, c := range testcases {
+ verify(t, c.jsondata, c.calldata, c.exp)
+ }
+
+}
+
+/*
+func TestReflect(t *testing.T) {
+ a := big.NewInt(0)
+ b := new(big.Int).SetBytes([]byte{0x00})
+ if !reflect.DeepEqual(a, b) {
+ t.Fatalf("Nope, %v != %v", a, b)
+ }
+}
+*/
+
+func TestCalldataDecoding(t *testing.T) {
+
+ // send(uint256) : a52c101e
+ // compareAndApprove(address,uint256,uint256) : 751e1079
+ // issue(address[],uint256) : 42958b54
+ jsondata := `
+[
+ {"type":"function","name":"send","inputs":[{"name":"a","type":"uint256"}]},
+ {"type":"function","name":"compareAndApprove","inputs":[{"name":"a","type":"address"},{"name":"a","type":"uint256"},{"name":"a","type":"uint256"}]},
+ {"type":"function","name":"issue","inputs":[{"name":"a","type":"address[]"},{"name":"a","type":"uint256"}]},
+ {"type":"function","name":"sam","inputs":[{"name":"a","type":"bytes"},{"name":"a","type":"bool"},{"name":"a","type":"uint256[]"}]}
+]`
+ //Expected failures
+ for _, hexdata := range []string{
+ "a52c101e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
+ "a52c101e000000000000000000000000000000000000000000000000000000000000001200",
+ "a52c101e00000000000000000000000000000000000000000000000000000000000000",
+ "a52c101e",
+ "a52c10",
+ "",
+ // Too short
+ "751e10790000000000000000000000000000000000000000000000000000000000000012",
+ "751e1079FFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ //Not valid multiple of 32
+ "deadbeef00000000000000000000000000000000000000000000000000000000000000",
+ //Too short 'issue'
+ "42958b5400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
+ // Too short compareAndApprove
+ "a52c101e00ff0000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
+ // From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
+ // contains a bool with illegal values
+ "a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
+ } {
+ _, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
+ if err == nil {
+ t.Errorf("Expected decoding to fail: %s", hexdata)
+ }
+ }
+
+ //Expected success
+ for _, hexdata := range []string{
+ // From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
+ "a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
+ "a52c101e0000000000000000000000000000000000000000000000000000000000000012",
+ "a52c101eFFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "751e1079000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "42958b54" +
+ // start of dynamic type
+ "0000000000000000000000000000000000000000000000000000000000000040" +
+ //uint256
+ "0000000000000000000000000000000000000000000000000000000000000001" +
+ // length of array
+ "0000000000000000000000000000000000000000000000000000000000000002" +
+ // array values
+ "000000000000000000000000000000000000000000000000000000000000dead" +
+ "000000000000000000000000000000000000000000000000000000000000beef",
+ } {
+ _, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
+ if err != nil {
+ t.Errorf("Unexpected failure on input %s:\n %v (%d bytes) ", hexdata, err, len(common.Hex2Bytes(hexdata)))
+ }
+ }
+}
+
+func TestSelectorUnmarshalling(t *testing.T) {
+ var (
+ db *AbiDb
+ err error
+ abistring []byte
+ abistruct abi.ABI
+ )
+
+ db, err = NewAbiDBFromFile("../../cmd/clef/4byte.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Printf("DB size %v\n", db.Size())
+ for id, selector := range db.db {
+
+ abistring, err = MethodSelectorToAbi(selector)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ abistruct, err = abi.JSON(strings.NewReader(string(abistring)))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ m, err := abistruct.MethodById(common.Hex2Bytes(id[2:]))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if m.Sig() != selector {
+ t.Errorf("Expected equality: %v != %v", m.Sig(), selector)
+ }
+ }
+
+}
+
+func TestCustomABI(t *testing.T) {
+ d, err := ioutil.TempDir("", "signer-4byte-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ filename := fmt.Sprintf("%s/4byte_custom.json", d)
+ abidb, err := NewAbiDBFromFiles("../../cmd/clef/4byte.json", filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Now we'll remove all existing signatures
+ abidb.db = make(map[string]string)
+ calldata := common.Hex2Bytes("a52c101edeadbeef")
+ _, err = abidb.LookupMethodSelector(calldata)
+ if err == nil {
+ t.Fatalf("Should not find a match on empty db")
+ }
+ if err = abidb.AddSignature("send(uint256)", calldata); err != nil {
+ t.Fatalf("Failed to save file: %v", err)
+ }
+ _, err = abidb.LookupMethodSelector(calldata)
+ if err != nil {
+ t.Fatalf("Should find a match for abi signature, got: %v", err)
+ }
+ //Check that it wrote to file
+ abidb2, err := NewAbiDBFromFile(filename)
+ if err != nil {
+ t.Fatalf("Failed to create new abidb: %v", err)
+ }
+ _, err = abidb2.LookupMethodSelector(calldata)
+ if err != nil {
+ t.Fatalf("Save failed: should find a match for abi signature after loading from disk")
+ }
+}
diff --git a/signer/core/api.go b/signer/core/api.go
new file mode 100644
index 000000000..1387041cc
--- /dev/null
+++ b/signer/core/api.go
@@ -0,0 +1,500 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "reflect"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+ "github.com/ethereum/go-ethereum/accounts/usbwallet"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// ExternalAPI defines the external API through which signing requests are made.
+type ExternalAPI interface {
+ // List available accounts
+ List(ctx context.Context) (Accounts, error)
+ // New request to create a new account
+ New(ctx context.Context) (accounts.Account, error)
+ // SignTransaction request to sign the specified transaction
+ SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error)
+ // Sign - request to sign the given data (plus prefix)
+ Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error)
+ // EcRecover - request to perform ecrecover
+ EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error)
+ // Export - request to export an account
+ Export(ctx context.Context, addr common.Address) (json.RawMessage, error)
+ // Import - request to import an account
+ Import(ctx context.Context, keyJSON json.RawMessage) (Account, error)
+}
+
+// SignerUI specifies what method a UI needs to implement to be able to be used as a UI for the signer
+type SignerUI interface {
+ // ApproveTx prompt the user for confirmation to request to sign Transaction
+ ApproveTx(request *SignTxRequest) (SignTxResponse, error)
+ // ApproveSignData prompt the user for confirmation to request to sign data
+ ApproveSignData(request *SignDataRequest) (SignDataResponse, error)
+ // ApproveExport prompt the user for confirmation to export encrypted Account json
+ ApproveExport(request *ExportRequest) (ExportResponse, error)
+ // ApproveImport prompt the user for confirmation to import Account json
+ ApproveImport(request *ImportRequest) (ImportResponse, error)
+ // ApproveListing prompt the user for confirmation to list accounts
+ // the list of accounts to list can be modified by the UI
+ ApproveListing(request *ListRequest) (ListResponse, error)
+ // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
+ ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error)
+ // ShowError displays error message to user
+ ShowError(message string)
+ // ShowInfo displays info message to user
+ ShowInfo(message string)
+ // OnApprovedTx notifies the UI about a transaction having been successfully signed.
+ // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient.
+ OnApprovedTx(tx ethapi.SignTransactionResult)
+ // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
+ // information
+ OnSignerStartup(info StartupInfo)
+}
+
+// SignerAPI defines the actual implementation of ExternalAPI
+type SignerAPI struct {
+ chainID *big.Int
+ am *accounts.Manager
+ UI SignerUI
+ validator *Validator
+}
+
+// Metadata about a request
+type Metadata struct {
+ Remote string `json:"remote"`
+ Local string `json:"local"`
+ Scheme string `json:"scheme"`
+}
+
+// MetadataFromContext extracts Metadata from a given context.Context
+func MetadataFromContext(ctx context.Context) Metadata {
+ m := Metadata{"NA", "NA", "NA"} // batman
+
+ if v := ctx.Value("remote"); v != nil {
+ m.Remote = v.(string)
+ }
+ if v := ctx.Value("scheme"); v != nil {
+ m.Scheme = v.(string)
+ }
+ if v := ctx.Value("local"); v != nil {
+ m.Local = v.(string)
+ }
+ return m
+}
+
+// String implements Stringer interface
+func (m Metadata) String() string {
+ s, err := json.Marshal(m)
+ if err == nil {
+ return string(s)
+ }
+ return err.Error()
+}
+
+// types for the requests/response types between signer and UI
+type (
+ // SignTxRequest contains info about a Transaction to sign
+ SignTxRequest struct {
+ Transaction SendTxArgs `json:"transaction"`
+ Callinfo []ValidationInfo `json:"call_info"`
+ Meta Metadata `json:"meta"`
+ }
+ // SignTxResponse result from SignTxRequest
+ SignTxResponse struct {
+ //The UI may make changes to the TX
+ Transaction SendTxArgs `json:"transaction"`
+ Approved bool `json:"approved"`
+ Password string `json:"password"`
+ }
+ // ExportRequest info about query to export accounts
+ ExportRequest struct {
+ Address common.Address `json:"address"`
+ Meta Metadata `json:"meta"`
+ }
+ // ExportResponse response to export-request
+ ExportResponse struct {
+ Approved bool `json:"approved"`
+ }
+ // ImportRequest info about request to import an Account
+ ImportRequest struct {
+ Meta Metadata `json:"meta"`
+ }
+ ImportResponse struct {
+ Approved bool `json:"approved"`
+ OldPassword string `json:"old_password"`
+ NewPassword string `json:"new_password"`
+ }
+ SignDataRequest struct {
+ Address common.MixedcaseAddress `json:"address"`
+ Rawdata hexutil.Bytes `json:"raw_data"`
+ Message string `json:"message"`
+ Hash hexutil.Bytes `json:"hash"`
+ Meta Metadata `json:"meta"`
+ }
+ SignDataResponse struct {
+ Approved bool `json:"approved"`
+ Password string
+ }
+ NewAccountRequest struct {
+ Meta Metadata `json:"meta"`
+ }
+ NewAccountResponse struct {
+ Approved bool `json:"approved"`
+ Password string `json:"password"`
+ }
+ ListRequest struct {
+ Accounts []Account `json:"accounts"`
+ Meta Metadata `json:"meta"`
+ }
+ ListResponse struct {
+ Accounts []Account `json:"accounts"`
+ }
+ Message struct {
+ Text string `json:"text"`
+ }
+ StartupInfo struct {
+ Info map[string]interface{} `json:"info"`
+ }
+)
+
+var ErrRequestDenied = errors.New("Request denied")
+
+type errorWrapper struct {
+ msg string
+ err error
+}
+
+func (ew errorWrapper) String() string {
+ return fmt.Sprintf("%s\n%s", ew.msg, ew.err)
+}
+
+// NewSignerAPI creates a new API that can be used for Account management.
+// ksLocation specifies the directory where to store the password protected private
+// key that is generated when a new Account is created.
+// noUSB disables USB support that is required to support hardware devices such as
+// ledger and trezor.
+func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abidb *AbiDb, lightKDF bool) *SignerAPI {
+ var (
+ backends []accounts.Backend
+ n, p = keystore.StandardScryptN, keystore.StandardScryptP
+ )
+ if lightKDF {
+ n, p = keystore.LightScryptN, keystore.LightScryptP
+ }
+ // support password based accounts
+ if len(ksLocation) > 0 {
+ backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
+ }
+ if !noUSB {
+ // Start a USB hub for Ledger hardware wallets
+ if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
+ } else {
+ backends = append(backends, ledgerhub)
+ log.Debug("Ledger support enabled")
+ }
+ // Start a USB hub for Trezor hardware wallets
+ if trezorhub, err := usbwallet.NewTrezorHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err))
+ } else {
+ backends = append(backends, trezorhub)
+ log.Debug("Trezor support enabled")
+ }
+ }
+ return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb)}
+}
+
+// List returns the set of wallet this signer manages. Each wallet can contain
+// multiple accounts.
+func (api *SignerAPI) List(ctx context.Context) (Accounts, error) {
+ var accs []Account
+ for _, wallet := range api.am.Wallets() {
+ for _, acc := range wallet.Accounts() {
+ acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address}
+ accs = append(accs, acc)
+ }
+ }
+ result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)})
+ if err != nil {
+ return nil, err
+ }
+ if result.Accounts == nil {
+ return nil, ErrRequestDenied
+
+ }
+ return result.Accounts, nil
+}
+
+// New creates a new password protected Account. The private key is protected with
+// the given password. Users are responsible to backup the private key that is stored
+// in the keystore location thas was specified when this API was created.
+func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) {
+ be := api.am.Backends(keystore.KeyStoreType)
+ if len(be) == 0 {
+ return accounts.Account{}, errors.New("password based accounts not supported")
+ }
+ resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)})
+
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ if !resp.Approved {
+ return accounts.Account{}, ErrRequestDenied
+ }
+ return be[0].(*keystore.KeyStore).NewAccount(resp.Password)
+}
+
+// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer.
+// it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow
+// UI-modifications to requests
+func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
+ modified := false
+ if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) {
+ log.Info("Sender-account changed by UI", "was", f0, "is", f1)
+ modified = true
+ }
+ if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) {
+ log.Info("Recipient-account changed by UI", "was", t0, "is", t1)
+ modified = true
+ }
+ if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 {
+ modified = true
+ log.Info("Gas changed by UI", "was", g0, "is", g1)
+ }
+ if g0, g1 := big.Int(original.Transaction.GasPrice), big.Int(new.Transaction.GasPrice); g0.Cmp(&g1) != 0 {
+ modified = true
+ log.Info("GasPrice changed by UI", "was", g0, "is", g1)
+ }
+ if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 {
+ modified = true
+ log.Info("Value changed by UI", "was", v0, "is", v1)
+ }
+ if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 {
+ d0s := ""
+ d1s := ""
+ if d0 != nil {
+ d0s = common.ToHex(*d0)
+ }
+ if d1 != nil {
+ d1s = common.ToHex(*d1)
+ }
+ if d1s != d0s {
+ modified = true
+ log.Info("Data changed by UI", "was", d0s, "is", d1s)
+ }
+ }
+ if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 {
+ modified = true
+ log.Info("Nonce changed by UI", "was", n0, "is", n1)
+ }
+ return modified
+}
+
+// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form
+func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
+ var (
+ err error
+ result SignTxResponse
+ )
+ msgs, err := api.validator.ValidateTransaction(&args, methodSelector)
+ if err != nil {
+ return nil, err
+ }
+
+ req := SignTxRequest{
+ Transaction: args,
+ Meta: MetadataFromContext(ctx),
+ Callinfo: msgs.Messages,
+ }
+ // Process approval
+ result, err = api.UI.ApproveTx(&req)
+ if err != nil {
+ return nil, err
+ }
+ if !result.Approved {
+ return nil, ErrRequestDenied
+ }
+ // Log changes made by the UI to the signing-request
+ logDiff(&req, &result)
+ var (
+ acc accounts.Account
+ wallet accounts.Wallet
+ )
+ acc = accounts.Account{Address: result.Transaction.From.Address()}
+ wallet, err = api.am.Find(acc)
+ if err != nil {
+ return nil, err
+ }
+ // Convert fields into a real transaction
+ var unsignedTx = result.Transaction.toTransaction()
+
+ // The one to sign is the one that was returned from the UI
+ signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, unsignedTx, api.chainID)
+ if err != nil {
+ api.UI.ShowError(err.Error())
+ return nil, err
+ }
+
+ rlpdata, err := rlp.EncodeToBytes(signedTx)
+ response := ethapi.SignTransactionResult{Raw: rlpdata, Tx: signedTx}
+
+ // Finally, send the signed tx to the UI
+ api.UI.OnApprovedTx(response)
+ // ...and to the external caller
+ return &response, nil
+
+}
+
+// Sign calculates an Ethereum ECDSA signature for:
+// keccack256("\x19Ethereum Signed Message:\n" + len(message) + message))
+//
+// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
+// where the V value will be 27 or 28 for legacy reasons.
+//
+// The key used to calculate the signature is decrypted with the given password.
+//
+// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
+func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) {
+ sighash, msg := SignHash(data)
+ // We make the request prior to looking up if we actually have the account, to prevent
+ // account-enumeration via the API
+ req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: MetadataFromContext(ctx)}
+ res, err := api.UI.ApproveSignData(req)
+
+ if err != nil {
+ return nil, err
+ }
+ if !res.Approved {
+ return nil, ErrRequestDenied
+ }
+ // Look up the wallet containing the requested signer
+ account := accounts.Account{Address: addr.Address()}
+ wallet, err := api.am.Find(account)
+ if err != nil {
+ return nil, err
+ }
+ // Assemble sign the data with the wallet
+ signature, err := wallet.SignHashWithPassphrase(account, res.Password, sighash)
+ if err != nil {
+ api.UI.ShowError(err.Error())
+ return nil, err
+ }
+ signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
+ return signature, nil
+}
+
+// EcRecover returns the address for the Account that was used to create the signature.
+// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
+// the address of:
+// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
+// addr = ecrecover(hash, signature)
+//
+// Note, the signature must conform to the secp256k1 curve R, S and V values, where
+// the V value must be be 27 or 28 for legacy reasons.
+//
+// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecover
+func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) {
+ if len(sig) != 65 {
+ return common.Address{}, fmt.Errorf("signature must be 65 bytes long")
+ }
+ if sig[64] != 27 && sig[64] != 28 {
+ return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
+ }
+ sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
+ hash, _ := SignHash(data)
+ rpk, err := crypto.Ecrecover(hash, sig)
+ if err != nil {
+ return common.Address{}, err
+ }
+ pubKey := crypto.ToECDSAPub(rpk)
+ recoveredAddr := crypto.PubkeyToAddress(*pubKey)
+ return recoveredAddr, nil
+}
+
+// SignHash is a helper function that calculates a hash for the given message that can be
+// safely used to calculate a signature from.
+//
+// The hash is calculated as
+// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
+//
+// This gives context to the signed message and prevents signing of transactions.
+func SignHash(data []byte) ([]byte, string) {
+ msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
+ return crypto.Keccak256([]byte(msg)), msg
+}
+
+// Export returns encrypted private key associated with the given address in web3 keystore format.
+func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
+ res, err := api.UI.ApproveExport(&ExportRequest{Address: addr, Meta: MetadataFromContext(ctx)})
+
+ if err != nil {
+ return nil, err
+ }
+ if !res.Approved {
+ return nil, ErrRequestDenied
+ }
+ // Look up the wallet containing the requested signer
+ wallet, err := api.am.Find(accounts.Account{Address: addr})
+ if err != nil {
+ return nil, err
+ }
+ if wallet.URL().Scheme != keystore.KeyStoreScheme {
+ return nil, fmt.Errorf("Account is not a keystore-account")
+ }
+ return ioutil.ReadFile(wallet.URL().Path)
+}
+
+// Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
+// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
+// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
+func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) {
+ be := api.am.Backends(keystore.KeyStoreType)
+
+ if len(be) == 0 {
+ return Account{}, errors.New("password based accounts not supported")
+ }
+ res, err := api.UI.ApproveImport(&ImportRequest{Meta: MetadataFromContext(ctx)})
+
+ if err != nil {
+ return Account{}, err
+ }
+ if !res.Approved {
+ return Account{}, ErrRequestDenied
+ }
+ acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, res.OldPassword, res.NewPassword)
+ if err != nil {
+ api.UI.ShowError(err.Error())
+ return Account{}, err
+ }
+ return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil
+}
diff --git a/signer/core/api_test.go b/signer/core/api_test.go
new file mode 100644
index 000000000..50ad02198
--- /dev/null
+++ b/signer/core/api_test.go
@@ -0,0 +1,386 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+//
+package core
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+ "github.com/ethereum/go-ethereum/cmd/utils"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+//Used for testing
+type HeadlessUI struct {
+ controller chan string
+}
+
+func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) {
+}
+
+func (ui *HeadlessUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ fmt.Printf("OnApproved called")
+}
+
+func (ui *HeadlessUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
+
+ switch <-ui.controller {
+ case "Y":
+ return SignTxResponse{request.Transaction, true, <-ui.controller}, nil
+ case "M": //Modify
+ old := big.Int(request.Transaction.Value)
+ newVal := big.NewInt(0).Add(&old, big.NewInt(1))
+ request.Transaction.Value = hexutil.Big(*newVal)
+ return SignTxResponse{request.Transaction, true, <-ui.controller}, nil
+ default:
+ return SignTxResponse{request.Transaction, false, ""}, nil
+ }
+}
+func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
+ if "Y" == <-ui.controller {
+ return SignDataResponse{true, <-ui.controller}, nil
+ }
+ return SignDataResponse{false, ""}, nil
+}
+func (ui *HeadlessUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
+
+ return ExportResponse{<-ui.controller == "Y"}, nil
+
+}
+func (ui *HeadlessUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
+
+ if "Y" == <-ui.controller {
+ return ImportResponse{true, <-ui.controller, <-ui.controller}, nil
+ }
+ return ImportResponse{false, "", ""}, nil
+}
+func (ui *HeadlessUI) ApproveListing(request *ListRequest) (ListResponse, error) {
+
+ switch <-ui.controller {
+ case "A":
+ return ListResponse{request.Accounts}, nil
+ case "1":
+ l := make([]Account, 1)
+ l[0] = request.Accounts[1]
+ return ListResponse{l}, nil
+ default:
+ return ListResponse{nil}, nil
+ }
+}
+func (ui *HeadlessUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
+
+ if "Y" == <-ui.controller {
+ return NewAccountResponse{true, <-ui.controller}, nil
+ }
+ return NewAccountResponse{false, ""}, nil
+}
+func (ui *HeadlessUI) ShowError(message string) {
+ //stdout is used by communication
+ fmt.Fprint(os.Stderr, message)
+}
+func (ui *HeadlessUI) ShowInfo(message string) {
+ //stdout is used by communication
+ fmt.Fprint(os.Stderr, message)
+}
+
+func tmpDirName(t *testing.T) string {
+ d, err := ioutil.TempDir("", "eth-keystore-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, err = filepath.EvalSymlinks(d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return d
+}
+
+func setup(t *testing.T) (*SignerAPI, chan string) {
+
+ controller := make(chan string, 10)
+
+ db, err := NewAbiDBFromFile("../../cmd/clef/4byte.json")
+ if err != nil {
+ utils.Fatalf(err.Error())
+ }
+ var (
+ ui = &HeadlessUI{controller}
+ api = NewSignerAPI(
+ 1,
+ tmpDirName(t),
+ true,
+ ui,
+ db,
+ true)
+ )
+ return api, controller
+}
+func createAccount(control chan string, api *SignerAPI, t *testing.T) {
+
+ control <- "Y"
+ control <- "apassword"
+ _, err := api.New(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Some time to allow changes to propagate
+ time.Sleep(250 * time.Millisecond)
+}
+func failCreateAccount(control chan string, api *SignerAPI, t *testing.T) {
+ control <- "N"
+ acc, err := api.New(context.Background())
+ if err != ErrRequestDenied {
+ t.Fatal(err)
+ }
+ if acc.Address != (common.Address{}) {
+ t.Fatal("Empty address should be returned")
+ }
+}
+func list(control chan string, api *SignerAPI, t *testing.T) []Account {
+ control <- "A"
+ list, err := api.List(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ return list
+}
+
+func TestNewAcc(t *testing.T) {
+
+ api, control := setup(t)
+ verifyNum := func(num int) {
+ if list := list(control, api, t); len(list) != num {
+ t.Errorf("Expected %d accounts, got %d", num, len(list))
+ }
+ }
+ // Testing create and create-deny
+ createAccount(control, api, t)
+ createAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ createAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ createAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ verifyNum(4)
+
+ // Testing listing:
+ // Listing one Account
+ control <- "1"
+ list, err := api.List(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(list) != 1 {
+ t.Fatalf("List should only show one Account")
+ }
+ // Listing denied
+ control <- "Nope"
+ list, err = api.List(context.Background())
+ if len(list) != 0 {
+ t.Fatalf("List should be empty")
+ }
+ if err != ErrRequestDenied {
+ t.Fatal("Expected deny")
+ }
+}
+
+func TestSignData(t *testing.T) {
+
+ api, control := setup(t)
+ //Create two accounts
+ createAccount(control, api, t)
+ createAccount(control, api, t)
+ control <- "1"
+ list, err := api.List(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ a := common.NewMixedcaseAddress(list[0].Address)
+
+ control <- "Y"
+ control <- "wrongpassword"
+ h, err := api.Sign(context.Background(), a, []byte("EHLO world"))
+ if h != nil {
+ t.Errorf("Expected nil-data, got %x", h)
+ }
+ if err != keystore.ErrDecrypt {
+ t.Errorf("Expected ErrLocked! %v", err)
+ }
+
+ control <- "No way"
+ h, err = api.Sign(context.Background(), a, []byte("EHLO world"))
+ if h != nil {
+ t.Errorf("Expected nil-data, got %x", h)
+ }
+ if err != ErrRequestDenied {
+ t.Errorf("Expected ErrRequestDenied! %v", err)
+ }
+
+ control <- "Y"
+ control <- "apassword"
+ h, err = api.Sign(context.Background(), a, []byte("EHLO world"))
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if h == nil || len(h) != 65 {
+ t.Errorf("Expected 65 byte signature (got %d bytes)", len(h))
+ }
+}
+func mkTestTx(from common.MixedcaseAddress) SendTxArgs {
+ to := common.NewMixedcaseAddress(common.HexToAddress("0x1337"))
+ gas := hexutil.Uint64(21000)
+ gasPrice := (hexutil.Big)(*big.NewInt(2000000000))
+ value := (hexutil.Big)(*big.NewInt(1e18))
+ nonce := (hexutil.Uint64)(0)
+ data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a"))
+ tx := SendTxArgs{
+ From: from,
+ To: &to,
+ Gas: gas,
+ GasPrice: gasPrice,
+ Value: value,
+ Data: &data,
+ Nonce: nonce}
+ return tx
+}
+
+func TestSignTx(t *testing.T) {
+
+ var (
+ list Accounts
+ res, res2 *ethapi.SignTransactionResult
+ err error
+ )
+
+ api, control := setup(t)
+ createAccount(control, api, t)
+ control <- "A"
+ list, err = api.List(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ a := common.NewMixedcaseAddress(list[0].Address)
+
+ methodSig := "test(uint)"
+ tx := mkTestTx(a)
+
+ control <- "Y"
+ control <- "wrongpassword"
+ res, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if res != nil {
+ t.Errorf("Expected nil-response, got %v", res)
+ }
+ if err != keystore.ErrDecrypt {
+ t.Errorf("Expected ErrLocked! %v", err)
+ }
+
+ control <- "No way"
+ res, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if res != nil {
+ t.Errorf("Expected nil-response, got %v", res)
+ }
+ if err != ErrRequestDenied {
+ t.Errorf("Expected ErrRequestDenied! %v", err)
+ }
+
+ control <- "Y"
+ control <- "apassword"
+ res, err = api.SignTransaction(context.Background(), tx, &methodSig)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ parsedTx := &types.Transaction{}
+ rlp.Decode(bytes.NewReader(res.Raw), parsedTx)
+ //The tx should NOT be modified by the UI
+ if parsedTx.Value().Cmp(tx.Value.ToInt()) != 0 {
+ t.Errorf("Expected value to be unchanged, expected %v got %v", tx.Value, parsedTx.Value())
+ }
+ control <- "Y"
+ control <- "apassword"
+
+ res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(res.Raw, res2.Raw) {
+ t.Error("Expected tx to be unmodified by UI")
+ }
+
+ //The tx is modified by the UI
+ control <- "M"
+ control <- "apassword"
+
+ res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ parsedTx2 := &types.Transaction{}
+ rlp.Decode(bytes.NewReader(res.Raw), parsedTx2)
+ //The tx should be modified by the UI
+ if parsedTx2.Value().Cmp(tx.Value.ToInt()) != 0 {
+ t.Errorf("Expected value to be unchanged, got %v", parsedTx.Value())
+ }
+
+ if bytes.Equal(res.Raw, res2.Raw) {
+ t.Error("Expected tx to be modified by UI")
+ }
+
+}
+
+/*
+func TestAsyncronousResponses(t *testing.T){
+
+ //Set up one account
+ api, control := setup(t)
+ createAccount(control, api, t)
+
+ // Two transactions, the second one with larger value than the first
+ tx1 := mkTestTx()
+ newVal := big.NewInt(0).Add((*big.Int) (tx1.Value), big.NewInt(1))
+ tx2 := mkTestTx()
+ tx2.Value = (*hexutil.Big)(newVal)
+
+ control <- "W" //wait
+ control <- "Y" //
+ control <- "apassword"
+ control <- "Y" //
+ control <- "apassword"
+
+ var err error
+
+ h1, err := api.SignTransaction(context.Background(), common.HexToAddress("1111"), tx1, nil)
+ h2, err := api.SignTransaction(context.Background(), common.HexToAddress("2222"), tx2, nil)
+
+
+ }
+*/
diff --git a/signer/core/auditlog.go b/signer/core/auditlog.go
new file mode 100644
index 000000000..d0ba733d2
--- /dev/null
+++ b/signer/core/auditlog.go
@@ -0,0 +1,110 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "context"
+
+ "encoding/json"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+type AuditLogger struct {
+ log log.Logger
+ api ExternalAPI
+}
+
+func (l *AuditLogger) List(ctx context.Context) (Accounts, error) {
+ l.log.Info("List", "type", "request", "metadata", MetadataFromContext(ctx).String())
+ res, e := l.api.List(ctx)
+
+ l.log.Info("List", "type", "response", "data", res.String())
+
+ return res, e
+}
+
+func (l *AuditLogger) New(ctx context.Context) (accounts.Account, error) {
+ return l.api.New(ctx)
+}
+
+func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
+ sel := "<nil>"
+ if methodSelector != nil {
+ sel = *methodSelector
+ }
+ l.log.Info("SignTransaction", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "tx", args.String(),
+ "methodSelector", sel)
+
+ res, e := l.api.SignTransaction(ctx, args, methodSelector)
+ if res != nil {
+ l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(res.Raw), "error", e)
+ } else {
+ l.log.Info("SignTransaction", "type", "response", "data", res, "error", e)
+ }
+ return res, e
+}
+
+func (l *AuditLogger) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) {
+ l.log.Info("Sign", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "addr", addr.String(), "data", common.Bytes2Hex(data))
+ b, e := l.api.Sign(ctx, addr, data)
+ l.log.Info("Sign", "type", "response", "data", common.Bytes2Hex(b), "error", e)
+ return b, e
+}
+
+func (l *AuditLogger) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) {
+ l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "data", common.Bytes2Hex(data))
+ a, e := l.api.EcRecover(ctx, data, sig)
+ l.log.Info("EcRecover", "type", "response", "addr", a.String(), "error", e)
+ return a, e
+}
+
+func (l *AuditLogger) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
+ l.log.Info("Export", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "addr", addr.Hex())
+ j, e := l.api.Export(ctx, addr)
+ // In this case, we don't actually log the json-response, which may be extra sensitive
+ l.log.Info("Export", "type", "response", "json response size", len(j), "error", e)
+ return j, e
+}
+
+func (l *AuditLogger) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) {
+ // Don't actually log the json contents
+ l.log.Info("Import", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "keyJSON size", len(keyJSON))
+ a, e := l.api.Import(ctx, keyJSON)
+ l.log.Info("Import", "type", "response", "addr", a.String(), "error", e)
+ return a, e
+}
+
+func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) {
+ l := log.New("api", "signer")
+ handler, err := log.FileHandler(path, log.LogfmtFormat())
+ if err != nil {
+ return nil, err
+ }
+ l.SetHandler(handler)
+ l.Info("Configured", "audit log", path)
+ return &AuditLogger{l, api}, nil
+}
diff --git a/signer/core/cliui.go b/signer/core/cliui.go
new file mode 100644
index 000000000..0d9b5f3d3
--- /dev/null
+++ b/signer/core/cliui.go
@@ -0,0 +1,247 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+package core
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "sync"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+type CommandlineUI struct {
+ in *bufio.Reader
+ mu sync.Mutex
+}
+
+func NewCommandlineUI() *CommandlineUI {
+ return &CommandlineUI{in: bufio.NewReader(os.Stdin)}
+}
+
+// readString reads a single line from stdin, trimming if from spaces, enforcing
+// non-emptyness.
+func (ui *CommandlineUI) readString() string {
+ for {
+ fmt.Printf("> ")
+ text, err := ui.in.ReadString('\n')
+ if err != nil {
+ log.Crit("Failed to read user input", "err", err)
+ }
+ if text = strings.TrimSpace(text); text != "" {
+ return text
+ }
+ }
+}
+
+// readPassword reads a single line from stdin, trimming it from the trailing new
+// line and returns it. The input will not be echoed.
+func (ui *CommandlineUI) readPassword() string {
+ fmt.Printf("Enter password to approve:\n")
+ fmt.Printf("> ")
+
+ text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
+ if err != nil {
+ log.Crit("Failed to read password", "err", err)
+ }
+ fmt.Println()
+ fmt.Println("-----------------------")
+ return string(text)
+}
+
+// readPassword reads a single line from stdin, trimming it from the trailing new
+// line and returns it. The input will not be echoed.
+func (ui *CommandlineUI) readPasswordText(inputstring string) string {
+ fmt.Printf("Enter %s:\n", inputstring)
+ fmt.Printf("> ")
+ text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
+ if err != nil {
+ log.Crit("Failed to read password", "err", err)
+ }
+ fmt.Println("-----------------------")
+ return string(text)
+}
+
+// confirm returns true if user enters 'Yes', otherwise false
+func (ui *CommandlineUI) confirm() bool {
+ fmt.Printf("Approve? [y/N]:\n")
+ if ui.readString() == "y" {
+ return true
+ }
+ fmt.Println("-----------------------")
+ return false
+}
+
+func showMetadata(metadata Metadata) {
+ fmt.Printf("Request context:\n\t%v -> %v -> %v\n", metadata.Remote, metadata.Scheme, metadata.Local)
+}
+
+// ApproveTx prompt the user for confirmation to request to sign Transaction
+func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+ weival := request.Transaction.Value.ToInt()
+ fmt.Printf("--------- Transaction request-------------\n")
+ if to := request.Transaction.To; to != nil {
+ fmt.Printf("to: %v\n", to.Original())
+ if !to.ValidChecksum() {
+ fmt.Printf("\nWARNING: Invalid checksum on to-address!\n\n")
+ }
+ } else {
+ fmt.Printf("to: <contact creation>\n")
+ }
+ fmt.Printf("from: %v\n", request.Transaction.From.String())
+ fmt.Printf("value: %v wei\n", weival)
+ if request.Transaction.Data != nil {
+ d := *request.Transaction.Data
+ if len(d) > 0 {
+ fmt.Printf("data: %v\n", common.Bytes2Hex(d))
+ }
+ }
+ if request.Callinfo != nil {
+ fmt.Printf("\nTransaction validation:\n")
+ for _, m := range request.Callinfo {
+ fmt.Printf(" * %s : %s", m.Typ, m.Message)
+ }
+ fmt.Println()
+
+ }
+ fmt.Printf("\n")
+ showMetadata(request.Meta)
+ fmt.Printf("-------------------------------------------\n")
+ if !ui.confirm() {
+ return SignTxResponse{request.Transaction, false, ""}, nil
+ }
+ return SignTxResponse{request.Transaction, true, ui.readPassword()}, nil
+}
+
+// ApproveSignData prompt the user for confirmation to request to sign data
+func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ fmt.Printf("-------- Sign data request--------------\n")
+ fmt.Printf("Account: %s\n", request.Address.String())
+ fmt.Printf("message: \n%q\n", request.Message)
+ fmt.Printf("raw data: \n%v\n", request.Rawdata)
+ fmt.Printf("message hash: %v\n", request.Hash)
+ fmt.Printf("-------------------------------------------\n")
+ showMetadata(request.Meta)
+ if !ui.confirm() {
+ return SignDataResponse{false, ""}, nil
+ }
+ return SignDataResponse{true, ui.readPassword()}, nil
+}
+
+// ApproveExport prompt the user for confirmation to export encrypted Account json
+func (ui *CommandlineUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ fmt.Printf("-------- Export Account request--------------\n")
+ fmt.Printf("A request has been made to export the (encrypted) keyfile\n")
+ fmt.Printf("Approving this operation means that the caller obtains the (encrypted) contents\n")
+ fmt.Printf("\n")
+ fmt.Printf("Account: %x\n", request.Address)
+ //fmt.Printf("keyfile: \n%v\n", request.file)
+ fmt.Printf("-------------------------------------------\n")
+ showMetadata(request.Meta)
+ return ExportResponse{ui.confirm()}, nil
+}
+
+// ApproveImport prompt the user for confirmation to import Account json
+func (ui *CommandlineUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ fmt.Printf("-------- Import Account request--------------\n")
+ fmt.Printf("A request has been made to import an encrypted keyfile\n")
+ fmt.Printf("-------------------------------------------\n")
+ showMetadata(request.Meta)
+ if !ui.confirm() {
+ return ImportResponse{false, "", ""}, nil
+ }
+ return ImportResponse{true, ui.readPasswordText("Old password"), ui.readPasswordText("New password")}, nil
+}
+
+// ApproveListing prompt the user for confirmation to list accounts
+// the list of accounts to list can be modified by the UI
+func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) {
+
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ fmt.Printf("-------- List Account request--------------\n")
+ fmt.Printf("A request has been made to list all accounts. \n")
+ fmt.Printf("You can select which accounts the caller can see\n")
+ for _, account := range request.Accounts {
+ fmt.Printf("\t[x] %v\n", account.Address.Hex())
+ }
+ fmt.Printf("-------------------------------------------\n")
+ showMetadata(request.Meta)
+ if !ui.confirm() {
+ return ListResponse{nil}, nil
+ }
+ return ListResponse{request.Accounts}, nil
+}
+
+// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
+func (ui *CommandlineUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
+
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ fmt.Printf("-------- New Account request--------------\n")
+ fmt.Printf("A request has been made to create a new. \n")
+ fmt.Printf("Approving this operation means that a new Account is created,\n")
+ fmt.Printf("and the address show to the caller\n")
+ showMetadata(request.Meta)
+ if !ui.confirm() {
+ return NewAccountResponse{false, ""}, nil
+ }
+ return NewAccountResponse{true, ui.readPassword()}, nil
+}
+
+// ShowError displays error message to user
+func (ui *CommandlineUI) ShowError(message string) {
+
+ fmt.Printf("ERROR: %v\n", message)
+}
+
+// ShowInfo displays info message to user
+func (ui *CommandlineUI) ShowInfo(message string) {
+ fmt.Printf("Info: %v\n", message)
+}
+
+func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ fmt.Printf("Transaction signed:\n ")
+ spew.Dump(tx.Tx)
+}
+
+func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) {
+
+ fmt.Printf("------- Signer info -------\n")
+ for k, v := range info.Info {
+ fmt.Printf("* %v : %v\n", k, v)
+ }
+}
diff --git a/signer/core/stdioui.go b/signer/core/stdioui.go
new file mode 100644
index 000000000..5640ed03b
--- /dev/null
+++ b/signer/core/stdioui.go
@@ -0,0 +1,113 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+//
+
+package core
+
+import (
+ "context"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+type StdIOUI struct {
+ client rpc.Client
+ mu sync.Mutex
+}
+
+func NewStdIOUI() *StdIOUI {
+ log.Info("NewStdIOUI")
+ client, err := rpc.DialContext(context.Background(), "stdio://")
+ if err != nil {
+ log.Crit("Could not create stdio client", "err", err)
+ }
+ return &StdIOUI{client: *client}
+}
+
+// dispatch sends a request over the stdio
+func (ui *StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error {
+ err := ui.client.Call(&reply, serviceMethod, args)
+ if err != nil {
+ log.Info("Error", "exc", err.Error())
+ }
+ return err
+}
+
+func (ui *StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
+ var result SignTxResponse
+ err := ui.dispatch("ApproveTx", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
+ var result SignDataResponse
+ err := ui.dispatch("ApproveSignData", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
+ var result ExportResponse
+ err := ui.dispatch("ApproveExport", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
+ var result ImportResponse
+ err := ui.dispatch("ApproveImport", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) {
+ var result ListResponse
+ err := ui.dispatch("ApproveListing", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
+ var result NewAccountResponse
+ err := ui.dispatch("ApproveNewAccount", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ShowError(message string) {
+ err := ui.dispatch("ShowError", &Message{message}, nil)
+ if err != nil {
+ log.Info("Error calling 'ShowError'", "exc", err.Error(), "msg", message)
+ }
+}
+
+func (ui *StdIOUI) ShowInfo(message string) {
+ err := ui.dispatch("ShowInfo", Message{message}, nil)
+ if err != nil {
+ log.Info("Error calling 'ShowInfo'", "exc", err.Error(), "msg", message)
+ }
+}
+func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ err := ui.dispatch("OnApprovedTx", tx, nil)
+ if err != nil {
+ log.Info("Error calling 'OnApprovedTx'", "exc", err.Error(), "tx", tx)
+ }
+}
+
+func (ui *StdIOUI) OnSignerStartup(info StartupInfo) {
+ err := ui.dispatch("OnSignerStartup", info, nil)
+ if err != nil {
+ log.Info("Error calling 'OnSignerStartup'", "exc", err.Error(), "info", info)
+ }
+}
diff --git a/signer/core/types.go b/signer/core/types.go
new file mode 100644
index 000000000..8386bd44e
--- /dev/null
+++ b/signer/core/types.go
@@ -0,0 +1,95 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "encoding/json"
+ "strings"
+
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+type Accounts []Account
+
+func (as Accounts) String() string {
+ var output []string
+ for _, a := range as {
+ output = append(output, a.String())
+ }
+ return strings.Join(output, "\n")
+}
+
+type Account struct {
+ Typ string `json:"type"`
+ URL accounts.URL `json:"url"`
+ Address common.Address `json:"address"`
+}
+
+func (a Account) String() string {
+ s, err := json.Marshal(a)
+ if err == nil {
+ return string(s)
+ }
+ return err.Error()
+}
+
+type ValidationInfo struct {
+ Typ string `json:"type"`
+ Message string `json:"message"`
+}
+type ValidationMessages struct {
+ Messages []ValidationInfo
+}
+
+// SendTxArgs represents the arguments to submit a transaction
+type SendTxArgs struct {
+ From common.MixedcaseAddress `json:"from"`
+ To *common.MixedcaseAddress `json:"to"`
+ Gas hexutil.Uint64 `json:"gas"`
+ GasPrice hexutil.Big `json:"gasPrice"`
+ Value hexutil.Big `json:"value"`
+ Nonce hexutil.Uint64 `json:"nonce"`
+ // We accept "data" and "input" for backwards-compatibility reasons.
+ Data *hexutil.Bytes `json:"data"`
+ Input *hexutil.Bytes `json:"input"`
+}
+
+func (t SendTxArgs) String() string {
+ s, err := json.Marshal(t)
+ if err == nil {
+ return string(s)
+ }
+ return err.Error()
+}
+
+func (args *SendTxArgs) toTransaction() *types.Transaction {
+ var input []byte
+ if args.Data != nil {
+ input = *args.Data
+ } else if args.Input != nil {
+ input = *args.Input
+ }
+ if args.To == nil {
+ return types.NewContractCreation(uint64(args.Nonce), (*big.Int)(&args.Value), uint64(args.Gas), (*big.Int)(&args.GasPrice), input)
+ }
+ return types.NewTransaction(uint64(args.Nonce), args.To.Address(), (*big.Int)(&args.Value), (uint64)(args.Gas), (*big.Int)(&args.GasPrice), input)
+}
diff --git a/signer/core/validation.go b/signer/core/validation.go
new file mode 100644
index 000000000..97bb3b685
--- /dev/null
+++ b/signer/core/validation.go
@@ -0,0 +1,163 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// The validation package contains validation checks for transactions
+// - ABI-data validation
+// - Transaction semantics validation
+// The package provides warnings for typical pitfalls
+
+func (vs *ValidationMessages) crit(msg string) {
+ vs.Messages = append(vs.Messages, ValidationInfo{"CRITICAL", msg})
+}
+func (vs *ValidationMessages) warn(msg string) {
+ vs.Messages = append(vs.Messages, ValidationInfo{"WARNING", msg})
+}
+func (vs *ValidationMessages) info(msg string) {
+ vs.Messages = append(vs.Messages, ValidationInfo{"Info", msg})
+}
+
+type Validator struct {
+ db *AbiDb
+}
+
+func NewValidator(db *AbiDb) *Validator {
+ return &Validator{db}
+}
+func testSelector(selector string, data []byte) (*decodedCallData, error) {
+ if selector == "" {
+ return nil, fmt.Errorf("selector not found")
+ }
+ abiData, err := MethodSelectorToAbi(selector)
+ if err != nil {
+ return nil, err
+ }
+ info, err := parseCallData(data, string(abiData))
+ if err != nil {
+ return nil, err
+ }
+ return info, nil
+
+}
+
+// validateCallData checks if the ABI-data + methodselector (if given) can be parsed and seems to match
+func (v *Validator) validateCallData(msgs *ValidationMessages, data []byte, methodSelector *string) {
+ if len(data) == 0 {
+ return
+ }
+ if len(data) < 4 {
+ msgs.warn("Tx contains data which is not valid ABI")
+ return
+ }
+ var (
+ info *decodedCallData
+ err error
+ )
+ // Check the provided one
+ if methodSelector != nil {
+ info, err = testSelector(*methodSelector, data)
+ if err != nil {
+ msgs.warn(fmt.Sprintf("Tx contains data, but provided ABI signature could not be matched: %v", err))
+ } else {
+ msgs.info(info.String())
+ //Successfull match. add to db if not there already (ignore errors there)
+ v.db.AddSignature(*methodSelector, data[:4])
+ }
+ return
+ }
+ // Check the db
+ selector, err := v.db.LookupMethodSelector(data[:4])
+ if err != nil {
+ msgs.warn(fmt.Sprintf("Tx contains data, but the ABI signature could not be found: %v", err))
+ return
+ }
+ info, err = testSelector(selector, data)
+ if err != nil {
+ msgs.warn(fmt.Sprintf("Tx contains data, but provided ABI signature could not be matched: %v", err))
+ } else {
+ msgs.info(info.String())
+ }
+}
+
+// validateSemantics checks if the transactions 'makes sense', and generate warnings for a couple of typical scenarios
+func (v *Validator) validate(msgs *ValidationMessages, txargs *SendTxArgs, methodSelector *string) error {
+ // Prevent accidental erroneous usage of both 'input' and 'data'
+ if txargs.Data != nil && txargs.Input != nil && !bytes.Equal(*txargs.Data, *txargs.Input) {
+ // This is a showstopper
+ return errors.New(`Ambiguous request: both "data" and "input" are set and are not identical`)
+ }
+ var (
+ data []byte
+ )
+ // Place data on 'data', and nil 'input'
+ if txargs.Input != nil {
+ txargs.Data = txargs.Input
+ txargs.Input = nil
+ }
+ if txargs.Data != nil {
+ data = *txargs.Data
+ }
+
+ if txargs.To == nil {
+ //Contract creation should contain sufficient data to deploy a contract
+ // A typical error is omitting sender due to some quirk in the javascript call
+ // e.g. https://github.com/ethereum/go-ethereum/issues/16106
+ if len(data) == 0 {
+ if txargs.Value.ToInt().Cmp(big.NewInt(0)) > 0 {
+ // Sending ether into black hole
+ return errors.New(`Tx will create contract with value but empty code!`)
+ }
+ // No value submitted at least
+ msgs.crit("Tx will create contract with empty code!")
+ } else if len(data) < 40 { //Arbitrary limit
+ msgs.warn(fmt.Sprintf("Tx will will create contract, but payload is suspiciously small (%d b)", len(data)))
+ }
+ // methodSelector should be nil for contract creation
+ if methodSelector != nil {
+ msgs.warn("Tx will create contract, but method selector supplied; indicating intent to call a method.")
+ }
+
+ } else {
+ if !txargs.To.ValidChecksum() {
+ msgs.warn("Invalid checksum on to-address")
+ }
+ // Normal transaction
+ if bytes.Equal(txargs.To.Address().Bytes(), common.Address{}.Bytes()) {
+ // Sending to 0
+ msgs.crit("Tx destination is the zero address!")
+ }
+ // Validate calldata
+ v.validateCallData(msgs, data, methodSelector)
+ }
+ return nil
+}
+
+// ValidateTransaction does a number of checks on the supplied transaction, and returns either a list of warnings,
+// or an error, indicating that the transaction should be immediately rejected
+func (v *Validator) ValidateTransaction(txArgs *SendTxArgs, methodSelector *string) (*ValidationMessages, error) {
+ msgs := &ValidationMessages{}
+ return msgs, v.validate(msgs, txArgs, methodSelector)
+}
diff --git a/signer/core/validation_test.go b/signer/core/validation_test.go
new file mode 100644
index 000000000..2b33a8630
--- /dev/null
+++ b/signer/core/validation_test.go
@@ -0,0 +1,139 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "fmt"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+)
+
+func hexAddr(a string) common.Address { return common.BytesToAddress(common.FromHex(a)) }
+func mixAddr(a string) (*common.MixedcaseAddress, error) {
+ return common.NewMixedcaseAddressFromString(a)
+}
+func toHexBig(h string) hexutil.Big {
+ b := big.NewInt(0).SetBytes(common.FromHex(h))
+ return hexutil.Big(*b)
+}
+func toHexUint(h string) hexutil.Uint64 {
+ b := big.NewInt(0).SetBytes(common.FromHex(h))
+ return hexutil.Uint64(b.Uint64())
+}
+func dummyTxArgs(t txtestcase) *SendTxArgs {
+ to, _ := mixAddr(t.to)
+ from, _ := mixAddr(t.from)
+ n := toHexUint(t.n)
+ gas := toHexUint(t.g)
+ gasPrice := toHexBig(t.gp)
+ value := toHexBig(t.value)
+ var (
+ data, input *hexutil.Bytes
+ )
+ if t.d != "" {
+ a := hexutil.Bytes(common.FromHex(t.d))
+ data = &a
+ }
+ if t.i != "" {
+ a := hexutil.Bytes(common.FromHex(t.i))
+ input = &a
+
+ }
+ return &SendTxArgs{
+ From: *from,
+ To: to,
+ Value: value,
+ Nonce: n,
+ GasPrice: gasPrice,
+ Gas: gas,
+ Data: data,
+ Input: input,
+ }
+}
+
+type txtestcase struct {
+ from, to, n, g, gp, value, d, i string
+ expectErr bool
+ numMessages int
+}
+
+func TestValidator(t *testing.T) {
+ var (
+ // use empty db, there are other tests for the abi-specific stuff
+ db, _ = NewEmptyAbiDB()
+ v = NewValidator(db)
+ )
+ testcases := []txtestcase{
+ // Invalid to checksum
+ {from: "000000000000000000000000000000000000dead", to: "000000000000000000000000000000000000dead",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
+ // valid 0x000000000000000000000000000000000000dEaD
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 0},
+ // conflicting input and data
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", i: "0x02", expectErr: true},
+ // Data can't be parsed
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x0102", numMessages: 1},
+ // Data (on Input) can't be parsed
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", i: "0x0102", numMessages: 1},
+ // Send to 0
+ {from: "000000000000000000000000000000000000dead", to: "0x0000000000000000000000000000000000000000",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
+ // Create empty contract (no value)
+ {from: "000000000000000000000000000000000000dead", to: "",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x00", numMessages: 1},
+ // Create empty contract (with value)
+ {from: "000000000000000000000000000000000000dead", to: "",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", expectErr: true},
+ // Small payload for create
+ {from: "000000000000000000000000000000000000dead", to: "",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", numMessages: 1},
+ }
+ for i, test := range testcases {
+ msgs, err := v.ValidateTransaction(dummyTxArgs(test), nil)
+ if err == nil && test.expectErr {
+ t.Errorf("Test %d, expected error", i)
+ for _, msg := range msgs.Messages {
+ fmt.Printf("* %s: %s\n", msg.Typ, msg.Message)
+ }
+ }
+ if err != nil && !test.expectErr {
+ t.Errorf("Test %d, unexpected error: %v", i, err)
+ }
+ if err == nil {
+ got := len(msgs.Messages)
+ if got != test.numMessages {
+ for _, msg := range msgs.Messages {
+ fmt.Printf("* %s: %s\n", msg.Typ, msg.Message)
+ }
+ t.Errorf("Test %d, expected %d messages, got %d", i, test.numMessages, got)
+ } else {
+ //Debug printout, remove later
+ for _, msg := range msgs.Messages {
+ fmt.Printf("* [%d] %s: %s\n", i, msg.Typ, msg.Message)
+ }
+ fmt.Println()
+ }
+ }
+ }
+}
diff --git a/signer/rules/deps/bignumber.js b/signer/rules/deps/bignumber.js
new file mode 100644
index 000000000..17c8851e2
--- /dev/null
+++ b/signer/rules/deps/bignumber.js
@@ -0,0 +1,4 @@
+/* bignumber.js v2.0.3 https://github.com/MikeMcl/bignumber.js/LICENCE */
+/* modified by zelig to fix https://github.com/robertkrimen/otto#regular-expression-incompatibility */
+!function(e){"use strict";function n(e){function a(e,n){var t,r,i,o,u,s,f=this;if(!(f instanceof a))return j&&L(26,"constructor call without new",e),new a(e,n);if(null!=n&&H(n,2,64,M,"base")){if(n=0|n,s=e+"",10==n)return f=new a(e instanceof a?e:s),U(f,P+f.e+1,k);if((o="number"==typeof e)&&0*e!=0||!new RegExp("^-?"+(t="["+O.slice(0,n)+"]+")+"(?:\\."+t+")?$",37>n?"i":"").test(s))return g(f,s,o,n);o?(f.s=0>1/e?(s=s.slice(1),-1):1,j&&s.replace(/^0\.0*|\./,"").length>15&&L(M,b,e),o=!1):f.s=45===s.charCodeAt(0)?(s=s.slice(1),-1):1,s=D(s,10,n,f.s)}else{if(e instanceof a)return f.s=e.s,f.e=e.e,f.c=(e=e.c)?e.slice():e,void(M=0);if((o="number"==typeof e)&&0*e==0){if(f.s=0>1/e?(e=-e,-1):1,e===~~e){for(r=0,i=e;i>=10;i/=10,r++);return f.e=r,f.c=[e],void(M=0)}s=e+""}else{if(!p.test(s=e+""))return g(f,s,o);f.s=45===s.charCodeAt(0)?(s=s.slice(1),-1):1}}for((r=s.indexOf("."))>-1&&(s=s.replace(".","")),(i=s.search(/e/i))>0?(0>r&&(r=i),r+=+s.slice(i+1),s=s.substring(0,i)):0>r&&(r=s.length),i=0;48===s.charCodeAt(i);i++);for(u=s.length;48===s.charCodeAt(--u););if(s=s.slice(i,u+1))if(u=s.length,o&&j&&u>15&&L(M,b,f.s*e),r=r-i-1,r>z)f.c=f.e=null;else if(G>r)f.c=[f.e=0];else{if(f.e=r,f.c=[],i=(r+1)%y,0>r&&(i+=y),u>i){for(i&&f.c.push(+s.slice(0,i)),u-=y;u>i;)f.c.push(+s.slice(i,i+=y));s=s.slice(i),i=y-s.length}else i-=u;for(;i--;s+="0");f.c.push(+s)}else f.c=[f.e=0];M=0}function D(e,n,t,i){var o,u,f,c,h,g,p,d=e.indexOf("."),m=P,w=k;for(37>t&&(e=e.toLowerCase()),d>=0&&(f=J,J=0,e=e.replace(".",""),p=new a(t),h=p.pow(e.length-d),J=f,p.c=s(l(r(h.c),h.e),10,n),p.e=p.c.length),g=s(e,t,n),u=f=g.length;0==g[--f];g.pop());if(!g[0])return"0";if(0>d?--u:(h.c=g,h.e=u,h.s=i,h=C(h,p,m,w,n),g=h.c,c=h.r,u=h.e),o=u+m+1,d=g[o],f=n/2,c=c||0>o||null!=g[o+1],c=4>w?(null!=d||c)&&(0==w||w==(h.s<0?3:2)):d>f||d==f&&(4==w||c||6==w&&1&g[o-1]||w==(h.s<0?8:7)),1>o||!g[0])e=c?l("1",-m):"0";else{if(g.length=o,c)for(--n;++g[--o]>n;)g[o]=0,o||(++u,g.unshift(1));for(f=g.length;!g[--f];);for(d=0,e="";f>=d;e+=O.charAt(g[d++]));e=l(e,u)}return e}function _(e,n,t,i){var o,u,s,c,h;if(t=null!=t&&H(t,0,8,i,v)?0|t:k,!e.c)return e.toString();if(o=e.c[0],s=e.e,null==n)h=r(e.c),h=19==i||24==i&&B>=s?f(h,s):l(h,s);else if(e=U(new a(e),n,t),u=e.e,h=r(e.c),c=h.length,19==i||24==i&&(u>=n||B>=u)){for(;n>c;h+="0",c++);h=f(h,u)}else if(n-=s,h=l(h,u),u+1>c){if(--n>0)for(h+=".";n--;h+="0");}else if(n+=u-c,n>0)for(u+1==c&&(h+=".");n--;h+="0");return e.s<0&&o?"-"+h:h}function x(e,n){var t,r,i=0;for(u(e[0])&&(e=e[0]),t=new a(e[0]);++i<e.length;){if(r=new a(e[i]),!r.s){t=r;break}n.call(t,r)&&(t=r)}return t}function F(e,n,t,r,i){return(n>e||e>t||e!=c(e))&&L(r,(i||"decimal places")+(n>e||e>t?" out of range":" not an integer"),e),!0}function I(e,n,t){for(var r=1,i=n.length;!n[--i];n.pop());for(i=n[0];i>=10;i/=10,r++);return(t=r+t*y-1)>z?e.c=e.e=null:G>t?e.c=[e.e=0]:(e.e=t,e.c=n),e}function L(e,n,t){var r=new Error(["new BigNumber","cmp","config","div","divToInt","eq","gt","gte","lt","lte","minus","mod","plus","precision","random","round","shift","times","toDigits","toExponential","toFixed","toFormat","toFraction","pow","toPrecision","toString","BigNumber"][e]+"() "+n+": "+t);throw r.name="BigNumber Error",M=0,r}function U(e,n,t,r){var i,o,u,s,f,l,c,a=e.c,h=R;if(a){e:{for(i=1,s=a[0];s>=10;s/=10,i++);if(o=n-i,0>o)o+=y,u=n,f=a[l=0],c=f/h[i-u-1]%10|0;else if(l=d((o+1)/y),l>=a.length){if(!r)break e;for(;a.length<=l;a.push(0));f=c=0,i=1,o%=y,u=o-y+1}else{for(f=s=a[l],i=1;s>=10;s/=10,i++);o%=y,u=o-y+i,c=0>u?0:f/h[i-u-1]%10|0}if(r=r||0>n||null!=a[l+1]||(0>u?f:f%h[i-u-1]),r=4>t?(c||r)&&(0==t||t==(e.s<0?3:2)):c>5||5==c&&(4==t||r||6==t&&(o>0?u>0?f/h[i-u]:0:a[l-1])%10&1||t==(e.s<0?8:7)),1>n||!a[0])return a.length=0,r?(n-=e.e+1,a[0]=h[n%y],e.e=-n||0):a[0]=e.e=0,e;if(0==o?(a.length=l,s=1,l--):(a.length=l+1,s=h[y-o],a[l]=u>0?m(f/h[i-u]%h[u])*s:0),r)for(;;){if(0==l){for(o=1,u=a[0];u>=10;u/=10,o++);for(u=a[0]+=s,s=1;u>=10;u/=10,s++);o!=s&&(e.e++,a[0]==N&&(a[0]=1));break}if(a[l]+=s,a[l]!=N)break;a[l--]=0,s=1}for(o=a.length;0===a[--o];a.pop());}e.e>z?e.c=e.e=null:e.e<G&&(e.c=[e.e=0])}return e}var C,M=0,T=a.prototype,q=new a(1),P=20,k=4,B=-7,$=21,G=-1e7,z=1e7,j=!0,H=F,V=!1,W=1,J=100,X={decimalSeparator:".",groupSeparator:",",groupSize:3,secondaryGroupSize:0,fractionGroupSeparator:" ",fractionGroupSize:0};return a.another=n,a.ROUND_UP=0,a.ROUND_DOWN=1,a.ROUND_CEIL=2,a.ROUND_FLOOR=3,a.ROUND_HALF_UP=4,a.ROUND_HALF_DOWN=5,a.ROUND_HALF_EVEN=6,a.ROUND_HALF_CEIL=7,a.ROUND_HALF_FLOOR=8,a.EUCLID=9,a.config=function(){var e,n,t=0,r={},i=arguments,s=i[0],f=s&&"object"==typeof s?function(){return s.hasOwnProperty(n)?null!=(e=s[n]):void 0}:function(){return i.length>t?null!=(e=i[t++]):void 0};return f(n="DECIMAL_PLACES")&&H(e,0,E,2,n)&&(P=0|e),r[n]=P,f(n="ROUNDING_MODE")&&H(e,0,8,2,n)&&(k=0|e),r[n]=k,f(n="EXPONENTIAL_AT")&&(u(e)?H(e[0],-E,0,2,n)&&H(e[1],0,E,2,n)&&(B=0|e[0],$=0|e[1]):H(e,-E,E,2,n)&&(B=-($=0|(0>e?-e:e)))),r[n]=[B,$],f(n="RANGE")&&(u(e)?H(e[0],-E,-1,2,n)&&H(e[1],1,E,2,n)&&(G=0|e[0],z=0|e[1]):H(e,-E,E,2,n)&&(0|e?G=-(z=0|(0>e?-e:e)):j&&L(2,n+" cannot be zero",e))),r[n]=[G,z],f(n="ERRORS")&&(e===!!e||1===e||0===e?(M=0,H=(j=!!e)?F:o):j&&L(2,n+w,e)),r[n]=j,f(n="CRYPTO")&&(e===!!e||1===e||0===e?(V=!(!e||!h||"object"!=typeof h),e&&!V&&j&&L(2,"crypto unavailable",h)):j&&L(2,n+w,e)),r[n]=V,f(n="MODULO_MODE")&&H(e,0,9,2,n)&&(W=0|e),r[n]=W,f(n="POW_PRECISION")&&H(e,0,E,2,n)&&(J=0|e),r[n]=J,f(n="FORMAT")&&("object"==typeof e?X=e:j&&L(2,n+" not an object",e)),r[n]=X,r},a.max=function(){return x(arguments,T.lt)},a.min=function(){return x(arguments,T.gt)},a.random=function(){var e=9007199254740992,n=Math.random()*e&2097151?function(){return m(Math.random()*e)}:function(){return 8388608*(1073741824*Math.random()|0)+(8388608*Math.random()|0)};return function(e){var t,r,i,o,u,s=0,f=[],l=new a(q);if(e=null!=e&&H(e,0,E,14)?0|e:P,o=d(e/y),V)if(h&&h.getRandomValues){for(t=h.getRandomValues(new Uint32Array(o*=2));o>s;)u=131072*t[s]+(t[s+1]>>>11),u>=9e15?(r=h.getRandomValues(new Uint32Array(2)),t[s]=r[0],t[s+1]=r[1]):(f.push(u%1e14),s+=2);s=o/2}else if(h&&h.randomBytes){for(t=h.randomBytes(o*=7);o>s;)u=281474976710656*(31&t[s])+1099511627776*t[s+1]+4294967296*t[s+2]+16777216*t[s+3]+(t[s+4]<<16)+(t[s+5]<<8)+t[s+6],u>=9e15?h.randomBytes(7).copy(t,s):(f.push(u%1e14),s+=7);s=o/7}else j&&L(14,"crypto unavailable",h);if(!s)for(;o>s;)u=n(),9e15>u&&(f[s++]=u%1e14);for(o=f[--s],e%=y,o&&e&&(u=R[y-e],f[s]=m(o/u)*u);0===f[s];f.pop(),s--);if(0>s)f=[i=0];else{for(i=-1;0===f[0];f.shift(),i-=y);for(s=1,u=f[0];u>=10;u/=10,s++);y>s&&(i-=y-s)}return l.e=i,l.c=f,l}}(),C=function(){function e(e,n,t){var r,i,o,u,s=0,f=e.length,l=n%A,c=n/A|0;for(e=e.slice();f--;)o=e[f]%A,u=e[f]/A|0,r=c*o+u*l,i=l*o+r%A*A+s,s=(i/t|0)+(r/A|0)+c*u,e[f]=i%t;return s&&e.unshift(s),e}function n(e,n,t,r){var i,o;if(t!=r)o=t>r?1:-1;else for(i=o=0;t>i;i++)if(e[i]!=n[i]){o=e[i]>n[i]?1:-1;break}return o}function r(e,n,t,r){for(var i=0;t--;)e[t]-=i,i=e[t]<n[t]?1:0,e[t]=i*r+e[t]-n[t];for(;!e[0]&&e.length>1;e.shift());}return function(i,o,u,s,f){var l,c,h,g,p,d,w,v,b,O,S,R,A,E,D,_,x,F=i.s==o.s?1:-1,I=i.c,L=o.c;if(!(I&&I[0]&&L&&L[0]))return new a(i.s&&o.s&&(I?!L||I[0]!=L[0]:L)?I&&0==I[0]||!L?0*F:F/0:0/0);for(v=new a(F),b=v.c=[],c=i.e-o.e,F=u+c+1,f||(f=N,c=t(i.e/y)-t(o.e/y),F=F/y|0),h=0;L[h]==(I[h]||0);h++);if(L[h]>(I[h]||0)&&c--,0>F)b.push(1),g=!0;else{for(E=I.length,_=L.length,h=0,F+=2,p=m(f/(L[0]+1)),p>1&&(L=e(L,p,f),I=e(I,p,f),_=L.length,E=I.length),A=_,O=I.slice(0,_),S=O.length;_>S;O[S++]=0);x=L.slice(),x.unshift(0),D=L[0],L[1]>=f/2&&D++;do p=0,l=n(L,O,_,S),0>l?(R=O[0],_!=S&&(R=R*f+(O[1]||0)),p=m(R/D),p>1?(p>=f&&(p=f-1),d=e(L,p,f),w=d.length,S=O.length,l=n(d,O,w,S),1==l&&(p--,r(d,w>_?x:L,w,f))):(0==p&&(l=p=1),d=L.slice()),w=d.length,S>w&&d.unshift(0),r(O,d,S,f),-1==l&&(S=O.length,l=n(L,O,_,S),1>l&&(p++,r(O,S>_?x:L,S,f))),S=O.length):0===l&&(p++,O=[0]),b[h++]=p,l&&O[0]?O[S++]=I[A]||0:(O=[I[A]],S=1);while((A++<E||null!=O[0])&&F--);g=null!=O[0],b[0]||b.shift()}if(f==N){for(h=1,F=b[0];F>=10;F/=10,h++);U(v,u+(v.e=h+c*y-1)+1,s,g)}else v.e=c,v.r=+g;return v}}(),g=function(){var e=/^(-?)0([xbo])(\w[\w.]*$)/i,n=/^([^.]+)\.$/,t=/^\.([^.]+)$/,r=/^-?(Infinity|NaN)$/,i=/^\s*\+([\w.])|^\s+|\s+$/g;return function(o,u,s,f){var l,c=s?u:u.replace(i,"$1");if(r.test(c))o.s=isNaN(c)?null:0>c?-1:1;else{if(!s&&(c=c.replace(e,function(e,n,t){return l="x"==(t=t.toLowerCase())?16:"b"==t?2:8,f&&f!=l?e:n}),f&&(l=f,c=c.replace(n,"$1").replace(t,"0.$1")),u!=c))return new a(c,l);j&&L(M,"not a"+(f?" base "+f:"")+" number",u),o.s=null}o.c=o.e=null,M=0}}(),T.absoluteValue=T.abs=function(){var e=new a(this);return e.s<0&&(e.s=1),e},T.ceil=function(){return U(new a(this),this.e+1,2)},T.comparedTo=T.cmp=function(e,n){return M=1,i(this,new a(e,n))},T.decimalPlaces=T.dp=function(){var e,n,r=this.c;if(!r)return null;if(e=((n=r.length-1)-t(this.e/y))*y,n=r[n])for(;n%10==0;n/=10,e--);return 0>e&&(e=0),e},T.dividedBy=T.div=function(e,n){return M=3,C(this,new a(e,n),P,k)},T.dividedToIntegerBy=T.divToInt=function(e,n){return M=4,C(this,new a(e,n),0,1)},T.equals=T.eq=function(e,n){return M=5,0===i(this,new a(e,n))},T.floor=function(){return U(new a(this),this.e+1,3)},T.greaterThan=T.gt=function(e,n){return M=6,i(this,new a(e,n))>0},T.greaterThanOrEqualTo=T.gte=function(e,n){return M=7,1===(n=i(this,new a(e,n)))||0===n},T.isFinite=function(){return!!this.c},T.isInteger=T.isInt=function(){return!!this.c&&t(this.e/y)>this.c.length-2},T.isNaN=function(){return!this.s},T.isNegative=T.isNeg=function(){return this.s<0},T.isZero=function(){return!!this.c&&0==this.c[0]},T.lessThan=T.lt=function(e,n){return M=8,i(this,new a(e,n))<0},T.lessThanOrEqualTo=T.lte=function(e,n){return M=9,-1===(n=i(this,new a(e,n)))||0===n},T.minus=T.sub=function(e,n){var r,i,o,u,s=this,f=s.s;if(M=10,e=new a(e,n),n=e.s,!f||!n)return new a(0/0);if(f!=n)return e.s=-n,s.plus(e);var l=s.e/y,c=e.e/y,h=s.c,g=e.c;if(!l||!c){if(!h||!g)return h?(e.s=-n,e):new a(g?s:0/0);if(!h[0]||!g[0])return g[0]?(e.s=-n,e):new a(h[0]?s:3==k?-0:0)}if(l=t(l),c=t(c),h=h.slice(),f=l-c){for((u=0>f)?(f=-f,o=h):(c=l,o=g),o.reverse(),n=f;n--;o.push(0));o.reverse()}else for(i=(u=(f=h.length)<(n=g.length))?f:n,f=n=0;i>n;n++)if(h[n]!=g[n]){u=h[n]<g[n];break}if(u&&(o=h,h=g,g=o,e.s=-e.s),n=(i=g.length)-(r=h.length),n>0)for(;n--;h[r++]=0);for(n=N-1;i>f;){if(h[--i]<g[i]){for(r=i;r&&!h[--r];h[r]=n);--h[r],h[i]+=N}h[i]-=g[i]}for(;0==h[0];h.shift(),--c);return h[0]?I(e,h,c):(e.s=3==k?-1:1,e.c=[e.e=0],e)},T.modulo=T.mod=function(e,n){var t,r,i=this;return M=11,e=new a(e,n),!i.c||!e.s||e.c&&!e.c[0]?new a(0/0):!e.c||i.c&&!i.c[0]?new a(i):(9==W?(r=e.s,e.s=1,t=C(i,e,0,3),e.s=r,t.s*=r):t=C(i,e,0,W),i.minus(t.times(e)))},T.negated=T.neg=function(){var e=new a(this);return e.s=-e.s||null,e},T.plus=T.add=function(e,n){var r,i=this,o=i.s;if(M=12,e=new a(e,n),n=e.s,!o||!n)return new a(0/0);if(o!=n)return e.s=-n,i.minus(e);var u=i.e/y,s=e.e/y,f=i.c,l=e.c;if(!u||!s){if(!f||!l)return new a(o/0);if(!f[0]||!l[0])return l[0]?e:new a(f[0]?i:0*o)}if(u=t(u),s=t(s),f=f.slice(),o=u-s){for(o>0?(s=u,r=l):(o=-o,r=f),r.reverse();o--;r.push(0));r.reverse()}for(o=f.length,n=l.length,0>o-n&&(r=l,l=f,f=r,n=o),o=0;n;)o=(f[--n]=f[n]+l[n]+o)/N|0,f[n]%=N;return o&&(f.unshift(o),++s),I(e,f,s)},T.precision=T.sd=function(e){var n,t,r=this,i=r.c;if(null!=e&&e!==!!e&&1!==e&&0!==e&&(j&&L(13,"argument"+w,e),e!=!!e&&(e=null)),!i)return null;if(t=i.length-1,n=t*y+1,t=i[t]){for(;t%10==0;t/=10,n--);for(t=i[0];t>=10;t/=10,n++);}return e&&r.e+1>n&&(n=r.e+1),n},T.round=function(e,n){var t=new a(this);return(null==e||H(e,0,E,15))&&U(t,~~e+this.e+1,null!=n&&H(n,0,8,15,v)?0|n:k),t},T.shift=function(e){var n=this;return H(e,-S,S,16,"argument")?n.times("1e"+c(e)):new a(n.c&&n.c[0]&&(-S>e||e>S)?n.s*(0>e?0:1/0):n)},T.squareRoot=T.sqrt=function(){var e,n,i,o,u,s=this,f=s.c,l=s.s,c=s.e,h=P+4,g=new a("0.5");if(1!==l||!f||!f[0])return new a(!l||0>l&&(!f||f[0])?0/0:f?s:1/0);if(l=Math.sqrt(+s),0==l||l==1/0?(n=r(f),(n.length+c)%2==0&&(n+="0"),l=Math.sqrt(n),c=t((c+1)/2)-(0>c||c%2),l==1/0?n="1e"+c:(n=l.toExponential(),n=n.slice(0,n.indexOf("e")+1)+c),i=new a(n)):i=new a(l+""),i.c[0])for(c=i.e,l=c+h,3>l&&(l=0);;)if(u=i,i=g.times(u.plus(C(s,u,h,1))),r(u.c).slice(0,l)===(n=r(i.c)).slice(0,l)){if(i.e<c&&--l,n=n.slice(l-3,l+1),"9999"!=n&&(o||"4999"!=n)){(!+n||!+n.slice(1)&&"5"==n.charAt(0))&&(U(i,i.e+P+2,1),e=!i.times(i).eq(s));break}if(!o&&(U(u,u.e+P+2,0),u.times(u).eq(s))){i=u;break}h+=4,l+=4,o=1}return U(i,i.e+P+1,k,e)},T.times=T.mul=function(e,n){var r,i,o,u,s,f,l,c,h,g,p,d,m,w,v,b=this,O=b.c,S=(M=17,e=new a(e,n)).c;if(!(O&&S&&O[0]&&S[0]))return!b.s||!e.s||O&&!O[0]&&!S||S&&!S[0]&&!O?e.c=e.e=e.s=null:(e.s*=b.s,O&&S?(e.c=[0],e.e=0):e.c=e.e=null),e;for(i=t(b.e/y)+t(e.e/y),e.s*=b.s,l=O.length,g=S.length,g>l&&(m=O,O=S,S=m,o=l,l=g,g=o),o=l+g,m=[];o--;m.push(0));for(w=N,v=A,o=g;--o>=0;){for(r=0,p=S[o]%v,d=S[o]/v|0,s=l,u=o+s;u>o;)c=O[--s]%v,h=O[s]/v|0,f=d*c+h*p,c=p*c+f%v*v+m[u]+r,r=(c/w|0)+(f/v|0)+d*h,m[u--]=c%w;m[u]=r}return r?++i:m.shift(),I(e,m,i)},T.toDigits=function(e,n){var t=new a(this);return e=null!=e&&H(e,1,E,18,"precision")?0|e:null,n=null!=n&&H(n,0,8,18,v)?0|n:k,e?U(t,e,n):t},T.toExponential=function(e,n){return _(this,null!=e&&H(e,0,E,19)?~~e+1:null,n,19)},T.toFixed=function(e,n){return _(this,null!=e&&H(e,0,E,20)?~~e+this.e+1:null,n,20)},T.toFormat=function(e,n){var t=_(this,null!=e&&H(e,0,E,21)?~~e+this.e+1:null,n,21);if(this.c){var r,i=t.split("."),o=+X.groupSize,u=+X.secondaryGroupSize,s=X.groupSeparator,f=i[0],l=i[1],c=this.s<0,a=c?f.slice(1):f,h=a.length;if(u&&(r=o,o=u,u=r,h-=r),o>0&&h>0){for(r=h%o||o,f=a.substr(0,r);h>r;r+=o)f+=s+a.substr(r,o);u>0&&(f+=s+a.slice(r)),c&&(f="-"+f)}t=l?f+X.decimalSeparator+((u=+X.fractionGroupSize)?l.replace(new RegExp("\\d{"+u+"}\\B","g"),"$&"+X.fractionGroupSeparator):l):f}return t},T.toFraction=function(e){var n,t,i,o,u,s,f,l,c,h=j,g=this,p=g.c,d=new a(q),m=t=new a(q),w=f=new a(q);if(null!=e&&(j=!1,s=new a(e),j=h,(!(h=s.isInt())||s.lt(q))&&(j&&L(22,"max denominator "+(h?"out of range":"not an integer"),e),e=!h&&s.c&&U(s,s.e+1,1).gte(q)?s:null)),!p)return g.toString();for(c=r(p),o=d.e=c.length-g.e-1,d.c[0]=R[(u=o%y)<0?y+u:u],e=!e||s.cmp(d)>0?o>0?d:m:s,u=z,z=1/0,s=new a(c),f.c[0]=0;l=C(s,d,0,1),i=t.plus(l.times(w)),1!=i.cmp(e);)t=w,w=i,m=f.plus(l.times(i=m)),f=i,d=s.minus(l.times(i=d)),s=i;return i=C(e.minus(t),w,0,1),f=f.plus(i.times(m)),t=t.plus(i.times(w)),f.s=m.s=g.s,o*=2,n=C(m,w,o,k).minus(g).abs().cmp(C(f,t,o,k).minus(g).abs())<1?[m.toString(),w.toString()]:[f.toString(),t.toString()],z=u,n},T.toNumber=function(){var e=this;return+e||(e.s?0*e.s:0/0)},T.toPower=T.pow=function(e){var n,t,r=m(0>e?-e:+e),i=this;if(!H(e,-S,S,23,"exponent")&&(!isFinite(e)||r>S&&(e/=0)||parseFloat(e)!=e&&!(e=0/0)))return new a(Math.pow(+i,e));for(n=J?d(J/y+2):0,t=new a(q);;){if(r%2){if(t=t.times(i),!t.c)break;n&&t.c.length>n&&(t.c.length=n)}if(r=m(r/2),!r)break;i=i.times(i),n&&i.c&&i.c.length>n&&(i.c.length=n)}return 0>e&&(t=q.div(t)),n?U(t,J,k):t},T.toPrecision=function(e,n){return _(this,null!=e&&H(e,1,E,24,"precision")?0|e:null,n,24)},T.toString=function(e){var n,t=this,i=t.s,o=t.e;return null===o?i?(n="Infinity",0>i&&(n="-"+n)):n="NaN":(n=r(t.c),n=null!=e&&H(e,2,64,25,"base")?D(l(n,o),0|e,10,i):B>=o||o>=$?f(n,o):l(n,o),0>i&&t.c[0]&&(n="-"+n)),n},T.truncated=T.trunc=function(){return U(new a(this),this.e+1,1)},T.valueOf=T.toJSON=function(){return this.toString()},null!=e&&a.config(e),a}function t(e){var n=0|e;return e>0||e===n?n:n-1}function r(e){for(var n,t,r=1,i=e.length,o=e[0]+"";i>r;){for(n=e[r++]+"",t=y-n.length;t--;n="0"+n);o+=n}for(i=o.length;48===o.charCodeAt(--i););return o.slice(0,i+1||1)}function i(e,n){var t,r,i=e.c,o=n.c,u=e.s,s=n.s,f=e.e,l=n.e;if(!u||!s)return null;if(t=i&&!i[0],r=o&&!o[0],t||r)return t?r?0:-s:u;if(u!=s)return u;if(t=0>u,r=f==l,!i||!o)return r?0:!i^t?1:-1;if(!r)return f>l^t?1:-1;for(s=(f=i.length)<(l=o.length)?f:l,u=0;s>u;u++)if(i[u]!=o[u])return i[u]>o[u]^t?1:-1;return f==l?0:f>l^t?1:-1}function o(e,n,t){return(e=c(e))>=n&&t>=e}function u(e){return"[object Array]"==Object.prototype.toString.call(e)}function s(e,n,t){for(var r,i,o=[0],u=0,s=e.length;s>u;){for(i=o.length;i--;o[i]*=n);for(o[r=0]+=O.indexOf(e.charAt(u++));r<o.length;r++)o[r]>t-1&&(null==o[r+1]&&(o[r+1]=0),o[r+1]+=o[r]/t|0,o[r]%=t)}return o.reverse()}function f(e,n){return(e.length>1?e.charAt(0)+"."+e.slice(1):e)+(0>n?"e":"e+")+n}function l(e,n){var t,r;if(0>n){for(r="0.";++n;r+="0");e=r+e}else if(t=e.length,++n>t){for(r="0",n-=t;--n;r+="0");e+=r}else t>n&&(e=e.slice(0,n)+"."+e.slice(n));return e}function c(e){return e=parseFloat(e),0>e?d(e):m(e)}var a,h,g,p=/^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,d=Math.ceil,m=Math.floor,w=" not a boolean or binary digit",v="rounding mode",b="number type has more than 15 significant digits",O="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_",N=1e14,y=14,S=9007199254740991,R=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13],A=1e7,E=1e9;if(a=n(),"function"==typeof define&&define.amd)define(function(){return a});else if("undefined"!=typeof module&&module.exports){if(module.exports=a,!h)try{h=require("crypto")}catch(D){}}else e.BigNumber=a}(this);
+//# sourceMappingURL=doc/bignumber.js.map
diff --git a/signer/rules/deps/bindata.go b/signer/rules/deps/bindata.go
new file mode 100644
index 000000000..0b27f4517
--- /dev/null
+++ b/signer/rules/deps/bindata.go
@@ -0,0 +1,235 @@
+// Code generated by go-bindata.
+// sources:
+// bignumber.js
+// DO NOT EDIT!
+
+package deps
+
+import (
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func bindataRead(data []byte, name string) ([]byte, error) {
+ gz, err := gzip.NewReader(bytes.NewBuffer(data))
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, gz)
+ clErr := gz.Close()
+
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+ if clErr != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+type asset struct {
+ bytes []byte
+ info os.FileInfo
+}
+
+type bindataFileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+}
+
+func (fi bindataFileInfo) Name() string {
+ return fi.name
+}
+func (fi bindataFileInfo) Size() int64 {
+ return fi.size
+}
+func (fi bindataFileInfo) Mode() os.FileMode {
+ return fi.mode
+}
+func (fi bindataFileInfo) ModTime() time.Time {
+ return fi.modTime
+}
+func (fi bindataFileInfo) IsDir() bool {
+ return false
+}
+func (fi bindataFileInfo) Sys() interface{} {
+ return nil
+}
+
+var _bignumberJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\xbc\x6b\x77\x9b\xc8\x93\x38\xfc\x7e\x3f\x85\xc4\xc6\x9c\x6e\x53\x20\x90\x9d\x38\x86\x14\x9c\x4c\x62\xe7\xe7\x79\x1c\x3b\x4f\x9c\xcc\xcc\xae\xa2\xc9\x91\x51\x23\x75\x82\x40\xe1\x62\xc7\x09\xfe\x7d\xf6\xff\xa9\x6e\x40\xf2\x25\xbb\xb3\x6f\x2c\xe8\x4b\x75\x75\x75\xdd\xbb\xf0\x68\x77\x70\x29\x17\x59\xbd\xba\x14\x85\xf3\xa5\x1c\x5c\x8d\x1d\xd7\xd9\x1b\x2c\xab\x6a\x5d\xfa\xa3\xd1\x42\x56\xcb\xfa\xd2\x89\xf3\xd5\xe8\xad\xfc\x2a\xde\xc6\xe9\x68\x7b\xf8\xe8\xf4\xe4\xd5\xd1\xd9\xab\xa3\xc1\xee\xe8\x3f\x46\xbb\x83\x55\x3e\x97\x89\x14\xf3\xc1\xe5\xcd\xe0\x87\x48\xe5\x62\x50\xe5\x83\x44\x7e\x7f\x0c\x5c\x91\x5f\x8a\xa2\xfa\x5a\xc8\x95\xc8\x46\x79\x55\xe5\xff\x59\x88\x45\x9d\xce\x0a\x5b\x7c\x5f\x17\xa2\x2c\x65\x9e\xd9\x32\x8b\xf3\xd5\x7a\x56\xc9\x4b\x99\xca\xea\x86\x96\x19\x26\x75\x16\x57\x32\xcf\x98\xe0\x3f\x8d\xba\x14\x83\xb2\x2a\x64\x5c\x19\x41\xd7\x31\x50\x5d\xfd\xdb\x8c\x09\xc8\xf8\xcf\xab\x59\x31\xa8\xa0\x00\x09\x39\xd4\x50\x42\x82\xd5\x52\x96\x81\x4c\xd8\x90\x25\x03\x99\x95\xd5\x2c\x8b\x45\x9e\x0c\x66\x9c\x17\xa2\xaa\x8b\x6c\xf0\xc5\x34\x4f\xd9\xf8\x19\x18\x71\x9e\x95\x55\x51\xc7\x55\x5e\x0c\xe2\x59\x9a\x0e\xae\x65\xb5\xcc\xeb\x6a\x90\x89\x6b\x03\x04\x87\x4c\x5c\xb7\xeb\x10\xc0\xac\x4e\xd3\x21\x66\xa6\xf9\x2f\x96\xc1\x18\x9e\xed\xc3\x5b\x30\x2e\x67\xa5\x30\x38\xff\x49\xfd\xe8\x36\x19\x94\x28\x2c\xc3\x00\xcf\x45\xcc\xba\x15\x13\x6c\x21\xdd\x41\x28\x12\x7e\xc9\xe1\x23\x4b\xe0\x9d\x95\x38\xc2\xf2\xe0\xab\x5a\x87\xe5\x68\xe8\xa3\x30\x10\xab\x9b\x35\x0d\x16\xdc\x34\xdd\x5d\x31\x44\xb7\x69\x86\x04\xec\xbd\x58\x1c\x7d\x5f\x33\xe3\x6f\x3b\x32\x2c\x56\xa1\x31\x31\xac\x73\xa7\x4c\x65\x2c\x98\x0b\x19\xb7\x8c\xa9\x65\x70\xcb\x60\x91\xff\xe9\x93\x63\x58\x95\x65\xf0\xe8\x89\x01\x7b\x07\x61\x16\x19\xd2\xf0\x0d\x83\x3b\x95\x28\x2b\x56\xf6\x84\x59\xb0\x04\x4a\xc8\x69\xbb\x79\xc4\x12\xa7\x44\x37\xf4\x46\x22\x62\x25\x96\x2d\x68\x8f\x83\xed\x71\xdf\x83\x2f\xa6\x59\x3a\x85\x58\xa7\xb3\x58\xb0\xd1\xdf\xee\x27\xc7\xdd\x6d\x3e\x39\x23\x20\xb8\xa9\xc8\x16\xd5\x32\xf4\x9e\x12\xa5\xdf\xc2\x25\xd1\x32\xc7\xa1\xc7\x7d\x02\xba\xff\x14\x11\x4b\x27\x5e\xce\x8a\x57\xf9\x5c\xbc\xac\x98\xcb\x1f\x5d\xa3\xc4\xd7\xac\x04\xcf\x85\x0c\x12\xa7\xe4\xb7\x22\x2d\x05\x11\xfa\x2e\x19\x7b\x22\x3b\x25\x0a\xa7\x84\xc4\x11\x28\x1c\x01\x89\x13\x23\xa3\xc7\x98\x47\xa2\x05\xcd\x7d\x01\x57\xb9\x9c\xb3\xb7\xe8\xfe\x6f\xb4\x46\x74\xd5\xb1\x6e\xd1\x41\xa0\x2d\x5a\xdc\x04\x22\xfe\xfb\xdf\xc4\x90\x79\xc1\x0a\x74\x41\xa2\x08\x64\x88\x9e\x1b\xc8\x11\x7a\x2e\x14\x96\xc5\x83\x1e\x35\x81\x85\x42\x68\x22\xa6\x1b\x04\x6e\x35\xaf\xf4\xfb\x1a\xae\xdb\x13\x51\xcd\xf7\x8f\x85\x07\xff\x17\xe2\xdd\xde\x12\x62\xac\xc0\xd2\x91\xd9\x5c\x7c\x3f\x4f\x98\xe1\x18\x9c\x87\xb6\x67\x9a\x6a\x7c\x77\x78\x86\x63\xd0\xa1\x71\x60\x92\xa0\x88\x59\x11\x2f\xd9\x48\x8c\x24\xe7\xa1\x1b\x31\x37\x2c\x4c\x93\x15\x28\x39\x14\x16\x5a\xdd\x3a\xd2\xf2\x38\xa8\x65\xeb\x4b\x92\xd4\x6c\xc1\x5c\x90\x9c\xfb\xdd\xf8\xb2\xe5\x02\x0e\x12\xdd\x60\xff\xf9\x7d\xb4\x25\x0f\x24\x91\x88\xd0\xac\xfb\xd1\x8f\x0c\xb4\xed\x9a\x07\xea\xb0\x36\xbb\x94\x50\x5b\x1e\xe7\x32\xd9\x9a\x0a\xb9\x69\x7e\x31\xcd\x7a\x8b\xed\x12\xa7\xdc\x15\x1c\x0a\x2c\x6c\x69\x7b\x50\x84\x3f\x38\x1d\x02\x1d\x07\x09\x73\x40\x84\x1f\xc8\x84\xbd\x09\x0b\xd5\x31\xa1\x1e\x77\x1a\x74\x07\xb2\x75\x6e\x53\x90\xc8\x0a\xcb\xe3\x3b\x37\xa0\xb7\x28\x2d\xbc\xe1\x50\x87\x52\xf3\x80\x34\xcd\xc4\x89\x9d\x75\x5d\x2e\x59\x4f\x25\x45\x12\xa8\x6d\xbc\x09\xea\x50\x06\xfc\xe1\x08\x09\x0a\x0e\x0f\xb6\x36\x47\x24\xbb\xb1\xbb\x7d\xdd\x6a\x2c\x6d\xac\x15\xad\x02\x69\xdb\x41\x69\xa1\xe1\x1a\xc4\x11\x3d\x3c\x2d\x1e\x83\xed\x6d\xbc\x45\xf7\xb6\xd7\x97\xaf\x49\x8f\x41\x05\x52\xeb\x4c\xd2\x96\x09\xc4\xb0\x84\x05\xac\x61\x8e\xe2\x0e\x9b\xc0\x0a\xdf\xc1\x35\x7e\x55\x2b\xee\x1d\x84\x95\x69\x2a\x51\xaa\xf2\xd3\xfc\x5a\x14\xaf\x66\xa5\x60\x9c\xc3\x3c\x44\xd7\x34\x59\x82\xbf\xc3\xef\xe8\x02\x8d\xb8\xc7\x55\xb0\x6e\x55\x5f\xc5\x61\x89\x6b\x67\x9d\x5f\x33\xd1\x6e\xcc\x9e\x73\xf8\x1d\x13\x58\x3b\x31\x96\x2c\x65\x05\x5b\x3a\x31\x87\xa5\x23\xb8\x12\x7a\x0e\x6b\x47\xe0\xda\x89\x7b\x4e\x5a\x60\xc9\x04\x54\xd4\x55\x63\x82\x8b\x8e\x69\x5c\xc4\xc5\xc4\xb6\x93\x69\xb0\x70\xd6\xf9\x9a\x71\xc5\x2e\xc3\xc5\xc4\x9d\xb6\x42\x64\xb8\x06\x35\xb9\xe1\x3c\xb2\xed\xda\xa7\x95\x70\x41\x4b\x61\x0d\x4b\xa7\x44\x09\x4b\x7c\xc5\x96\xb0\x86\x15\x5c\x13\xfc\x05\x2e\x9d\x18\x62\x5c\x3a\x05\xd4\xa8\x70\xca\xb1\xb6\x56\x96\x07\x73\x5c\x4c\xf2\x29\x24\x98\x8d\xc6\x10\x63\xdc\x34\x6e\x98\x37\x8d\x36\x0f\x8b\x49\x6e\x79\x53\x88\x71\x3f\xbc\x8e\x5a\x93\x31\x6f\x9a\x98\x9b\x26\x73\x11\xaf\x9b\xe6\x1a\x91\x2d\x9d\xf2\x85\x1b\xed\xf9\x63\xce\xfd\x79\x98\x34\xcd\x1c\x31\x31\x4d\xb6\xaf\x46\xc4\x4d\xf3\x0c\xf1\xda\x34\x3d\x73\x31\xc9\x6d\x6f\xba\x3d\xe9\xb9\x7f\xc0\x39\x78\xb4\xa2\xde\xa0\xc0\x38\x4a\x99\xe1\x19\x60\xaf\xb8\x4f\x1b\xed\xd8\xb7\xa3\x0f\xe6\x10\x73\x3a\x49\xdb\xce\x02\xcb\x22\x52\xe5\xd3\x30\x0b\x38\xed\x03\x5d\xc8\x9b\x86\x59\x56\x0d\x0b\xa7\xce\xca\xa5\x4c\x2a\xe6\x71\x2d\x98\x5b\x34\x1e\xb6\x14\xd6\x1d\x73\x75\xdc\x86\x11\x24\x21\xce\x03\x61\xe1\xb9\x12\xd9\x97\x15\x5b\x4c\xe6\x96\x35\xe5\x3c\x10\x98\x32\x01\x35\xbf\x6d\xd5\x98\xd8\xf0\xe2\xe7\x87\xbc\x58\x12\x2f\xd2\x11\x55\xa8\x89\x56\x91\x9d\xad\xc0\x85\xe7\x20\xe1\x8a\x47\x6e\x53\xf9\x5f\x61\x48\xea\xbc\x03\xe8\x54\xf9\x85\x56\x3d\xea\xbc\x73\xd2\xf5\x13\x77\x4a\x26\xd8\x11\x40\x60\xc8\x06\x2f\xb1\x60\x42\x31\x16\x7a\x87\x88\xb2\x69\xc6\xfb\x88\xd2\x34\x7f\x0b\xb1\x8c\x12\xb6\x84\x92\xfb\xa9\xfa\xe9\x15\x82\xc0\x8f\xac\x35\xd9\x9c\x30\x25\x7e\x23\x98\x3d\x2c\x62\x8c\x56\xed\xdc\x05\xca\xea\x10\xb3\xa6\xf9\x2d\xc4\x9a\x6b\xc5\x10\x64\x61\x1c\x2c\x95\xc0\x42\x4c\x1a\x6f\x89\xb4\x68\xdd\x0a\x2c\x39\x0e\x36\x96\xb0\xc4\x54\xb5\x92\x66\x0b\x63\x65\x79\x6c\x3b\x0b\x5d\x75\x70\x34\xdd\x31\x82\xcc\xb6\x5b\x48\x3c\xd8\xcc\xb6\xb0\xb6\x63\xe8\x86\xd6\x96\x87\x18\x9b\x66\x3b\x87\xdf\x99\xd4\x53\xae\x7c\xe1\x9a\x66\x1e\x19\xb6\x61\x2d\xfd\xe5\xe6\x64\xbe\xdf\xf3\xaa\xd0\xd5\x0a\x9a\x09\x62\x35\xad\x05\xe8\x09\xaa\xce\xa5\xa1\xb7\xc0\xb2\xe4\x8b\x4e\xac\x03\x85\x7b\xd1\xf7\xcb\x29\x87\x61\xe1\x94\xfc\x67\x85\x45\x70\x59\x88\xd9\xd7\xdb\xcc\x21\x7f\x8b\x55\x50\x10\xcc\x0a\x8b\x9e\x4b\xaa\x0d\x2e\xc7\x2d\x97\x14\xc4\x27\xba\x9b\x65\xa1\x68\x1a\x11\x56\x4d\x23\x86\x18\x33\xc1\x39\xe9\xfa\x02\x98\x6c\x1a\x63\x2e\x62\xb9\x9a\xa5\x03\xa5\x81\x4a\x83\x5b\xfd\xf0\xc8\x18\x90\x5f\x97\x27\x83\x62\x96\x2d\x84\xe1\x1b\x83\x2c\xaf\x06\xb3\x6c\x20\xb3\x4a\x2c\x44\x61\x70\xf2\x51\x86\x5b\xfa\xf2\x44\xaf\xae\xcf\x90\xe8\x51\xa0\x07\x12\xb3\x5e\x1e\xb2\x89\x6d\xcb\x69\x90\x75\x1a\x47\x19\x01\xcc\x26\xee\xf4\x57\x7e\x00\x6d\xd4\xaa\x76\x6f\x6c\x8f\x87\x3f\x22\xe1\xc4\xc4\x53\x8a\xdd\xfd\x37\x61\xa5\x1a\x26\x42\xa9\x6e\x9f\xd1\x6f\x05\xd4\x94\x71\xd8\x12\x9d\xd3\x0e\x2d\x8d\x12\x11\xf9\xa8\x28\xf2\x82\x4d\x0c\x7a\xfe\x4d\x2e\xce\xb4\x3b\x03\x46\xbc\x5a\x1b\xca\xc9\x4d\xe4\xc2\x00\x63\x2e\xaf\xf4\xdf\x0f\xf9\x49\x56\x19\x60\x88\x6f\x06\x18\x8b\x4a\xfd\x11\x06\x18\x69\xa5\xfe\xd0\xe3\x4a\x66\x75\x49\xbf\xf9\xdc\x00\x63\x9d\xaa\x97\x75\x21\x62\x49\xfe\xbb\x01\x46\x31\xcb\xe6\xf9\x8a\x1e\xf2\x3a\xa3\x31\x4a\x6f\x18\x60\x54\x72\x25\x68\x70\x95\xbf\x96\x0b\x59\xe9\xc7\xa3\xef\xeb\x3c\x13\x59\x25\x67\xa9\x7a\x3f\x96\xdf\xc5\x5c\x3f\xe5\xc5\x6a\x56\xe9\xc7\x62\xa6\xb6\x48\x2b\xe5\xd7\xaa\xe9\xdd\xd6\x8a\x9d\xac\x1b\x60\x6c\x36\x39\x9d\x88\xa9\x65\x30\x3e\x30\xac\xcc\x32\xfc\x81\x61\x55\x3c\xa8\x96\x45\x7e\x3d\x28\x9c\x6c\xb6\x12\xb8\x19\xac\xe9\x64\xc0\x5b\x74\xa1\xd8\x10\xf4\x63\xc7\x65\x9a\xa4\x7d\x1c\x01\x29\xc4\x30\x23\x95\x02\x4b\x7c\x4f\xfa\x65\xc6\x7f\x0a\x5f\xdb\x7a\x24\xe7\x74\x46\x47\x5d\xaa\xa3\x2e\xd5\x51\x2b\x7f\x46\x29\xa2\xcc\x96\xe0\x86\x39\xcf\x2d\xbc\x81\x1a\x33\x48\x70\x36\x49\xd1\x25\xc3\x90\x8c\x96\x13\x69\xd7\xb6\x37\xdd\xf1\xdc\xc6\xed\x75\x4e\x8a\x73\xc6\x72\xcb\xe3\xa3\x1b\x0e\x69\x88\xb3\xce\xec\x29\xd7\xb0\xe0\x4a\x72\x06\x42\x3b\x01\x5d\xe7\x0b\x4c\x83\x99\x76\x01\x5c\xe2\x41\x8c\x95\x2b\xea\x41\xbe\xa3\x56\xce\xed\x1b\xcb\xd3\x0e\xa6\xd6\xe7\x84\x76\x4a\xce\x8c\xf7\x10\xf5\xad\x39\x12\x62\x74\xc3\x3a\x72\xfd\x7b\xe8\xde\x2a\xd9\x2e\xc8\xe6\x65\x9d\xcd\x9b\x4d\x52\x8b\x8c\x14\xa3\x19\x89\x9f\xec\x74\x33\xc8\xf5\xda\x0f\xab\x88\xc5\x4d\x53\xb4\x16\xb0\x6a\x9a\x0a\x91\x89\x2d\x0b\x18\x87\x4f\x9b\xe6\xa9\xd6\x5a\xfb\x6a\x44\xa1\x2c\x20\x79\x1d\x79\xe8\x46\x75\xe8\x46\x2d\x1a\x53\xdf\xf5\x67\x93\x94\x60\xef\x78\xae\xe9\x6d\x03\xeb\x2c\x63\xd6\x34\xc3\xd9\xc6\xf4\x0f\x3a\x5a\xd1\xb9\x47\xa4\x6c\x85\x0a\xb6\x68\x08\x2e\x27\xd9\xce\xcd\x14\x48\xda\xec\xac\x69\x5c\xee\xab\x66\x25\x85\x20\x94\xcb\x80\x98\x47\xac\x87\x91\x42\x89\x1e\xa4\xb6\xcd\xfd\xad\x46\x8b\xf8\x61\x39\xb9\xb1\xf3\x29\x10\x7d\x91\x50\x5e\xb1\x0e\xe9\x9d\xe5\xa4\x9e\xf2\xdd\xd2\x77\x39\x14\x4a\x4b\x07\x5a\x4b\xba\x88\xa9\xd6\x30\x39\x7a\x50\x6b\x96\xaa\xd5\xb9\xd4\xea\x5c\xf2\x8d\x8b\x4c\x7d\x16\x96\xb4\xfe\x9d\x21\xa5\x3a\xba\x21\x96\xa4\x9d\x1d\x61\x59\x7a\x67\x78\x66\x9a\x4c\x3d\x91\x31\xd7\x6a\x97\x98\x78\x92\x2a\x28\xf4\x3b\xc4\x33\xcd\x55\x01\x91\xd4\x26\x57\xa0\x44\xef\x56\xa3\x33\xdb\x72\xae\x70\xa6\x5c\x06\xe2\x34\xad\xeb\x6e\x85\x23\xee\xab\x30\xe1\x88\x17\x6f\x14\x0e\xbd\x1a\xdb\xb2\xfd\x24\x5b\xaf\x94\xec\x7d\xc0\x99\xb3\x2e\xf2\x2a\xa7\x70\x0b\xbe\xb5\x76\xc2\xe3\xf0\x0e\xc7\x2e\x7c\xc5\x7d\xf8\x0d\xed\x03\x78\x82\x63\x0f\xde\xa0\xed\x89\x03\xf8\x81\xf4\xf7\x0b\x0e\x5d\xf8\x17\x1e\xc3\x1f\x38\xf4\xe0\x4f\xf4\xe0\x77\xf4\x5c\x17\xfe\xc2\x9f\xad\xe6\xbf\x10\xeb\x59\x31\xab\xf2\xc2\x27\xf7\x73\x51\xe4\xf5\x7a\xab\x09\xba\x26\xf9\x43\xf8\x7b\x50\x8a\x38\xcf\xe6\xb3\xe2\xe6\x4d\xdf\xe8\x42\xd2\x2a\xa1\x37\xf7\xe6\x0e\x8c\x7b\x5d\x6a\xf8\x6d\xd0\xb3\xd8\x2c\xcb\xab\xa5\x28\x30\x83\x99\xf3\xfe\xfc\xe3\xd9\xeb\xcf\x1f\xdf\xa1\xdb\xbf\xbc\x3e\xff\xf3\x0c\xbd\xfe\xf5\xd5\xd1\xc9\x29\x8e\xfb\xd7\xe3\xd3\xf3\xf3\xf7\xb8\xd7\xbf\xff\xeb\xe5\xe9\x31\xcd\xdf\xbf\xdb\xa2\x80\x3c\xbd\xdb\x76\xf4\xc7\xd1\x19\x3e\xbb\xdb\xa6\xa0\x1f\xdc\x6d\xd3\x4b\x3c\x87\x99\x73\xf4\xf1\xd5\xe9\xc9\x6b\x3c\x84\x99\xa3\x6d\x03\xf6\xa9\x17\xad\x02\x95\x3e\x24\x61\xc1\x9f\xb7\x20\x71\x56\x2c\xea\x95\xc8\x2a\xe2\x3c\x49\xee\x55\x42\xac\x66\xe4\x97\x5f\x44\x5c\x6d\xa2\xe6\x32\xda\x02\xd3\x92\xa5\x74\x96\xb3\xf2\xfc\x3a\x7b\x57\xe4\x6b\x51\x54\x37\x2c\xe3\x91\x56\x19\x4c\x60\x39\xc9\xa6\xdc\xa7\x60\x78\xe0\xde\xfa\x0f\x27\xcb\x2e\x8d\x50\x6d\xe6\xc8\x49\x45\xce\x65\x37\xab\x8f\xaf\x59\x86\xc6\xeb\xa3\x57\x27\x6f\x5f\x9e\x7e\x7e\x77\xfa\xf2\xd5\xd1\x85\xc1\xc9\x7f\x14\xe0\xc2\x11\x8c\x21\x23\xe5\xf3\x0e\xdd\x86\xa2\xc1\x49\x36\xc5\x77\xa0\xe6\x28\x02\x9d\x9c\xbd\xf9\xfc\xf6\xfc\xf5\xd1\x66\xca\xf3\x6e\xca\xd7\xad\x29\x5f\xf5\x94\xa3\xbf\xde\x9d\x9f\x1d\x9d\x7d\x38\x79\x79\xfa\xf9\xe5\x07\x9a\x43\xde\x11\x8f\xfe\xa5\x5c\x21\xb0\x8f\xc0\x6d\x67\x53\x8b\x37\xdd\xc6\xe0\x37\x02\x47\xa3\x9e\xa8\x07\x6f\xca\x7d\x5a\xd0\x3e\xda\x1e\x62\x33\xea\x65\x6e\x28\x22\x5b\xf8\x82\x73\xde\x22\x30\xf9\x0d\x9e\x4c\x5b\xbc\x5f\x9e\xbd\x39\x7a\x6c\x6d\xdb\xbb\xbb\xb8\xb7\x81\xfc\xa6\x5b\xfc\xc7\x2f\x17\x77\x1b\x11\xbd\x41\x9b\xfd\xb8\x8b\x80\xaf\x33\x66\x90\x59\xc6\x20\x9e\x65\xe4\x39\x5d\x8a\xc1\x0f\x51\xe4\x06\x88\x0d\x7a\x6f\xe0\x47\x8b\xde\xd1\xfb\xf7\xe7\xef\xd5\x11\x30\x81\x88\xc3\xa1\x68\x1a\x0f\x11\x45\xd3\x90\x36\x11\x11\x23\x45\xf0\x2f\x64\x5f\xa8\x8f\x47\xc7\x7e\xbe\xb5\xc8\x35\x01\xd5\x30\xbf\x68\x78\xaf\xde\xff\xd7\xbb\x0f\xe7\xff\x13\xbc\x3f\x70\xc8\xa8\x75\xb8\x6c\x9a\x8e\x35\x87\x1d\x6b\x2e\x39\x08\xd3\x1c\xfe\xa1\xf2\x03\xb4\x86\x11\x17\x37\xeb\x2a\x1f\xd4\xd9\xec\x6a\x26\xd3\xd9\x65\x2a\x0c\x58\xf2\xc7\x71\xf8\x43\xe3\xf0\xf6\xfc\xf5\xc7\xd3\xf3\x7b\x8c\x72\xd8\x51\xee\xcf\x2d\x46\xf9\x53\x4f\x78\x77\xfe\xe7\xe7\x77\xef\x8f\x5e\x9d\x5c\x9c\x9c\x9f\x3d\xc2\x8e\xbf\x6f\x4d\xf9\x5d\x4f\x39\x3e\x7f\xff\xb6\xe5\xa9\x07\xf2\x25\xa2\xbf\x50\x6c\x9f\x44\xeb\xc0\xb6\xe3\x36\xf8\xfe\x05\xc5\x2d\xcc\x9c\xd5\xec\x3b\x3e\x14\xaa\xef\x6c\x23\xce\x1f\x9c\xb4\xe2\x6a\xa8\xcc\xfe\xd7\xa1\x0b\x3d\x54\xfb\x7d\x0f\x34\x06\x1e\xba\xee\x81\x77\x78\x38\x7e\xba\x7f\xb0\xef\x1e\x1e\x8e\x21\xc3\xb7\xb3\x6a\xd9\x8e\x67\x7c\x57\x98\x63\xf7\xf0\xc0\x7b\xea\x3d\xa2\x26\x56\xec\xde\x58\xfe\x98\x3e\x78\xbe\xf7\xfc\xf9\x33\xf7\xf9\x2e\xf3\xdc\x83\xbd\x83\x7d\xef\xf9\x78\x7f\xf7\xce\xbc\xc6\xe5\x16\xeb\x46\xdd\xef\xd9\xe8\x8a\xad\x3c\xf3\xbd\xe4\x31\xba\x90\xe0\x64\x0a\x69\x6b\x93\xbe\x29\x6f\x4e\xb4\x01\xa9\xd8\x9c\xa0\xb7\x4f\xf1\xa8\xf0\xdf\x41\x8e\x73\x26\xc8\x61\xfb\x83\xcb\x84\x2d\x4d\x73\xe9\x2c\x44\xf5\x5e\xad\xfb\xc7\x2c\xad\x45\xa9\xcd\x7b\x85\x0f\x3a\x54\x80\xf9\x51\x66\xd5\xde\xf8\x65\x51\xcc\x6e\x58\xbe\x8b\x63\xce\x83\x3c\x2c\x03\x5e\xa3\xb7\xe7\xb9\x07\xe3\xdd\x6a\x52\x4e\x2d\x56\x4d\x4a\xcb\x9b\x86\x61\xe8\x79\x1c\xea\x10\x0f\x85\xf7\x34\x62\xc5\x3f\x00\x3a\xe6\x1c\x08\x06\x16\x24\xfa\x1a\x0e\x16\x4a\xfa\x59\xa2\x1d\xc7\x7a\xc7\x13\xde\x3e\x87\xd2\xc2\x31\x0f\x4a\xcc\x47\xe3\x3e\xb8\x54\x3b\xd2\x64\xfc\xed\xa6\xda\xde\xcd\x56\x23\x61\x7e\xd0\x23\x3e\x7e\xee\xed\x1f\xec\x1f\x1e\x3c\x3b\xf0\xdc\x67\x4f\x9f\xed\xb2\x3d\xcf\x24\x0c\xb8\xe5\xb9\x87\x87\x4f\x3d\xef\xd9\xf8\xe0\xe0\xe0\xd9\xae\xc6\xc5\xda\x1f\x1f\xee\x1f\x3e\x3b\x18\x1f\xea\x96\xf1\xd4\xf2\x9e\x1d\x1c\x1c\x8c\x3d\xfd\xbe\xd7\xee\x7e\x7f\xfa\xe2\x85\xf7\x8c\xeb\x97\xa7\xd3\x17\x2f\x9e\x73\x8b\x1e\x9f\x4d\x7b\x7a\xdc\xc5\xe9\x80\x3b\x71\xbe\xbe\x61\x15\x85\xf7\x8f\x6c\xf5\x40\x6f\xf5\x40\x6f\x55\xc9\x95\xb7\xff\x2b\xcd\xa0\xd2\x49\xa5\xf6\xdc\xda\x6d\x66\x8c\x03\x2d\x1b\xd6\xa6\xc9\x92\x49\x69\x59\x53\x6c\xc1\x07\xda\x83\x4a\x26\xb6\x5d\x4e\x41\x90\x57\x9d\x9b\xa6\x20\x6d\x8d\xef\x27\x37\xb6\x98\x42\x42\x47\xb2\x62\xf9\xa8\xe6\xbb\x35\x57\x3e\x16\x35\x05\x89\xf6\xb0\xa0\xb4\x6d\xae\x13\x56\x25\x4f\x70\x22\xfb\xac\xa4\x0e\x3f\x6c\xaf\x9d\xe2\xd2\x14\x9d\xb3\xe1\x20\x6d\xbc\xd1\x8b\x97\xca\x9b\x4c\xee\x7b\x93\xca\x55\xbc\x09\xc9\x53\xa4\xb1\x76\xd9\x3b\x68\xa9\x23\x50\x42\xea\xc4\x98\x40\x7a\x7b\xcb\x38\xbc\xda\x16\xf2\x3e\x5a\x12\x77\xc2\xcf\x3b\x82\xd3\xc5\xff\x24\x3e\x3b\x2f\x21\xc6\x6c\xf4\xb2\xd1\xe9\x03\x81\x7d\x02\x3e\x48\x6c\x3b\xe0\x39\x8a\x49\x32\xdd\x79\x09\xb5\x7a\xa0\x81\x50\x60\xbc\x9b\x5b\xf5\x6e\x0a\x12\xd3\xdd\xdc\x2a\x76\x5e\xee\xbe\xb4\xc8\xeb\x60\x72\x54\x29\xe1\x2e\x68\x20\xb7\xe2\xdd\x1a\x68\x1a\xca\x9d\xaa\x13\xeb\xd2\x34\x45\x9f\xbe\x2a\xef\x84\xcc\xd9\x83\x08\x4f\xe5\x99\x86\x58\xf0\x1c\xab\xb0\x88\x3c\xdf\xf6\x74\x18\xa6\xa9\x9b\xa3\x1b\x54\xa1\x54\xf9\x69\x52\x00\x13\x39\x1d\x62\x36\x91\x53\xfe\x93\x10\x97\xd3\x90\x5e\xf4\x34\xed\x58\xb7\x48\xe4\x9b\x45\x8b\xcd\xa2\x5d\x02\x41\x12\x58\xda\xbd\x98\x54\x53\x1b\x25\x48\xa4\xa7\x17\xd9\xa4\x22\x60\x2e\xd0\x1b\xca\xdd\xc2\x52\x03\xa8\x59\x07\x7b\x43\x32\xdb\xb4\xbf\xee\x5e\x25\x10\xdd\x99\xf3\xe0\xf6\xbe\x5e\xeb\x23\x58\xbd\xdd\x74\x93\xe4\x85\x6b\xb8\x82\x4b\x38\x87\x0b\x78\x0f\x2f\xe1\x08\x5e\xc3\x67\xf8\x0e\xc7\x28\x9d\x12\x31\x77\x4a\xb5\x25\x38\x41\xe9\xc4\x70\x8a\xb9\x13\xeb\x7b\xb4\x13\xd3\x3c\x51\x18\x9c\x9a\xe6\x29\x05\x56\x5d\x64\xa5\xd5\xa4\x74\x4a\xd3\xcc\xe9\x0f\x3b\x89\x86\xa7\x4d\x43\x83\x87\x48\x23\xfd\x53\x1e\x9d\x98\xa6\x8b\x48\x6d\x4d\x33\x3c\x8d\xdc\xdd\x63\xff\x78\xe4\xfa\xee\xc8\xd5\xbc\x7a\xd5\x6a\xdb\x63\x0e\x97\x78\xa5\x73\xed\x31\x4a\x47\xd8\xb9\x23\xe0\x18\x6b\x2b\xb6\x3c\x48\x9a\x86\x25\x78\x06\x31\x56\x4c\x3a\xa4\x72\xed\x8a\xe5\xea\x01\x8e\xf1\x78\x74\xd3\xb8\x1c\x96\xe8\x06\xa7\x93\xe5\x14\x91\x9d\x4c\x96\x53\x8a\xe7\x82\x65\x1b\x94\x53\x7b\xd8\x37\x9b\x66\x6c\xdb\xe0\x86\xc7\xfc\x52\x6b\x06\x8f\xc3\x02\x87\xee\x46\xc8\x8e\xf0\xa4\x63\xe8\xcf\x78\xda\x3d\x52\x10\x79\x6c\xe1\x18\xd6\x48\xe1\x1d\xa3\x4d\x5a\x1e\xe7\xb0\x0e\x3d\xd3\x64\xa7\x28\xd8\x29\xac\x21\xe1\x70\x82\x82\x9d\xe8\xc7\xad\xf9\x1b\xa8\x1c\x5e\xe2\x67\x38\xc7\x93\xfe\xaa\xe0\x33\x87\x0b\x3c\xef\xc2\xae\xcf\xe1\x45\x70\x3e\xb9\x20\xb5\xe2\xf2\xe0\x3b\x9e\x76\x12\x04\xdf\x7b\x3e\x77\x39\xbc\x56\x74\x86\xd3\x89\x37\x0d\x31\x19\x8d\x4d\xf3\xb5\x65\x05\xf3\x7c\xb0\x46\x97\x24\x91\x9d\xc2\x39\x7c\x86\x0b\x0e\x6e\x98\x46\xec\x3d\x9e\xd3\xf0\xcf\x43\xbc\x30\x4d\xf6\x1e\xdf\xef\x26\x16\x3b\x9f\x78\x8a\x28\x5c\xed\xea\xfd\xe8\xb5\xda\x4e\xc4\xd6\xa1\x4a\x4a\xaf\x31\xb1\x3d\x0e\xf3\xcd\xde\xae\x71\xde\x6d\x68\x83\xb1\x5a\x6d\x0e\xe7\x70\x4d\xab\x79\x88\x29\xcd\xb5\x6d\x28\xd8\x1c\xae\xc3\xcf\xd1\x77\xff\x14\xae\x21\xe1\x9c\xfb\x14\xf8\xae\x4d\x93\xa5\xb8\x46\x05\xba\xdf\xdd\x5d\xe0\xe1\xb5\x69\xce\xb7\xb7\x5b\xb0\x73\x98\xc3\x05\x21\x61\xb7\x4b\xdc\xc3\xa0\xdf\xaf\x17\x2a\x04\x2c\x4b\x4d\xba\x68\x11\xb8\x50\x08\x6c\xa1\xcd\x7d\xd2\xa4\xdd\xd0\x73\x54\xd9\xcd\xcb\xc9\x92\x08\xbf\x86\xd4\x34\x89\x60\x51\x7b\x12\x27\x93\x97\x44\x29\x9f\x9d\xe3\x84\x9e\xa7\x70\x81\x1e\x0f\xae\x97\x32\x15\x8c\xbd\xb4\xac\x17\x47\x5d\x52\xe4\x5c\x27\x4c\x8f\x49\x91\x2f\x70\xd3\x06\x97\x4a\x12\x2e\x3b\x09\xa6\xa0\x3c\x41\x3c\xd3\x7a\x62\x89\x1e\x1c\x23\x0d\x09\x8e\x95\xe2\x3e\x56\x8a\x5b\x31\xf1\x47\x76\x05\xb5\xc5\xae\x1c\x81\x4b\x2b\x56\x69\x44\xcb\x83\x12\x16\x6d\x26\x99\x3a\x62\xb8\x72\x0a\xb4\x16\x9d\x5a\xbc\x52\xba\xfc\x61\x88\x87\xa3\xbf\x99\x1d\x71\x97\x4d\xbe\x5f\xe6\x53\xce\x3e\x5d\x4f\x3e\x5d\x3b\xd3\xdd\x27\x7c\x24\x21\xa3\xde\xc9\xdf\xce\xd4\xe2\x9f\x9c\x27\x23\xa8\x70\xf4\xf7\x27\xa7\x6d\x79\x32\x82\x02\x47\x7f\xdb\x11\x3b\xc9\x12\x99\xc9\xea\xa6\x39\x9b\x9d\x51\xb3\xa4\x61\xe5\xee\x27\x8b\x29\x58\xbc\xf9\xfb\x53\x69\x35\x9f\x4a\xeb\xc9\x68\xf1\xc0\xfb\xba\xaf\xa3\xb0\x8c\x6a\xbf\xee\xaf\x8f\x24\x18\x4f\x3c\x43\x09\x6e\xa1\x2f\x45\x63\xce\x73\xa7\x44\x59\x9e\xcd\xce\x58\xac\xe3\x48\xdf\x0d\xe3\xc8\xf6\x7c\xaf\xbf\xf2\x18\x92\x16\x8a\x31\xee\x01\x09\xd8\x38\x7c\xda\x72\x75\x16\x0f\x8d\xef\x06\x22\xab\xb0\xba\x77\xad\x15\x79\xcf\x7c\xe3\x92\x3c\xef\x68\xec\x3f\x87\xc4\x34\x93\x21\xa6\x91\xf0\xb3\x5b\x4e\x6f\x2c\xc5\x04\xb6\xd7\xc8\x34\xb2\xfd\x7b\x05\x86\xeb\x50\x0b\x87\x7a\x88\xf1\x3d\x75\x19\x43\xca\x83\x2f\xfa\x8a\xd2\x50\x4e\xbc\x61\xb1\x24\x32\x06\x97\xb3\x52\x0c\x0c\x2b\xf1\x0d\x83\x93\x7f\xdf\xe6\x71\x6b\x0e\xb4\x71\xda\xef\x6d\xee\xc4\x98\xb7\x09\x17\x78\x8b\xae\x3a\xdd\x0f\xce\xec\xb2\xcc\xd3\xba\x12\xca\x07\x44\xf5\xfe\xf0\xc4\xdb\x7b\xb8\xa5\x2c\xef\xdf\x03\x30\xe1\x94\x24\x86\xe2\x16\x3e\x38\xb1\x90\xe9\x23\xd1\x40\x77\x1f\xa2\xe6\x03\xfd\x55\x49\xb4\x31\x57\x73\xf2\xd5\x7a\x56\x88\xf9\x87\x1c\x3f\x38\xf1\x6a\x8d\xdb\x34\xef\x41\xbc\x45\x0f\xa4\x02\xb0\x55\x58\xa1\xe6\xb7\xe9\x9b\x77\x2a\x6f\x8f\x1f\x9c\xf9\xfa\xb1\x9c\x44\xa1\x4a\x3b\x5a\xa3\x54\xf4\x44\xad\xd3\x54\xbb\xe9\x8c\x65\x58\x74\x77\x8b\x1e\xd9\x07\x8d\xe6\xe8\x86\xf3\xdd\x1b\xc8\x90\xc2\x23\xed\xc3\x65\x3b\x9e\x8b\xe8\x06\x99\x92\x2e\x41\x32\xda\x82\x73\x43\xa1\xa2\x4c\xb7\x25\xc7\x5c\x5e\xc9\xb9\x98\xff\x76\x83\xea\xf9\x57\x3b\xdb\x83\x57\xf7\x77\x06\xef\xe0\x2b\xdf\x02\xa1\xd2\xee\x62\x21\x8a\x0e\x96\x6a\xf8\x15\xc0\xfd\x47\x00\xba\xe0\x29\x80\xe2\x5b\x3d\x4b\x89\x4e\xe2\xdb\xaf\xa6\x3f\x05\xd2\x6a\x8f\x53\x3b\x49\xf3\xbc\xf8\xe7\x47\xbc\xa7\x26\x2d\x0a\x31\xab\x44\xf1\x61\x39\xcb\x90\xa2\xc1\x5f\x2d\xfc\xec\x91\x23\x0e\xdd\x7b\x10\xce\x8b\x23\xda\x82\x62\x97\x45\x25\x7e\x05\xeb\x80\xac\x08\xb2\xec\x91\x7d\x70\x1d\xf9\x67\x04\x58\x96\xc7\xa4\x87\xc4\xc3\x2d\x0d\x87\x9a\x63\xf4\xa8\x96\xfc\xd8\x3e\xff\x7a\xb8\x69\x6e\xb1\x4e\xa8\xdb\x3a\xbe\x1a\x6b\x58\x67\xb3\xb3\x47\xe6\xab\xa1\x65\x3b\x42\x2c\x66\x95\xbc\x12\xd8\xbe\x3c\x42\x70\x3d\xfc\x85\xab\x27\xfc\xb7\x28\xf2\xff\x09\x27\x17\x5b\xfe\x9f\xb8\x53\x9a\x91\x8a\xb2\x6c\x8f\x23\xfd\xe5\x71\x3c\x7f\xe4\x38\xf4\x82\xdd\xf4\xed\xb3\x48\x7f\x7d\x16\x87\xca\xde\xfe\xef\x87\xa1\x6e\x8e\xf0\x83\x53\xd6\x97\xf7\x40\xdd\x8d\x18\x14\x8c\x04\x4b\x47\xd5\x6a\xbd\x55\x62\x88\x5b\xbc\x9e\xa9\x5a\x9e\x61\xd2\x34\xc3\xec\xae\xfe\x54\x8e\x23\x19\xcd\xe1\xa6\xc0\x8a\x14\x98\x9d\x41\xe9\xac\xd3\xba\x64\x82\x07\xca\xaa\xa0\x3a\x41\x50\x39\xea\xd1\x0d\x2c\xb1\x74\x62\x58\xa0\x68\x55\x48\xda\x34\x43\x7d\xd1\x3a\x5c\x36\xcd\x70\xd1\x01\x5b\x46\xac\x85\x27\xb8\xaf\xd7\x5c\x44\xa5\xdf\xad\x3b\x5c\x6a\x57\x76\xab\xba\x60\x40\xcf\x0f\x67\xd1\xc0\xa8\xf4\xf7\x10\xbf\x46\xb6\xeb\xbb\xca\xd6\xa7\x58\xb1\x94\x2b\x3f\x56\xdd\x49\x2f\x7b\xbf\x2e\xc1\xd4\x8e\xb5\x1b\xc0\x6a\x74\xc3\x84\x47\x2c\x41\x3b\x81\x1c\x97\xdc\x67\x31\xa6\x90\xe3\x82\xac\x41\x21\xae\x44\x41\xb6\x0a\x32\x4c\xd4\x05\x6f\xbe\xb9\x03\xda\xea\xbe\xdd\x0a\x6a\x58\x8d\x2c\xe9\x6f\xad\xf9\x0b\x96\xf5\x77\xfb\x9c\x47\x89\x9f\x41\x82\x19\xba\x81\x0c\xb3\x20\xd3\x81\xcf\x72\x92\x4d\x87\xb8\x20\xad\xf9\xb3\x46\x7a\x7b\x41\x2f\x9b\xcb\x04\x0a\x7d\x73\x24\xaf\x78\x01\x0b\xcc\x41\x11\x40\x38\x25\xe1\xc5\xe4\x06\xbe\xad\x52\x15\x9d\xdf\xdb\xdd\x54\xeb\x9b\xe9\x49\xd1\xba\xb8\xd4\x94\xe1\x99\xed\x05\x32\x4c\xf4\xf5\xc8\x52\x5d\xb1\xbe\x58\xa8\xd0\x4b\x17\x5a\xc9\xa0\x30\xcd\x21\x75\x14\x53\x9a\x3c\xc5\x8c\x07\xb6\x4d\x4f\xb0\x9c\xc8\xa9\x85\x67\xb7\xf4\x6b\x23\xcd\x52\x77\x19\x14\x2a\xd3\x51\x04\xcb\x3e\x52\xb6\xed\xb8\xd7\xf8\xea\x94\x4e\x98\x80\x25\xc4\xdc\x57\x87\xa8\x4f\xcc\xf3\x3d\xd8\xba\xcc\x00\xa1\x14\xe1\x2a\x9f\xd7\x29\x09\xcb\x2a\x9f\x3f\xc2\xe1\xfa\xd6\x5c\xd5\x20\x6e\xcc\x9e\x77\x97\xb7\x87\xd2\x89\x9b\x66\x28\x9c\xb2\x69\x04\x89\xf6\x50\x17\x2e\x44\x1b\x06\xf7\xa9\xa9\x69\xa4\xea\x95\xdb\xbd\x92\xfb\xec\x10\xf1\xcf\x88\x15\x4a\x44\x94\xed\x86\x0a\x5f\x31\x09\x02\x5c\xd8\xe3\xaa\xa9\x80\xca\x29\x77\xb1\xe0\xfe\xa6\xeb\x4f\x0e\x52\x0b\x28\xab\x1c\x75\x51\xcb\x04\xd7\x36\x21\x23\x6d\x25\xe6\xa8\x9e\xfe\xa9\xef\xa0\xce\x5a\xfb\xbb\xda\x58\x92\xf4\x91\xfb\x31\x7f\x8c\x32\x1d\x5d\x20\xa7\x78\xb3\x95\xfa\xf1\xa3\x52\x9f\xff\x5a\xea\xf3\x87\x52\xdf\xed\xa9\x15\xfb\x1a\x55\x7c\xa8\xab\x40\x46\x37\x90\xa8\x70\x36\xed\xc5\xbe\x6e\x9a\x61\xa9\xc5\x9e\xb4\x4b\x7a\x77\x9d\xbc\x93\xf2\x44\x4b\x79\xba\x25\xe5\xf4\x4c\x6e\xa0\x1a\x48\xfd\x91\xf4\xdd\xdd\x5c\x89\x75\x8d\x15\xab\x39\x29\x36\x56\x92\x28\x27\xbd\x58\xe7\x58\xdb\x6d\xde\x2c\x0f\xdd\x88\x95\x58\x43\x81\x29\xf7\x59\x8e\x76\x0e\x05\x26\x1c\x8a\x8d\xcc\x06\xb9\x6d\x07\xc5\x46\x9c\xb7\xba\xda\x9b\xb9\xa4\x0b\x77\x32\x4c\xbb\x47\x37\xcc\xed\x4c\xd5\xdd\xa5\x40\xee\x69\x82\x05\x64\x98\xd3\xea\x6e\x90\x05\x3c\x47\x96\x4c\x6c\x3b\x9b\x62\x32\xc9\xa6\x56\x4a\x7f\x72\x3e\x3a\x6b\x5c\xa0\x86\x1d\x3c\xeb\xce\x35\x37\x4d\x96\xf4\x21\x57\xce\xc1\xb2\x4a\x0e\x24\x1f\x09\x94\x8a\x57\xfa\x3a\x00\x52\xf3\xdb\x27\xad\xcf\x59\x65\x3d\xf4\x49\x4b\x2c\x34\xd1\xfb\x0c\xaa\x18\xaa\xf4\xbd\x69\x7a\x43\xa4\x77\x57\xff\x30\x9d\x7f\xdb\x03\xa3\xcb\x39\x1b\x2a\x05\x0f\x62\xa8\x87\xb7\x59\x58\x4e\xc2\x73\xdf\xf3\xab\x50\xf6\x5e\x1f\x64\x58\xed\xde\x58\x24\x10\x72\x52\xb5\x5a\x23\xa8\x5a\x77\xaf\x52\xee\x5e\x46\xee\x9e\x4e\x63\x4a\x52\x0b\x95\x0a\xb4\xda\x3e\x0a\xb4\xfa\x5b\x4b\xd3\x2c\xc8\x05\x0a\x89\xb2\xe4\x5b\x0a\xcb\xe3\xa0\xcc\x9c\x2a\x7b\x78\x4c\xfc\x1f\x11\x15\xa6\x2b\x91\x44\xd3\xf4\xf9\xe3\xa7\x9c\x9b\xe6\x47\x56\xc1\xbf\xff\x2d\xac\xde\xd3\xba\x53\x60\xec\xc2\x73\xf0\x9e\xea\xca\xa7\xcc\xff\xca\xa1\xa2\x75\xd5\xa9\x3c\x24\xf9\x1d\x85\xa3\x6e\x75\x2e\xe0\x02\xbc\x67\x5b\xf4\xe4\x51\xd6\xca\xbc\xe1\x09\xc3\x52\xb5\x33\x2d\x2b\x67\xa4\x65\x32\xa5\x64\x4c\x93\xd9\x17\xba\x68\xe6\x82\x66\x94\xbb\xea\x1e\xc8\xf5\x3d\x52\x4a\x99\x3a\xff\xf2\x5b\x3d\x2b\xc4\xfb\x3c\xaf\x88\x01\xbe\x15\xd5\x63\xce\xfa\x03\x3b\x4f\x22\x58\x3a\x25\x45\x7a\xaa\x90\xea\x9d\xb5\x0f\x8b\x96\x5a\x86\xeb\x3c\xd5\xc1\x1e\xb1\x05\xd9\x65\x92\xcc\x64\x4b\xf4\xf4\x38\x32\xd9\xae\x0a\xeb\x69\x80\xea\x8f\xdc\x91\xeb\x27\x51\xa9\x10\x0c\x94\x7d\x55\xa9\x7f\xc2\x8b\x11\xe7\xba\x0a\x60\x8a\xe8\x8d\xdc\x88\x4e\x91\x25\x1c\x58\x57\xc6\x63\xc5\x7c\x67\x8c\xaa\x8a\x31\xd3\x35\x52\xb0\x0d\x20\xd3\x86\x9a\xc5\x96\xc7\x47\x63\x6e\x33\x37\x8c\x9b\x26\xde\x19\xd3\x30\x05\x31\x43\x4d\x4e\x9f\x91\x34\xde\x29\x75\x51\xe6\x39\xdb\xd4\x64\x6f\x2a\x2c\x85\xc1\x2d\x8f\x5b\x31\x07\xd9\x52\x20\xe3\xdc\xef\x9e\x53\xcb\x30\x48\x53\xd3\x79\x28\x43\xa9\xb2\x61\x90\x62\x6c\x2d\x61\x4f\x6d\x3f\x25\x83\x19\xe8\xfa\x57\x09\x64\x69\xf5\xd1\xd6\xda\x01\x7a\xc5\x4a\xa8\x61\x09\x9e\xba\x9c\x63\xb5\x13\xf3\x1e\x8d\x94\x6b\x37\xae\x60\xd2\x89\xf9\x76\xbb\xd2\x89\xd2\x11\x2f\x62\xd3\xb4\xed\x74\x0b\xf9\xd4\xde\x83\x94\x78\xdf\x38\x3c\x3c\x3c\x34\x14\x8f\xb2\xbc\x69\x8c\xfd\xf6\x95\xf3\x9f\x6c\x68\x65\x4d\x33\xb4\xb2\xbe\x10\xd9\x34\x8d\xa7\x06\x62\xd6\x55\x06\xba\xc4\xf4\xec\x23\x93\x20\x1d\x61\xbd\xb3\xc6\x40\x31\x27\x0e\x65\x8b\xbc\xe4\x8e\xf8\xc6\xca\xed\x6a\x85\x61\xae\x66\xd4\x50\xb7\x33\x5c\x0e\x75\xb7\xd7\x6e\x38\xff\x29\xb1\x6e\xe7\x2c\x2d\xdc\x87\x94\xfe\xe4\xe8\xdd\xf6\x81\x4d\xb7\xa4\x07\x5f\x5b\x33\xae\x60\x90\x15\xaf\xd3\xff\xc9\x4f\x6d\xeb\x80\xba\x04\xea\x4a\xa7\x50\x35\x57\x9f\xe3\xa5\x13\xc3\x05\x92\x1d\x3b\xb8\x63\xc7\x78\x97\x39\x3d\x37\xcd\x0b\x9d\x41\x32\xcd\x8b\xad\xcc\xe9\xf0\x92\x0c\xa7\xf6\x00\xce\x4d\x73\xa8\x47\x0c\x2f\x9a\xe6\x82\x7e\xf4\xdb\x79\x5f\x5f\x21\xda\xf8\x5f\x79\x27\xbb\x78\xe9\x94\x40\x90\x23\x5d\x6b\xe1\xea\xfa\x15\x97\xfb\xdb\xf5\x18\x1c\x44\x5b\x92\x56\xb1\x4b\x15\xc9\x58\x15\x13\x3a\x61\xda\x43\x49\x37\xb9\xb3\x05\x5e\xf4\x8f\x8a\xc7\x56\x78\x0e\xe7\x78\x01\x17\xb8\x82\x5c\x99\x15\xe5\xe4\x91\x49\x49\xad\x05\xac\x70\x32\x55\xb6\x6a\xb5\x55\x7e\x94\x17\xec\x1a\xcf\xe0\x0a\x5f\x92\xab\x1a\xd8\x76\x1e\xa2\x1b\x6c\x8a\xe4\xd7\x78\x31\xc9\xa7\x3b\x57\x30\x57\x0f\xa3\xab\xc6\x85\x12\x53\xa8\x31\xb7\xca\xa0\x0e\xf3\x80\xc7\x78\xae\xee\x4d\x76\xae\x60\x89\xe7\x93\x52\x0f\x4a\x70\xbe\x1b\x5b\xcb\xdd\x35\xc4\xb8\xde\x8d\xad\x64\xe7\x6a\xf7\xca\x5a\x4d\xea\xa9\x55\x40\x81\x2c\x1e\x5d\xab\x1b\x82\x84\x46\x73\x6b\xbe\xbb\x84\xd5\xa4\xb6\xed\x29\xc6\x3b\xd7\x01\x8d\xc3\xa2\x63\x87\x22\xb2\x2c\xe9\xaf\x7a\x67\x90\x6c\xdb\x0a\xa4\x66\x8b\xb6\x6c\xed\x1f\xaa\xf6\xc1\xbd\xcb\x41\x8f\x94\xfb\xf3\xed\x52\x39\x7d\x51\xa8\x5c\xa4\x0c\x1f\x2a\xf8\xe7\xbd\x82\x07\x11\x91\x41\xa0\xe5\xfc\x4a\xa3\xb2\xa5\x4b\x1e\x0f\xcb\x3e\xb7\xa1\xd8\x83\xfb\xc9\x43\x1e\x91\x65\xf1\xda\x85\xa9\x41\x83\x54\x95\x77\xff\x37\x60\x63\x57\x03\xeb\xcc\x54\x07\x73\xec\x76\x30\x55\x0d\xdf\xa3\x14\xfb\x25\x4c\xef\x17\x30\x3d\xa5\xc3\x75\x9c\xbb\xe5\x36\x3a\xe5\x3a\x95\x95\x2e\x4d\xcf\xd1\xfa\xcb\xe9\x0b\x79\xa0\xa6\xd7\x87\xb5\x3c\x50\x62\x37\xaa\xab\xe2\x21\x4f\x90\x84\x25\x45\x39\x51\x25\xda\x5d\xfc\x0d\x33\x8c\xa3\xa4\xd7\x5b\x7e\x02\xcb\x4d\xf9\x53\x1b\xe6\x14\x98\x93\x27\x07\x35\x16\xb0\xb4\xb1\xe0\x90\x87\xae\x69\x2e\x43\xb7\xe3\xee\xe5\x4e\xde\x34\x39\x24\x38\x6b\xbf\x89\x60\x2e\x14\x3c\x58\x86\x45\x50\x58\x98\xf3\xc4\xc2\xd2\xea\xfb\x0a\xc8\x79\x50\x87\xaa\x7c\xbe\xed\x50\xcb\x17\x9c\x43\xac\x6a\xea\x0d\xdb\xb0\x12\x7e\x5b\x61\x1a\x25\xd6\x5f\xce\xfd\x12\x27\x8b\x82\x44\xeb\x2f\xe7\x41\x59\x12\x8f\xd2\x4d\x66\x72\xeb\x4b\xa1\x4f\x9f\xe6\x3f\x0d\xab\xb6\x8c\xdb\x4f\x9f\x7e\x33\xc0\x58\x18\x1c\x8c\x27\xa6\xf1\x00\x46\xb7\x02\xf7\x53\xee\x27\x9b\xc2\x5c\x7d\xd8\xed\xd0\x47\xdd\xbe\x7b\x4a\x13\xbf\xc0\x42\xab\xca\x35\x2e\x9c\x18\xe6\xfd\xbd\x3a\xac\xb0\xda\xbc\x5c\x63\x72\xe7\xc6\xbd\x67\x17\xf6\x05\x87\x1e\x94\xd8\x97\x62\x7f\xc1\x25\xb0\x21\xa3\x48\x5e\xe5\x70\x18\xe7\x4d\x53\x3a\x69\xc5\xbe\x29\xe3\xa2\xcb\x23\xc6\x60\xac\x66\xdf\x07\x73\x91\xe5\x2b\x99\xd1\x56\x06\x86\xc5\x96\x91\x71\xaf\x06\xf8\xb1\x12\x60\x81\xc3\xa5\x69\xaa\x84\xcb\x47\x56\x82\x76\xcc\x3c\xee\x2c\x2a\xc1\xbe\xf1\xa8\xf4\x3b\x37\x74\xdd\xc7\xfe\xdb\x65\xe8\xda\x5c\x17\x6c\x4d\x7c\x3a\x77\x04\xf6\x89\xa3\x85\x23\x6c\x0f\xe6\xca\xaa\xe3\xfb\x09\xab\x31\xdf\xb9\xe1\x2f\xdc\xe8\xc6\xaa\xfd\x7a\x4a\x0b\x0b\xda\x4b\xbc\x5a\xb3\x39\x0f\xdd\x88\x82\x85\xb9\xbf\xf2\x4b\xa8\xf1\x07\xfc\x20\x6f\xa3\x27\x45\xcc\x21\xd1\x90\xdc\x20\x45\x32\xf7\x73\x95\x1d\x54\xb2\xa2\x5c\x80\xb4\xb5\x92\xd7\x9c\x83\x37\xa4\x10\x68\xb5\xa6\x08\x89\x57\x78\x0d\xd7\x28\x61\x85\xc9\xdd\x91\x12\x57\x9c\x22\x17\x09\x73\x2c\xdb\x90\x6a\xd3\x37\xe7\x14\xdc\xc8\x4e\xef\x49\x7c\xc5\x44\x17\x4b\x72\xb8\xd6\xab\x27\x1d\xcc\xce\xa4\x13\xc4\xaa\x43\x49\x6e\xa1\x94\x38\x25\xae\x9c\x12\x17\x4e\x09\xf9\x2e\x8e\x21\xc3\x57\x8c\xac\x6b\x0e\x5f\x79\x0b\x77\xc1\x9d\xd9\x65\xc9\xb8\x42\xfd\x15\x4b\xa0\x7a\xac\x97\xbf\xf0\xa2\xc9\x6a\xeb\x0c\xe0\x7a\xeb\x65\xea\x4f\x92\xed\xbe\x6a\xbb\x0f\x7e\x60\xad\xdd\xf9\x2a\xd7\x35\xc2\x0f\x23\xdf\x2d\xc7\xda\x12\x4d\x43\x06\x38\x72\x77\x85\xa3\xf3\x41\x7a\xee\xbb\xfc\x5a\xa5\x15\xd7\xf9\xf5\x2f\xa2\xa1\x55\x57\x4d\x65\x09\xde\xa5\x07\xc8\x41\xe8\x5d\xf5\xf1\x1e\x18\xa2\x55\xf7\xaa\xfe\x67\xd8\x65\x35\x99\xe0\x4d\x53\x84\x17\x14\x03\x8d\xd0\xe5\x4d\xb3\x9e\x15\xa5\x38\x4e\xf3\x59\xc5\x04\x57\x72\x32\x64\x02\x09\x9d\x7b\x37\x0d\xca\x8f\x5d\xe7\xd7\xcc\x92\x20\x78\x97\x61\xf9\x3d\x9a\xb3\xdf\x47\x37\xd6\x98\xfb\x2e\x6c\xa4\xb0\xad\x48\x2d\x76\xc6\xea\x57\x5d\x8b\xb4\x6e\x19\x0c\x2b\x27\x6e\x2b\x45\x33\xd3\xac\xfa\x6c\xa8\x0a\x8c\x36\xaf\x98\x71\x5d\x1e\xbc\x62\xc5\x68\xcc\xa1\x2b\x5a\x0e\x24\x6e\x7c\x3c\xc8\x4c\x53\xa5\x35\xe4\x5d\x30\xf2\x0e\x98\x3b\xd9\xf8\x0a\xbf\x39\x73\x79\xc5\x2a\xce\x21\x53\x56\xf2\x77\xf8\xda\x5b\xc9\xbe\x48\xfc\x9f\x9b\x35\x55\x15\xb7\xff\x2b\x33\x0d\xe3\xfd\xf6\x60\x35\xa7\x3c\x76\xa6\x5d\x7c\x5b\x11\xff\x62\xe5\x88\x60\x2b\x28\x45\xc4\x3c\x92\x14\x6c\x18\xdd\x1d\x99\x01\x6e\x28\x55\x14\x49\x6a\x9d\xbc\xfd\x0c\x8d\xb3\xd9\x99\xe1\x2b\x57\x9c\xe8\xdb\xfb\x07\x2d\x92\xea\x0b\xd3\xf1\xd3\xee\x13\xd3\xe8\x35\x4b\x59\x06\x39\x07\xb7\x11\xe0\xb9\x20\xb9\xff\x5b\x88\x64\x73\x42\x7c\x12\x25\xaa\xcf\xef\x86\xd0\x62\x55\x17\xd1\xf5\x8b\xb6\xcc\x5e\xd4\x59\xdc\x66\x7b\xd4\xf3\x3f\xbf\x0b\xd0\xf7\x0f\x57\xb3\xb4\x16\xe7\x09\x4d\xcf\x7f\xbf\x38\x7f\x24\x13\xae\x53\xdb\x1b\x51\xbb\xdd\xd0\xbf\xab\x3a\x25\x75\x3e\xdb\xd4\x4b\x54\x9b\x58\xd6\x6d\x7a\x6a\x8a\xd0\x6d\x1a\x81\x88\x59\x94\xf9\x99\xed\xdd\xa9\xaf\xd8\x54\x56\x68\x21\xf3\x40\x6e\x8a\x50\x72\xf5\x9d\x8a\x65\x18\x81\x0c\x8b\xd6\x03\xcd\x50\xa8\x6c\xa3\x65\x18\x50\xe1\x8d\xdd\x7f\xcb\x51\xd9\x76\x90\x51\xf4\x67\x65\x3c\xc8\x2d\xcc\x6e\xdb\x42\x90\x3b\x5f\x25\xe6\x77\xbf\x4a\x94\x3c\xe8\xdd\xc0\x7c\xf3\xbd\x9f\xe5\x35\x8d\xc7\x37\x88\xca\xfb\xb9\x41\xe1\xc4\x90\x53\x54\xa4\xbe\x29\x2a\x49\xa7\x3b\xa5\xaa\x9f\xa1\x18\x2f\x73\xc4\x56\x96\xea\x61\xa6\xc3\x34\x87\xca\x89\x29\x30\x37\xcd\x61\xae\x8a\xba\x9a\xa6\xbf\x0d\xab\xa2\x22\x72\x7d\xbb\xf4\x6b\xe5\xb8\x0c\xb1\x87\x51\x6b\x00\x6e\x58\x43\x81\x09\x62\x0a\x43\xd9\x34\xc3\x9c\xf7\x5e\xb1\xeb\x0f\xe5\xdf\x95\x2e\x6b\xb9\x73\xc5\x96\x84\x69\xd7\xae\x8b\x8b\x58\xd2\xa7\x5c\xf8\x0b\x96\xf6\x74\xe2\x51\xe2\x93\x33\xef\x06\x65\x58\x07\xb5\xce\x22\xcb\x49\x3d\x1d\x62\x3e\xa9\xfb\x60\x9e\x5a\x42\x6a\xe8\xa0\xf6\x9f\x49\x63\x1a\xb9\xfe\x66\xb9\x0d\x15\xf3\xbb\xb7\xb7\x4c\xe8\x8f\x7f\x42\x72\xa6\xab\x10\xb7\xaa\x7d\x6a\x62\x8c\xf6\xa3\xbf\x89\x2e\x8e\x1c\xa8\x52\xb8\xa9\x81\x78\xae\xde\x37\xe5\xe7\x3d\x8b\xea\xef\x91\xc4\xd6\xb9\x95\x0f\xbe\xff\x21\xf7\x46\x45\x5b\xb5\x2a\x94\xef\xbf\x77\xa2\xbd\xb6\xdf\x80\x6e\x38\x46\xda\x76\x90\x4f\xe4\x74\x17\xb3\xb6\x1e\x6c\x52\xa0\x3b\xb5\xf0\xbc\x4f\x03\x88\x2e\x30\x26\x42\xf1\xa0\x78\xd1\x4f\x2e\x2c\x8b\xe7\x93\x62\x1a\x56\xea\x6b\x5d\xad\x53\xf2\x49\x61\x79\x24\xce\xfa\x01\x5d\x0e\xfa\xc9\xa2\xae\xe9\xa8\x6a\x5c\x6a\x98\xee\x60\xd5\xeb\xcf\xed\xbb\x80\x7e\x67\xc9\xb6\x7e\x64\x9b\xaa\xa2\x48\x6c\x22\x75\xcb\x70\x0c\x4b\x6c\x5c\x62\xc1\x2d\xe6\x86\x59\x64\x90\xdf\x24\x2c\x83\x5b\xd9\x06\x60\x7a\x87\xc5\x75\xd9\x5a\xd6\xb9\xc5\x86\xeb\x18\x81\x65\x65\xe4\x04\xab\x6f\xd0\x04\x16\x96\xe8\x0b\x0c\xab\x8d\xc8\x5a\x56\x16\x56\x9b\x69\x06\x64\x36\x56\x81\x6d\x6f\x4d\xb5\xb0\xd0\x33\x2b\x65\x33\x36\x75\x65\xfa\x93\xf7\x2d\x9c\x33\xbe\x89\xd1\x36\x98\xc6\x1b\xe6\x18\x08\xbc\x63\x48\x81\x2c\xf4\x9c\x09\xee\xaf\x88\x0f\x68\x33\x33\x1d\xf7\xeb\x6a\x87\x4f\x73\x8b\x7d\x72\x3e\xcd\x77\x79\xd4\xd0\xaf\xc5\x99\x98\x58\xf6\x34\xa2\xc7\xe8\xc9\x88\xdc\x26\x65\x70\x63\x21\x53\x58\xe9\x67\x75\xd5\x0a\xd7\xd8\x56\xeb\x0e\x2e\xf3\x3c\x15\xb3\x6c\x90\x17\x83\x4b\x99\xcd\x8a\x9b\xc1\x9c\xc2\x4d\x03\xae\x50\x7f\x49\x25\xb3\xc5\x60\x95\xcf\x85\x01\x97\xdd\x87\xe9\x03\x62\xd4\xc1\x72\x56\x0e\x56\x79\x21\x06\xd5\x72\x96\x0d\xbc\xa7\x83\x52\x2e\x32\x99\xc8\x78\x96\x55\x1a\x48\x69\xc0\x39\x1a\xae\x37\xde\xdb\x7f\xfa\xec\xe0\xf9\xe1\xec\x32\x9e\x8b\x64\xb1\x94\x5f\xbe\xa6\xab\x2c\x5f\x7f\x2b\xca\xaa\xbe\xba\xfe\x7e\xf3\xe3\xe5\x6f\xaf\x5e\x1f\x1d\xbf\xf9\xd7\xc9\xef\xff\xdf\xe9\xdb\xb3\xf3\x77\xff\xff\xfb\x8b\x0f\x1f\xff\xf8\xf3\xaf\xff\xfa\xef\x27\x9f\x0d\x38\x43\x4f\x78\xfb\x70\x83\xde\x3e\x5c\xdc\x2f\xec\xf5\xe0\x3d\x4e\x3c\x32\x3f\x9e\xeb\x82\x27\xf6\xc0\x13\xfb\xe0\x89\xa7\xe0\x89\x67\xe0\x89\x03\xf0\xc4\x73\xf0\xc4\x21\x78\x82\x06\x09\xcf\xa3\x3f\x63\xfa\xb3\x37\x85\x97\xea\x43\x8e\x23\xf4\xc4\xa1\xfa\xa2\x4a\x55\x51\x1a\xdd\xf1\x6c\x8a\x9d\xe7\x22\x91\x99\x30\x4d\xfd\xeb\xcc\x56\x73\xae\x1f\xd9\x43\x53\x33\xbb\xdd\x7c\xb7\x69\xd4\x99\x1e\x37\xdf\x54\x7f\xab\x0b\x1b\x61\x9a\xfa\xd7\x21\x2f\xab\xa8\xf4\x05\xc0\xdd\x26\x9c\xc1\x70\xc9\xab\xe2\xe6\xe7\x12\x0b\xf1\xad\x96\x85\x60\x6d\x3d\xa8\xc1\x6f\xe3\x59\x15\x2f\xd9\x6b\xfe\xf3\x56\x73\xa0\x70\xfa\x2f\xcb\x70\x76\xdb\x66\x05\xfe\x63\x34\xfa\xcf\x41\x99\xd7\x45\x2c\xde\xce\xd6\x6b\x99\x2d\x3e\xbe\x3f\xc5\x79\x1e\xdf\xf9\xf7\x1a\xce\x6a\xb6\xfe\x8f\xff\x17\x00\x00\xff\xff\x2f\x88\x72\xca\xa2\x43\x00\x00")
+
+func bignumberJsBytes() ([]byte, error) {
+ return bindataRead(
+ _bignumberJs,
+ "bignumber.js",
+ )
+}
+
+func bignumberJs() (*asset, error) {
+ bytes, err := bignumberJsBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "bignumber.js", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+// Asset loads and returns the asset for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func Asset(name string) ([]byte, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
+ }
+ return a.bytes, nil
+ }
+ return nil, fmt.Errorf("Asset %s not found", name)
+}
+
+// MustAsset is like Asset but panics when Asset would return an error.
+// It simplifies safe initialization of global variables.
+func MustAsset(name string) []byte {
+ a, err := Asset(name)
+ if err != nil {
+ panic("asset: Asset(" + name + "): " + err.Error())
+ }
+
+ return a
+}
+
+// AssetInfo loads and returns the asset info for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func AssetInfo(name string) (os.FileInfo, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
+ }
+ return a.info, nil
+ }
+ return nil, fmt.Errorf("AssetInfo %s not found", name)
+}
+
+// AssetNames returns the names of the assets.
+func AssetNames() []string {
+ names := make([]string, 0, len(_bindata))
+ for name := range _bindata {
+ names = append(names, name)
+ }
+ return names
+}
+
+// _bindata is a table, holding each asset generator, mapped to its name.
+var _bindata = map[string]func() (*asset, error){
+ "bignumber.js": bignumberJs,
+}
+
+// AssetDir returns the file names below a certain
+// directory embedded in the file by go-bindata.
+// For example if you run go-bindata on data/... and data contains the
+// following hierarchy:
+// data/
+// foo.txt
+// img/
+// a.png
+// b.png
+// then AssetDir("data") would return []string{"foo.txt", "img"}
+// AssetDir("data/img") would return []string{"a.png", "b.png"}
+// AssetDir("foo.txt") and AssetDir("notexist") would return an error
+// AssetDir("") will return []string{"data"}.
+func AssetDir(name string) ([]string, error) {
+ node := _bintree
+ if len(name) != 0 {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ pathList := strings.Split(cannonicalName, "/")
+ for _, p := range pathList {
+ node = node.Children[p]
+ if node == nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ }
+ }
+ if node.Func != nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ rv := make([]string, 0, len(node.Children))
+ for childName := range node.Children {
+ rv = append(rv, childName)
+ }
+ return rv, nil
+}
+
+type bintree struct {
+ Func func() (*asset, error)
+ Children map[string]*bintree
+}
+
+var _bintree = &bintree{nil, map[string]*bintree{
+ "bignumber.js": {bignumberJs, map[string]*bintree{}},
+}}
+
+// RestoreAsset restores an asset under the given directory
+func RestoreAsset(dir, name string) error {
+ data, err := Asset(name)
+ if err != nil {
+ return err
+ }
+ info, err := AssetInfo(name)
+ if err != nil {
+ return err
+ }
+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
+ if err != nil {
+ return err
+ }
+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// RestoreAssets restores an asset under the given directory recursively
+func RestoreAssets(dir, name string) error {
+ children, err := AssetDir(name)
+ // File
+ if err != nil {
+ return RestoreAsset(dir, name)
+ }
+ // Dir
+ for _, child := range children {
+ err = RestoreAssets(dir, filepath.Join(name, child))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func _filePath(dir, name string) string {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
+}
diff --git a/signer/rules/deps/deps.go b/signer/rules/deps/deps.go
new file mode 100644
index 000000000..5ee00b900
--- /dev/null
+++ b/signer/rules/deps/deps.go
@@ -0,0 +1,21 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+// Package deps contains the console JavaScript dependencies Go embedded.
+package deps
+
+//go:generate go-bindata -nometadata -pkg deps -o bindata.go bignumber.js
+//go:generate gofmt -w -s bindata.go
diff --git a/signer/rules/rules.go b/signer/rules/rules.go
new file mode 100644
index 000000000..fa270436d
--- /dev/null
+++ b/signer/rules/rules.go
@@ -0,0 +1,248 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package rules
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/signer/core"
+ "github.com/ethereum/go-ethereum/signer/rules/deps"
+ "github.com/ethereum/go-ethereum/signer/storage"
+ "github.com/robertkrimen/otto"
+)
+
+var (
+ BigNumber_JS = deps.MustAsset("bignumber.js")
+)
+
+// consoleOutput is an override for the console.log and console.error methods to
+// stream the output into the configured output stream instead of stdout.
+func consoleOutput(call otto.FunctionCall) otto.Value {
+ output := []string{"JS:> "}
+ for _, argument := range call.ArgumentList {
+ output = append(output, fmt.Sprintf("%v", argument))
+ }
+ fmt.Fprintln(os.Stdout, strings.Join(output, " "))
+ return otto.Value{}
+}
+
+// rulesetUi provides an implementation of SignerUI that evaluates a javascript
+// file for each defined UI-method
+type rulesetUi struct {
+ next core.SignerUI // The next handler, for manual processing
+ storage storage.Storage
+ credentials storage.Storage
+ jsRules string // The rules to use
+}
+
+func NewRuleEvaluator(next core.SignerUI, jsbackend, credentialsBackend storage.Storage) (*rulesetUi, error) {
+ c := &rulesetUi{
+ next: next,
+ storage: jsbackend,
+ credentials: credentialsBackend,
+ jsRules: "",
+ }
+
+ return c, nil
+}
+
+func (r *rulesetUi) Init(javascriptRules string) error {
+ r.jsRules = javascriptRules
+ return nil
+}
+func (r *rulesetUi) execute(jsfunc string, jsarg interface{}) (otto.Value, error) {
+
+ // Instantiate a fresh vm engine every time
+ vm := otto.New()
+ // Set the native callbacks
+ consoleObj, _ := vm.Get("console")
+ consoleObj.Object().Set("log", consoleOutput)
+ consoleObj.Object().Set("error", consoleOutput)
+ vm.Set("storage", r.storage)
+
+ // Load bootstrap libraries
+ script, err := vm.Compile("bignumber.js", BigNumber_JS)
+ if err != nil {
+ log.Warn("Failed loading libraries", "err", err)
+ return otto.UndefinedValue(), err
+ }
+ vm.Run(script)
+
+ // Run the actual rule implementation
+ _, err = vm.Run(r.jsRules)
+ if err != nil {
+ log.Warn("Execution failed", "err", err)
+ return otto.UndefinedValue(), err
+ }
+
+ // And the actual call
+ // All calls are objects with the parameters being keys in that object.
+ // To provide additional insulation between js and go, we serialize it into JSON on the Go-side,
+ // and deserialize it on the JS side.
+
+ jsonbytes, err := json.Marshal(jsarg)
+ if err != nil {
+ log.Warn("failed marshalling data", "data", jsarg)
+ return otto.UndefinedValue(), err
+ }
+ // Now, we call foobar(JSON.parse(<jsondata>)).
+ var call string
+ if len(jsonbytes) > 0 {
+ call = fmt.Sprintf("%v(JSON.parse(%v))", jsfunc, string(jsonbytes))
+ } else {
+ call = fmt.Sprintf("%v()", jsfunc)
+ }
+ return vm.Run(call)
+}
+
+func (r *rulesetUi) checkApproval(jsfunc string, jsarg []byte, err error) (bool, error) {
+ if err != nil {
+ return false, err
+ }
+ v, err := r.execute(jsfunc, string(jsarg))
+ if err != nil {
+ log.Info("error occurred during execution", "error", err)
+ return false, err
+ }
+ result, err := v.ToString()
+ if err != nil {
+ log.Info("error occurred during response unmarshalling", "error", err)
+ return false, err
+ }
+ if result == "Approve" {
+ log.Info("Op approved")
+ return true, nil
+ } else if result == "Reject" {
+ log.Info("Op rejected")
+ return false, nil
+ }
+ return false, fmt.Errorf("Unknown response")
+}
+
+func (r *rulesetUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
+ jsonreq, err := json.Marshal(request)
+ approved, err := r.checkApproval("ApproveTx", jsonreq, err)
+ if err != nil {
+ log.Info("Rule-based approval error, going to manual", "error", err)
+ return r.next.ApproveTx(request)
+ }
+
+ if approved {
+ return core.SignTxResponse{
+ Transaction: request.Transaction,
+ Approved: true,
+ Password: r.lookupPassword(request.Transaction.From.Address()),
+ },
+ nil
+ }
+ return core.SignTxResponse{Approved: false}, err
+}
+
+func (r *rulesetUi) lookupPassword(address common.Address) string {
+ return r.credentials.Get(strings.ToLower(address.String()))
+}
+
+func (r *rulesetUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
+ jsonreq, err := json.Marshal(request)
+ approved, err := r.checkApproval("ApproveSignData", jsonreq, err)
+ if err != nil {
+ log.Info("Rule-based approval error, going to manual", "error", err)
+ return r.next.ApproveSignData(request)
+ }
+ if approved {
+ return core.SignDataResponse{Approved: true, Password: r.lookupPassword(request.Address.Address())}, nil
+ }
+ return core.SignDataResponse{Approved: false, Password: ""}, err
+}
+
+func (r *rulesetUi) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
+ jsonreq, err := json.Marshal(request)
+ approved, err := r.checkApproval("ApproveExport", jsonreq, err)
+ if err != nil {
+ log.Info("Rule-based approval error, going to manual", "error", err)
+ return r.next.ApproveExport(request)
+ }
+ if approved {
+ return core.ExportResponse{Approved: true}, nil
+ }
+ return core.ExportResponse{Approved: false}, err
+}
+
+func (r *rulesetUi) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
+ // This cannot be handled by rules, requires setting a password
+ // dispatch to next
+ return r.next.ApproveImport(request)
+}
+
+func (r *rulesetUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
+ jsonreq, err := json.Marshal(request)
+ approved, err := r.checkApproval("ApproveListing", jsonreq, err)
+ if err != nil {
+ log.Info("Rule-based approval error, going to manual", "error", err)
+ return r.next.ApproveListing(request)
+ }
+ if approved {
+ return core.ListResponse{Accounts: request.Accounts}, nil
+ }
+ return core.ListResponse{}, err
+}
+
+func (r *rulesetUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
+ // This cannot be handled by rules, requires setting a password
+ // dispatch to next
+ return r.next.ApproveNewAccount(request)
+}
+
+func (r *rulesetUi) ShowError(message string) {
+ log.Error(message)
+ r.next.ShowError(message)
+}
+
+func (r *rulesetUi) ShowInfo(message string) {
+ log.Info(message)
+ r.next.ShowInfo(message)
+}
+func (r *rulesetUi) OnSignerStartup(info core.StartupInfo) {
+ jsonInfo, err := json.Marshal(info)
+ if err != nil {
+ log.Warn("failed marshalling data", "data", info)
+ return
+ }
+ r.next.OnSignerStartup(info)
+ _, err = r.execute("OnSignerStartup", string(jsonInfo))
+ if err != nil {
+ log.Info("error occurred during execution", "error", err)
+ }
+}
+
+func (r *rulesetUi) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ jsonTx, err := json.Marshal(tx)
+ if err != nil {
+ log.Warn("failed marshalling transaction", "tx", tx)
+ return
+ }
+ _, err = r.execute("OnApprovedTx", string(jsonTx))
+ if err != nil {
+ log.Info("error occurred during execution", "error", err)
+ }
+}
diff --git a/signer/rules/rules_test.go b/signer/rules/rules_test.go
new file mode 100644
index 000000000..6a0403500
--- /dev/null
+++ b/signer/rules/rules_test.go
@@ -0,0 +1,631 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+//
+package rules
+
+import (
+ "fmt"
+ "math/big"
+ "strings"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/signer/core"
+ "github.com/ethereum/go-ethereum/signer/storage"
+)
+
+const JS = `
+/**
+This is an example implementation of a Javascript rule file.
+
+When the signer receives a request over the external API, the corresponding method is evaluated.
+Three things can happen:
+
+1. The method returns "Approve". This means the operation is permitted.
+2. The method returns "Reject". This means the operation is rejected.
+3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
+that the operation will continue to manual processing, via the regular UI method chosen by the user.
+
+[*] Note: Future version of the ruleset may use more complex json-based returnvalues, making it possible to not
+only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
+accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
+
+**/
+
+function ApproveListing(request){
+ console.log("In js approve listing");
+ console.log(request.accounts[3].Address)
+ console.log(request.meta.Remote)
+ return "Approve"
+}
+
+function ApproveTx(request){
+ console.log("test");
+ console.log("from");
+ return "Reject";
+}
+
+function test(thing){
+ console.log(thing.String())
+}
+
+`
+
+func mixAddr(a string) (*common.MixedcaseAddress, error) {
+ return common.NewMixedcaseAddressFromString(a)
+}
+
+type alwaysDenyUi struct{}
+
+func (alwaysDenyUi) OnSignerStartup(info core.StartupInfo) {
+}
+
+func (alwaysDenyUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
+ return core.SignTxResponse{Transaction: request.Transaction, Approved: false, Password: ""}, nil
+}
+
+func (alwaysDenyUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
+ return core.SignDataResponse{Approved: false, Password: ""}, nil
+}
+
+func (alwaysDenyUi) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
+ return core.ExportResponse{Approved: false}, nil
+}
+
+func (alwaysDenyUi) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
+ return core.ImportResponse{Approved: false, OldPassword: "", NewPassword: ""}, nil
+}
+
+func (alwaysDenyUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
+ return core.ListResponse{Accounts: nil}, nil
+}
+
+func (alwaysDenyUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
+ return core.NewAccountResponse{Approved: false, Password: ""}, nil
+}
+
+func (alwaysDenyUi) ShowError(message string) {
+ panic("implement me")
+}
+
+func (alwaysDenyUi) ShowInfo(message string) {
+ panic("implement me")
+}
+
+func (alwaysDenyUi) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ panic("implement me")
+}
+
+func initRuleEngine(js string) (*rulesetUi, error) {
+ r, err := NewRuleEvaluator(&alwaysDenyUi{}, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
+ if err != nil {
+ return nil, fmt.Errorf("failed to create js engine: %v", err)
+ }
+ if err = r.Init(js); err != nil {
+ return nil, fmt.Errorf("failed to load bootstrap js: %v", err)
+ }
+ return r, nil
+}
+
+func TestListRequest(t *testing.T) {
+ accs := make([]core.Account, 5)
+
+ for i := range accs {
+ addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
+ acc := core.Account{
+ Address: common.BytesToAddress(common.Hex2Bytes(addr)),
+ URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
+ }
+ accs[i] = acc
+ }
+
+ js := `function ApproveListing(){ return "Approve" }`
+
+ r, err := initRuleEngine(js)
+ if err != nil {
+ t.Errorf("Couldn't create evaluator %v", err)
+ return
+ }
+ resp, err := r.ApproveListing(&core.ListRequest{
+ Accounts: accs,
+ Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
+ })
+ if len(resp.Accounts) != len(accs) {
+ t.Errorf("Expected check to resolve to 'Approve'")
+ }
+}
+
+func TestSignTxRequest(t *testing.T) {
+
+ js := `
+ function ApproveTx(r){
+ console.log("transaction.from", r.transaction.from);
+ console.log("transaction.to", r.transaction.to);
+ console.log("transaction.value", r.transaction.value);
+ console.log("transaction.nonce", r.transaction.nonce);
+ if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
+ if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
+ }`
+
+ r, err := initRuleEngine(js)
+ if err != nil {
+ t.Errorf("Couldn't create evaluator %v", err)
+ return
+ }
+ to, err := mixAddr("000000000000000000000000000000000000dead")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ from, err := mixAddr("0000000000000000000000000000000000001337")
+
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ fmt.Printf("to %v", to.Address().String())
+ resp, err := r.ApproveTx(&core.SignTxRequest{
+ Transaction: core.SendTxArgs{
+ From: *from,
+ To: to},
+ Callinfo: nil,
+ Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
+ })
+ if err != nil {
+ t.Errorf("Unexpected error %v", err)
+ }
+ if !resp.Approved {
+ t.Errorf("Expected check to resolve to 'Approve'")
+ }
+}
+
+type dummyUi struct {
+ calls []string
+}
+
+func (d *dummyUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
+ d.calls = append(d.calls, "ApproveTx")
+ return core.SignTxResponse{}, core.ErrRequestDenied
+}
+
+func (d *dummyUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
+ d.calls = append(d.calls, "ApproveSignData")
+ return core.SignDataResponse{}, core.ErrRequestDenied
+}
+
+func (d *dummyUi) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
+ d.calls = append(d.calls, "ApproveExport")
+ return core.ExportResponse{}, core.ErrRequestDenied
+}
+
+func (d *dummyUi) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
+ d.calls = append(d.calls, "ApproveImport")
+ return core.ImportResponse{}, core.ErrRequestDenied
+}
+
+func (d *dummyUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
+ d.calls = append(d.calls, "ApproveListing")
+ return core.ListResponse{}, core.ErrRequestDenied
+}
+
+func (d *dummyUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
+ d.calls = append(d.calls, "ApproveNewAccount")
+ return core.NewAccountResponse{}, core.ErrRequestDenied
+}
+
+func (d *dummyUi) ShowError(message string) {
+ d.calls = append(d.calls, "ShowError")
+}
+
+func (d *dummyUi) ShowInfo(message string) {
+ d.calls = append(d.calls, "ShowInfo")
+}
+
+func (d *dummyUi) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ d.calls = append(d.calls, "OnApprovedTx")
+}
+func (d *dummyUi) OnSignerStartup(info core.StartupInfo) {
+}
+
+//TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
+func TestForwarding(t *testing.T) {
+
+ js := ""
+ ui := &dummyUi{make([]string, 0)}
+ jsBackend := storage.NewEphemeralStorage()
+ credBackend := storage.NewEphemeralStorage()
+ r, err := NewRuleEvaluator(ui, jsBackend, credBackend)
+ if err != nil {
+ t.Fatalf("Failed to create js engine: %v", err)
+ }
+ if err = r.Init(js); err != nil {
+ t.Fatalf("Failed to load bootstrap js: %v", err)
+ }
+ r.ApproveSignData(nil)
+ r.ApproveTx(nil)
+ r.ApproveImport(nil)
+ r.ApproveNewAccount(nil)
+ r.ApproveListing(nil)
+ r.ApproveExport(nil)
+ r.ShowError("test")
+ r.ShowInfo("test")
+
+ //This one is not forwarded
+ r.OnApprovedTx(ethapi.SignTransactionResult{})
+
+ expCalls := 8
+ if len(ui.calls) != expCalls {
+
+ t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ","))
+
+ }
+
+}
+
+func TestMissingFunc(t *testing.T) {
+ r, err := initRuleEngine(JS)
+ if err != nil {
+ t.Errorf("Couldn't create evaluator %v", err)
+ return
+ }
+
+ _, err = r.execute("MissingMethod", "test")
+
+ if err == nil {
+ t.Error("Expected error")
+ }
+
+ approved, err := r.checkApproval("MissingMethod", nil, nil)
+ if err == nil {
+ t.Errorf("Expected missing method to yield error'")
+ }
+ if approved {
+ t.Errorf("Expected missing method to cause non-approval")
+ }
+ fmt.Printf("Err %v", err)
+
+}
+func TestStorage(t *testing.T) {
+
+ js := `
+ function testStorage(){
+ storage.Put("mykey", "myvalue")
+ a = storage.Get("mykey")
+
+ storage.Put("mykey", ["a", "list"]) // Should result in "a,list"
+ a += storage.Get("mykey")
+
+
+ storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]"
+ a += storage.Get("mykey")
+
+
+ storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
+ a += storage.Get("mykey")
+
+ a += storage.Get("missingkey") //Missing keys should result in empty string
+ storage.Put("","missing key==noop") // Can't store with 0-length key
+ a += storage.Get("") // Should result in ''
+
+ var b = new BigNumber(2)
+ var c = new BigNumber(16)//"0xf0",16)
+ var d = b.plus(c)
+ console.log(d)
+ return a
+ }
+`
+ r, err := initRuleEngine(js)
+ if err != nil {
+ t.Errorf("Couldn't create evaluator %v", err)
+ return
+ }
+
+ v, err := r.execute("testStorage", nil)
+
+ if err != nil {
+ t.Errorf("Unexpected error %v", err)
+ }
+
+ retval, err := v.ToString()
+
+ if err != nil {
+ t.Errorf("Unexpected error %v", err)
+ }
+ exp := `myvaluea,list[object Object]{"an":"object"}`
+ if retval != exp {
+ t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
+ }
+ fmt.Printf("Err %v", err)
+
+}
+
+const ExampleTxWindow = `
+ function big(str){
+ if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
+ return new BigNumber(str)
+ }
+
+ // Time window: 1 week
+ var window = 1000* 3600*24*7;
+
+ // Limit : 1 ether
+ var limit = new BigNumber("1e18");
+
+ function isLimitOk(transaction){
+ var value = big(transaction.value)
+ // Start of our window function
+ var windowstart = new Date().getTime() - window;
+
+ var txs = [];
+ var stored = storage.Get('txs');
+
+ if(stored != ""){
+ txs = JSON.parse(stored)
+ }
+ // First, remove all that have passed out of the time-window
+ var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
+ console.log(txs, newtxs.length);
+
+ // Secondly, aggregate the current sum
+ sum = new BigNumber(0)
+
+ sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
+ console.log("ApproveTx > Sum so far", sum);
+ console.log("ApproveTx > Requested", value.toNumber());
+
+ // Would we exceed weekly limit ?
+ return sum.plus(value).lt(limit)
+
+ }
+ function ApproveTx(r){
+ console.log(r)
+ console.log(typeof(r))
+ if (isLimitOk(r.transaction)){
+ return "Approve"
+ }
+ return "Nope"
+ }
+
+ /**
+ * OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
+ * 'response_str' contains the return value that will be sent to the external caller.
+ * The return value from this method is ignore - the reason for having this callback is to allow the
+ * ruleset to keep track of approved transactions.
+ *
+ * When implementing rate-limited rules, this callback should be used.
+ * If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
+ * then accepts the transaction, this method will be called.
+ *
+ * TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
+ */
+ function OnApprovedTx(resp){
+ var value = big(resp.tx.value)
+ var txs = []
+ // Load stored transactions
+ var stored = storage.Get('txs');
+ if(stored != ""){
+ txs = JSON.parse(stored)
+ }
+ // Add this to the storage
+ txs.push({tstamp: new Date().getTime(), value: value});
+ storage.Put("txs", JSON.stringify(txs));
+ }
+
+`
+
+func dummyTx(value hexutil.Big) *core.SignTxRequest {
+
+ to, _ := mixAddr("000000000000000000000000000000000000dead")
+ from, _ := mixAddr("000000000000000000000000000000000000dead")
+ n := hexutil.Uint64(3)
+ gas := hexutil.Uint64(21000)
+ gasPrice := hexutil.Big(*big.NewInt(2000000))
+
+ return &core.SignTxRequest{
+ Transaction: core.SendTxArgs{
+ From: *from,
+ To: to,
+ Value: value,
+ Nonce: n,
+ GasPrice: gasPrice,
+ Gas: gas,
+ },
+ Callinfo: []core.ValidationInfo{
+ {Typ: "Warning", Message: "All your base are bellong to us"},
+ },
+ Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
+ }
+}
+func dummyTxWithV(value uint64) *core.SignTxRequest {
+
+ v := big.NewInt(0).SetUint64(value)
+ h := hexutil.Big(*v)
+ return dummyTx(h)
+}
+func dummySigned(value *big.Int) *types.Transaction {
+ to := common.HexToAddress("000000000000000000000000000000000000dead")
+ gas := uint64(21000)
+ gasPrice := big.NewInt(2000000)
+ data := make([]byte, 0)
+ return types.NewTransaction(3, to, value, gas, gasPrice, data)
+
+}
+func TestLimitWindow(t *testing.T) {
+
+ r, err := initRuleEngine(ExampleTxWindow)
+ if err != nil {
+ t.Errorf("Couldn't create evaluator %v", err)
+ return
+ }
+
+ // 0.3 ether: 429D069189E0000 wei
+ v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
+ h := hexutil.Big(*v)
+ // The first three should succeed
+ for i := 0; i < 3; i++ {
+ unsigned := dummyTx(h)
+ resp, err := r.ApproveTx(unsigned)
+ if err != nil {
+ t.Errorf("Unexpected error %v", err)
+ }
+ if !resp.Approved {
+ t.Errorf("Expected check to resolve to 'Approve'")
+ }
+ // Create a dummy signed transaction
+
+ response := ethapi.SignTransactionResult{
+ Tx: dummySigned(v),
+ Raw: common.Hex2Bytes("deadbeef"),
+ }
+ r.OnApprovedTx(response)
+ }
+ // Fourth should fail
+ resp, err := r.ApproveTx(dummyTx(h))
+ if resp.Approved {
+ t.Errorf("Expected check to resolve to 'Reject'")
+ }
+
+}
+
+// dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
+type dontCallMe struct {
+ t *testing.T
+}
+
+func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
+}
+
+func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+ return core.SignTxResponse{}, core.ErrRequestDenied
+}
+
+func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+ return core.SignDataResponse{}, core.ErrRequestDenied
+}
+
+func (d *dontCallMe) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+ return core.ExportResponse{}, core.ErrRequestDenied
+}
+
+func (d *dontCallMe) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+ return core.ImportResponse{}, core.ErrRequestDenied
+}
+
+func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+ return core.ListResponse{}, core.ErrRequestDenied
+}
+
+func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+ return core.NewAccountResponse{}, core.ErrRequestDenied
+}
+
+func (d *dontCallMe) ShowError(message string) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+}
+
+func (d *dontCallMe) ShowInfo(message string) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+}
+
+func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ d.t.Fatalf("Did not expect next-handler to be called")
+}
+
+//TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
+// if it does, that would be bad since developers may rely on that to store data,
+// instead of using the disk-based data storage
+func TestContextIsCleared(t *testing.T) {
+
+ js := `
+ function ApproveTx(){
+ if (typeof foobar == 'undefined') {
+ foobar = "Approve"
+ }
+ console.log(foobar)
+ if (foobar == "Approve"){
+ foobar = "Reject"
+ }else{
+ foobar = "Approve"
+ }
+ return foobar
+ }
+ `
+ ui := &dontCallMe{t}
+ r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
+ if err != nil {
+ t.Fatalf("Failed to create js engine: %v", err)
+ }
+ if err = r.Init(js); err != nil {
+ t.Fatalf("Failed to load bootstrap js: %v", err)
+ }
+ tx := dummyTxWithV(0)
+ r1, err := r.ApproveTx(tx)
+ r2, err := r.ApproveTx(tx)
+ if r1.Approved != r2.Approved {
+ t.Errorf("Expected execution context to be cleared between executions")
+ }
+}
+
+func TestSignData(t *testing.T) {
+
+ js := `function ApproveListing(){
+ return "Approve"
+}
+function ApproveSignData(r){
+ if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
+ {
+ if(r.message.indexOf("bazonk") >= 0){
+ return "Approve"
+ }
+ return "Reject"
+ }
+ // Otherwise goes to manual processing
+}`
+ r, err := initRuleEngine(js)
+ if err != nil {
+ t.Errorf("Couldn't create evaluator %v", err)
+ return
+ }
+ message := []byte("baz bazonk foo")
+ hash, msg := core.SignHash(message)
+ raw := hexutil.Bytes(message)
+ addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
+
+ fmt.Printf("address %v %v\n", addr.String(), addr.Original())
+ resp, err := r.ApproveSignData(&core.SignDataRequest{
+ Address: *addr,
+ Message: msg,
+ Hash: hash,
+ Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
+ Rawdata: raw,
+ })
+ if err != nil {
+ t.Fatalf("Unexpected error %v", err)
+ }
+ if !resp.Approved {
+ t.Fatalf("Expected approved")
+ }
+}
diff --git a/signer/storage/aes_gcm_storage.go b/signer/storage/aes_gcm_storage.go
new file mode 100644
index 000000000..1ac347558
--- /dev/null
+++ b/signer/storage/aes_gcm_storage.go
@@ -0,0 +1,164 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+//
+package storage
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/json"
+ "io"
+ "io/ioutil"
+ "os"
+
+ "github.com/ethereum/go-ethereum/log"
+)
+
+type storedCredential struct {
+ // The iv
+ Iv []byte `json:"iv"`
+ // The ciphertext
+ CipherText []byte `json:"c"`
+}
+
+// AESEncryptedStorage is a storage type which is backed by a json-faile. The json-file contains
+// key-value mappings, where the keys are _not_ encrypted, only the values are.
+type AESEncryptedStorage struct {
+ // File to read/write credentials
+ filename string
+ // Key stored in base64
+ key []byte
+}
+
+// NewAESEncryptedStorage creates a new encrypted storage backed by the given file/key
+func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage {
+ return &AESEncryptedStorage{
+ filename: filename,
+ key: key,
+ }
+}
+
+// Put stores a value by key. 0-length keys results in no-op
+func (s *AESEncryptedStorage) Put(key, value string) {
+ if len(key) == 0 {
+ return
+ }
+ data, err := s.readEncryptedStorage()
+ if err != nil {
+ log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
+ return
+ }
+ ciphertext, iv, err := encrypt(s.key, []byte(value))
+ if err != nil {
+ log.Warn("Failed to encrypt entry", "err", err)
+ return
+ }
+ encrypted := storedCredential{Iv: iv, CipherText: ciphertext}
+ data[key] = encrypted
+ if err = s.writeEncryptedStorage(data); err != nil {
+ log.Warn("Failed to write entry", "err", err)
+ }
+}
+
+// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
+func (s *AESEncryptedStorage) Get(key string) string {
+ if len(key) == 0 {
+ return ""
+ }
+ data, err := s.readEncryptedStorage()
+ if err != nil {
+ log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
+ return ""
+ }
+ encrypted, exist := data[key]
+ if !exist {
+ log.Warn("Key does not exist", "key", key)
+ return ""
+ }
+ entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText)
+ if err != nil {
+ log.Warn("Failed to decrypt key", "key", key)
+ return ""
+ }
+ return string(entry)
+}
+
+// readEncryptedStorage reads the file with encrypted creds
+func (s *AESEncryptedStorage) readEncryptedStorage() (map[string]storedCredential, error) {
+ creds := make(map[string]storedCredential)
+ raw, err := ioutil.ReadFile(s.filename)
+
+ if err != nil {
+ if os.IsNotExist(err) {
+ // Doesn't exist yet
+ return creds, nil
+
+ } else {
+ log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
+ }
+ }
+ if err = json.Unmarshal(raw, &creds); err != nil {
+ log.Warn("Failed to unmarshal encrypted storage", "err", err, "file", s.filename)
+ return nil, err
+ }
+ return creds, nil
+}
+
+// writeEncryptedStorage write the file with encrypted creds
+func (s *AESEncryptedStorage) writeEncryptedStorage(creds map[string]storedCredential) error {
+ raw, err := json.Marshal(creds)
+ if err != nil {
+ return err
+ }
+ if err = ioutil.WriteFile(s.filename, raw, 0600); err != nil {
+ return err
+ }
+ return nil
+}
+
+func encrypt(key []byte, plaintext []byte) ([]byte, []byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, nil, err
+ }
+ aesgcm, err := cipher.NewGCM(block)
+ nonce := make([]byte, aesgcm.NonceSize())
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+ return nil, nil, err
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
+ return ciphertext, nonce, nil
+}
+
+func decrypt(key []byte, nonce []byte, ciphertext []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+ aesgcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, err
+ }
+ plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return nil, err
+ }
+ return plaintext, nil
+}
diff --git a/signer/storage/aes_gcm_storage_test.go b/signer/storage/aes_gcm_storage_test.go
new file mode 100644
index 000000000..77804905a
--- /dev/null
+++ b/signer/storage/aes_gcm_storage_test.go
@@ -0,0 +1,115 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+//
+package storage
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/mattn/go-colorable"
+)
+
+func TestEncryption(t *testing.T) {
+ // key := []byte("AES256Key-32Characters1234567890")
+ // plaintext := []byte(value)
+ key := []byte("AES256Key-32Characters1234567890")
+ plaintext := []byte("exampleplaintext")
+
+ c, iv, err := encrypt(key, plaintext)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Printf("Ciphertext %x, nonce %x\n", c, iv)
+
+ p, err := decrypt(key, iv, c)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Printf("Plaintext %v\n", string(p))
+ if !bytes.Equal(plaintext, p) {
+ t.Errorf("Failed: expected plaintext recovery, got %v expected %v", string(plaintext), string(p))
+ }
+}
+
+func TestFileStorage(t *testing.T) {
+
+ a := map[string]storedCredential{
+ "secret": {
+ Iv: common.Hex2Bytes("cdb30036279601aeee60f16b"),
+ CipherText: common.Hex2Bytes("f311ac49859d7260c2c464c28ffac122daf6be801d3cfd3edcbde7e00c9ff74f"),
+ },
+ "secret2": {
+ Iv: common.Hex2Bytes("afb8a7579bf971db9f8ceeed"),
+ CipherText: common.Hex2Bytes("2df87baf86b5073ef1f03e3cc738de75b511400f5465bb0ddeacf47ae4dc267d"),
+ },
+ }
+ d, err := ioutil.TempDir("", "eth-encrypted-storage-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ stored := &AESEncryptedStorage{
+ filename: fmt.Sprintf("%v/vault.json", d),
+ key: []byte("AES256Key-32Characters1234567890"),
+ }
+ stored.writeEncryptedStorage(a)
+ read := &AESEncryptedStorage{
+ filename: fmt.Sprintf("%v/vault.json", d),
+ key: []byte("AES256Key-32Characters1234567890"),
+ }
+ creds, err := read.readEncryptedStorage()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for k, v := range a {
+ if v2, exist := creds[k]; !exist {
+ t.Errorf("Missing entry %v", k)
+ } else {
+ if !bytes.Equal(v.CipherText, v2.CipherText) {
+ t.Errorf("Wrong ciphertext, expected %x got %x", v.CipherText, v2.CipherText)
+ }
+ if !bytes.Equal(v.Iv, v2.Iv) {
+ t.Errorf("Wrong iv")
+ }
+ }
+ }
+}
+func TestEnd2End(t *testing.T) {
+ log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(3), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
+
+ d, err := ioutil.TempDir("", "eth-encrypted-storage-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s1 := &AESEncryptedStorage{
+ filename: fmt.Sprintf("%v/vault.json", d),
+ key: []byte("AES256Key-32Characters1234567890"),
+ }
+ s2 := &AESEncryptedStorage{
+ filename: fmt.Sprintf("%v/vault.json", d),
+ key: []byte("AES256Key-32Characters1234567890"),
+ }
+
+ s1.Put("bazonk", "foobar")
+ if v := s2.Get("bazonk"); v != "foobar" {
+ t.Errorf("Expected bazonk->foobar, got '%v'", v)
+ }
+}
diff --git a/signer/storage/storage.go b/signer/storage/storage.go
new file mode 100644
index 000000000..60f4e3892
--- /dev/null
+++ b/signer/storage/storage.go
@@ -0,0 +1,62 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum 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.
+//
+// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+//
+
+package storage
+
+import (
+ "fmt"
+)
+
+type Storage interface {
+ // Put stores a value by key. 0-length keys results in no-op
+ Put(key, value string)
+ // Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
+ Get(key string) string
+}
+
+// EphemeralStorage is an in-memory storage that does
+// not persist values to disk. Mainly used for testing
+type EphemeralStorage struct {
+ data map[string]string
+ namespace string
+}
+
+func (s *EphemeralStorage) Put(key, value string) {
+ if len(key) == 0 {
+ return
+ }
+ fmt.Printf("storage: put %v -> %v\n", key, value)
+ s.data[key] = value
+}
+
+func (s *EphemeralStorage) Get(key string) string {
+ if len(key) == 0 {
+ return ""
+ }
+ fmt.Printf("storage: get %v\n", key)
+ if v, exist := s.data[key]; exist {
+ return v
+ }
+ return ""
+}
+
+func NewEphemeralStorage() Storage {
+ s := &EphemeralStorage{
+ data: make(map[string]string),
+ }
+ return s
+}