forked from mirror/go-ethereum
cmd/clef, signer: make fourbyte its own package, break dep cycle (#19450)
* cmd/clef, signer: make fourbytes its own package, break dep cycle * signer/fourbyte: pull in a sanitized 4byte databaseChrisChinchilla-patch-3
parent
26b50e3ebe
commit
d5af3a584c
File diff suppressed because one or more lines are too long
@ -1,257 +0,0 @@ |
||||
// 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" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"regexp" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
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 val := arg.value.(type) { |
||||
case fmt.Stringer: |
||||
value = val.String() |
||||
default: |
||||
value = fmt.Sprintf("%v", val) |
||||
} |
||||
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) |
||||
} |
||||
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 |
||||
} |
||||
if err := json.Unmarshal(raw, &db.db); err != nil { |
||||
return nil, err |
||||
} |
||||
return db, nil |
||||
} |
||||
|
||||
// NewAbiDBFromFiles loads both the standard signature database (resource file)and a custom database.
|
||||
// The latter will be used to write new values into if they are submitted via the API
|
||||
func NewAbiDBFromFiles(raw []byte, custom string) (*AbiDb, error) { |
||||
|
||||
db := &AbiDb{make(map[string]string), make(map[string]string), custom} |
||||
db.customdbPath = custom |
||||
|
||||
if err := json.Unmarshal(raw, &db.db); err != nil { |
||||
return nil, err |
||||
} |
||||
// 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 |
||||
} |
||||
if err := json.Unmarshal(raw, &db.customdb); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
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 := hex.EncodeToString(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 |
||||
} |
||||
|
||||
// AddSignature 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 := hex.EncodeToString(data[:4]) |
||||
return db.saveCustomAbi(selector, sig) |
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,164 @@ |
||||
// 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 fourbyte |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"regexp" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
// decodedCallData is an internal type to represent a method call parsed according
|
||||
// to an ABI method signature.
|
||||
type decodedCallData struct { |
||||
signature string |
||||
name string |
||||
inputs []decodedArgument |
||||
} |
||||
|
||||
// decodedArgument is an internal type to represent an argument parsed according
|
||||
// to an ABI method signature.
|
||||
type decodedArgument struct { |
||||
soltype abi.Argument |
||||
value interface{} |
||||
} |
||||
|
||||
// String implements stringer interface, tries to use the underlying value-type
|
||||
func (arg decodedArgument) String() string { |
||||
var value string |
||||
switch val := arg.value.(type) { |
||||
case fmt.Stringer: |
||||
value = val.String() |
||||
default: |
||||
value = fmt.Sprintf("%v", val) |
||||
} |
||||
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, ",")) |
||||
} |
||||
|
||||
// verifySelector checks whether the ABI encoded data blob matches the requested
|
||||
// function signature.
|
||||
func verifySelector(selector string, calldata []byte) (*decodedCallData, error) { |
||||
// Parse the selector into an ABI JSON spec
|
||||
abidata, err := parseSelector(selector) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Parse the call data according to the requested selector
|
||||
return parseCallData(calldata, string(abidata)) |
||||
} |
||||
|
||||
// selectorRegexp is used to validate that a 4byte database selector corresponds
|
||||
// to a valid ABI function declaration.
|
||||
//
|
||||
// Note, although uppercase letters are not part of the ABI spec, this regexp
|
||||
// still accepts it as the general format is valid. It will be rejected later
|
||||
// by the type checker.
|
||||
var selectorRegexp = regexp.MustCompile(`^([^\)]+)\(([A-Za-z0-9,\[\]]*)\)`) |
||||
|
||||
// parseSelector converts a method selector into an ABI JSON spec. The returned
|
||||
// data is a valid JSON string which can be consumed by the standard abi package.
|
||||
func parseSelector(selector string) ([]byte, error) { |
||||
// Define a tiny fake ABI struct for JSON marshalling
|
||||
type fakeArg struct { |
||||
Type string `json:"type"` |
||||
} |
||||
type fakeABI struct { |
||||
Name string `json:"name"` |
||||
Type string `json:"type"` |
||||
Inputs []fakeArg `json:"inputs"` |
||||
} |
||||
// Validate the selector and extract it's components
|
||||
groups := selectorRegexp.FindStringSubmatch(selector) |
||||
if len(groups) != 3 { |
||||
return nil, fmt.Errorf("invalid selector %s (%v matches)", selector, len(groups)) |
||||
} |
||||
name := groups[1] |
||||
args := groups[2] |
||||
|
||||
// Reassemble the fake ABI and constuct the JSON
|
||||
arguments := make([]fakeArg, 0) |
||||
if len(args) > 0 { |
||||
for _, arg := range strings.Split(args, ",") { |
||||
arguments = append(arguments, fakeArg{arg}) |
||||
} |
||||
} |
||||
return json.Marshal([]fakeABI{{name, "function", arguments}}) |
||||
} |
||||
|
||||
// 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) { |
||||
// Validate the call data that it has the 4byte prefix and the rest divisible by 32 bytes
|
||||
if len(calldata) < 4 { |
||||
return nil, fmt.Errorf("invalid call data, incomplete method signature (%d bytes < 4)", len(calldata)) |
||||
} |
||||
sigdata := calldata[:4] |
||||
|
||||
argdata := calldata[4:] |
||||
if len(argdata)%32 != 0 { |
||||
return nil, fmt.Errorf("invalid call data; length should be a multiple of 32 bytes (was %d)", len(argdata)) |
||||
} |
||||
// Validate the called method and upack the call data accordingly
|
||||
abispec, err := abi.JSON(strings.NewReader(abidata)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("invalid method signature (%s): %v", abidata, err) |
||||
} |
||||
method, err := abispec.MethodById(sigdata) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
values, err := method.Inputs.UnpackValues(argdata) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Everything valid, assemble the call infos for the signer
|
||||
decoded := decodedCallData{signature: method.Sig(), name: method.Name} |
||||
for i := 0; i < len(method.Inputs); i++ { |
||||
decoded.inputs = append(decoded.inputs, decodedArgument{ |
||||
soltype: method.Inputs[i], |
||||
value: values[i], |
||||
}) |
||||
} |
||||
// 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
|
||||
// be possible to stuff extra data into the arguments, which is not detected
|
||||
// by merely decoding the data.
|
||||
encoded, err := method.Inputs.PackValues(values) |
||||
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 |
||||
} |
@ -0,0 +1,143 @@ |
||||
// Copyright 2019 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/>.
|
||||
|
||||
//go:generate go-bindata -nometadata -o 4byte.go -pkg fourbyte 4byte.json
|
||||
//go:generate gofmt -s -w 4byte.go
|
||||
|
||||
// Package fourbyte contains the 4byte database.
|
||||
package fourbyte |
||||
|
||||
import ( |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
) |
||||
|
||||
// Database is a 4byte database with the possibility of maintaining an immutable
|
||||
// set (embedded) into the process and a mutable set (loaded and written to file).
|
||||
type Database struct { |
||||
embedded map[string]string |
||||
custom map[string]string |
||||
customPath string |
||||
} |
||||
|
||||
// newEmpty exists for testing purposes.
|
||||
func newEmpty() *Database { |
||||
return &Database{ |
||||
embedded: make(map[string]string), |
||||
custom: make(map[string]string), |
||||
} |
||||
} |
||||
|
||||
// New loads the standard signature database embedded in the package.
|
||||
func New() (*Database, error) { |
||||
return NewWithFile("") |
||||
} |
||||
|
||||
// NewFromFile loads signature database from file, and errors if the file is not
|
||||
// valid JSON. The constructor does no other validation of contents. This method
|
||||
// does not load the embedded 4byte database.
|
||||
//
|
||||
// The provided path will be used to write new values into if they are submitted
|
||||
// via the API.
|
||||
func NewFromFile(path string) (*Database, error) { |
||||
raw, err := os.Open(path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer raw.Close() |
||||
|
||||
db := newEmpty() |
||||
if err := json.NewDecoder(raw).Decode(&db.embedded); err != nil { |
||||
return nil, err |
||||
} |
||||
return db, nil |
||||
} |
||||
|
||||
// NewWithFile loads both the standard signature database (embedded resource
|
||||
// file) as well as a custom database. The latter will be used to write new
|
||||
// values into if they are submitted via the API.
|
||||
func NewWithFile(path string) (*Database, error) { |
||||
db := &Database{make(map[string]string), make(map[string]string), path} |
||||
db.customPath = path |
||||
|
||||
blob, err := Asset("4byte.json") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if err := json.Unmarshal(blob, &db.embedded); err != nil { |
||||
return nil, err |
||||
} |
||||
// Custom file may not exist. Will be created during save, if needed.
|
||||
if _, err := os.Stat(path); err == nil { |
||||
if blob, err = ioutil.ReadFile(path); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := json.Unmarshal(blob, &db.custom); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
return db, nil |
||||
} |
||||
|
||||
// Size returns the number of 4byte entries in the embedded and custom datasets.
|
||||
func (db *Database) Size() (int, int) { |
||||
return len(db.embedded), len(db.custom) |
||||
} |
||||
|
||||
// Selector checks the given 4byte ID against the known ABI methods.
|
||||
//
|
||||
// This method does not validate the match, it's assumed the caller will do.
|
||||
func (db *Database) Selector(id []byte) (string, error) { |
||||
if len(id) < 4 { |
||||
return "", fmt.Errorf("expected 4-byte id, got %d", len(id)) |
||||
} |
||||
sig := hex.EncodeToString(id[:4]) |
||||
if selector, exists := db.embedded[sig]; exists { |
||||
return selector, nil |
||||
} |
||||
if selector, exists := db.custom[sig]; exists { |
||||
return selector, nil |
||||
} |
||||
return "", fmt.Errorf("signature %v not found", sig) |
||||
} |
||||
|
||||
// AddSelector inserts a new 4byte entry into the database. If custom database
|
||||
// saving is enabled, the new dataset is also persisted to disk.
|
||||
//
|
||||
// Node, this method does _not_ validate the correctness of the data. It assumes
|
||||
// the caller has already done so.
|
||||
func (db *Database) AddSelector(selector string, data []byte) error { |
||||
// If the selector is already known, skip duplicating it
|
||||
if len(data) < 4 { |
||||
return nil |
||||
} |
||||
if _, err := db.Selector(data[:4]); err == nil { |
||||
return nil |
||||
} |
||||
// Inject the custom selector into the database and persist if needed
|
||||
db.custom[hex.EncodeToString(data[:4])] = selector |
||||
if db.customPath == "" { |
||||
return nil |
||||
} |
||||
blob, err := json.Marshal(db.custom) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return ioutil.WriteFile(db.customPath, blob, 0600) |
||||
} |
@ -0,0 +1,91 @@ |
||||
// 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 fourbyte |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io/ioutil" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
// Tests that all the selectors contained in the 4byte database are valid.
|
||||
func TestEmbeddedDatabase(t *testing.T) { |
||||
db, err := New() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
for id, selector := range db.embedded { |
||||
abistring, err := parseSelector(selector) |
||||
if err != nil { |
||||
t.Errorf("Failed to convert selector to ABI: %v", err) |
||||
continue |
||||
} |
||||
abistruct, err := abi.JSON(strings.NewReader(string(abistring))) |
||||
if err != nil { |
||||
t.Errorf("Failed to parse ABI: %v", err) |
||||
continue |
||||
} |
||||
m, err := abistruct.MethodById(common.Hex2Bytes(id)) |
||||
if err != nil { |
||||
t.Errorf("Failed to get method by id (%s): %v", id, err) |
||||
continue |
||||
} |
||||
if m.Sig() != selector { |
||||
t.Errorf("Selector mismatch: have %v, want %v", m.Sig(), selector) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tests that custom 4byte datasets can be handled too.
|
||||
func TestCustomDatabase(t *testing.T) { |
||||
// Create a new custom 4byte database with no embedded component
|
||||
tmpdir, err := ioutil.TempDir("", "signer-4byte-test") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
filename := fmt.Sprintf("%s/4byte_custom.json", tmpdir) |
||||
|
||||
db, err := NewWithFile(filename) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
db.embedded = make(map[string]string) |
||||
|
||||
// Ensure the database is empty, insert and verify
|
||||
calldata := common.Hex2Bytes("a52c101edeadbeef") |
||||
if _, err = db.Selector(calldata); err == nil { |
||||
t.Fatalf("Should not find a match on empty database") |
||||
} |
||||
if err = db.AddSelector("send(uint256)", calldata); err != nil { |
||||
t.Fatalf("Failed to save file: %v", err) |
||||
} |
||||
if _, err = db.Selector(calldata); err != nil { |
||||
t.Fatalf("Failed to find a match for abi signature: %v", err) |
||||
} |
||||
// Check that the file as persisted to disk by creating a new instance
|
||||
db2, err := NewFromFile(filename) |
||||
if err != nil { |
||||
t.Fatalf("Failed to create new abidb: %v", err) |
||||
} |
||||
if _, err = db2.Selector(calldata); err != nil { |
||||
t.Fatalf("Failed to find a match for persisted abi signature: %v", err) |
||||
} |
||||
} |
@ -0,0 +1,117 @@ |
||||
// 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 fourbyte |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/signer/core" |
||||
) |
||||
|
||||
// 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 (db *Database) ValidateTransaction(selector *string, tx *core.SendTxArgs) (*core.ValidationMessages, error) { |
||||
messages := new(core.ValidationMessages) |
||||
|
||||
// Prevent accidental erroneous usage of both 'input' and 'data' (show stopper)
|
||||
if tx.Data != nil && tx.Input != nil && !bytes.Equal(*tx.Data, *tx.Input) { |
||||
return nil, errors.New(`ambiguous request: both "data" and "input" are set and are not identical`) |
||||
} |
||||
// Place data on 'data', and nil 'input'
|
||||
var data []byte |
||||
if tx.Input != nil { |
||||
tx.Data = tx.Input |
||||
tx.Input = nil |
||||
} |
||||
if tx.Data != nil { |
||||
data = *tx.Data |
||||
} |
||||
// Contract creation doesn't validate call data, handle first
|
||||
if tx.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 { |
||||
// Prevent sending ether into black hole (show stopper)
|
||||
if tx.Value.ToInt().Cmp(big.NewInt(0)) > 0 { |
||||
return nil, errors.New("tx will create contract with value but empty code") |
||||
} |
||||
// No value submitted at least, critically Warn, but don't blow up
|
||||
messages.Crit("Transaction will create contract with empty code") |
||||
} else if len(data) < 40 { // arbitrary heuristic limit
|
||||
messages.Warn(fmt.Sprintf("Transaction will will create contract, but payload is suspiciously small (%d bytes)", len(data))) |
||||
} |
||||
// Method selector should be nil for contract creation
|
||||
if selector != nil { |
||||
messages.Warn("Transaction will create contract, but method selector supplied, indicating intent to call a method") |
||||
} |
||||
return messages, nil |
||||
} |
||||
// Not a contract creation, validate as a plain transaction
|
||||
if !tx.To.ValidChecksum() { |
||||
messages.Warn("Invalid checksum on recipient address") |
||||
} |
||||
if bytes.Equal(tx.To.Address().Bytes(), common.Address{}.Bytes()) { |
||||
messages.Crit("Transaction recipient is the zero address") |
||||
} |
||||
// Semantic fields validated, try to make heads or tails of the call data
|
||||
db.validateCallData(selector, data, messages) |
||||
return messages, nil |
||||
} |
||||
|
||||
// validateCallData checks if the ABI call-data + method selector (if given) can
|
||||
// be parsed and seems to match.
|
||||
func (db *Database) validateCallData(selector *string, data []byte, messages *core.ValidationMessages) { |
||||
// If the data is empty, we have a plain value transfer, nothing more to do
|
||||
if len(data) == 0 { |
||||
return |
||||
} |
||||
// Validate the call data that it has the 4byte prefix and the rest divisible by 32 bytes
|
||||
if len(data) < 4 { |
||||
messages.Warn("Transaction data is not valid ABI (missing the 4 byte call prefix)") |
||||
return |
||||
} |
||||
if n := len(data) - 4; n%32 != 0 { |
||||
messages.Warn(fmt.Sprintf("Transaction data is not valid ABI (length should be a multiple of 32 (was %d))", n)) |
||||
} |
||||
// If a custom method selector was provided, validate with that
|
||||
if selector != nil { |
||||
if info, err := verifySelector(*selector, data); err != nil { |
||||
messages.Warn(fmt.Sprintf("Transaction contains data, but provided ABI signature could not be matched: %v", err)) |
||||
} else { |
||||
messages.Info(info.String()) |
||||
db.AddSelector(*selector, data[:4]) |
||||
} |
||||
return |
||||
} |
||||
// No method selector was provided, check the database for embedded ones
|
||||
embedded, err := db.Selector(data[:4]) |
||||
if err != nil { |
||||
messages.Warn(fmt.Sprintf("Transaction contains data, but the ABI signature could not be found: %v", err)) |
||||
return |
||||
} |
||||
if info, err := verifySelector(embedded, data); err != nil { |
||||
messages.Warn(fmt.Sprintf("Transaction contains data, but provided ABI signature could not be varified: %v", err)) |
||||
} else { |
||||
messages.Info(info.String()) |
||||
} |
||||
} |
@ -0,0 +1,137 @@ |
||||
// 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 fourbyte |
||||
|
||||
import ( |
||||
"math/big" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/signer/core" |
||||
) |
||||
|
||||
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) *core.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 &core.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 TestTransactionValidation(t *testing.T) { |
||||
var ( |
||||
// use empty db, there are other tests for the abi-specific stuff
|
||||
db = newEmpty() |
||||
) |
||||
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 := db.ValidateTransaction(nil, dummyTxArgs(test)) |
||||
if err == nil && test.expectErr { |
||||
t.Errorf("Test %d, expected error", i) |
||||
for _, msg := range msgs.Messages { |
||||
t.Logf("* %s: %s", 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 { |
||||
t.Logf("* %s: %s", 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 { |
||||
t.Logf("* [%d] %s: %s", i, msg.Typ, msg.Message) |
||||
} |
||||
t.Log() |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue