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
This commit is contained in:
committed by
Péter Szilágyi
parent
de2a7bb764
commit
ec3db0f56c
256
signer/core/abihelper.go
Normal file
256
signer/core/abihelper.go
Normal file
@ -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)
|
||||
}
|
247
signer/core/abihelper_test.go
Normal file
247
signer/core/abihelper_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
500
signer/core/api.go
Normal file
500
signer/core/api.go
Normal file
@ -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
|
||||
}
|
386
signer/core/api_test.go
Normal file
386
signer/core/api_test.go
Normal file
@ -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)
|
||||
|
||||
|
||||
}
|
||||
*/
|
110
signer/core/auditlog.go
Normal file
110
signer/core/auditlog.go
Normal file
@ -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
|
||||
}
|
247
signer/core/cliui.go
Normal file
247
signer/core/cliui.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
113
signer/core/stdioui.go
Normal file
113
signer/core/stdioui.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
95
signer/core/types.go
Normal file
95
signer/core/types.go
Normal file
@ -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)
|
||||
}
|
163
signer/core/validation.go
Normal file
163
signer/core/validation.go
Normal file
@ -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)
|
||||
}
|
139
signer/core/validation_test.go
Normal file
139
signer/core/validation_test.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user