mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
swarm: merge branch 'master' into fix-initial-peers-msg-races
This commit is contained in:
commit
f741f38667
27 changed files with 1318 additions and 762 deletions
File diff suppressed because one or more lines are too long
|
|
@ -14,8 +14,6 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// signer is a utility that can be used so sign transactions and
|
|
||||||
// arbitrary data.
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -51,22 +49,23 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/ethereum/go-ethereum/signer/core"
|
"github.com/ethereum/go-ethereum/signer/core"
|
||||||
|
"github.com/ethereum/go-ethereum/signer/fourbyte"
|
||||||
"github.com/ethereum/go-ethereum/signer/rules"
|
"github.com/ethereum/go-ethereum/signer/rules"
|
||||||
"github.com/ethereum/go-ethereum/signer/storage"
|
"github.com/ethereum/go-ethereum/signer/storage"
|
||||||
"gopkg.in/urfave/cli.v1"
|
"gopkg.in/urfave/cli.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const legalWarning = `
|
const legalWarning = `
|
||||||
WARNING!
|
WARNING!
|
||||||
|
|
||||||
Clef is an account management tool. It may, like any software, contain bugs.
|
Clef is an account management tool. It may, like any software, contain bugs.
|
||||||
|
|
||||||
Please take care to
|
Please take care to
|
||||||
- backup your keystore files,
|
- backup your keystore files,
|
||||||
- verify that the keystore(s) can be opened with your password.
|
- verify that the keystore(s) can be opened with your password.
|
||||||
|
|
||||||
Clef is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
Clef 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
|
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
PURPOSE. See the GNU General Public License for more details.
|
PURPOSE. See the GNU General Public License for more details.
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -104,11 +103,6 @@ var (
|
||||||
Name: "signersecret",
|
Name: "signersecret",
|
||||||
Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash",
|
Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash",
|
||||||
}
|
}
|
||||||
dBFlag = cli.StringFlag{
|
|
||||||
Name: "4bytedb",
|
|
||||||
Usage: "File containing 4byte-identifiers",
|
|
||||||
Value: "./4byte.json",
|
|
||||||
}
|
|
||||||
customDBFlag = cli.StringFlag{
|
customDBFlag = cli.StringFlag{
|
||||||
Name: "4bytedb-custom",
|
Name: "4bytedb-custom",
|
||||||
Usage: "File used for writing new 4byte-identifiers submitted via API",
|
Usage: "File used for writing new 4byte-identifiers submitted via API",
|
||||||
|
|
@ -145,7 +139,7 @@ var (
|
||||||
configdirFlag,
|
configdirFlag,
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `
|
||||||
The init command generates a master seed which Clef can use to store credentials and data needed for
|
The init command generates a master seed which Clef can use to store credentials and data needed for
|
||||||
the rule-engine to work.`,
|
the rule-engine to work.`,
|
||||||
}
|
}
|
||||||
attestCommand = cli.Command{
|
attestCommand = cli.Command{
|
||||||
|
|
@ -159,10 +153,10 @@ the rule-engine to work.`,
|
||||||
signerSecretFlag,
|
signerSecretFlag,
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `
|
||||||
The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of
|
The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of
|
||||||
incoming requests.
|
incoming requests.
|
||||||
|
|
||||||
Whenever you make an edit to the rule file, you need to use attestation to tell
|
Whenever you make an edit to the rule file, you need to use attestation to tell
|
||||||
Clef that the file is 'safe' to execute.`,
|
Clef that the file is 'safe' to execute.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +171,7 @@ Clef that the file is 'safe' to execute.`,
|
||||||
signerSecretFlag,
|
signerSecretFlag,
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `
|
||||||
The setpw command stores a password for a given address (keyfile). If you enter a blank passphrase, it will
|
The setpw command stores a password for a given address (keyfile). If you enter a blank passphrase, it will
|
||||||
remove any stored credential for that address (keyfile)
|
remove any stored credential for that address (keyfile)
|
||||||
`}
|
`}
|
||||||
gendocCommand = cli.Command{
|
gendocCommand = cli.Command{
|
||||||
|
|
@ -206,7 +200,6 @@ func init() {
|
||||||
utils.RPCEnabledFlag,
|
utils.RPCEnabledFlag,
|
||||||
rpcPortFlag,
|
rpcPortFlag,
|
||||||
signerSecretFlag,
|
signerSecretFlag,
|
||||||
dBFlag,
|
|
||||||
customDBFlag,
|
customDBFlag,
|
||||||
auditLogFlag,
|
auditLogFlag,
|
||||||
ruleFlag,
|
ruleFlag,
|
||||||
|
|
@ -273,12 +266,12 @@ func initializeSecrets(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
fmt.Printf("A master seed has been generated into %s\n", location)
|
fmt.Printf("A master seed has been generated into %s\n", location)
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
This is required to be able to store credentials, such as :
|
This is required to be able to store credentials, such as :
|
||||||
* Passwords for keystores (used by rule engine)
|
* Passwords for keystores (used by rule engine)
|
||||||
* Storage for javascript rules
|
* Storage for javascript rules
|
||||||
* Hash of rule-file
|
* Hash of rule-file
|
||||||
|
|
||||||
You should treat that file with utmost secrecy, and make a backup of it.
|
You should treat that file with utmost secrecy, and make a backup of it.
|
||||||
NOTE: This file does not contain your accounts. Those need to be backed up separately!
|
NOTE: This file does not contain your accounts. Those need to be backed up separately!
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
@ -365,13 +358,14 @@ func signer(c *cli.Context) error {
|
||||||
log.Info("Using CLI as UI-channel")
|
log.Info("Using CLI as UI-channel")
|
||||||
ui = core.NewCommandlineUI()
|
ui = core.NewCommandlineUI()
|
||||||
}
|
}
|
||||||
fourByteDb := c.GlobalString(dBFlag.Name)
|
// 4bytedb data
|
||||||
fourByteLocal := c.GlobalString(customDBFlag.Name)
|
fourByteLocal := c.GlobalString(customDBFlag.Name)
|
||||||
db, err := core.NewAbiDBFromFiles(fourByteDb, fourByteLocal)
|
db, err := fourbyte.NewWithFile(fourByteLocal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatalf(err.Error())
|
utils.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
log.Info("Loaded 4byte db", "signatures", db.Size(), "file", fourByteDb, "local", fourByteLocal)
|
embeds, locals := db.Size()
|
||||||
|
log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
api core.ExternalAPI
|
api core.ExternalAPI
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,12 @@ func TestACT(t *testing.T) {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
initCluster(t)
|
cluster := newTestCluster(t, clusterSize)
|
||||||
|
defer cluster.Shutdown()
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
f func(t *testing.T)
|
f func(t *testing.T, cluster *testCluster)
|
||||||
}{
|
}{
|
||||||
{"Password", testPassword},
|
{"Password", testPassword},
|
||||||
{"PK", testPK},
|
{"PK", testPK},
|
||||||
|
|
@ -65,7 +66,9 @@ func TestACT(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, tc.f)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tc.f(t, cluster)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +77,7 @@ func TestACT(t *testing.T) {
|
||||||
// The parties participating - node (publisher), uploads to second node then disappears. Content which was uploaded
|
// The parties participating - node (publisher), uploads to second node then disappears. Content which was uploaded
|
||||||
// is then fetched through 2nd node. since the tested code is not key-aware - we can just
|
// is then fetched through 2nd node. since the tested code is not key-aware - we can just
|
||||||
// fetch from the 2nd node using HTTP BasicAuth
|
// fetch from the 2nd node using HTTP BasicAuth
|
||||||
func testPassword(t *testing.T) {
|
func testPassword(t *testing.T, cluster *testCluster) {
|
||||||
dataFilename := testutil.TempFileWithContent(t, data)
|
dataFilename := testutil.TempFileWithContent(t, data)
|
||||||
defer os.RemoveAll(dataFilename)
|
defer os.RemoveAll(dataFilename)
|
||||||
|
|
||||||
|
|
@ -226,7 +229,7 @@ func testPassword(t *testing.T) {
|
||||||
// The parties participating - node (publisher), uploads to second node (which is also the grantee) then disappears.
|
// The parties participating - node (publisher), uploads to second node (which is also the grantee) then disappears.
|
||||||
// Content which was uploaded is then fetched through the grantee's http proxy. Since the tested code is private-key aware,
|
// Content which was uploaded is then fetched through the grantee's http proxy. Since the tested code is private-key aware,
|
||||||
// the test will fail if the proxy's given private key is not granted on the ACT.
|
// the test will fail if the proxy's given private key is not granted on the ACT.
|
||||||
func testPK(t *testing.T) {
|
func testPK(t *testing.T, cluster *testCluster) {
|
||||||
dataFilename := testutil.TempFileWithContent(t, data)
|
dataFilename := testutil.TempFileWithContent(t, data)
|
||||||
defer os.RemoveAll(dataFilename)
|
defer os.RemoveAll(dataFilename)
|
||||||
|
|
||||||
|
|
@ -359,13 +362,13 @@ func testPK(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// testACTWithoutBogus tests the creation of the ACT manifest end-to-end, without any bogus entries (i.e. default scenario = 3 nodes 1 unauthorized)
|
// testACTWithoutBogus tests the creation of the ACT manifest end-to-end, without any bogus entries (i.e. default scenario = 3 nodes 1 unauthorized)
|
||||||
func testACTWithoutBogus(t *testing.T) {
|
func testACTWithoutBogus(t *testing.T, cluster *testCluster) {
|
||||||
testACT(t, 0)
|
testACT(t, cluster, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testACTWithBogus tests the creation of the ACT manifest end-to-end, with 100 bogus entries (i.e. 100 EC keys + default scenario = 3 nodes 1 unauthorized = 103 keys in the ACT manifest)
|
// testACTWithBogus tests the creation of the ACT manifest end-to-end, with 100 bogus entries (i.e. 100 EC keys + default scenario = 3 nodes 1 unauthorized = 103 keys in the ACT manifest)
|
||||||
func testACTWithBogus(t *testing.T) {
|
func testACTWithBogus(t *testing.T, cluster *testCluster) {
|
||||||
testACT(t, 100)
|
testACT(t, cluster, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testACT tests the e2e creation, uploading and downloading of an ACT access control with both EC keys AND password protection
|
// testACT tests the e2e creation, uploading and downloading of an ACT access control with both EC keys AND password protection
|
||||||
|
|
@ -373,7 +376,7 @@ func testACTWithBogus(t *testing.T) {
|
||||||
// set and also protects the ACT with a password. the third node should fail decoding the reference as it will not be granted access.
|
// set and also protects the ACT with a password. the third node should fail decoding the reference as it will not be granted access.
|
||||||
// the third node then then tries to download using a correct password (and succeeds) then uses a wrong password and fails.
|
// the third node then then tries to download using a correct password (and succeeds) then uses a wrong password and fails.
|
||||||
// the publisher uploads through one of the nodes then disappears.
|
// the publisher uploads through one of the nodes then disappears.
|
||||||
func testACT(t *testing.T, bogusEntries int) {
|
func testACT(t *testing.T, cluster *testCluster, bogusEntries int) {
|
||||||
var uploadThroughNode = cluster.Nodes[0]
|
var uploadThroughNode = cluster.Nodes[0]
|
||||||
client := swarmapi.NewClient(uploadThroughNode.URL)
|
client := swarmapi.NewClient(uploadThroughNode.URL)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,15 +59,6 @@ func init() {
|
||||||
|
|
||||||
const clusterSize = 3
|
const clusterSize = 3
|
||||||
|
|
||||||
var clusteronce sync.Once
|
|
||||||
var cluster *testCluster
|
|
||||||
|
|
||||||
func initCluster(t *testing.T) {
|
|
||||||
clusteronce.Do(func() {
|
|
||||||
cluster = newTestCluster(t, clusterSize)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func serverFunc(api *api.API) swarmhttp.TestServer {
|
func serverFunc(api *api.API) swarmhttp.TestServer {
|
||||||
return swarmhttp.NewServer(api, "")
|
return swarmhttp.NewServer(api, "")
|
||||||
}
|
}
|
||||||
|
|
@ -165,10 +156,8 @@ outer:
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testCluster) Shutdown() {
|
func (c *testCluster) Shutdown() {
|
||||||
for _, node := range c.Nodes {
|
c.Stop()
|
||||||
node.Shutdown()
|
c.Cleanup()
|
||||||
}
|
|
||||||
os.RemoveAll(c.TmpDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testCluster) Stop() {
|
func (c *testCluster) Stop() {
|
||||||
|
|
@ -179,16 +168,35 @@ func (c *testCluster) Stop() {
|
||||||
|
|
||||||
func (c *testCluster) StartNewNodes(t *testing.T, size int) {
|
func (c *testCluster) StartNewNodes(t *testing.T, size int) {
|
||||||
c.Nodes = make([]*testNode, 0, size)
|
c.Nodes = make([]*testNode, 0, size)
|
||||||
|
|
||||||
|
errors := make(chan error, size)
|
||||||
|
nodes := make(chan *testNode, size)
|
||||||
for i := 0; i < size; i++ {
|
for i := 0; i < size; i++ {
|
||||||
dir := filepath.Join(c.TmpDir, fmt.Sprintf("swarm%02d", i))
|
go func(nodeIndex int) {
|
||||||
if err := os.Mkdir(dir, 0700); err != nil {
|
dir := filepath.Join(c.TmpDir, fmt.Sprintf("swarm%02d", nodeIndex))
|
||||||
t.Fatal(err)
|
if err := os.Mkdir(dir, 0700); err != nil {
|
||||||
|
errors <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node := newTestNode(t, dir)
|
||||||
|
node.Name = fmt.Sprintf("swarm%02d", nodeIndex)
|
||||||
|
nodes <- node
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
select {
|
||||||
|
case node := <-nodes:
|
||||||
|
c.Nodes = append(c.Nodes, node)
|
||||||
|
case err := <-errors:
|
||||||
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node := newTestNode(t, dir)
|
if t.Failed() {
|
||||||
node.Name = fmt.Sprintf("swarm%02d", i)
|
c.Shutdown()
|
||||||
|
t.FailNow()
|
||||||
c.Nodes = append(c.Nodes, node)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,12 @@ func TestSwarmUp(t *testing.T) {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
initCluster(t)
|
cluster := newTestCluster(t, clusterSize)
|
||||||
|
defer cluster.Shutdown()
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
f func(t *testing.T)
|
f func(t *testing.T, cluster *testCluster)
|
||||||
}{
|
}{
|
||||||
{"NoEncryption", testNoEncryption},
|
{"NoEncryption", testNoEncryption},
|
||||||
{"Encrypted", testEncrypted},
|
{"Encrypted", testEncrypted},
|
||||||
|
|
@ -60,31 +61,33 @@ func TestSwarmUp(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, tc.f)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tc.f(t, cluster)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// testNoEncryption tests that running 'swarm up' makes the resulting file
|
// testNoEncryption tests that running 'swarm up' makes the resulting file
|
||||||
// available from all nodes via the HTTP API
|
// available from all nodes via the HTTP API
|
||||||
func testNoEncryption(t *testing.T) {
|
func testNoEncryption(t *testing.T, cluster *testCluster) {
|
||||||
testDefault(false, t)
|
testDefault(t, cluster, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testEncrypted tests that running 'swarm up --encrypted' makes the resulting file
|
// testEncrypted tests that running 'swarm up --encrypted' makes the resulting file
|
||||||
// available from all nodes via the HTTP API
|
// available from all nodes via the HTTP API
|
||||||
func testEncrypted(t *testing.T) {
|
func testEncrypted(t *testing.T, cluster *testCluster) {
|
||||||
testDefault(true, t)
|
testDefault(t, cluster, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRecursiveNoEncryption(t *testing.T) {
|
func testRecursiveNoEncryption(t *testing.T, cluster *testCluster) {
|
||||||
testRecursive(false, t)
|
testRecursive(t, cluster, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRecursiveEncrypted(t *testing.T) {
|
func testRecursiveEncrypted(t *testing.T, cluster *testCluster) {
|
||||||
testRecursive(true, t)
|
testRecursive(t, cluster, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDefault(toEncrypt bool, t *testing.T) {
|
func testDefault(t *testing.T, cluster *testCluster, toEncrypt bool) {
|
||||||
tmpFileName := testutil.TempFileWithContent(t, data)
|
tmpFileName := testutil.TempFileWithContent(t, data)
|
||||||
defer os.Remove(tmpFileName)
|
defer os.Remove(tmpFileName)
|
||||||
|
|
||||||
|
|
@ -189,7 +192,7 @@ func testDefault(toEncrypt bool, t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRecursive(toEncrypt bool, t *testing.T) {
|
func testRecursive(t *testing.T, cluster *testCluster, toEncrypt bool) {
|
||||||
tmpUploadDir, err := ioutil.TempDir("", "swarm-test")
|
tmpUploadDir, err := ioutil.TempDir("", "swarm-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -279,14 +282,14 @@ func testRecursive(toEncrypt bool, t *testing.T) {
|
||||||
|
|
||||||
// testDefaultPathAll tests swarm recursive upload with relative and absolute
|
// testDefaultPathAll tests swarm recursive upload with relative and absolute
|
||||||
// default paths and with encryption.
|
// default paths and with encryption.
|
||||||
func testDefaultPathAll(t *testing.T) {
|
func testDefaultPathAll(t *testing.T, cluster *testCluster) {
|
||||||
testDefaultPath(false, false, t)
|
testDefaultPath(t, cluster, false, false)
|
||||||
testDefaultPath(false, true, t)
|
testDefaultPath(t, cluster, false, true)
|
||||||
testDefaultPath(true, false, t)
|
testDefaultPath(t, cluster, true, false)
|
||||||
testDefaultPath(true, true, t)
|
testDefaultPath(t, cluster, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDefaultPath(toEncrypt bool, absDefaultPath bool, t *testing.T) {
|
func testDefaultPath(t *testing.T, cluster *testCluster, toEncrypt bool, absDefaultPath bool) {
|
||||||
tmp, err := ioutil.TempDir("", "swarm-defaultpath-test")
|
tmp, err := ioutil.TempDir("", "swarm-defaultpath-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
|
"github.com/ethereum/go-ethereum/metrics/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type exp struct {
|
type exp struct {
|
||||||
|
|
@ -42,6 +43,7 @@ func Exp(r metrics.Registry) {
|
||||||
// http.HandleFunc("/debug/vars", e.expHandler)
|
// http.HandleFunc("/debug/vars", e.expHandler)
|
||||||
// haven't found an elegant way, so just use a different endpoint
|
// haven't found an elegant way, so just use a different endpoint
|
||||||
http.Handle("/debug/metrics", h)
|
http.Handle("/debug/metrics", h)
|
||||||
|
http.Handle("/debug/metrics/prometheus", prometheus.Handler(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpHandler will return an expvar powered metrics handler.
|
// ExpHandler will return an expvar powered metrics handler.
|
||||||
|
|
|
||||||
115
metrics/prometheus/collector.go
Normal file
115
metrics/prometheus/collector.go
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
package prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
typeGaugeTpl = "# TYPE %s gauge\n"
|
||||||
|
typeCounterTpl = "# TYPE %s counter\n"
|
||||||
|
typeSummaryTpl = "# TYPE %s summary\n"
|
||||||
|
keyValueTpl = "%s %v\n\n"
|
||||||
|
keyQuantileTagValueTpl = "%s {quantile=\"%s\"} %v\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
// collector is a collection of byte buffers that aggregate Prometheus reports
|
||||||
|
// for different metric types.
|
||||||
|
type collector struct {
|
||||||
|
buff *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCollector createa a new Prometheus metric aggregator.
|
||||||
|
func newCollector() *collector {
|
||||||
|
return &collector{
|
||||||
|
buff: &bytes.Buffer{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) addCounter(name string, m metrics.Counter) {
|
||||||
|
c.writeGaugeCounter(name, m.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) addGauge(name string, m metrics.Gauge) {
|
||||||
|
c.writeGaugeCounter(name, m.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) addGaugeFloat64(name string, m metrics.GaugeFloat64) {
|
||||||
|
c.writeGaugeCounter(name, m.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) addHistogram(name string, m metrics.Histogram) {
|
||||||
|
pv := []float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999}
|
||||||
|
ps := m.Percentiles(pv)
|
||||||
|
c.writeSummaryCounter(name, m.Count())
|
||||||
|
for i := range pv {
|
||||||
|
c.writeSummaryPercentile(name, strconv.FormatFloat(pv[i], 'f', -1, 64), ps[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) addMeter(name string, m metrics.Meter) {
|
||||||
|
c.writeGaugeCounter(name, m.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) addTimer(name string, m metrics.Timer) {
|
||||||
|
pv := []float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999}
|
||||||
|
ps := m.Percentiles(pv)
|
||||||
|
c.writeSummaryCounter(name, m.Count())
|
||||||
|
for i := range pv {
|
||||||
|
c.writeSummaryPercentile(name, strconv.FormatFloat(pv[i], 'f', -1, 64), ps[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) addResettingTimer(name string, m metrics.ResettingTimer) {
|
||||||
|
if len(m.Values()) <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ps := m.Percentiles([]float64{50, 95, 99})
|
||||||
|
val := m.Values()
|
||||||
|
c.writeSummaryCounter(name, len(val))
|
||||||
|
c.writeSummaryPercentile(name, "0.50", ps[0])
|
||||||
|
c.writeSummaryPercentile(name, "0.95", ps[1])
|
||||||
|
c.writeSummaryPercentile(name, "0.99", ps[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) writeGaugeCounter(name string, value interface{}) {
|
||||||
|
name = mutateKey(name)
|
||||||
|
c.buff.WriteString(fmt.Sprintf(typeGaugeTpl, name))
|
||||||
|
c.buff.WriteString(fmt.Sprintf(keyValueTpl, name, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) writeSummaryCounter(name string, value interface{}) {
|
||||||
|
name = mutateKey(name + "_count")
|
||||||
|
c.buff.WriteString(fmt.Sprintf(typeCounterTpl, name))
|
||||||
|
c.buff.WriteString(fmt.Sprintf(keyValueTpl, name, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) writeSummaryPercentile(name, p string, value interface{}) {
|
||||||
|
name = mutateKey(name)
|
||||||
|
c.buff.WriteString(fmt.Sprintf(typeSummaryTpl, name))
|
||||||
|
c.buff.WriteString(fmt.Sprintf(keyQuantileTagValueTpl, name, p, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mutateKey(key string) string {
|
||||||
|
return strings.Replace(key, "/", "_", -1)
|
||||||
|
}
|
||||||
68
metrics/prometheus/prometheus.go
Normal file
68
metrics/prometheus/prometheus.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
// Package prometheus exposes go-metrics into a Prometheus format.
|
||||||
|
package prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler returns an HTTP handler which dump metrics in Prometheus format.
|
||||||
|
func Handler(reg metrics.Registry) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Gather and pre-sort the metrics to avoid random listings
|
||||||
|
var names []string
|
||||||
|
reg.Each(func(name string, i interface{}) {
|
||||||
|
names = append(names, name)
|
||||||
|
})
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
// Aggregate all the metris into a Prometheus collector
|
||||||
|
c := newCollector()
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
i := reg.Get(name)
|
||||||
|
|
||||||
|
switch m := i.(type) {
|
||||||
|
case metrics.Counter:
|
||||||
|
c.addCounter(name, m.Snapshot())
|
||||||
|
case metrics.Gauge:
|
||||||
|
c.addGauge(name, m.Snapshot())
|
||||||
|
case metrics.GaugeFloat64:
|
||||||
|
c.addGaugeFloat64(name, m.Snapshot())
|
||||||
|
case metrics.Histogram:
|
||||||
|
c.addHistogram(name, m.Snapshot())
|
||||||
|
case metrics.Meter:
|
||||||
|
c.addMeter(name, m.Snapshot())
|
||||||
|
case metrics.Timer:
|
||||||
|
c.addTimer(name, m.Snapshot())
|
||||||
|
case metrics.ResettingTimer:
|
||||||
|
c.addResettingTimer(name, m.Snapshot())
|
||||||
|
default:
|
||||||
|
log.Warn("Unknown Prometheus metric type", "type", fmt.Sprintf("%T", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
w.Header().Add("Content-Length", fmt.Sprint(c.buff.Len()))
|
||||||
|
w.Write(c.buff.Bytes())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -482,7 +482,7 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
|
||||||
if extapi, err := external.NewExternalBackend(conf.ExternalSigner); err == nil {
|
if extapi, err := external.NewExternalBackend(conf.ExternalSigner); err == nil {
|
||||||
backends = append(backends, extapi)
|
backends = append(backends, extapi)
|
||||||
} else {
|
} else {
|
||||||
log.Info("Error configuring external signer", "error", err)
|
return nil, "", fmt.Errorf("error connecting to external signer: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(backends) == 0 {
|
if len(backends) == 0 {
|
||||||
|
|
|
||||||
|
|
@ -1,260 +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/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 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
|
|
||||||
}
|
|
||||||
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 := 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 := common.ToHex(data[:4])
|
|
||||||
return db.saveCustomAbi(selector, sig)
|
|
||||||
}
|
|
||||||
|
|
@ -92,12 +92,25 @@ type UIClientAPI interface {
|
||||||
RegisterUIServer(api *UIServerAPI)
|
RegisterUIServer(api *UIServerAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validator defines the methods required to validate a transaction against some
|
||||||
|
// sanity defaults as well as any underlying 4byte method database.
|
||||||
|
//
|
||||||
|
// Use fourbyte.Database as an implementation. It is separated out of this package
|
||||||
|
// to allow pieces of the signer package to be used without having to load the
|
||||||
|
// 7MB embedded 4byte dump.
|
||||||
|
type Validator interface {
|
||||||
|
// 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).
|
||||||
|
ValidateTransaction(selector *string, tx *SendTxArgs) (*ValidationMessages, error)
|
||||||
|
}
|
||||||
|
|
||||||
// SignerAPI defines the actual implementation of ExternalAPI
|
// SignerAPI defines the actual implementation of ExternalAPI
|
||||||
type SignerAPI struct {
|
type SignerAPI struct {
|
||||||
chainID *big.Int
|
chainID *big.Int
|
||||||
am *accounts.Manager
|
am *accounts.Manager
|
||||||
UI UIClientAPI
|
UI UIClientAPI
|
||||||
validator *Validator
|
validator Validator
|
||||||
rejectMode bool
|
rejectMode bool
|
||||||
credentials storage.Storage
|
credentials storage.Storage
|
||||||
}
|
}
|
||||||
|
|
@ -235,11 +248,11 @@ var ErrRequestDenied = errors.New("Request denied")
|
||||||
// key that is generated when a new Account is created.
|
// key that is generated when a new Account is created.
|
||||||
// noUSB disables USB support that is required to support hardware devices such as
|
// noUSB disables USB support that is required to support hardware devices such as
|
||||||
// ledger and trezor.
|
// ledger and trezor.
|
||||||
func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, abidb *AbiDb, advancedMode bool, credentials storage.Storage) *SignerAPI {
|
func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI {
|
||||||
if advancedMode {
|
if advancedMode {
|
||||||
log.Info("Clef is in advanced mode: will warn instead of reject")
|
log.Info("Clef is in advanced mode: will warn instead of reject")
|
||||||
}
|
}
|
||||||
signer := &SignerAPI{big.NewInt(chainID), am, ui, NewValidator(abidb), !advancedMode, credentials}
|
signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials}
|
||||||
if !noUSB {
|
if !noUSB {
|
||||||
signer.startUSBListener()
|
signer.startUSBListener()
|
||||||
}
|
}
|
||||||
|
|
@ -458,7 +471,7 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth
|
||||||
err error
|
err error
|
||||||
result SignTxResponse
|
result SignTxResponse
|
||||||
)
|
)
|
||||||
msgs, err := api.validator.ValidateTransaction(&args, methodSelector)
|
msgs, err := api.validator.ValidateTransaction(methodSelector, &args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
package core
|
package core_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -34,6 +34,8 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/signer/core"
|
||||||
|
"github.com/ethereum/go-ethereum/signer/fourbyte"
|
||||||
"github.com/ethereum/go-ethereum/signer/storage"
|
"github.com/ethereum/go-ethereum/signer/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -43,56 +45,56 @@ type headlessUi struct {
|
||||||
inputCh chan string // to send password
|
inputCh chan string // to send password
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *headlessUi) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
|
func (ui *headlessUi) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
||||||
input := <-ui.inputCh
|
input := <-ui.inputCh
|
||||||
return UserInputResponse{Text: input}, nil
|
return core.UserInputResponse{Text: input}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *headlessUi) OnSignerStartup(info StartupInfo) {}
|
func (ui *headlessUi) OnSignerStartup(info core.StartupInfo) {}
|
||||||
func (ui *headlessUi) RegisterUIServer(api *UIServerAPI) {}
|
func (ui *headlessUi) RegisterUIServer(api *core.UIServerAPI) {}
|
||||||
func (ui *headlessUi) OnApprovedTx(tx ethapi.SignTransactionResult) {}
|
func (ui *headlessUi) OnApprovedTx(tx ethapi.SignTransactionResult) {}
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
|
func (ui *headlessUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
|
||||||
|
|
||||||
switch <-ui.approveCh {
|
switch <-ui.approveCh {
|
||||||
case "Y":
|
case "Y":
|
||||||
return SignTxResponse{request.Transaction, true}, nil
|
return core.SignTxResponse{request.Transaction, true}, nil
|
||||||
case "M": // modify
|
case "M": // modify
|
||||||
// The headless UI always modifies the transaction
|
// The headless UI always modifies the transaction
|
||||||
old := big.Int(request.Transaction.Value)
|
old := big.Int(request.Transaction.Value)
|
||||||
newVal := big.NewInt(0).Add(&old, big.NewInt(1))
|
newVal := big.NewInt(0).Add(&old, big.NewInt(1))
|
||||||
request.Transaction.Value = hexutil.Big(*newVal)
|
request.Transaction.Value = hexutil.Big(*newVal)
|
||||||
return SignTxResponse{request.Transaction, true}, nil
|
return core.SignTxResponse{request.Transaction, true}, nil
|
||||||
default:
|
default:
|
||||||
return SignTxResponse{request.Transaction, false}, nil
|
return core.SignTxResponse{request.Transaction, false}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
|
func (ui *headlessUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
|
||||||
approved := "Y" == <-ui.approveCh
|
approved := "Y" == <-ui.approveCh
|
||||||
return SignDataResponse{approved}, nil
|
return core.SignDataResponse{approved}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveListing(request *ListRequest) (ListResponse, error) {
|
func (ui *headlessUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
|
||||||
approval := <-ui.approveCh
|
approval := <-ui.approveCh
|
||||||
//fmt.Printf("approval %s\n", approval)
|
//fmt.Printf("approval %s\n", approval)
|
||||||
switch approval {
|
switch approval {
|
||||||
case "A":
|
case "A":
|
||||||
return ListResponse{request.Accounts}, nil
|
return core.ListResponse{request.Accounts}, nil
|
||||||
case "1":
|
case "1":
|
||||||
l := make([]accounts.Account, 1)
|
l := make([]accounts.Account, 1)
|
||||||
l[0] = request.Accounts[1]
|
l[0] = request.Accounts[1]
|
||||||
return ListResponse{l}, nil
|
return core.ListResponse{l}, nil
|
||||||
default:
|
default:
|
||||||
return ListResponse{nil}, nil
|
return core.ListResponse{nil}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
|
func (ui *headlessUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
|
||||||
if "Y" == <-ui.approveCh {
|
if "Y" == <-ui.approveCh {
|
||||||
return NewAccountResponse{true}, nil
|
return core.NewAccountResponse{true}, nil
|
||||||
}
|
}
|
||||||
return NewAccountResponse{false}, nil
|
return core.NewAccountResponse{false}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *headlessUi) ShowError(message string) {
|
func (ui *headlessUi) ShowError(message string) {
|
||||||
|
|
@ -117,18 +119,18 @@ func tmpDirName(t *testing.T) string {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(t *testing.T) (*SignerAPI, *headlessUi) {
|
func setup(t *testing.T) (*core.SignerAPI, *headlessUi) {
|
||||||
db, err := NewAbiDBFromFile("../../cmd/clef/4byte.json")
|
db, err := fourbyte.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err.Error())
|
t.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
ui := &headlessUi{make(chan string, 20), make(chan string, 20)}
|
ui := &headlessUi{make(chan string, 20), make(chan string, 20)}
|
||||||
am := StartClefAccountManager(tmpDirName(t), true, true)
|
am := core.StartClefAccountManager(tmpDirName(t), true, true)
|
||||||
api := NewSignerAPI(am, 1337, true, ui, db, true, &storage.NoStorage{})
|
api := core.NewSignerAPI(am, 1337, true, ui, db, true, &storage.NoStorage{})
|
||||||
return api, ui
|
return api, ui
|
||||||
|
|
||||||
}
|
}
|
||||||
func createAccount(ui *headlessUi, api *SignerAPI, t *testing.T) {
|
func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
|
||||||
ui.approveCh <- "Y"
|
ui.approveCh <- "Y"
|
||||||
ui.inputCh <- "a_long_password"
|
ui.inputCh <- "a_long_password"
|
||||||
_, err := api.New(context.Background())
|
_, err := api.New(context.Background())
|
||||||
|
|
@ -139,7 +141,7 @@ func createAccount(ui *headlessUi, api *SignerAPI, t *testing.T) {
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func failCreateAccountWithPassword(ui *headlessUi, api *SignerAPI, password string, t *testing.T) {
|
func failCreateAccountWithPassword(ui *headlessUi, api *core.SignerAPI, password string, t *testing.T) {
|
||||||
|
|
||||||
ui.approveCh <- "Y"
|
ui.approveCh <- "Y"
|
||||||
// We will be asked three times to provide a suitable password
|
// We will be asked three times to provide a suitable password
|
||||||
|
|
@ -156,10 +158,10 @@ func failCreateAccountWithPassword(ui *headlessUi, api *SignerAPI, password stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func failCreateAccount(ui *headlessUi, api *SignerAPI, t *testing.T) {
|
func failCreateAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
|
||||||
ui.approveCh <- "N"
|
ui.approveCh <- "N"
|
||||||
addr, err := api.New(context.Background())
|
addr, err := api.New(context.Background())
|
||||||
if err != ErrRequestDenied {
|
if err != core.ErrRequestDenied {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if addr != (common.Address{}) {
|
if addr != (common.Address{}) {
|
||||||
|
|
@ -167,7 +169,7 @@ func failCreateAccount(ui *headlessUi, api *SignerAPI, t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func list(ui *headlessUi, api *SignerAPI, t *testing.T) ([]common.Address, error) {
|
func list(ui *headlessUi, api *core.SignerAPI, t *testing.T) ([]common.Address, error) {
|
||||||
ui.approveCh <- "A"
|
ui.approveCh <- "A"
|
||||||
return api.List(context.Background())
|
return api.List(context.Background())
|
||||||
|
|
||||||
|
|
@ -216,19 +218,19 @@ func TestNewAcc(t *testing.T) {
|
||||||
if len(list) != 0 {
|
if len(list) != 0 {
|
||||||
t.Fatalf("List should be empty")
|
t.Fatalf("List should be empty")
|
||||||
}
|
}
|
||||||
if err != ErrRequestDenied {
|
if err != core.ErrRequestDenied {
|
||||||
t.Fatal("Expected deny")
|
t.Fatal("Expected deny")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mkTestTx(from common.MixedcaseAddress) SendTxArgs {
|
func mkTestTx(from common.MixedcaseAddress) core.SendTxArgs {
|
||||||
to := common.NewMixedcaseAddress(common.HexToAddress("0x1337"))
|
to := common.NewMixedcaseAddress(common.HexToAddress("0x1337"))
|
||||||
gas := hexutil.Uint64(21000)
|
gas := hexutil.Uint64(21000)
|
||||||
gasPrice := (hexutil.Big)(*big.NewInt(2000000000))
|
gasPrice := (hexutil.Big)(*big.NewInt(2000000000))
|
||||||
value := (hexutil.Big)(*big.NewInt(1e18))
|
value := (hexutil.Big)(*big.NewInt(1e18))
|
||||||
nonce := (hexutil.Uint64)(0)
|
nonce := (hexutil.Uint64)(0)
|
||||||
data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a"))
|
data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a"))
|
||||||
tx := SendTxArgs{
|
tx := core.SendTxArgs{
|
||||||
From: from,
|
From: from,
|
||||||
To: &to,
|
To: &to,
|
||||||
Gas: gas,
|
Gas: gas,
|
||||||
|
|
@ -272,7 +274,7 @@ func TestSignTx(t *testing.T) {
|
||||||
if res != nil {
|
if res != nil {
|
||||||
t.Errorf("Expected nil-response, got %v", res)
|
t.Errorf("Expected nil-response, got %v", res)
|
||||||
}
|
}
|
||||||
if err != ErrRequestDenied {
|
if err != core.ErrRequestDenied {
|
||||||
t.Errorf("Expected ErrRequestDenied! %v", err)
|
t.Errorf("Expected ErrRequestDenied! %v", err)
|
||||||
}
|
}
|
||||||
// Sign with correct password
|
// Sign with correct password
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
package core
|
package core_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -26,9 +26,10 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/signer/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
var typesStandard = Types{
|
var typesStandard = core.Types{
|
||||||
"EIP712Domain": {
|
"EIP712Domain": {
|
||||||
{
|
{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
|
|
@ -147,7 +148,7 @@ var jsonTypedData = `
|
||||||
|
|
||||||
const primaryType = "Mail"
|
const primaryType = "Mail"
|
||||||
|
|
||||||
var domainStandard = TypedDataDomain{
|
var domainStandard = core.TypedDataDomain{
|
||||||
"Ether Mail",
|
"Ether Mail",
|
||||||
"1",
|
"1",
|
||||||
big.NewInt(1),
|
big.NewInt(1),
|
||||||
|
|
@ -167,7 +168,7 @@ var messageStandard = map[string]interface{}{
|
||||||
"contents": "Hello, Bob!",
|
"contents": "Hello, Bob!",
|
||||||
}
|
}
|
||||||
|
|
||||||
var typedData = TypedData{
|
var typedData = core.TypedData{
|
||||||
Types: typesStandard,
|
Types: typesStandard,
|
||||||
PrimaryType: primaryType,
|
PrimaryType: primaryType,
|
||||||
Domain: domainStandard,
|
Domain: domainStandard,
|
||||||
|
|
@ -188,7 +189,7 @@ func TestSignData(t *testing.T) {
|
||||||
|
|
||||||
control.approveCh <- "Y"
|
control.approveCh <- "Y"
|
||||||
control.inputCh <- "wrongpassword"
|
control.inputCh <- "wrongpassword"
|
||||||
signature, err := api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
|
signature, err := api.SignData(context.Background(), core.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
|
||||||
if signature != nil {
|
if signature != nil {
|
||||||
t.Errorf("Expected nil-data, got %x", signature)
|
t.Errorf("Expected nil-data, got %x", signature)
|
||||||
}
|
}
|
||||||
|
|
@ -196,17 +197,17 @@ func TestSignData(t *testing.T) {
|
||||||
t.Errorf("Expected ErrLocked! '%v'", err)
|
t.Errorf("Expected ErrLocked! '%v'", err)
|
||||||
}
|
}
|
||||||
control.approveCh <- "No way"
|
control.approveCh <- "No way"
|
||||||
signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
|
signature, err = api.SignData(context.Background(), core.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
|
||||||
if signature != nil {
|
if signature != nil {
|
||||||
t.Errorf("Expected nil-data, got %x", signature)
|
t.Errorf("Expected nil-data, got %x", signature)
|
||||||
}
|
}
|
||||||
if err != ErrRequestDenied {
|
if err != core.ErrRequestDenied {
|
||||||
t.Errorf("Expected ErrRequestDenied! '%v'", err)
|
t.Errorf("Expected ErrRequestDenied! '%v'", err)
|
||||||
}
|
}
|
||||||
// text/plain
|
// text/plain
|
||||||
control.approveCh <- "Y"
|
control.approveCh <- "Y"
|
||||||
control.inputCh <- "a_long_password"
|
control.inputCh <- "a_long_password"
|
||||||
signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
|
signature, err = api.SignData(context.Background(), core.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -226,13 +227,13 @@ func TestSignData(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDomainChainId(t *testing.T) {
|
func TestDomainChainId(t *testing.T) {
|
||||||
withoutChainID := TypedData{
|
withoutChainID := core.TypedData{
|
||||||
Types: Types{
|
Types: core.Types{
|
||||||
"EIP712Domain": []Type{
|
"EIP712Domain": []core.Type{
|
||||||
{Name: "name", Type: "string"},
|
{Name: "name", Type: "string"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Domain: TypedDataDomain{
|
Domain: core.TypedDataDomain{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -241,14 +242,14 @@ func TestDomainChainId(t *testing.T) {
|
||||||
t.Errorf("Expected the chainId key to not be present in the domain map")
|
t.Errorf("Expected the chainId key to not be present in the domain map")
|
||||||
}
|
}
|
||||||
|
|
||||||
withChainID := TypedData{
|
withChainID := core.TypedData{
|
||||||
Types: Types{
|
Types: core.Types{
|
||||||
"EIP712Domain": []Type{
|
"EIP712Domain": []core.Type{
|
||||||
{Name: "name", Type: "string"},
|
{Name: "name", Type: "string"},
|
||||||
{Name: "chainId", Type: "uint256"},
|
{Name: "chainId", Type: "uint256"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Domain: TypedDataDomain{
|
Domain: core.TypedDataDomain{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
ChainId: big.NewInt(1),
|
ChainId: big.NewInt(1),
|
||||||
},
|
},
|
||||||
|
|
@ -383,7 +384,7 @@ func TestMalformedDomainkeys(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
var malformedDomainTypedData TypedData
|
var malformedDomainTypedData core.TypedData
|
||||||
err := json.Unmarshal([]byte(jsonTypedData), &malformedDomainTypedData)
|
err := json.Unmarshal([]byte(jsonTypedData), &malformedDomainTypedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unmarshalling failed '%v'", err)
|
t.Fatalf("unmarshalling failed '%v'", err)
|
||||||
|
|
@ -471,7 +472,7 @@ func TestMalformedTypesAndExtradata(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
var malformedTypedData TypedData
|
var malformedTypedData core.TypedData
|
||||||
err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData)
|
err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unmarshalling failed '%v'", err)
|
t.Fatalf("unmarshalling failed '%v'", err)
|
||||||
|
|
@ -567,7 +568,7 @@ func TestTypeMismatch(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
var mismatchTypedData TypedData
|
var mismatchTypedData core.TypedData
|
||||||
err := json.Unmarshal([]byte(jsonTypedData), &mismatchTypedData)
|
err := json.Unmarshal([]byte(jsonTypedData), &mismatchTypedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unmarshalling failed '%v'", err)
|
t.Fatalf("unmarshalling failed '%v'", err)
|
||||||
|
|
@ -589,7 +590,7 @@ func TestTypeOverflow(t *testing.T) {
|
||||||
//{
|
//{
|
||||||
// "test": 65536 <-- test defined as uint8
|
// "test": 65536 <-- test defined as uint8
|
||||||
//}
|
//}
|
||||||
var overflowTypedData TypedData
|
var overflowTypedData core.TypedData
|
||||||
err := json.Unmarshal([]byte(jsonTypedData), &overflowTypedData)
|
err := json.Unmarshal([]byte(jsonTypedData), &overflowTypedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unmarshalling failed '%v'", err)
|
t.Fatalf("unmarshalling failed '%v'", err)
|
||||||
|
|
@ -667,7 +668,7 @@ func TestArray(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
var arrayTypedData TypedData
|
var arrayTypedData core.TypedData
|
||||||
err := json.Unmarshal([]byte(jsonTypedData), &arrayTypedData)
|
err := json.Unmarshal([]byte(jsonTypedData), &arrayTypedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unmarshalling failed '%v'", err)
|
t.Fatalf("unmarshalling failed '%v'", err)
|
||||||
|
|
@ -780,7 +781,7 @@ func TestCustomTypeAsArray(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
`
|
`
|
||||||
var malformedTypedData TypedData
|
var malformedTypedData core.TypedData
|
||||||
err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData)
|
err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unmarshalling failed '%v'", err)
|
t.Fatalf("unmarshalling failed '%v'", err)
|
||||||
|
|
@ -792,8 +793,7 @@ func TestCustomTypeAsArray(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFormatter(t *testing.T) {
|
func TestFormatter(t *testing.T) {
|
||||||
|
var d core.TypedData
|
||||||
var d TypedData
|
|
||||||
err := json.Unmarshal([]byte(jsonTypedData), &d)
|
err := json.Unmarshal([]byte(jsonTypedData), &d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unmarshalling failed '%v'", err)
|
t.Fatalf("unmarshalling failed '%v'", err)
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,13 @@ const (
|
||||||
INFO = "Info"
|
INFO = "Info"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (vs *ValidationMessages) crit(msg string) {
|
func (vs *ValidationMessages) Crit(msg string) {
|
||||||
vs.Messages = append(vs.Messages, ValidationInfo{CRIT, msg})
|
vs.Messages = append(vs.Messages, ValidationInfo{CRIT, msg})
|
||||||
}
|
}
|
||||||
func (vs *ValidationMessages) warn(msg string) {
|
func (vs *ValidationMessages) Warn(msg string) {
|
||||||
vs.Messages = append(vs.Messages, ValidationInfo{WARN, msg})
|
vs.Messages = append(vs.Messages, ValidationInfo{WARN, msg})
|
||||||
}
|
}
|
||||||
func (vs *ValidationMessages) info(msg string) {
|
func (vs *ValidationMessages) Info(msg string) {
|
||||||
vs.Messages = append(vs.Messages, ValidationInfo{INFO, msg})
|
vs.Messages = append(vs.Messages, ValidationInfo{INFO, msg})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,146 +17,11 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The validation package contains validation checks for transactions
|
var printable7BitAscii = regexp.MustCompile("^[A-Za-z0-9!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ]+$")
|
||||||
// - ABI-data validation
|
|
||||||
// - Transaction semantics validation
|
|
||||||
// The package provides warnings for typical pitfalls
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if arglen := len(data) - 4; arglen%32 != 0 {
|
|
||||||
msgs.warn(fmt.Sprintf("Not ABI-encoded data; length should be a multiple of 32 (was %d)", arglen))
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var Printable7BitAscii = regexp.MustCompile("^[A-Za-z0-9!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ]+$")
|
|
||||||
|
|
||||||
// ValidatePasswordFormat returns an error if the password is too short, or consists of characters
|
// ValidatePasswordFormat returns an error if the password is too short, or consists of characters
|
||||||
// outside the range of the printable 7bit ascii set
|
// outside the range of the printable 7bit ascii set
|
||||||
|
|
@ -164,7 +29,7 @@ func ValidatePasswordFormat(password string) error {
|
||||||
if len(password) < 10 {
|
if len(password) < 10 {
|
||||||
return errors.New("password too short (<10 characters)")
|
return errors.New("password too short (<10 characters)")
|
||||||
}
|
}
|
||||||
if !Printable7BitAscii.MatchString(password) {
|
if !printable7BitAscii.MatchString(password) {
|
||||||
return errors.New("password contains invalid characters - only 7bit printable ascii allowed")
|
return errors.New("password contains invalid characters - only 7bit printable ascii allowed")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -16,126 +16,7 @@
|
||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPasswordValidation(t *testing.T) {
|
func TestPasswordValidation(t *testing.T) {
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
|
|
|
||||||
271
signer/fourbyte/4byte.go
Normal file
271
signer/fourbyte/4byte.go
Normal file
File diff suppressed because one or more lines are too long
1
signer/fourbyte/4byte.json
Normal file
1
signer/fourbyte/4byte.json
Normal file
File diff suppressed because one or more lines are too long
164
signer/fourbyte/abi.go
Normal file
164
signer/fourbyte/abi.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,22 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
// Copyright 2018 The go-ethereum Authors
|
||||||
// This file is part of go-ethereum.
|
// This file is part of the go-ethereum library.
|
||||||
//
|
//
|
||||||
// go-ethereum is free software: you can redistribute it and/or modify
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// 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
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
//
|
||||||
// go-ethereum is distributed in the hope that it will be useful,
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU Lesser General Public License for more details.
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package core
|
package fourbyte
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -29,7 +27,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {
|
func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {
|
||||||
|
|
||||||
abispec, err := abi.JSON(strings.NewReader(jsondata))
|
abispec, err := abi.JSON(strings.NewReader(jsondata))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -53,6 +50,7 @@ func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewUnpacker(t *testing.T) {
|
func TestNewUnpacker(t *testing.T) {
|
||||||
type unpackTest struct {
|
type unpackTest struct {
|
||||||
jsondata string
|
jsondata string
|
||||||
|
|
@ -96,11 +94,9 @@ func TestNewUnpacker(t *testing.T) {
|
||||||
for _, c := range testcases {
|
for _, c := range testcases {
|
||||||
verify(t, c.jsondata, c.calldata, c.exp)
|
verify(t, c.jsondata, c.calldata, c.exp)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalldataDecoding(t *testing.T) {
|
func TestCalldataDecoding(t *testing.T) {
|
||||||
|
|
||||||
// send(uint256) : a52c101e
|
// send(uint256) : a52c101e
|
||||||
// compareAndApprove(address,uint256,uint256) : 751e1079
|
// compareAndApprove(address,uint256,uint256) : 751e1079
|
||||||
// issue(address[],uint256) : 42958b54
|
// issue(address[],uint256) : 42958b54
|
||||||
|
|
@ -111,7 +107,7 @@ func TestCalldataDecoding(t *testing.T) {
|
||||||
{"type":"function","name":"issue","inputs":[{"name":"a","type":"address[]"},{"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[]"}]}
|
{"type":"function","name":"sam","inputs":[{"name":"a","type":"bytes"},{"name":"a","type":"bool"},{"name":"a","type":"uint256[]"}]}
|
||||||
]`
|
]`
|
||||||
//Expected failures
|
// Expected failures
|
||||||
for i, hexdata := range []string{
|
for i, hexdata := range []string{
|
||||||
"a52c101e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
"a52c101e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
||||||
"a52c101e000000000000000000000000000000000000000000000000000000000000001200",
|
"a52c101e000000000000000000000000000000000000000000000000000000000000001200",
|
||||||
|
|
@ -122,9 +118,9 @@ func TestCalldataDecoding(t *testing.T) {
|
||||||
// Too short
|
// Too short
|
||||||
"751e10790000000000000000000000000000000000000000000000000000000000000012",
|
"751e10790000000000000000000000000000000000000000000000000000000000000012",
|
||||||
"751e1079FFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
"751e1079FFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
//Not valid multiple of 32
|
// Not valid multiple of 32
|
||||||
"deadbeef00000000000000000000000000000000000000000000000000000000000000",
|
"deadbeef00000000000000000000000000000000000000000000000000000000000000",
|
||||||
//Too short 'issue'
|
// Too short 'issue'
|
||||||
"42958b5400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
"42958b5400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
||||||
// Too short compareAndApprove
|
// Too short compareAndApprove
|
||||||
"a52c101e00ff0000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
"a52c101e00ff0000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
||||||
|
|
@ -137,7 +133,7 @@ func TestCalldataDecoding(t *testing.T) {
|
||||||
t.Errorf("test %d: expected decoding to fail: %s", i, hexdata)
|
t.Errorf("test %d: expected decoding to fail: %s", i, hexdata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Expected success
|
// Expected success
|
||||||
for i, hexdata := range []string{
|
for i, hexdata := range []string{
|
||||||
// From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
|
// From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
|
||||||
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
|
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
|
||||||
|
|
@ -147,7 +143,7 @@ func TestCalldataDecoding(t *testing.T) {
|
||||||
"42958b54" +
|
"42958b54" +
|
||||||
// start of dynamic type
|
// start of dynamic type
|
||||||
"0000000000000000000000000000000000000000000000000000000000000040" +
|
"0000000000000000000000000000000000000000000000000000000000000040" +
|
||||||
//uint256
|
// uint256
|
||||||
"0000000000000000000000000000000000000000000000000000000000000001" +
|
"0000000000000000000000000000000000000000000000000000000000000001" +
|
||||||
// length of array
|
// length of array
|
||||||
"0000000000000000000000000000000000000000000000000000000000000002" +
|
"0000000000000000000000000000000000000000000000000000000000000002" +
|
||||||
|
|
@ -162,79 +158,7 @@ func TestCalldataDecoding(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectorUnmarshalling(t *testing.T) {
|
func TestMaliciousABIStrings(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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMaliciousAbiStrings(t *testing.T) {
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"func(uint256,uint256,[]uint256)",
|
"func(uint256,uint256,[]uint256)",
|
||||||
"func(uint256,uint256,uint256,)",
|
"func(uint256,uint256,uint256,)",
|
||||||
|
|
@ -242,7 +166,7 @@ func TestMaliciousAbiStrings(t *testing.T) {
|
||||||
}
|
}
|
||||||
data := common.Hex2Bytes("4401a6e40000000000000000000000000000000000000000000000000000000000000012")
|
data := common.Hex2Bytes("4401a6e40000000000000000000000000000000000000000000000000000000000000012")
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
_, err := testSelector(tt, data)
|
_, err := verifySelector(tt, data)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("test %d: expected error for selector '%v'", i, tt)
|
t.Errorf("test %d: expected error for selector '%v'", i, tt)
|
||||||
}
|
}
|
||||||
143
signer/fourbyte/fourbyte.go
Normal file
143
signer/fourbyte/fourbyte.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
91
signer/fourbyte/fourbyte_test.go
Normal file
91
signer/fourbyte/fourbyte_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
117
signer/fourbyte/validation.go
Normal file
117
signer/fourbyte/validation.go
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
137
signer/fourbyte/validation_test.go
Normal file
137
signer/fourbyte/validation_test.go
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,8 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/testutil"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/swarm/storage"
|
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||||
"github.com/ethereum/go-ethereum/swarm/storage/feed/lookup"
|
"github.com/ethereum/go-ethereum/swarm/storage/feed/lookup"
|
||||||
|
|
||||||
|
|
@ -43,7 +45,13 @@ func serverFunc(api *api.API) swarmhttp.TestServer {
|
||||||
func TestClientUploadDownloadRaw(t *testing.T) {
|
func TestClientUploadDownloadRaw(t *testing.T) {
|
||||||
testClientUploadDownloadRaw(false, t)
|
testClientUploadDownloadRaw(false, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientUploadDownloadRawEncrypted(t *testing.T) {
|
func TestClientUploadDownloadRawEncrypted(t *testing.T) {
|
||||||
|
if testutil.RaceEnabled {
|
||||||
|
t.Skip("flaky with -race on Travis")
|
||||||
|
// See: https://github.com/ethersphere/go-ethereum/issues/1254
|
||||||
|
}
|
||||||
|
|
||||||
testClientUploadDownloadRaw(true, t)
|
testClientUploadDownloadRaw(true, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,44 +33,45 @@ type TestServer interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestSwarmServer(t *testing.T, serverFunc func(*api.API) TestServer, resolver api.Resolver) *TestSwarmServer {
|
func NewTestSwarmServer(t *testing.T, serverFunc func(*api.API) TestServer, resolver api.Resolver) *TestSwarmServer {
|
||||||
dir, err := ioutil.TempDir("", "swarm-storage-test")
|
swarmDir, err := ioutil.TempDir("", "swarm-storage-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
storeparams := storage.NewDefaultLocalStoreParams()
|
|
||||||
storeparams.DbCapacity = 5000000
|
storeParams := storage.NewDefaultLocalStoreParams()
|
||||||
storeparams.CacheCapacity = 5000
|
storeParams.DbCapacity = 5000000
|
||||||
storeparams.Init(dir)
|
storeParams.CacheCapacity = 5000
|
||||||
localStore, err := storage.NewLocalStore(storeparams, nil)
|
storeParams.Init(swarmDir)
|
||||||
|
localStore, err := storage.NewLocalStore(storeParams, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(dir)
|
os.RemoveAll(swarmDir)
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
fileStore := storage.NewFileStore(localStore, storage.NewFileStoreParams())
|
fileStore := storage.NewFileStore(localStore, storage.NewFileStoreParams())
|
||||||
|
|
||||||
// Swarm feeds test setup
|
// Swarm feeds test setup
|
||||||
feedsDir, err := ioutil.TempDir("", "swarm-feeds-test")
|
feedsDir, err := ioutil.TempDir("", "swarm-feeds-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rhparams := &feed.HandlerParams{}
|
feeds, err := feed.NewTestHandler(feedsDir, &feed.HandlerParams{})
|
||||||
rh, err := feed.NewTestHandler(feedsDir, rhparams)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a := api.NewAPI(fileStore, resolver, rh.Handler, nil)
|
swarmApi := api.NewAPI(fileStore, resolver, feeds.Handler, nil)
|
||||||
srv := httptest.NewServer(serverFunc(a))
|
apiServer := httptest.NewServer(serverFunc(swarmApi))
|
||||||
|
|
||||||
tss := &TestSwarmServer{
|
tss := &TestSwarmServer{
|
||||||
Server: srv,
|
Server: apiServer,
|
||||||
FileStore: fileStore,
|
FileStore: fileStore,
|
||||||
dir: dir,
|
dir: swarmDir,
|
||||||
Hasher: storage.MakeHashFunc(storage.DefaultHash)(),
|
Hasher: storage.MakeHashFunc(storage.DefaultHash)(),
|
||||||
cleanup: func() {
|
cleanup: func() {
|
||||||
srv.Close()
|
apiServer.Close()
|
||||||
rh.Close()
|
fileStore.Close()
|
||||||
os.RemoveAll(dir)
|
feeds.Close()
|
||||||
|
os.RemoveAll(swarmDir)
|
||||||
os.RemoveAll(feedsDir)
|
os.RemoveAll(feedsDir)
|
||||||
},
|
},
|
||||||
CurrentTime: 42,
|
CurrentTime: 42,
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,12 @@ func TestLDBStoreCollectGarbage(t *testing.T) {
|
||||||
initialCap := defaultMaxGCRound / 100
|
initialCap := defaultMaxGCRound / 100
|
||||||
cap := initialCap / 2
|
cap := initialCap / 2
|
||||||
t.Run(fmt.Sprintf("A/%d/%d", cap, cap*4), testLDBStoreCollectGarbage)
|
t.Run(fmt.Sprintf("A/%d/%d", cap, cap*4), testLDBStoreCollectGarbage)
|
||||||
|
|
||||||
|
if testutil.RaceEnabled {
|
||||||
|
t.Skip("only the simplest case run as others are flaky with race")
|
||||||
|
// Note: some tests fail consistently and even locally with `-race`
|
||||||
|
}
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("B/%d/%d", cap, cap*4), testLDBStoreRemoveThenCollectGarbage)
|
t.Run(fmt.Sprintf("B/%d/%d", cap, cap*4), testLDBStoreRemoveThenCollectGarbage)
|
||||||
|
|
||||||
// at max round
|
// at max round
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue