major refactoring: node manager, tx queue, accounts, jail

- node: signals and node reset, fixes #152
- tests update (testify is used)
- node manager refactored, race conditions fixed
- node wrapper has been removed, we rely on go-ethereum node now
- tx queue refactored
- jail refactored
- backend and API introduced
This commit is contained in:
Victor Farazdagi 2017-05-16 15:09:52 +03:00
parent 9c679d6bb7
commit 349103de1a
60 changed files with 5348 additions and 4670 deletions

View File

@ -47,9 +47,11 @@ statusgo-ios-simulator-mainnet: xgo
@echo "iOS framework cross compilation done (mainnet)."
ci:
build/env.sh go test -v -cover ./geth
build/env.sh go test -v -cover ./geth/params
build/env.sh go test -v -cover ./geth/api
build/env.sh go test -v -cover ./geth/common
build/env.sh go test -v -cover ./geth/jail
build/env.sh go test -v -cover ./geth/node
build/env.sh go test -v -cover ./geth/params
build/env.sh go test -v -cover ./extkeys
generate:
@ -110,12 +112,16 @@ lint:
test:
@build/env.sh echo "mode: set" > coverage-all.out
build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth
build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth/api
@build/env.sh tail -n +2 coverage.out >> coverage-all.out
build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth/params
build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth/common
@build/env.sh tail -n +2 coverage.out >> coverage-all.out
build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth/jail
@build/env.sh tail -n +2 coverage.out >> coverage-all.out
build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth/node
@build/env.sh tail -n +2 coverage.out >> coverage-all.out
build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth/params
@build/env.sh tail -n +2 coverage.out >> coverage-all.out
build/env.sh go test -coverprofile=coverage.out -covermode=set ./extkeys
@build/env.sh tail -n +2 coverage.out >> coverage-all.out
build/env.sh go test -coverprofile=coverage.out -covermode=set ./cmd/statusd
@ -123,13 +129,13 @@ test:
@build/env.sh go tool cover -html=coverage-all.out -o coverage.html
@build/env.sh go tool cover -func=coverage-all.out
test-geth:
build/env.sh go test -v -coverprofile=coverage.out ./geth
test-api:
build/env.sh go test -v -coverprofile=coverage.out -coverpkg=./geth/node ./geth/api
@build/env.sh go tool cover -html=coverage.out -o coverage.html
@build/env.sh go tool cover -func=coverage.out
test-params:
build/env.sh go test -v -coverprofile=coverage.out ./geth/params
test-common:
build/env.sh go test -v -coverprofile=coverage.out ./geth/common
@build/env.sh go tool cover -html=coverage.out -o coverage.html
@build/env.sh go tool cover -func=coverage.out
@ -138,6 +144,16 @@ test-jail:
@build/env.sh go tool cover -html=coverage.out -o coverage.html
@build/env.sh go tool cover -func=coverage.out
test-node:
build/env.sh go test -v -coverprofile=coverage.out ./geth/node
@build/env.sh go tool cover -html=coverage.out -o coverage.html
@build/env.sh go tool cover -func=coverage.out
test-params:
build/env.sh go test -v -coverprofile=coverage.out ./geth/params
@build/env.sh go tool cover -html=coverage.out -o coverage.html
@build/env.sh go tool cover -func=coverage.out
test-extkeys:
build/env.sh go test -v -coverprofile=coverage.out ./extkeys
@build/env.sh go tool cover -html=coverage.out -o coverage.html

View File

@ -3,7 +3,6 @@ package main
import (
"fmt"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/params"
"gopkg.in/urfave/cli.v1"
)
@ -27,12 +26,16 @@ func faucetCommandHandler(ctx *cli.Context) error {
}
fmt.Println("Starting Status Faucet node..")
if err = geth.CreateAndRunNode(config); err != nil {
if err := statusAPI.StartNode(config); err != nil {
return err
}
// wait till node has been stopped
geth.NodeManagerInstance().Node().GethStack().Wait()
node, err := statusAPI.NodeManager().Node()
if err != nil {
return nil
}
node.Wait()
return nil
}

View File

@ -3,7 +3,6 @@ package main
import (
"fmt"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/params"
"gopkg.in/urfave/cli.v1"
)
@ -31,12 +30,16 @@ func lesCommandHandler(ctx *cli.Context) error {
}
fmt.Println("Starting Light Status node..")
if err = geth.CreateAndRunNode(config); err != nil {
if err := statusAPI.StartNode(config); err != nil {
return err
}
// wait till node has been stopped
geth.NodeManagerInstance().Node().GethStack().Wait()
node, err := statusAPI.NodeManager().Node()
if err != nil {
return nil
}
node.Wait()
return nil
}

View File

@ -6,16 +6,81 @@ import (
"fmt"
"os"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/jail"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/params"
)
//export GenerateConfig
func GenerateConfig(datadir *C.char, networkID C.int, devMode C.int) *C.char {
config, err := params.NewNodeConfig(C.GoString(datadir), uint64(networkID), devMode == 1)
if err != nil {
return makeJSONResponse(err)
}
outBytes, err := json.Marshal(&config)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(outBytes))
}
//export StartNode
func StartNode(configJSON *C.char) *C.char {
config, err := params.LoadNodeConfig(C.GoString(configJSON))
if err != nil {
return makeJSONResponse(err)
}
err = statusAPI.StartNodeNonBlocking(config)
return makeJSONResponse(err)
}
//export StopNode
func StopNode() *C.char {
return makeJSONResponse(statusAPI.StopNode())
}
//export ResumeNode
func ResumeNode() *C.char {
err := fmt.Errorf("%v: %v", common.ErrDeprecatedMethod.Error(), "ResumeNode")
return makeJSONResponse(err)
}
//export ResetChainData
func ResetChainData() *C.char {
return makeJSONResponse(statusAPI.ResetChainData())
}
//export StopNodeRPCServer
func StopNodeRPCServer() *C.char {
err := fmt.Errorf("%v: %v", common.ErrDeprecatedMethod.Error(), "StopNodeRPCServer")
return makeJSONResponse(err)
}
//export StartNodeRPCServer
func StartNodeRPCServer() *C.char {
err := fmt.Errorf("%v: %v", common.ErrDeprecatedMethod.Error(), "StartNodeRPCServer")
return makeJSONResponse(err)
}
//export PopulateStaticPeers
func PopulateStaticPeers() *C.char {
err := fmt.Errorf("%v: %v", common.ErrDeprecatedMethod.Error(), "PopulateStaticPeers")
return makeJSONResponse(err)
}
//export AddPeer
func AddPeer(url *C.char) *C.char {
err := fmt.Errorf("%v: %v", common.ErrDeprecatedMethod.Error(), "AddPeer")
return makeJSONResponse(err)
}
//export CreateAccount
func CreateAccount(password *C.char) *C.char {
// This is equivalent to creating an account from the command line,
// just modified to handle the function arg passing
address, pubKey, mnemonic, err := geth.CreateAccount(C.GoString(password))
address, pubKey, mnemonic, err := statusAPI.CreateAccount(C.GoString(password))
errString := ""
if err != nil {
@ -23,21 +88,19 @@ func CreateAccount(password *C.char) *C.char {
errString = err.Error()
}
out := geth.AccountInfo{
out := common.AccountInfo{
Address: address,
PubKey: pubKey,
Mnemonic: mnemonic,
Error: errString,
}
outBytes, _ := json.Marshal(&out)
return C.CString(string(outBytes))
}
//export CreateChildAccount
func CreateChildAccount(parentAddress, password *C.char) *C.char {
address, pubKey, err := geth.CreateChildAccount(C.GoString(parentAddress), C.GoString(password))
address, pubKey, err := statusAPI.CreateChildAccount(C.GoString(parentAddress), C.GoString(password))
errString := ""
if err != nil {
@ -45,20 +108,18 @@ func CreateChildAccount(parentAddress, password *C.char) *C.char {
errString = err.Error()
}
out := geth.AccountInfo{
out := common.AccountInfo{
Address: address,
PubKey: pubKey,
Error: errString,
}
outBytes, _ := json.Marshal(&out)
return C.CString(string(outBytes))
}
//export RecoverAccount
func RecoverAccount(password, mnemonic *C.char) *C.char {
address, pubKey, err := geth.RecoverAccount(C.GoString(password), C.GoString(mnemonic))
address, pubKey, err := statusAPI.RecoverAccount(C.GoString(password), C.GoString(mnemonic))
errString := ""
if err != nil {
@ -66,41 +127,40 @@ func RecoverAccount(password, mnemonic *C.char) *C.char {
errString = err.Error()
}
out := geth.AccountInfo{
out := common.AccountInfo{
Address: address,
PubKey: pubKey,
Mnemonic: C.GoString(mnemonic),
Error: errString,
}
outBytes, _ := json.Marshal(&out)
return C.CString(string(outBytes))
}
//export VerifyAccountPassword
func VerifyAccountPassword(keyStoreDir, address, password *C.char) *C.char {
_, err := geth.VerifyAccountPassword(C.GoString(keyStoreDir), C.GoString(address), C.GoString(password))
return makeJSONErrorResponse(err)
_, err := statusAPI.VerifyAccountPassword(C.GoString(keyStoreDir), C.GoString(address), C.GoString(password))
return makeJSONResponse(err)
}
//export Login
func Login(address, password *C.char) *C.char {
// loads a key file (for a given address), tries to decrypt it using the password, to verify ownership
// if verified, purges all the previous identities from Whisper, and injects verified key as shh identity
err := geth.SelectAccount(C.GoString(address), C.GoString(password))
return makeJSONErrorResponse(err)
err := statusAPI.SelectAccount(C.GoString(address), C.GoString(password))
return makeJSONResponse(err)
}
//export Logout
func Logout() *C.char {
// This is equivalent to clearing whisper identities
err := geth.Logout()
return makeJSONErrorResponse(err)
err := statusAPI.Logout()
return makeJSONResponse(err)
}
//export CompleteTransaction
func CompleteTransaction(id, password *C.char) *C.char {
txHash, err := geth.CompleteTransaction(C.GoString(id), C.GoString(password))
txHash, err := statusAPI.CompleteTransaction(C.GoString(id), C.GoString(password))
errString := ""
if err != nil {
@ -108,7 +168,7 @@ func CompleteTransaction(id, password *C.char) *C.char {
errString = err.Error()
}
out := geth.CompleteTransactionResult{
out := common.CompleteTransactionResult{
ID: C.GoString(id),
Hash: txHash.Hex(),
Error: errString,
@ -120,12 +180,12 @@ func CompleteTransaction(id, password *C.char) *C.char {
//export CompleteTransactions
func CompleteTransactions(ids, password *C.char) *C.char {
out := geth.CompleteTransactionsResult{}
out.Results = make(map[string]geth.CompleteTransactionResult)
out := common.CompleteTransactionsResult{}
out.Results = make(map[string]common.CompleteTransactionResult)
results := geth.CompleteTransactions(C.GoString(ids), C.GoString(password))
results := statusAPI.CompleteTransactions(C.GoString(ids), C.GoString(password))
for txID, result := range results {
txResult := geth.CompleteTransactionResult{
txResult := common.CompleteTransactionResult{
ID: txID,
Hash: result.Hash.Hex(),
}
@ -141,7 +201,7 @@ func CompleteTransactions(ids, password *C.char) *C.char {
//export DiscardTransaction
func DiscardTransaction(id *C.char) *C.char {
err := geth.DiscardTransaction(C.GoString(id))
err := statusAPI.DiscardTransaction(C.GoString(id))
errString := ""
if err != nil {
@ -149,7 +209,7 @@ func DiscardTransaction(id *C.char) *C.char {
errString = err.Error()
}
out := geth.DiscardTransactionResult{
out := common.DiscardTransactionResult{
ID: C.GoString(id),
Error: errString,
}
@ -160,12 +220,12 @@ func DiscardTransaction(id *C.char) *C.char {
//export DiscardTransactions
func DiscardTransactions(ids *C.char) *C.char {
out := geth.DiscardTransactionsResult{}
out.Results = make(map[string]geth.DiscardTransactionResult)
out := common.DiscardTransactionsResult{}
out.Results = make(map[string]common.DiscardTransactionResult)
results := geth.DiscardTransactions(C.GoString(ids))
results := statusAPI.DiscardTransactions(C.GoString(ids))
for txID, result := range results {
txResult := geth.DiscardTransactionResult{
txResult := common.DiscardTransactionResult{
ID: txID,
}
if result.Error != nil {
@ -178,112 +238,31 @@ func DiscardTransactions(ids *C.char) *C.char {
return C.CString(string(outBytes))
}
//export GenerateConfig
func GenerateConfig(datadir *C.char, networkID C.int, devMode C.int) *C.char {
config, err := params.NewNodeConfig(C.GoString(datadir), uint64(networkID), devMode == 1)
if err != nil {
return makeJSONErrorResponse(err)
}
outBytes, err := json.Marshal(&config)
if err != nil {
return makeJSONErrorResponse(err)
}
return C.CString(string(outBytes))
}
//export StartNode
func StartNode(configJSON *C.char) *C.char {
config, err := params.LoadNodeConfig(C.GoString(configJSON))
if err != nil {
return makeJSONErrorResponse(err)
}
err = geth.CreateAndRunNode(config)
return makeJSONErrorResponse(err)
}
//export StopNode
func StopNode() *C.char {
err := geth.NodeManagerInstance().StopNode()
return makeJSONErrorResponse(err)
}
//export ResumeNode
func ResumeNode() *C.char {
err := geth.NodeManagerInstance().ResumeNode()
return makeJSONErrorResponse(err)
}
//export ResetChainData
func ResetChainData() *C.char {
err := geth.NodeManagerInstance().ResetChainData()
return makeJSONErrorResponse(err)
}
//export StopNodeRPCServer
func StopNodeRPCServer() *C.char {
_, err := geth.NodeManagerInstance().StopNodeRPCServer()
return makeJSONErrorResponse(err)
}
//export StartNodeRPCServer
func StartNodeRPCServer() *C.char {
_, err := geth.NodeManagerInstance().StartNodeRPCServer()
return makeJSONErrorResponse(err)
}
//export InitJail
func InitJail(js *C.char) {
jail.Init(C.GoString(js))
statusAPI.JailBaseJS(C.GoString(js))
}
//export Parse
func Parse(chatID *C.char, js *C.char) *C.char {
res := jail.GetInstance().Parse(C.GoString(chatID), C.GoString(js))
res := statusAPI.JailParse(C.GoString(chatID), C.GoString(js))
return C.CString(res)
}
//export Call
func Call(chatID *C.char, path *C.char, params *C.char) *C.char {
res := jail.GetInstance().Call(C.GoString(chatID), C.GoString(path), C.GoString(params))
res := statusAPI.JailCall(C.GoString(chatID), C.GoString(path), C.GoString(params))
return C.CString(res)
}
//export PopulateStaticPeers
func PopulateStaticPeers() {
geth.NodeManagerInstance().PopulateStaticPeers()
}
//export AddPeer
func AddPeer(url *C.char) *C.char {
success, err := geth.NodeManagerInstance().AddPeer(C.GoString(url))
func makeJSONResponse(err error) *C.char {
errString := ""
if err != nil {
fmt.Fprintln(os.Stderr, err)
errString = err.Error()
}
out := geth.AddPeerResult{
Success: success,
Error: errString,
}
outBytes, _ := json.Marshal(&out)
return C.CString(string(outBytes))
}
func makeJSONErrorResponse(err error) *C.char {
errString := ""
if err != nil {
fmt.Fprintln(os.Stderr, err)
errString = err.Error()
}
out := geth.JSONError{
out := common.APIResponse{
Error: errString,
}
outBytes, _ := json.Marshal(&out)

View File

@ -6,6 +6,7 @@ import (
"path/filepath"
"runtime"
"github.com/status-im/status-go/geth/api"
"github.com/status-im/status-go/geth/params"
"gopkg.in/urfave/cli.v1"
)
@ -14,6 +15,7 @@ var (
gitCommit = "rely on linker: -ldflags -X main.GitCommit"
buildStamp = "rely on linker: -ldflags -X main.buildStamp"
app = makeApp(gitCommit)
statusAPI = api.NewStatusAPI()
)
var (

View File

@ -12,24 +12,28 @@ import (
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/les/status"
gethparams "github.com/ethereum/go-ethereum/params"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
"github.com/status-im/status-go/static"
)
const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000"
var testConfig *geth.TestConfig
func init() {
// error is ignored, as it will occur on non-test compilation only, and there testConfig is not used at all
// (we have to use "main" package due to restrictions on including C imports into *_test packages)
testConfig, _ = geth.LoadTestConfig()
}
var nodeConfigJSON = `{
"NetworkId": ` + strconv.Itoa(params.RopstenNetworkID) + `,
"DataDir": "` + TestDataDir + `",
"HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `,
"WSPort": ` + strconv.Itoa(TestConfig.Node.WSPort) + `,
"LogEnabled": true,
"LogLevel": "INFO"
}`
// nolint: deadcode
func testExportedAPI(t *testing.T, done chan struct{}) {
@ -48,13 +52,9 @@ func testExportedAPI(t *testing.T, done chan struct{}) {
testResetChainData,
},
{
"pause node",
"stop/resume node",
testStopResumeNode,
},
{
"restart node RPC",
testRestartNodeRPC,
},
{
"create main and child accounts",
testCreateChildAccount,
@ -102,6 +102,7 @@ func testExportedAPI(t *testing.T, done chan struct{}) {
}
for _, test := range tests {
t.Logf("=== RUN %s", test.name)
if ok := test.fn(t); !ok {
break
}
@ -117,25 +118,25 @@ func testVerifyAccountPassword(t *testing.T) bool {
}
defer os.RemoveAll(tmpDir) // nolint: errcheck
if err = geth.ImportTestAccount(tmpDir, "test-account1.pk"); err != nil {
if err = common.ImportTestAccount(tmpDir, "test-account1.pk"); err != nil {
t.Fatal(err)
}
if err = geth.ImportTestAccount(tmpDir, "test-account2.pk"); err != nil {
if err = common.ImportTestAccount(tmpDir, "test-account2.pk"); err != nil {
t.Fatal(err)
}
// rename account file (to see that file's internals reviewed, when locating account key)
accountFilePathOriginal := filepath.Join(tmpDir, "test-account1.pk")
accountFilePath := filepath.Join(tmpDir, "foo"+testConfig.Account1.Address+"bar.pk")
accountFilePath := filepath.Join(tmpDir, "foo"+TestConfig.Account1.Address+"bar.pk")
if err := os.Rename(accountFilePathOriginal, accountFilePath); err != nil {
t.Fatal(err)
}
response := geth.JSONError{}
response := common.APIResponse{}
rawResponse := VerifyAccountPassword(
C.CString(tmpDir),
C.CString(testConfig.Account1.Address),
C.CString(testConfig.Account1.Password))
C.CString(TestConfig.Account1.Address),
C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
t.Errorf("cannot decode response (%s): %v", C.GoString(rawResponse), err)
@ -251,7 +252,7 @@ func testGetDefaultConfig(t *testing.T) bool {
}
func testResetChainData(t *testing.T) bool {
resetChainDataResponse := geth.JSONError{}
resetChainDataResponse := common.APIResponse{}
rawResponse := ResetChainData()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &resetChainDataResponse); err != nil {
@ -263,7 +264,7 @@ func testResetChainData(t *testing.T) bool {
return false
}
time.Sleep(testConfig.Node.SyncSeconds * time.Second) // allow to re-sync blockchain
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to re-sync blockchain
testCompleteTransaction(t)
@ -272,17 +273,17 @@ func testResetChainData(t *testing.T) bool {
func testStopResumeNode(t *testing.T) bool {
// to make sure that we start with empty account (which might get populated during previous tests)
if err := geth.Logout(); err != nil {
if err := statusAPI.Logout(); err != nil {
t.Fatal(err)
}
whisperService, err := geth.NodeManagerInstance().WhisperService()
whisperService, err := statusAPI.NodeManager().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create an account
address1, pubKey1, _, err := geth.CreateAccount(testConfig.Account1.Password)
address1, pubKey1, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
@ -295,8 +296,8 @@ func testStopResumeNode(t *testing.T) bool {
}
// select account
loginResponse := geth.JSONError{}
rawResponse := Login(C.CString(address1), C.CString(testConfig.Account1.Password))
loginResponse := common.APIResponse{}
rawResponse := Login(C.CString(address1), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
@ -314,7 +315,7 @@ func testStopResumeNode(t *testing.T) bool {
// stop and resume node, then make sure that selected account is still selected
// nolint: dupl
stopNodeFn := func() bool {
response := geth.JSONError{}
response := common.APIResponse{}
rawResponse = StopNode()
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
@ -331,11 +332,12 @@ func testStopResumeNode(t *testing.T) bool {
// nolint: dupl
resumeNodeFn := func() bool {
response := geth.JSONError{}
rawResponse = ResumeNode()
response := common.APIResponse{}
rawResponse = StartNode(C.CString(nodeConfigJSON))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
t.Errorf("cannot decode ResumeNode response (%s): %v", C.GoString(rawResponse), err)
t.Errorf("cannot decode StartNode response (%s): %v", C.GoString(rawResponse), err)
return false
}
if response.Error != "" {
@ -353,8 +355,10 @@ func testStopResumeNode(t *testing.T) bool {
return false
}
time.Sleep(5 * time.Second) // allow to start (instead of using blocking version of start, of filter event)
// now, verify that we still have account logged in
whisperService, err = geth.NodeManagerInstance().WhisperService()
whisperService, err = statusAPI.NodeManager().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
@ -368,65 +372,21 @@ func testStopResumeNode(t *testing.T) bool {
return true
}
func testRestartNodeRPC(t *testing.T) bool {
// stop RPC
stopNodeRPCServerResponse := geth.JSONError{}
rawResponse := StopNodeRPCServer()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &stopNodeRPCServerResponse); err != nil {
t.Errorf("cannot decode StopNodeRPCServer response (%s): %v", C.GoString(rawResponse), err)
return false
}
if stopNodeRPCServerResponse.Error != "" {
t.Errorf("unexpected error: %s", stopNodeRPCServerResponse.Error)
return false
}
// start again RPC
startNodeRPCServerResponse := geth.JSONError{}
rawResponse = StartNodeRPCServer()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &startNodeRPCServerResponse); err != nil {
t.Errorf("cannot decode StartNodeRPCServer response (%s): %v", C.GoString(rawResponse), err)
return false
}
if startNodeRPCServerResponse.Error != "" {
t.Errorf("unexpected error: %s", startNodeRPCServerResponse.Error)
return false
}
// start when we have RPC already running
startNodeRPCServerResponse = geth.JSONError{}
rawResponse = StartNodeRPCServer()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &startNodeRPCServerResponse); err != nil {
t.Errorf("cannot decode StartNodeRPCServer response (%s): %v", C.GoString(rawResponse), err)
return false
}
expectedError := "HTTP RPC already running on localhost:8645"
if startNodeRPCServerResponse.Error != expectedError {
t.Errorf("expected error not thrown: %s", expectedError)
return false
}
return true
}
func testCreateChildAccount(t *testing.T) bool {
// to make sure that we start with empty account (which might get populated during previous tests)
if err := geth.Logout(); err != nil {
if err := statusAPI.Logout(); err != nil {
t.Fatal(err)
}
keyStore, err := geth.NodeManagerInstance().AccountKeyStore()
keyStore, err := statusAPI.NodeManager().AccountKeyStore()
if err != nil {
t.Error(err)
return false
}
// create an account
createAccountResponse := geth.AccountInfo{}
rawResponse := CreateAccount(C.CString(testConfig.Account1.Password))
createAccountResponse := common.AccountInfo{}
rawResponse := CreateAccount(C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &createAccountResponse); err != nil {
t.Errorf("cannot decode CreateAccount response (%s): %v", C.GoString(rawResponse), err)
@ -440,14 +400,14 @@ func testCreateChildAccount(t *testing.T) bool {
address, pubKey, mnemonic := createAccountResponse.Address, createAccountResponse.PubKey, createAccountResponse.Mnemonic
t.Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
account, err := geth.ParseAccountString(address)
account, err := common.ParseAccountString(address)
if err != nil {
t.Errorf("can not get account from address: %v", err)
return false
}
// obtain decrypted key, and make sure that extended key (which will be used as root for sub-accounts) is present
_, key, err := keyStore.AccountDecryptedKey(account, testConfig.Account1.Password)
_, key, err := keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return false
@ -459,27 +419,27 @@ func testCreateChildAccount(t *testing.T) bool {
}
// try creating sub-account, w/o selecting main account i.e. w/o login to main account
createSubAccountResponse := geth.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(testConfig.Account1.Password))
createSubAccountResponse := common.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if createSubAccountResponse.Error != geth.ErrNoAccountSelected.Error() {
if createSubAccountResponse.Error != node.ErrNoAccountSelected.Error() {
t.Errorf("expected error is not returned (tried to create sub-account w/o login): %v", createSubAccountResponse.Error)
return false
}
err = geth.SelectAccount(address, testConfig.Account1.Password)
err = statusAPI.SelectAccount(address, TestConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
// try to create sub-account with wrong password
createSubAccountResponse = geth.AccountInfo{}
createSubAccountResponse = common.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString("wrong password"))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse); err != nil {
@ -493,8 +453,8 @@ func testCreateChildAccount(t *testing.T) bool {
}
// create sub-account (from implicit parent)
createSubAccountResponse1 := geth.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(testConfig.Account1.Password))
createSubAccountResponse1 := common.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse1); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
@ -507,8 +467,8 @@ func testCreateChildAccount(t *testing.T) bool {
}
// make sure that sub-account index automatically progresses
createSubAccountResponse2 := geth.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(testConfig.Account1.Password))
createSubAccountResponse2 := common.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse2); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
@ -525,8 +485,8 @@ func testCreateChildAccount(t *testing.T) bool {
}
// create sub-account (from explicit parent)
createSubAccountResponse3 := geth.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(createSubAccountResponse2.Address), C.CString(testConfig.Account1.Password))
createSubAccountResponse3 := common.AccountInfo{}
rawResponse = CreateChildAccount(C.CString(createSubAccountResponse2.Address), C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse3); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
@ -549,10 +509,10 @@ func testCreateChildAccount(t *testing.T) bool {
}
func testRecoverAccount(t *testing.T) bool {
keyStore, _ := geth.NodeManagerInstance().AccountKeyStore()
keyStore, _ := statusAPI.NodeManager().AccountKeyStore()
// create an account
address, pubKey, mnemonic, err := geth.CreateAccount(testConfig.Account1.Password)
address, pubKey, mnemonic, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
@ -560,8 +520,8 @@ func testRecoverAccount(t *testing.T) bool {
t.Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
// try recovering using password + mnemonic
recoverAccountResponse := geth.AccountInfo{}
rawResponse := RecoverAccount(C.CString(testConfig.Account1.Password), C.CString(mnemonic))
recoverAccountResponse := common.AccountInfo{}
rawResponse := RecoverAccount(C.CString(TestConfig.Account1.Password), C.CString(mnemonic))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &recoverAccountResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
@ -578,24 +538,24 @@ func testRecoverAccount(t *testing.T) bool {
}
// now test recovering, but make sure that account/key file is removed i.e. simulate recovering on a new device
account, err := geth.ParseAccountString(address)
account, err := common.ParseAccountString(address)
if err != nil {
t.Errorf("can not get account from address: %v", err)
}
account, key, err := keyStore.AccountDecryptedKey(account, testConfig.Account1.Password)
account, key, err := keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return false
}
extChild2String := key.ExtendedKey.String()
if err = keyStore.Delete(account, testConfig.Account1.Password); err != nil {
if err = keyStore.Delete(account, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot remove account: %v", err)
}
recoverAccountResponse = geth.AccountInfo{}
rawResponse = RecoverAccount(C.CString(testConfig.Account1.Password), C.CString(mnemonic))
recoverAccountResponse = common.AccountInfo{}
rawResponse = RecoverAccount(C.CString(TestConfig.Account1.Password), C.CString(mnemonic))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &recoverAccountResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
@ -612,7 +572,7 @@ func testRecoverAccount(t *testing.T) bool {
}
// make sure that extended key exists and is imported ok too
_, key, err = keyStore.AccountDecryptedKey(account, testConfig.Account1.Password)
_, key, err = keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return false
@ -622,8 +582,8 @@ func testRecoverAccount(t *testing.T) bool {
}
// make sure that calling import several times, just returns from cache (no error is expected)
recoverAccountResponse = geth.AccountInfo{}
rawResponse = RecoverAccount(C.CString(testConfig.Account1.Password), C.CString(mnemonic))
recoverAccountResponse = common.AccountInfo{}
rawResponse = RecoverAccount(C.CString(TestConfig.Account1.Password), C.CString(mnemonic))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &recoverAccountResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
@ -640,7 +600,7 @@ func testRecoverAccount(t *testing.T) bool {
}
// time to login with recovered data
whisperService, err := geth.NodeManagerInstance().WhisperService()
whisperService, err := statusAPI.NodeManager().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
@ -649,7 +609,7 @@ func testRecoverAccount(t *testing.T) bool {
if whisperService.HasKeyPair(pubKeyCheck) {
t.Error("identity already present in whisper")
}
err = geth.SelectAccount(addressCheck, testConfig.Account1.Password)
err = statusAPI.SelectAccount(addressCheck, TestConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
@ -663,20 +623,20 @@ func testRecoverAccount(t *testing.T) bool {
func testAccountSelect(t *testing.T) bool {
// test to see if the account was injected in whisper
whisperService, err := geth.NodeManagerInstance().WhisperService()
whisperService, err := statusAPI.NodeManager().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create an account
address1, pubKey1, _, err := geth.CreateAccount(testConfig.Account1.Password)
address1, pubKey1, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
t.Logf("Account created: {address: %s, key: %s}", address1, pubKey1)
address2, pubKey2, _, err := geth.CreateAccount(testConfig.Account1.Password)
address2, pubKey2, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Error("Test failed: could not create account")
return false
@ -689,7 +649,7 @@ func testAccountSelect(t *testing.T) bool {
}
// try selecting with wrong password
loginResponse := geth.JSONError{}
loginResponse := common.APIResponse{}
rawResponse := Login(C.CString(address1), C.CString("wrongPassword"))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
@ -702,8 +662,8 @@ func testAccountSelect(t *testing.T) bool {
return false
}
loginResponse = geth.JSONError{}
rawResponse = Login(C.CString(address1), C.CString(testConfig.Account1.Password))
loginResponse = common.APIResponse{}
rawResponse = Login(C.CString(address1), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
@ -723,8 +683,8 @@ func testAccountSelect(t *testing.T) bool {
t.Error("identity already present in whisper")
}
loginResponse = geth.JSONError{}
rawResponse = Login(C.CString(address2), C.CString(testConfig.Account1.Password))
loginResponse = common.APIResponse{}
rawResponse = Login(C.CString(address2), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
@ -746,14 +706,14 @@ func testAccountSelect(t *testing.T) bool {
}
func testAccountLogout(t *testing.T) bool {
whisperService, err := geth.NodeManagerInstance().WhisperService()
whisperService, err := statusAPI.NodeManager().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
return false
}
// create an account
address, pubKey, _, err := geth.CreateAccount(testConfig.Account1.Password)
address, pubKey, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
@ -766,7 +726,7 @@ func testAccountLogout(t *testing.T) bool {
}
// select/login
err = geth.SelectAccount(address, testConfig.Account1.Password)
err = statusAPI.SelectAccount(address, TestConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
@ -776,7 +736,7 @@ func testAccountLogout(t *testing.T) bool {
return false
}
logoutResponse := geth.JSONError{}
logoutResponse := common.APIResponse{}
rawResponse := Logout()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &logoutResponse); err != nil {
@ -800,7 +760,7 @@ func testAccountLogout(t *testing.T) bool {
func testCompleteTransaction(t *testing.T) bool {
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
lightEthereum, err := statusAPI.NodeManager().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return false
@ -810,31 +770,33 @@ func testCompleteTransaction(t *testing.T) bool {
// reset queue
backend.TransactionQueue().Reset()
time.Sleep(5 * time.Second) // allow to sync
// log into account from which transactions will be sent
if err = geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
if err = statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
return false
}
// make sure you panic if transaction complete doesn't return
queuedTxCompleted := make(chan struct{}, 1)
abortPanic := make(chan struct{}, 1)
geth.PanicAfter(10*time.Second, abortPanic, "testCompleteTransaction")
common.PanicAfter(10*time.Second, abortPanic, "testCompleteTransaction")
// replace transaction notification handler
var txHash = ""
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope geth.SignalEnvelope
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope node.SignalEnvelope
if err = json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
completeTxResponse := geth.CompleteTransactionResult{}
rawResponse := CompleteTransaction(C.CString(event["id"].(string)), C.CString(testConfig.Account1.Password))
completeTxResponse := common.CompleteTransactionResult{}
rawResponse := CompleteTransaction(C.CString(event["id"].(string)), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &completeTxResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
@ -854,8 +816,8 @@ func testCompleteTransaction(t *testing.T) bool {
// this call blocks, up until Complete Transaction is called
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
@ -869,7 +831,7 @@ func testCompleteTransaction(t *testing.T) bool {
return false
}
if reflect.DeepEqual(txHashCheck, common.Hash{}) {
if reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
t.Error("Test failed: transaction was never queued or completed")
return false
}
@ -884,7 +846,7 @@ func testCompleteTransaction(t *testing.T) bool {
func testCompleteMultipleQueuedTransactions(t *testing.T) bool {
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
lightEthereum, err := statusAPI.NodeManager().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return false
@ -895,8 +857,8 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool {
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
return false
}
@ -906,14 +868,14 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool {
allTestTxCompleted := make(chan struct{}, 1)
// replace transaction notification handler
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope geth.SignalEnvelope
var envelope node.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be completed in a single call, once aggregated): {id: %s}\n", txID)
@ -925,8 +887,8 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool {
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
@ -934,7 +896,7 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool {
return
}
if reflect.DeepEqual(txHashCheck, common.Hash{}) {
if reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
t.Error("transaction returned empty hash")
return
}
@ -952,8 +914,8 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool {
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
// complete
resultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(testConfig.Account1.Password))
resultsStruct := geth.CompleteTransactionsResult{}
resultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password))
resultsStruct := common.CompleteTransactionsResult{}
if err := json.Unmarshal([]byte(C.GoString(resultsString)), &resultsStruct); err != nil {
t.Error(err)
return
@ -1025,7 +987,7 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool {
func testDiscardTransaction(t *testing.T) bool {
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
lightEthereum, err := statusAPI.NodeManager().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return false
@ -1036,25 +998,25 @@ func testDiscardTransaction(t *testing.T) bool {
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
if err = geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
if err = statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
return false
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
geth.PanicAfter(20*time.Second, completeQueuedTransaction, "TestDiscardQueuedTransactions")
common.PanicAfter(20*time.Second, completeQueuedTransaction, "testDiscardTransaction")
// replace transaction notification handler
var txID string
txFailedEventCalled := false
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope geth.SignalEnvelope
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope node.SignalEnvelope
if err = json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
@ -1065,7 +1027,7 @@ func testDiscardTransaction(t *testing.T) bool {
}
// discard
discardResponse := geth.DiscardTransactionResult{}
discardResponse := common.DiscardTransactionResult{}
rawResponse := DiscardTransaction(C.CString(txID))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &discardResponse); err != nil {
@ -1078,7 +1040,7 @@ func testDiscardTransaction(t *testing.T) bool {
}
// try completing discarded transaction
_, err = geth.CompleteTransaction(txID, testConfig.Account1.Password)
_, err = statusAPI.CompleteTransaction(txID, TestConfig.Account1.Password)
if err != status.ErrQueuedTxIDNotFound {
t.Error("expects tx not found, but call to CompleteTransaction succeeded")
return
@ -1093,7 +1055,7 @@ func testDiscardTransaction(t *testing.T) bool {
completeQueuedTransaction <- struct{}{} // so that timeout is aborted
}
if envelope.Type == geth.EventTransactionFailed {
if envelope.Type == node.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
@ -1105,7 +1067,7 @@ func testDiscardTransaction(t *testing.T) bool {
}
receivedErrCode := event["error_code"].(string)
if receivedErrCode != geth.SendTransactionDiscardedErrorCode {
if receivedErrCode != node.SendTransactionDiscardedErrorCode {
t.Errorf("unexpected error code received: got %v", receivedErrCode)
return
}
@ -1116,8 +1078,8 @@ func testDiscardTransaction(t *testing.T) bool {
// this call blocks, and should return when DiscardQueuedTransaction() is called
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != status.ErrQueuedTxDiscarded {
@ -1125,7 +1087,7 @@ func testDiscardTransaction(t *testing.T) bool {
return false
}
if !reflect.DeepEqual(txHashCheck, common.Hash{}) {
if !reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
t.Error("transaction returned hash, while it shouldn't")
return false
}
@ -1145,7 +1107,7 @@ func testDiscardTransaction(t *testing.T) bool {
func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
lightEthereum, err := statusAPI.NodeManager().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return false
@ -1156,8 +1118,8 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
return false
}
@ -1168,14 +1130,14 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
// replace transaction notification handler
txFailedEventCallCount := 0
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope geth.SignalEnvelope
var envelope node.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
@ -1188,7 +1150,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
txIDs <- txID
}
if envelope.Type == geth.EventTransactionFailed {
if envelope.Type == node.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
@ -1200,7 +1162,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
}
receivedErrCode := event["error_code"].(string)
if receivedErrCode != geth.SendTransactionDiscardedErrorCode {
if receivedErrCode != node.SendTransactionDiscardedErrorCode {
t.Errorf("unexpected error code received: got %v", receivedErrCode)
return
}
@ -1215,8 +1177,8 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != status.ErrQueuedTxDiscarded {
@ -1224,7 +1186,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
return
}
if !reflect.DeepEqual(txHashCheck, common.Hash{}) {
if !reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
t.Error("transaction returned hash, while it shouldn't")
return
}
@ -1243,7 +1205,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
// discard
discardResultsString := DiscardTransactions(C.CString(string(updatedTxIDStrings)))
discardResultsStruct := geth.DiscardTransactionsResult{}
discardResultsStruct := common.DiscardTransactionsResult{}
if err := json.Unmarshal([]byte(C.GoString(discardResultsString)), &discardResultsStruct); err != nil {
t.Error(err)
return
@ -1256,8 +1218,8 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool {
}
// try completing discarded transaction
completeResultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(testConfig.Account1.Password))
completeResultsStruct := geth.CompleteTransactionsResult{}
completeResultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password))
completeResultsStruct := common.CompleteTransactionsResult{}
if err := json.Unmarshal([]byte(C.GoString(completeResultsString)), &completeResultsStruct); err != nil {
t.Error(err)
return
@ -1353,7 +1315,7 @@ func testJailFunctionCall(t *testing.T) bool {
InitJail(C.CString(""))
// load Status JS and add test command to it
statusJS := geth.LoadFromFile(filepath.Join(geth.RootDir, "geth/jail/testdata/status.js")) + `;
statusJS := string(static.MustAsset("testdata/jail/status.js")) + `;
_status_catalog.commands["testCommand"] = function (params) {
return params.val * params.val;
};`
@ -1384,41 +1346,44 @@ func testJailFunctionCall(t *testing.T) bool {
func startTestNode(t *testing.T) <-chan struct{} {
syncRequired := false
if _, err := os.Stat(geth.TestDataDir); os.IsNotExist(err) {
if _, err := os.Stat(TestDataDir); os.IsNotExist(err) {
syncRequired = true
}
// inject test accounts
if err := geth.ImportTestAccount(filepath.Join(geth.TestDataDir, "keystore"), "test-account1.pk"); err != nil {
if err := common.ImportTestAccount(filepath.Join(TestDataDir, "keystore"), "test-account1.pk"); err != nil {
panic(err)
}
if err := geth.ImportTestAccount(filepath.Join(geth.TestDataDir, "keystore"), "test-account2.pk"); err != nil {
if err := common.ImportTestAccount(filepath.Join(TestDataDir, "keystore"), "test-account2.pk"); err != nil {
panic(err)
}
waitForNodeStart := make(chan struct{}, 1)
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
t.Log(jsonEvent)
var envelope geth.SignalEnvelope
var envelope node.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventNodeCrashed {
geth.TriggerDefaultNodeNotificationHandler(jsonEvent)
if envelope.Type == node.EventNodeCrashed {
node.TriggerDefaultNodeNotificationHandler(jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
if envelope.Type == node.EventTransactionQueued {
}
if envelope.Type == geth.EventNodeStarted {
if envelope.Type == node.EventNodeStarted {
t.Log("Node started, but we wait till it be ready")
}
if envelope.Type == node.EventNodeReady {
// manually add static nodes (LES auto-discovery is not stable yet)
PopulateStaticPeers()
// sync
if syncRequired {
t.Logf("Sync is required, it will take %d seconds", testConfig.Node.SyncSeconds)
time.Sleep(testConfig.Node.SyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done
t.Logf("Sync is required, it will take %d seconds", TestConfig.Node.SyncSeconds)
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done
} else {
time.Sleep(5 * time.Second)
}
@ -1429,16 +1394,8 @@ func startTestNode(t *testing.T) <-chan struct{} {
})
go func() {
configJSON := `{
"NetworkId": ` + strconv.Itoa(params.RopstenNetworkID) + `,
"DataDir": "` + geth.TestDataDir + `",
"HTTPPort": ` + strconv.Itoa(testConfig.Node.HTTPPort) + `,
"WSPort": ` + strconv.Itoa(testConfig.Node.WSPort) + `,
"LogEnabled": true,
"LogLevel": "INFO"
}`
response := StartNode(C.CString(configJSON))
responseErr := geth.JSONError{}
response := StartNode(C.CString(nodeConfigJSON))
responseErr := common.APIResponse{}
if err := json.Unmarshal([]byte(C.GoString(response)), &responseErr); err != nil {
panic(err)

View File

@ -5,7 +5,8 @@ import (
"fmt"
"path/filepath"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
"gopkg.in/urfave/cli.v1"
)
@ -125,21 +126,20 @@ func wnode(ctx *cli.Context) error {
// import test accounts
if ctx.BoolT(WhisperInjectTestAccounts.Name) {
if err = geth.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account1.pk"); err != nil {
if err = common.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account1.pk"); err != nil {
return err
}
if err = geth.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account2.pk"); err != nil {
if err = common.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account2.pk"); err != nil {
return err
}
}
if err = geth.CreateAndRunNode(config); err != nil {
if err := statusAPI.StartNode(config); err != nil {
return err
}
// inject test accounts into Whisper
if ctx.BoolT(WhisperInjectTestAccounts.Name) {
testConfig, _ := geth.LoadTestConfig()
testConfig, _ := common.LoadTestConfig()
if err = injectAccountIntoWhisper(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
return err
}
@ -149,7 +149,11 @@ func wnode(ctx *cli.Context) error {
}
// wait till node has been stopped
geth.NodeManagerInstance().Node().GethStack().Wait()
node, err := statusAPI.NodeManager().Node()
if err != nil {
return nil
}
node.Wait()
return nil
}
@ -237,20 +241,20 @@ func makeWhisperNodeConfig(ctx *cli.Context) (*params.NodeConfig, error) {
// injectAccountIntoWhisper adds key pair into Whisper. Similar to Select/Login,
// but allows multiple accounts to be injected.
func injectAccountIntoWhisper(address, password string) error {
nodeManager := geth.NodeManagerInstance()
nodeManager := statusAPI.NodeManager()
keyStore, err := nodeManager.AccountKeyStore()
if err != nil {
return err
}
account, err := geth.ParseAccountString(address)
account, err := common.ParseAccountString(address)
if err != nil {
return geth.ErrAddressToAccountMappingFailure
return node.ErrAddressToAccountMappingFailure
}
_, accountKey, err := keyStore.AccountDecryptedKey(account, password)
if err != nil {
return fmt.Errorf("%s: %v", geth.ErrAccountToKeyMappingFailure.Error(), err)
return fmt.Errorf("%s: %v", node.ErrAccountToKeyMappingFailure.Error(), err)
}
whisperService, err := nodeManager.WhisperService()

View File

@ -1,679 +0,0 @@
package geth_test
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/geth"
)
func TestVerifyAccountPassword(t *testing.T) {
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(keyStoreDir) // nolint: errcheck
emptyKeyStoreDir, err := ioutil.TempDir(os.TempDir(), "empty")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(emptyKeyStoreDir) // nolint: errcheck
// import account keys
if err = geth.ImportTestAccount(keyStoreDir, "test-account1.pk"); err != nil {
t.Fatal(err)
}
if err = geth.ImportTestAccount(keyStoreDir, "test-account2.pk"); err != nil {
t.Fatal(err)
}
account1Address := common.BytesToAddress(common.FromHex(testConfig.Account1.Address))
testCases := []struct {
name string
keyPath string
address string
password string
expectedError error
}{
{
"correct address, correct password (decrypt should succeed)",
keyStoreDir,
testConfig.Account1.Address,
testConfig.Account1.Password,
nil,
},
{
"correct address, correct password, non-existent key store",
filepath.Join(keyStoreDir, "non-existent-folder"),
testConfig.Account1.Address,
testConfig.Account1.Password,
fmt.Errorf("cannot traverse key store folder: lstat %s/non-existent-folder: no such file or directory", keyStoreDir),
},
{
"correct address, correct password, empty key store (pk is not there)",
emptyKeyStoreDir,
testConfig.Account1.Address,
testConfig.Account1.Password,
fmt.Errorf("cannot locate account for address: %x", account1Address),
},
{
"wrong address, correct password",
keyStoreDir,
"0x79791d3e8f2daa1f7fec29649d152c0ada3cc535",
testConfig.Account1.Password,
fmt.Errorf("cannot locate account for address: %s", "79791d3e8f2daa1f7fec29649d152c0ada3cc535"),
},
{
"correct address, wrong password",
keyStoreDir,
testConfig.Account1.Address,
"wrong password", // wrong password
errors.New("could not decrypt key with given passphrase"),
},
}
for _, testCase := range testCases {
t.Log(testCase.name)
accountKey, err := geth.VerifyAccountPassword(testCase.keyPath, testCase.address, testCase.password)
if !reflect.DeepEqual(err, testCase.expectedError) {
t.Fatalf("unexpected error: expected \n'%v', got \n'%v'", testCase.expectedError, err)
}
if err == nil {
if accountKey == nil {
t.Error("no error reported, but account key is missing")
}
accountAddress := common.BytesToAddress(common.FromHex(testCase.address))
if accountKey.Address != accountAddress {
t.Fatalf("account mismatch: have %x, want %x", accountKey.Address, accountAddress)
}
}
}
}
func TestAccountsList(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Fatal(err)
return
}
les, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("expected LES service: %v", err)
}
accounts := les.StatusBackend.AccountManager().Accounts()
if err = geth.Logout(); err != nil {
t.Fatal(err)
}
// make sure that we start with empty accounts list (nobody has logged in yet)
if len(accounts) != 0 {
t.Error("accounts returned, while there should be none (we haven't logged in yet)")
return
}
// create an account
address, _, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
// ensure that there is still no accounts returned
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 0 {
t.Error("accounts returned, while there should be none (we haven't logged in yet)")
return
}
// select account (sub-accounts will be created for this key)
err = geth.SelectAccount(address, testConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
// at this point main account should show up
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 1 {
t.Error("exactly single account is expected (main account)")
return
}
if string(accounts[0].Hex()) != "0x"+address {
t.Errorf("main account is not retured as the first key: got %s, expected %s",
accounts[0].Hex(), "0x"+address)
return
}
// create sub-account 1
subAccount1, subPubKey1, err := geth.CreateChildAccount("", testConfig.Account1.Password)
if err != nil {
t.Errorf("cannot create sub-account: %v", err)
return
}
// now we expect to see both main account and sub-account 1
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 2 {
t.Error("exactly 2 accounts are expected (main + sub-account 1)")
return
}
if string(accounts[0].Hex()) != "0x"+address {
t.Errorf("main account is not retured as the first key: got %s, expected %s",
accounts[0].Hex(), "0x"+address)
return
}
if string(accounts[1].Hex()) != "0x"+subAccount1 {
t.Errorf("subAcount1 not returned: got %s, expected %s", accounts[1].Hex(), "0x"+subAccount1)
return
}
// create sub-account 2, index automatically progresses
subAccount2, subPubKey2, err := geth.CreateChildAccount("", testConfig.Account1.Password)
if err != nil {
t.Errorf("cannot create sub-account: %v", err)
}
if subAccount1 == subAccount2 || subPubKey1 == subPubKey2 {
t.Error("sub-account index auto-increament failed")
return
}
// finally, all 3 accounts should show up (main account, sub-accounts 1 and 2)
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 3 {
t.Errorf("unexpected number of accounts: expected %d, got %d", 3, len(accounts))
return
}
if string(accounts[0].Hex()) != "0x"+address {
t.Errorf("main account is not retured as the first key: got %s, expected %s",
accounts[0].Hex(), "0x"+address)
return
}
subAccount1MatchesKey1 := string(accounts[1].Hex()) != "0x"+subAccount1
subAccount1MatchesKey2 := string(accounts[2].Hex()) != "0x"+subAccount1
if !subAccount1MatchesKey1 && !subAccount1MatchesKey2 {
t.Errorf("subAcount1 not returned: got %s, expected %s", accounts[1].Hex(), "0x"+subAccount1)
return
}
subAccount2MatchesKey1 := string(accounts[1].Hex()) != "0x"+subAccount2
subAccount2MatchesKey2 := string(accounts[2].Hex()) != "0x"+subAccount2
if !subAccount2MatchesKey1 && !subAccount2MatchesKey2 {
t.Errorf("subAcount2 not returned: got %s, expected %s", accounts[2].Hex(), "0x"+subAccount1)
return
}
}
func TestAccountsCreateChildAccount(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Fatal(err)
}
// to make sure that we start with empty account (which might get populated during previous tests)
if err = geth.Logout(); err != nil {
t.Fatal(err)
}
keyStore, err := geth.NodeManagerInstance().AccountKeyStore()
if err != nil {
t.Fatal(err)
}
// create an account
address, pubKey, mnemonic, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
t.Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
account, err := geth.ParseAccountString(address)
if err != nil {
t.Errorf("can not get account from address: %v", err)
return
}
// obtain decrypted key, and make sure that extended key (which will be used as root for sub-accounts) is present
_, key, err := keyStore.AccountDecryptedKey(account, testConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return
}
if key.ExtendedKey == nil {
t.Error("CKD#2 has not been generated for new account")
return
}
// try creating sub-account, w/o selecting main account i.e. w/o login to main account
_, _, err = geth.CreateChildAccount("", testConfig.Account1.Password)
if !reflect.DeepEqual(err, geth.ErrNoAccountSelected) {
t.Errorf("expected error is not returned (tried to create sub-account w/o login): %v", err)
return
}
err = geth.SelectAccount(address, testConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
// try to create sub-account with wrong password
_, _, err = geth.CreateChildAccount("", "wrong password")
if !reflect.DeepEqual(err, errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase")) {
t.Errorf("expected error is not returned (tried to create sub-account with wrong password): %v", err)
return
}
// create sub-account (from implicit parent)
subAccount1, subPubKey1, err := geth.CreateChildAccount("", testConfig.Account1.Password)
if err != nil {
t.Errorf("cannot create sub-account: %v", err)
return
}
// make sure that sub-account index automatically progresses
subAccount2, subPubKey2, err := geth.CreateChildAccount("", testConfig.Account1.Password)
if err != nil {
t.Errorf("cannot create sub-account: %v", err)
}
if subAccount1 == subAccount2 || subPubKey1 == subPubKey2 {
t.Error("sub-account index auto-increament failed")
return
}
// create sub-account (from explicit parent)
subAccount3, subPubKey3, err := geth.CreateChildAccount(subAccount2, testConfig.Account1.Password)
if err != nil {
t.Errorf("cannot create sub-account: %v", err)
}
if subAccount1 == subAccount3 || subPubKey1 == subPubKey3 || subAccount2 == subAccount3 || subPubKey2 == subPubKey3 {
t.Error("sub-account index auto-increament failed")
return
}
}
func TestAccountsRecoverAccount(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Fatal(err)
}
keyStore, _ := geth.NodeManagerInstance().AccountKeyStore()
// create an account
address, pubKey, mnemonic, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
t.Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
// try recovering using password + mnemonic
addressCheck, pubKeyCheck, err := geth.RecoverAccount(testConfig.Account1.Password, mnemonic)
if err != nil {
t.Errorf("recover account failed: %v", err)
return
}
if address != addressCheck || pubKey != pubKeyCheck {
t.Error("recover account details failed to pull the correct details")
}
// now test recovering, but make sure that account/key file is removed i.e. simulate recovering on a new device
account, err := geth.ParseAccountString(address)
if err != nil {
t.Errorf("can not get account from address: %v", err)
}
account, key, err := keyStore.AccountDecryptedKey(account, testConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return
}
extChild2String := key.ExtendedKey.String()
if err := keyStore.Delete(account, testConfig.Account1.Password); err != nil {
t.Errorf("cannot remove account: %v", err)
}
addressCheck, pubKeyCheck, err = geth.RecoverAccount(testConfig.Account1.Password, mnemonic)
if err != nil {
t.Errorf("recover account failed (for non-cached account): %v", err)
return
}
if address != addressCheck || pubKey != pubKeyCheck {
t.Error("recover account details failed to pull the correct details (for non-cached account)")
}
// make sure that extended key exists and is imported ok too
_, key, err = keyStore.AccountDecryptedKey(account, testConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return
}
if extChild2String != key.ExtendedKey.String() {
t.Errorf("CKD#2 key mismatch, expected: %s, got: %s", extChild2String, key.ExtendedKey.String())
}
// make sure that calling import several times, just returns from cache (no error is expected)
addressCheck, pubKeyCheck, err = geth.RecoverAccount(testConfig.Account1.Password, mnemonic)
if err != nil {
t.Errorf("recover account failed (for non-cached account): %v", err)
return
}
if address != addressCheck || pubKey != pubKeyCheck {
t.Error("recover account details failed to pull the correct details (for non-cached account)")
}
// time to login with recovered data
whisperService, err := geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(pubKeyCheck) {
t.Error("identity already present in whisper")
}
err = geth.SelectAccount(addressCheck, testConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
if !whisperService.HasKeyPair(pubKeyCheck) {
t.Errorf("identity not injected into whisper: %v", err)
}
}
func TestAccountSelect(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Fatal(err)
}
// test to see if the account was injected in whisper
whisperService, err := geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create an account
address1, pubKey1, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
t.Logf("Account created: {address: %s, key: %s}", address1, pubKey1)
address2, pubKey2, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Error("Test failed: could not create account")
return
}
t.Logf("Account created: {address: %s, key: %s}", address2, pubKey2)
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity already present in whisper")
}
// try selecting with wrong password
err = geth.SelectAccount(address1, "wrongPassword")
if err == nil {
t.Error("select account is expected to throw error: wrong password used")
return
}
err = geth.SelectAccount(address1, testConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
if !whisperService.HasKeyPair(pubKey1) {
t.Errorf("identity not injected into whisper: %v", err)
}
// select another account, make sure that previous account is wiped out from Whisper cache
if whisperService.HasKeyPair(pubKey2) {
t.Error("identity already present in whisper")
}
err = geth.SelectAccount(address2, testConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
if !whisperService.HasKeyPair(pubKey2) {
t.Errorf("identity not injected into whisper: %v", err)
}
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity should be removed, but it is still present in whisper")
}
}
func TestAccountsLogout(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Fatal(err)
}
whisperService, err := geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create an account
address, pubKey, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
// make sure that identity doesn't exist (yet) in Whisper
if whisperService.HasKeyPair(pubKey) {
t.Error("identity already present in whisper")
}
// select/login
err = geth.SelectAccount(address, testConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
if !whisperService.HasKeyPair(pubKey) {
t.Error("identity not injected into whisper")
}
if err = geth.Logout(); err != nil {
t.Errorf("cannot logout: %v", err)
}
// now, logout and check if identity is removed indeed
if whisperService.HasKeyPair(pubKey) {
t.Error("identity not cleared from whisper")
}
}
func TestAccountsSelectedAccountOnNodeRestart(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Fatal(err)
}
// we need to make sure that selected account is injected as identity into Whisper
whisperService, err := geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create test accounts
address1, pubKey1, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
t.Logf("account1 created: {address: %s, key: %s}", address1, pubKey1)
address2, pubKey2, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
t.Logf("account2 created: {address: %s, key: %s}", address2, pubKey2)
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity already present in whisper")
}
// make sure that no account is selected by default
if geth.NodeManagerInstance().SelectedAccount != nil {
t.Error("account selected, but should not be")
return
}
// select account
err = geth.SelectAccount(address1, "wrongPassword")
if err == nil {
t.Error("select account is expected to throw error: wrong password used")
return
}
err = geth.SelectAccount(address1, testConfig.Account1.Password)
if err != nil {
t.Errorf("could not select account: %v", err)
return
}
if !whisperService.HasKeyPair(pubKey1) {
t.Errorf("identity not injected into whisper: %v", err)
}
// select another account, make sure that previous account is wiped out from Whisper cache
if whisperService.HasKeyPair(pubKey2) {
t.Error("identity already present in whisper")
}
err = geth.SelectAccount(address2, testConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
if !whisperService.HasKeyPair(pubKey2) {
t.Errorf("identity not injected into whisper: %v", err)
}
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity should be removed, but it is still present in whisper")
}
// stop node (and all of its sub-protocols)
if err := geth.NodeManagerInstance().StopNode(); err != nil {
t.Fatal(err)
}
// make sure that account is still selected
if geth.NodeManagerInstance().SelectedAccount == nil {
t.Error("no selected account")
return
}
if geth.NodeManagerInstance().SelectedAccount.Address.Hex() != "0x"+address2 {
t.Error("incorrect address selected")
return
}
// resume node
if err := geth.NodeManagerInstance().ResumeNode(); err != nil {
t.Fatal(err)
}
// re-check selected account (account2 MUST be selected)
if geth.NodeManagerInstance().SelectedAccount == nil {
t.Error("no selected account")
return
}
if geth.NodeManagerInstance().SelectedAccount.Address.Hex() != "0x"+address2 {
t.Error("incorrect address selected")
return
}
// make sure that Whisper gets identity re-injected
whisperService, err = geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
if !whisperService.HasKeyPair(pubKey2) {
t.Errorf("identity not injected into whisper: %v", err)
}
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity should not be present, but it is still present in whisper")
}
}
func TestAccountsNodeRestartWithNoSelectedAccount(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Fatal(err)
}
if err = geth.Logout(); err != nil {
t.Fatal(err)
}
// we need to make sure that selected account is injected as identity into Whisper
whisperService, err := geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create test accounts
address1, pubKey1, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
t.Logf("account1 created: {address: %s, key: %s}", address1, pubKey1)
// make sure that identity is not present
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity already present in whisper")
}
// make sure that no account is selected
if geth.NodeManagerInstance().SelectedAccount != nil {
t.Error("account selected, but should not be")
return
}
// stop node (and all of its sub-protocols)
if err := geth.NodeManagerInstance().StopNode(); err != nil {
t.Fatal(err)
}
// make sure that no account is selected
if geth.NodeManagerInstance().SelectedAccount != nil {
t.Error("account selected, but should not be")
return
}
// resume node
if err := geth.NodeManagerInstance().ResumeNode(); err != nil {
t.Fatal(err)
}
// make sure that no account is selected
if geth.NodeManagerInstance().SelectedAccount != nil {
t.Error("account selected, but should not be")
return
}
// make sure that Whisper gets identity re-injected
whisperService, err = geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity should not be present, but it is present in whisper")
}
}

171
geth/api/api.go Normal file
View File

@ -0,0 +1,171 @@
package api
import (
"github.com/ethereum/go-ethereum/accounts/keystore"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/params"
)
// StatusAPI provides API to access Status related functionality.
type StatusAPI struct {
b *StatusBackend
}
// NewStatusAPI create a new StatusAPI instance
func NewStatusAPI() *StatusAPI {
return &StatusAPI{
b: NewStatusBackend(),
}
}
// NodeManager returns reference to node manager
func (api *StatusAPI) NodeManager() common.NodeManager {
return api.b.NodeManager()
}
// AccountManager returns reference to account manager
func (api *StatusAPI) AccountManager() common.AccountManager {
return api.b.AccountManager()
}
// JailManager returns reference to jail
func (api *StatusAPI) JailManager() common.JailManager {
return api.b.JailManager()
}
// StartNode start Status node, fails if node is already started
func (api *StatusAPI) StartNode(config *params.NodeConfig) error {
nodeStarted, err := api.b.StartNode(config)
if err != nil {
return err
}
<-nodeStarted // do not return up until backend is ready
return nil
}
// StartNodeNonBlocking start Status node, fails if node is already started
// Returns immediately w/o waiting for node to start (relies on listening
// for node.started signal)
func (api *StatusAPI) StartNodeNonBlocking(config *params.NodeConfig) error {
_, err := api.b.StartNode(config)
if err != nil {
return err
}
return nil
}
// StopNode stop Status node. Stopped node cannot be resumed.
func (api *StatusAPI) StopNode() error {
return api.b.StopNode()
}
// RestartNode restart running Status node, fails if node is not running
func (api *StatusAPI) RestartNode() error {
nodeStarted, err := api.b.RestartNode()
if err != nil {
return err
}
<-nodeStarted // do not return up until backend is ready
return nil
}
// ResetChainData remove chain data from data directory.
// Node is stopped, and new node is started, with clean data directory.
func (api *StatusAPI) ResetChainData() error {
nodeStarted, err := api.b.ResetChainData()
if err != nil {
return err
}
<-nodeStarted // do not return up until backend is ready
return nil
}
// PopulateStaticPeers connects current node with our publicly available LES/SHH/Swarm cluster
func (api *StatusAPI) PopulateStaticPeers() error {
return api.b.nodeManager.PopulateStaticPeers()
}
// AddPeer adds new static peer node
func (api *StatusAPI) AddPeer(url string) error {
return api.b.nodeManager.AddPeer(url)
}
// CreateAccount creates an internal geth account
// BIP44-compatible keys are generated: CKD#1 is stored as account key, CKD#2 stored as sub-account root
// Public key of CKD#1 is returned, with CKD#2 securely encoded into account key file (to be used for
// sub-account derivations)
func (api *StatusAPI) CreateAccount(password string) (address, pubKey, mnemonic string, err error) {
return api.b.CreateAccount(password)
}
// CreateChildAccount creates sub-account for an account identified by parent address.
// CKD#2 is used as root for master accounts (when parentAddress is "").
// Otherwise (when parentAddress != ""), child is derived directly from parent.
func (api *StatusAPI) CreateChildAccount(parentAddress, password string) (address, pubKey string, err error) {
return api.b.CreateChildAccount(parentAddress, password)
}
// RecoverAccount re-creates master key using given details.
// Once master key is re-generated, it is inserted into keystore (if not already there).
func (api *StatusAPI) RecoverAccount(password, mnemonic string) (address, pubKey string, err error) {
return api.b.RecoverAccount(password, mnemonic)
}
// VerifyAccountPassword tries to decrypt a given account key file, with a provided password.
// If no error is returned, then account is considered verified.
func (api *StatusAPI) VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error) {
return api.b.VerifyAccountPassword(keyStoreDir, address, password)
}
// SelectAccount selects current account, by verifying that address has corresponding account which can be decrypted
// using provided password. Once verification is done, decrypted key is injected into Whisper (as a single identity,
// all previous identities are removed).
func (api *StatusAPI) SelectAccount(address, password string) error {
return api.b.SelectAccount(address, password)
}
// Logout clears whisper identities
func (api *StatusAPI) Logout() error {
return api.b.Logout()
}
// CompleteTransaction instructs backend to complete sending of a given transaction
func (api *StatusAPI) CompleteTransaction(id, password string) (gethcommon.Hash, error) {
return api.b.CompleteTransaction(id, password)
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func (api *StatusAPI) CompleteTransactions(ids, password string) map[string]common.RawCompleteTransactionResult {
return api.b.CompleteTransactions(ids, password)
}
// DiscardTransaction discards a given transaction from transaction queue
func (api *StatusAPI) DiscardTransaction(id string) error {
return api.b.DiscardTransaction(id)
}
// DiscardTransactions discards given multiple transactions from transaction queue
func (api *StatusAPI) DiscardTransactions(ids string) map[string]common.RawDiscardTransactionResult {
return api.b.DiscardTransactions(ids)
}
// Parse creates a new jail cell context, with the given chatID as identifier.
// New context executes provided JavaScript code, right after the initialization.
func (api *StatusAPI) JailParse(chatID string, js string) string {
return api.b.jailManager.Parse(chatID, js)
}
// Call executes given JavaScript function w/i a jail cell context identified by the chatID.
// Jail cell is clonned before call is executed i.e. all calls execute w/i their own contexts.
func (api *StatusAPI) JailCall(chatID string, path string, args string) string {
return api.b.jailManager.Call(chatID, path, args)
}
// BaseJS allows to setup initial JavaScript to be loaded on each jail.Parse()
func (api *StatusAPI) JailBaseJS(js string) {
api.b.jailManager.BaseJS(js)
}

208
geth/api/backend.go Normal file
View File

@ -0,0 +1,208 @@
package api
import (
"github.com/ethereum/go-ethereum/accounts/keystore"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/jail"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
)
// StatusBackend implements Status.im service
type StatusBackend struct {
nodeManager common.NodeManager
accountManager common.AccountManager
txQueueManager common.TxQueueManager
jailManager common.JailManager
}
// NewStatusBackend create a new NewStatusBackend instance
func NewStatusBackend() *StatusBackend {
defer log.Info("Status backend initialized")
nodeManager := node.NewNodeManager()
accountManager := node.NewAccountManager(nodeManager)
return &StatusBackend{
nodeManager: nodeManager,
accountManager: accountManager,
txQueueManager: node.NewTxQueueManager(nodeManager, accountManager),
jailManager: jail.New(nodeManager),
}
}
// NodeManager returns reference to node manager
func (m *StatusBackend) NodeManager() common.NodeManager {
return m.nodeManager
}
// AccountManager returns reference to account manager
func (m *StatusBackend) AccountManager() common.AccountManager {
return m.accountManager
}
// JailManager returns reference to jail
func (m *StatusBackend) JailManager() common.JailManager {
return m.jailManager
}
// IsNodeRunning confirm that node is running
func (m *StatusBackend) IsNodeRunning() bool {
return m.nodeManager.IsNodeRunning()
}
// StartNode start Status node, fails if node is already started
func (m *StatusBackend) StartNode(config *params.NodeConfig) (<-chan struct{}, error) {
backendReady := make(chan struct{})
nodeStarted, err := m.nodeManager.StartNode(config)
if err != nil {
return nil, err
}
go m.onNodeStart(backendReady, nodeStarted)
return backendReady, err
}
func (m *StatusBackend) onNodeStart(backendReady chan struct{}, nodeStarted <-chan struct{}) {
defer close(backendReady)
<-nodeStarted
if err := m.registerHandlers(); err != nil {
log.Error("Handler registration failed", "err", err)
}
m.accountManager.ReSelectAccount()
}
// RestartNode restart running Status node, fails if node is not running
func (m *StatusBackend) RestartNode() (<-chan struct{}, error) {
backendReady := make(chan struct{})
nodeRestarted, err := m.nodeManager.RestartNode()
if err != nil {
return nil, err
}
go m.onNodeStart(backendReady, nodeRestarted)
return backendReady, err
}
// StopNode stop Status node. Stopped node cannot be resumed.
func (m *StatusBackend) StopNode() error {
return m.nodeManager.StopNode()
}
// ResetChainData remove chain data from data directory.
// Node is stopped, and new node is started, with clean data directory.
func (m *StatusBackend) ResetChainData() (<-chan struct{}, error) {
backendReady := make(chan struct{})
nodeRestarted, err := m.nodeManager.ResetChainData()
if err != nil {
return nil, err
}
go m.onNodeStart(backendReady, nodeRestarted)
return backendReady, err
}
// CreateAccount creates an internal geth account
// BIP44-compatible keys are generated: CKD#1 is stored as account key, CKD#2 stored as sub-account root
// Public key of CKD#1 is returned, with CKD#2 securely encoded into account key file (to be used for
// sub-account derivations)
func (m *StatusBackend) CreateAccount(password string) (address, pubKey, mnemonic string, err error) {
return m.accountManager.CreateAccount(password)
}
// CreateChildAccount creates sub-account for an account identified by parent address.
// CKD#2 is used as root for master accounts (when parentAddress is "").
// Otherwise (when parentAddress != ""), child is derived directly from parent.
func (m *StatusBackend) CreateChildAccount(parentAddress, password string) (address, pubKey string, err error) {
return m.accountManager.CreateChildAccount(parentAddress, password)
}
// RecoverAccount re-creates master key using given details.
// Once master key is re-generated, it is inserted into keystore (if not already there).
func (m *StatusBackend) RecoverAccount(password, mnemonic string) (address, pubKey string, err error) {
return m.accountManager.RecoverAccount(password, mnemonic)
}
// VerifyAccountPassword tries to decrypt a given account key file, with a provided password.
// If no error is returned, then account is considered verified.
func (m *StatusBackend) VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error) {
return m.accountManager.VerifyAccountPassword(keyStoreDir, address, password)
}
// SelectAccount selects current account, by verifying that address has corresponding account which can be decrypted
// using provided password. Once verification is done, decrypted key is injected into Whisper (as a single identity,
// all previous identities are removed).
func (m *StatusBackend) SelectAccount(address, password string) error {
return m.accountManager.SelectAccount(address, password)
}
// ReSelectAccount selects previously selected account, often, after node restart.
func (m *StatusBackend) ReSelectAccount() error {
return m.accountManager.ReSelectAccount()
}
// Logout clears whisper identities
func (m *StatusBackend) Logout() error {
return m.accountManager.Logout()
}
// SelectedAccount returns currently selected account
func (m *StatusBackend) SelectedAccount() (*common.SelectedExtKey, error) {
return m.accountManager.SelectedAccount()
}
// CompleteTransaction instructs backend to complete sending of a given transaction
func (m *StatusBackend) CompleteTransaction(id, password string) (gethcommon.Hash, error) {
return m.txQueueManager.CompleteTransaction(id, password)
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func (m *StatusBackend) CompleteTransactions(ids, password string) map[string]common.RawCompleteTransactionResult {
return m.txQueueManager.CompleteTransactions(ids, password)
}
// DiscardTransaction discards a given transaction from transaction queue
func (m *StatusBackend) DiscardTransaction(id string) error {
return m.txQueueManager.DiscardTransaction(id)
}
// DiscardTransactions discards given multiple transactions from transaction queue
func (m *StatusBackend) DiscardTransactions(ids string) map[string]common.RawDiscardTransactionResult {
return m.txQueueManager.DiscardTransactions(ids)
}
// registerHandlers attaches Status callback handlers to running node
func (m *StatusBackend) registerHandlers() error {
runningNode, err := m.nodeManager.Node()
if err != nil {
return err
}
var lightEthereum *les.LightEthereum
if err := runningNode.Service(&lightEthereum); err != nil {
log.Error("Cannot get light ethereum service", "error", err)
}
lightEthereum.StatusBackend.SetAccountsFilterHandler(m.accountManager.AccountsListRequestHandler())
log.Info("Registered handler", "fn", "AccountsFilterHandler")
lightEthereum.StatusBackend.SetTransactionQueueHandler(m.txQueueManager.TransactionQueueHandler())
log.Info("Registered handler", "fn", "TransactionQueueHandler")
lightEthereum.StatusBackend.SetTransactionReturnHandler(m.txQueueManager.TransactionReturnHandler())
log.Info("Registered handler", "fn", "TransactionReturnHandler")
m.ReSelectAccount()
log.Info("Account reselected")
node.SendSignal(node.SignalEnvelope{
Type: node.EventNodeReady,
Event: struct{}{},
})
return nil
}

View File

@ -0,0 +1,335 @@
package api_test
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/les"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
)
func (s *BackendTestSuite) TestAccountsList() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
runningNode, err := s.backend.NodeManager().Node()
require.NoError(err)
require.NotNil(runningNode)
var lesService *les.LightEthereum
require.NoError(runningNode.Service(&lesService))
require.NotNil(lesService)
accounts := lesService.StatusBackend.AccountManager().Accounts()
for _, acc := range accounts {
fmt.Println(acc.Hex())
}
// make sure that we start with empty accounts list (nobody has logged in yet)
require.Zero(len(accounts), "accounts returned, while there should be none (we haven't logged in yet)")
// create an account
address, _, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
// ensure that there is still no accounts returned
accounts = lesService.StatusBackend.AccountManager().Accounts()
require.Zero(len(accounts), "accounts returned, while there should be none (we haven't logged in yet)")
// select account (sub-accounts will be created for this key)
err = s.backend.SelectAccount(address, TestConfig.Account1.Password)
require.NoError(err, "account selection failed")
// at this point main account should show up
accounts = lesService.StatusBackend.AccountManager().Accounts()
require.Equal(1, len(accounts), "exactly single account is expected (main account)")
require.Equal(string(accounts[0].Hex()), "0x"+address,
fmt.Sprintf("main account is not retured as the first key: got %s, expected %s", accounts[0].Hex(), "0x"+address))
// create sub-account 1
subAccount1, subPubKey1, err := s.backend.CreateChildAccount("", TestConfig.Account1.Password)
require.NoError(err, "cannot create sub-account")
// now we expect to see both main account and sub-account 1
accounts = lesService.StatusBackend.AccountManager().Accounts()
require.Equal(2, len(accounts), "exactly 2 accounts are expected (main + sub-account 1)")
require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key")
require.Equal(string(accounts[1].Hex()), "0x"+subAccount1, "subAcount1 not returned")
// create sub-account 2, index automatically progresses
subAccount2, subPubKey2, err := s.backend.CreateChildAccount("", TestConfig.Account1.Password)
require.NoError(err, "cannot create sub-account")
require.False(subAccount1 == subAccount2 || subPubKey1 == subPubKey2, "sub-account index auto-increament failed")
// finally, all 3 accounts should show up (main account, sub-accounts 1 and 2)
accounts = lesService.StatusBackend.AccountManager().Accounts()
require.Equal(3, len(accounts), "unexpected number of accounts")
require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key")
subAccount1MatchesKey1 := string(accounts[1].Hex()) != "0x"+subAccount1
subAccount1MatchesKey2 := string(accounts[2].Hex()) != "0x"+subAccount1
require.False(!subAccount1MatchesKey1 && !subAccount1MatchesKey2, "subAcount1 not returned")
subAccount2MatchesKey1 := string(accounts[1].Hex()) != "0x"+subAccount2
subAccount2MatchesKey2 := string(accounts[2].Hex()) != "0x"+subAccount2
require.False(!subAccount2MatchesKey1 && !subAccount2MatchesKey2, "subAcount2 not returned")
}
func (s *BackendTestSuite) TestCreateChildAccount() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
keyStore, err := s.backend.NodeManager().AccountKeyStore()
require.NoError(err)
require.NotNil(keyStore)
// create an account
address, pubKey, mnemonic, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
s.T().Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
account, err := common.ParseAccountString(address)
require.NoError(err, "can not get account from address")
// obtain decrypted key, and make sure that extended key (which will be used as root for sub-accounts) is present
_, key, err := keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
require.NoError(err, "can not obtain decrypted account key")
require.NotNil(key.ExtendedKey, "CKD#2 has not been generated for new account")
// try creating sub-account, w/o selecting main account i.e. w/o login to main account
_, _, err = s.backend.CreateChildAccount("", TestConfig.Account1.Password)
require.EqualError(node.ErrNoAccountSelected, err.Error(), "expected error is not returned (tried to create sub-account w/o login)")
err = s.backend.SelectAccount(address, TestConfig.Account1.Password)
require.NoError(err, "cannot select account")
// try to create sub-account with wrong password
_, _, err = s.backend.CreateChildAccount("", "wrong password")
expectedErr := errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase")
require.EqualError(expectedErr, err.Error(), "create sub-account with wrong password")
// create sub-account (from implicit parent)
subAccount1, subPubKey1, err := s.backend.CreateChildAccount("", TestConfig.Account1.Password)
require.NoError(err, "cannot create sub-account")
// make sure that sub-account index automatically progresses
subAccount2, subPubKey2, err := s.backend.CreateChildAccount("", TestConfig.Account1.Password)
require.NoError(err)
require.False(subAccount1 == subAccount2 || subPubKey1 == subPubKey2, "sub-account index auto-increament failed")
// create sub-account (from explicit parent)
subAccount3, subPubKey3, err := s.backend.CreateChildAccount(subAccount2, TestConfig.Account1.Password)
require.NoError(err)
require.False(subAccount1 == subAccount3 || subPubKey1 == subPubKey3 || subAccount2 == subAccount3 || subPubKey2 == subPubKey3)
}
func (s *BackendTestSuite) TestRecoverAccount() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
keyStore, err := s.backend.NodeManager().AccountKeyStore()
require.NoError(err)
require.NotNil(keyStore)
// create an account
address, pubKey, mnemonic, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
s.T().Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
// try recovering using password + mnemonic
addressCheck, pubKeyCheck, err := s.backend.RecoverAccount(TestConfig.Account1.Password, mnemonic)
require.NoError(err, "recover account failed")
require.False(address != addressCheck || pubKey != pubKeyCheck, "incorrect accound details recovered")
// now test recovering, but make sure that account/key file is removed i.e. simulate recovering on a new device
account, err := common.ParseAccountString(address)
require.NoError(err, "can not get account from address")
account, key, err := keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
require.NoError(err, "can not obtain decrypted account key")
extChild2String := key.ExtendedKey.String()
require.NoError(keyStore.Delete(account, TestConfig.Account1.Password), "cannot remove account")
addressCheck, pubKeyCheck, err = s.backend.RecoverAccount(TestConfig.Account1.Password, mnemonic)
require.NoError(err, "recover account failed (for non-cached account)")
require.False(address != addressCheck || pubKey != pubKeyCheck,
"incorrect account details recovered (for non-cached account)")
// make sure that extended key exists and is imported ok too
_, key, err = keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
require.NoError(err)
require.Equal(extChild2String, key.ExtendedKey.String(), "CKD#2 key mismatch")
// make sure that calling import several times, just returns from cache (no error is expected)
addressCheck, pubKeyCheck, err = s.backend.RecoverAccount(TestConfig.Account1.Password, mnemonic)
require.NoError(err, "recover account failed (for non-cached account)")
require.False(address != addressCheck || pubKey != pubKeyCheck,
"incorrect account details recovered (for non-cached account)")
// time to login with recovered data
whisperService := s.WhisperService()
// make sure that identity is not (yet injected)
require.False(whisperService.HasKeyPair(pubKeyCheck), "identity already present in whisper")
require.NoError(s.backend.SelectAccount(addressCheck, TestConfig.Account1.Password))
require.True(whisperService.HasKeyPair(pubKeyCheck), "identity not injected into whisper")
}
func (s *BackendTestSuite) TestSelectAccount() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
// test to see if the account was injected in whisper
whisperService := s.WhisperService()
// create an account
address1, pubKey1, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
s.T().Logf("Account created: {address: %s, key: %s}", address1, pubKey1)
address2, pubKey2, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
s.T().Logf("Account created: {address: %s, key: %s}", address2, pubKey2)
// make sure that identity is not (yet injected)
require.False(whisperService.HasKeyPair(pubKey1), "identity already present in whisper")
// try selecting with wrong password
err = s.backend.SelectAccount(address1, "wrongPassword")
expectedErr := errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase")
require.EqualError(expectedErr, err.Error(), "select account is expected to throw error: wrong password used")
err = s.backend.SelectAccount(address1, TestConfig.Account1.Password)
require.NoError(err)
require.True(whisperService.HasKeyPair(pubKey1), "identity not injected into whisper")
// select another account, make sure that previous account is wiped out from Whisper cache
require.False(whisperService.HasKeyPair(pubKey2), "identity already present in whisper")
require.NoError(s.backend.SelectAccount(address2, TestConfig.Account1.Password))
require.True(whisperService.HasKeyPair(pubKey2), "identity not injected into whisper")
require.False(whisperService.HasKeyPair(pubKey1), "identity should be removed, but it is still present in whisper")
}
func (s *BackendTestSuite) TestLogout() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
whisperService := s.WhisperService()
// create an account
address, pubKey, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
// make sure that identity doesn't exist (yet) in Whisper
require.False(whisperService.HasKeyPair(pubKey), "identity already present in whisper")
require.NoError(s.backend.SelectAccount(address, TestConfig.Account1.Password))
require.True(whisperService.HasKeyPair(pubKey), "identity not injected into whisper")
require.NoError(s.backend.Logout())
require.False(whisperService.HasKeyPair(pubKey), "identity not cleared from whisper")
}
func (s *BackendTestSuite) TestSelectedAccountOnRestart() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
// we need to make sure that selected account is injected as identity into Whisper
whisperService := s.WhisperService()
// create test accounts
address1, pubKey1, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
address2, pubKey2, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
// make sure that identity is not (yet injected)
require.False(whisperService.HasKeyPair(pubKey1), "identity already present in whisper")
// make sure that no account is selected by default
selectedAccount, err := s.backend.SelectedAccount()
require.EqualError(node.ErrNoAccountSelected, err.Error(), "account selected, but should not be")
require.Nil(selectedAccount)
// select account
err = s.backend.SelectAccount(address1, "wrongPassword")
expectedErr := errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase")
require.EqualError(expectedErr, err.Error())
require.NoError(s.backend.SelectAccount(address1, TestConfig.Account1.Password))
require.True(whisperService.HasKeyPair(pubKey1), "identity not injected into whisper")
// select another account, make sure that previous account is wiped out from Whisper cache
require.False(whisperService.HasKeyPair(pubKey2), "identity already present in whisper")
require.NoError(s.backend.SelectAccount(address2, TestConfig.Account1.Password))
require.True(whisperService.HasKeyPair(pubKey2), "identity not injected into whisper")
require.False(whisperService.HasKeyPair(pubKey1), "identity should be removed, but it is still present in whisper")
// stop node (and all of its sub-protocols)
nodeConfig, err := s.NodeManager.NodeConfig()
require.NoError(err)
require.NotNil(nodeConfig)
preservedNodeConfig := *nodeConfig
require.NoError(s.NodeManager.StopNode())
// make sure that account is still selected
selectedAccount, err = s.backend.SelectedAccount()
require.NoError(err)
require.NotNil(selectedAccount)
require.Equal(selectedAccount.Address.Hex(), "0x"+address2, "incorrect address selected")
// resume node
nodeStarted, err := s.backend.StartNode(&preservedNodeConfig)
require.NoError(err)
<-nodeStarted
// re-check selected account (account2 MUST be selected)
selectedAccount, err = s.backend.SelectedAccount()
require.NoError(err)
require.NotNil(selectedAccount)
require.Equal(selectedAccount.Address.Hex(), "0x"+address2, "incorrect address selected")
// make sure that Whisper gets identity re-injected
whisperService = s.WhisperService()
require.True(whisperService.HasKeyPair(pubKey2), "identity not injected into whisper")
require.False(whisperService.HasKeyPair(pubKey1), "identity should not be present, but it is still present in whisper")
// now restart node using RestartNode() method, and make sure that account is still available
s.RestartTestNode()
defer s.StopTestBackend()
whisperService = s.WhisperService()
require.True(whisperService.HasKeyPair(pubKey2), "identity not injected into whisper")
require.False(whisperService.HasKeyPair(pubKey1), "identity should not be present, but it is still present in whisper")
// now logout, and make sure that on restart no account is selected (i.e. logout works properly)
require.NoError(s.backend.Logout())
s.RestartTestNode()
whisperService = s.WhisperService()
require.False(whisperService.HasKeyPair(pubKey2), "identity not injected into whisper")
require.False(whisperService.HasKeyPair(pubKey1), "identity should not be present, but it is still present in whisper")
selectedAccount, err = s.backend.SelectedAccount()
require.EqualError(node.ErrNoAccountSelected, err.Error())
require.Nil(selectedAccount)
}

View File

@ -0,0 +1,741 @@
package api_test
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/jail"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
"github.com/status-im/status-go/static"
)
const (
whisperMessage1 = `test message 1 (K1 -> K2, signed+encrypted, from us)`
whisperMessage2 = `test message 2 (K1 -> K1, signed+encrypted to ourselves)`
whisperMessage3 = `test message 3 (K1 -> "", signed broadcast)`
whisperMessage4 = `test message 4 ("" -> "", anon broadcast)`
whisperMessage5 = `test message 5 ("" -> K1, encrypted anon broadcast)`
whisperMessage6 = `test message 6 (K2 -> K1, signed+encrypted, to us)`
txSendFolder = "testdata/jail/tx-send/"
testChatID = "testChat"
)
func (s *BackendTestSuite) TestJailContractDeployment() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
jailInstance := s.backend.JailManager()
require.NotNil(jailInstance)
jailInstance.Parse(testChatID, "")
// obtain VM for a given chat (to send custom JS to jailed version of Send())
vm, err := jailInstance.JailCellVM(testChatID)
require.NoError(err)
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
common.PanicAfter(30*time.Second, completeQueuedTransaction, s.T().Name())
// replace transaction notification handler
var txHash gethcommon.Hash
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string))
//t.Logf("Transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
s.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
var err error
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"]))
log.Info("contract transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex())
close(completeQueuedTransaction)
}
})
_, err = vm.Run(`
var responseValue = null;
var testContract = web3.eth.contract([{"constant":true,"inputs":[{"name":"a","type":"int256"}],"name":"double","outputs":[{"name":"","type":"int256"}],"payable":false,"type":"function"}]);
var test = testContract.new(
{
from: '` + TestConfig.Account1.Address + `',
data: '0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029',
gas: '` + strconv.Itoa(params.DefaultGas) + `'
}, function (e, contract){
if (!e) {
responseValue = contract.transactionHash
}
})
`)
require.NoError(err)
<-completeQueuedTransaction
responseValue, err := vm.Get("responseValue")
require.NoError(err, "vm.Get() failed")
response, err := responseValue.ToString()
require.NoError(err, "cannot parse result")
expectedResponse := txHash.Hex()
require.Equal(expectedResponse, response, "expected response is not returned")
}
func (s *BackendTestSuite) TestJailSendQueuedTransaction() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
jailInstance := s.backend.JailManager()
require.NotNil(jailInstance)
// log into account from which transactions will be sent
require.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
txParams := `{
"from": "` + TestConfig.Account1.Address + `",
"to": "0xf82da7547534045b4e00442bc89e16186cf8c272",
"value": "0.000001"
}`
txCompletedSuccessfully := make(chan struct{})
txCompletedCounter := make(chan struct{})
txHashes := make(chan gethcommon.Hash)
// replace transaction notification handler
requireMessageId := false
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
messageId, ok := event["message_id"].(string)
s.True(ok, "Message id is required, but not found")
if requireMessageId {
if len(messageId) == 0 {
s.Fail("Message id is required, but not provided")
return
}
} else {
if len(messageId) != 0 {
s.Fail("Message id is not required, but provided")
return
}
}
log.Info("Transaction queued (will be completed shortly)", "id", event["id"].(string))
var txHash gethcommon.Hash
if txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password); err != nil {
s.Fail(fmt.Sprintf("cannot complete queued transaction[%v]: %v", event["id"], err))
} else {
log.Info("Transaction complete", "URL", "https://rinkeby.etherscan.io/tx/%s"+txHash.Hex())
}
txCompletedSuccessfully <- struct{}{} // so that timeout is aborted
txHashes <- txHash
txCompletedCounter <- struct{}{}
}
})
type testCommand struct {
command string
params string
expectedResponse string
}
type testCase struct {
name string
file string
requireMessageId bool
commands []testCommand
}
tests := []testCase{
{
// no context or message id
name: "Case 1: no message id or context in inited JS",
file: "no-message-id-or-context.js",
requireMessageId: false,
commands: []testCommand{
{
`["commands", "send"]`,
txParams,
`{"result": {"transaction-hash":"TX_HASH"}}`,
},
{
`["commands", "getBalance"]`,
`{"address": "` + TestConfig.Account1.Address + `"}`,
`{"result": {"balance":42}}`,
},
},
},
{
// context is present in inited JS (but no message id is there)
name: "Case 2: context is present in inited JS (but no message id is there)",
file: "context-no-message-id.js",
requireMessageId: false,
commands: []testCommand{
{
`["commands", "send"]`,
txParams,
`{"result": {"context":{"` + jail.SendTransactionRequest + `":true},"result":{"transaction-hash":"TX_HASH"}}}`,
},
{
`["commands", "getBalance"]`,
`{"address": "` + TestConfig.Account1.Address + `"}`,
`{"result": {"context":{},"result":{"balance":42}}}`, // note empty (but present) context!
},
},
},
{
// message id is present in inited JS, but no context is there
name: "Case 3: message id is present, context is not present",
file: "message-id-no-context.js",
requireMessageId: true,
commands: []testCommand{
{
`["commands", "send"]`,
txParams,
`{"result": {"transaction-hash":"TX_HASH"}}`,
},
{
`["commands", "getBalance"]`,
`{"address": "` + TestConfig.Account1.Address + `"}`,
`{"result": {"balance":42}}`, // note empty context!
},
},
},
{
// both message id and context are present in inited JS (this UC is what we normally expect to see)
name: "Case 4: both message id and context are present",
file: "tx-send.js",
requireMessageId: true,
commands: []testCommand{
{
`["commands", "send"]`,
txParams,
`{"result": {"context":{"eth_sendTransaction":true,"message_id":"foobar"},"result":{"transaction-hash":"TX_HASH"}}}`,
},
{
`["commands", "getBalance"]`,
`{"address": "` + TestConfig.Account1.Address + `"}`,
`{"result": {"context":{"message_id":"42"},"result":{"balance":42}}}`, // message id in context, but default one is used!
},
},
},
}
for _, test := range tests {
jailInstance.BaseJS(string(static.MustAsset(txSendFolder + test.file)))
common.PanicAfter(60*time.Second, txCompletedSuccessfully, test.name)
jailInstance.Parse(testChatID, ``)
requireMessageId = test.requireMessageId
for _, command := range test.commands {
go func(jail common.JailManager, test testCase, command testCommand) {
log.Info(fmt.Sprintf("->%s: %s", test.name, command.command))
response := jail.Call(testChatID, command.command, command.params)
var txHash gethcommon.Hash
if command.command == `["commands", "send"]` {
txHash = <-txHashes
}
expectedResponse := strings.Replace(command.expectedResponse, "TX_HASH", txHash.Hex(), 1)
s.Equal(expectedResponse, response)
}(jailInstance, test, command)
}
<-txCompletedCounter
}
}
//
func (s *BackendTestSuite) TestGasEstimation() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
jailInstance := s.backend.JailManager()
require.NotNil(jailInstance)
jailInstance.Parse(testChatID, "")
// obtain VM for a given chat (to send custom JS to jailed version of Send())
vm, err := jailInstance.JailCellVM(testChatID)
require.NoError(err)
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
common.PanicAfter(30*time.Second, completeQueuedTransaction, s.T().Name())
// replace transaction notification handler
var txHash gethcommon.Hash
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string))
//t.Logf("Transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
s.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
var err error
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"]))
log.Info("contract transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex())
close(completeQueuedTransaction)
}
})
_, err = vm.Run(`
var responseValue = null;
var testContract = web3.eth.contract([{"constant":true,"inputs":[{"name":"a","type":"int256"}],"name":"double","outputs":[{"name":"","type":"int256"}],"payable":false,"type":"function"}]);
var test = testContract.new(
{
from: '` + TestConfig.Account1.Address + `',
data: '0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029',
}, function (e, contract){
if (!e) {
responseValue = contract.transactionHash
}
})
`)
s.NoError(err)
<-completeQueuedTransaction
responseValue, err := vm.Get("responseValue")
s.NoError(err, "vm.Get() failed")
response, err := responseValue.ToString()
s.NoError(err, "cannot parse result")
expectedResponse := txHash.Hex()
s.Equal(expectedResponse, response, "expected response is not returned")
s.T().Logf("estimation complete: %s", response)
}
func (s *BackendTestSuite) TestJailWhisper() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
jailInstance := s.backend.JailManager()
require.NotNil(jailInstance)
whisperService := s.WhisperService()
whisperAPI := whisper.NewPublicWhisperAPI(whisperService)
accountManager := s.backend.AccountManager()
// account1
_, accountKey1, err := accountManager.AddressToDecryptedAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
require.NoError(err)
accountKey1Hex := gethcommon.ToHex(crypto.FromECDSAPub(&accountKey1.PrivateKey.PublicKey))
_, err = whisperService.AddKeyPair(accountKey1.PrivateKey)
require.NoError(err, fmt.Sprintf("identity not injected: %v", accountKey1Hex))
if ok, err := whisperAPI.HasKeyPair(accountKey1Hex); err != nil || !ok {
require.FailNow(fmt.Sprintf("identity not injected: %v", accountKey1Hex))
}
// account2
_, accountKey2, err := accountManager.AddressToDecryptedAccount(TestConfig.Account2.Address, TestConfig.Account2.Password)
require.NoError(err)
accountKey2Hex := gethcommon.ToHex(crypto.FromECDSAPub(&accountKey2.PrivateKey.PublicKey))
_, err = whisperService.AddKeyPair(accountKey2.PrivateKey)
require.NoError(err, fmt.Sprintf("identity not injected: %v", accountKey2Hex))
if ok, err := whisperAPI.HasKeyPair(accountKey2Hex); err != nil || !ok {
require.FailNow(fmt.Sprintf("identity not injected: %v", accountKey2Hex))
}
passedTests := map[string]bool{
whisperMessage1: false,
whisperMessage2: false,
whisperMessage3: false,
whisperMessage4: false,
whisperMessage5: false,
whisperMessage6: false,
}
installedFilters := map[string]string{
whisperMessage1: "",
whisperMessage2: "",
whisperMessage3: "",
whisperMessage4: "",
whisperMessage5: "",
whisperMessage6: "",
}
testCases := []struct {
name string
testCode string
useFilter bool
}{
{
"test 0: ensure correct version of Whisper is used",
`
var expectedVersion = '0x5';
if (web3.version.whisper != expectedVersion) {
throw 'unexpected shh version, expected: ' + expectedVersion + ', got: ' + web3.version.whisper;
}
`,
false,
},
{
"test 1: encrypted signed message from us (From != nil && To != nil)",
`
var identity1 = '` + accountKey1Hex + `';
if (!web3.shh.hasKeyPair(identity1)) {
throw 'idenitity "` + accountKey1Hex + `" not found in whisper';
}
var identity2 = '` + accountKey2Hex + `';
if (!web3.shh.hasKeyPair(identity2)) {
throw 'idenitity "` + accountKey2Hex + `" not found in whisper';
}
var topic = makeTopic();
var payload = '` + whisperMessage1 + `';
// start watching for messages
var filter = shh.filter({
type: "asym",
sig: identity1,
key: identity2,
topics: [topic]
});
console.log(JSON.stringify(filter));
// post message
var message = {
type: "asym",
sig: identity1,
key: identity2,
topic: topic,
payload: payload,
ttl: 20,
};
var err = shh.post(message)
if (err !== null) {
throw 'message not sent: ' + message;
}
var filterName = '` + whisperMessage1 + `';
var filterId = filter.filterId;
if (!filterId) {
throw 'filter not installed properly';
}
`,
true,
},
{
"test 2: encrypted signed message to yourself (From != nil && To != nil)",
`
var identity = '` + accountKey1Hex + `';
if (!web3.shh.hasKeyPair(identity)) {
throw 'idenitity "` + accountKey1Hex + `" not found in whisper';
}
var topic = makeTopic();
var payload = '` + whisperMessage2 + `';
// start watching for messages
var filter = shh.filter({
type: "asym",
sig: identity,
key: identity,
topics: [topic],
});
// post message
var message = {
type: "asym",
sig: identity,
key: identity,
topic: topic,
payload: payload,
ttl: 20,
};
var err = shh.post(message)
if (err !== null) {
throw 'message not sent: ' + message;
}
var filterName = '` + whisperMessage2 + `';
var filterId = filter.filterId;
if (!filterId) {
throw 'filter not installed properly';
}
`,
true,
},
{
"test 3: signed (known sender) broadcast (From != nil && To == nil)",
`
var identity = '` + accountKey1Hex + `';
if (!web3.shh.hasKeyPair(identity)) {
throw 'idenitity "` + accountKey1Hex + `" not found in whisper';
}
var topic = makeTopic();
var payload = '` + whisperMessage3 + `';
// generate symmetric key
var keyid = shh.generateSymmetricKey();
if (!shh.hasSymmetricKey(keyid)) {
throw new Error('key not found');
}
// start watching for messages
var filter = shh.filter({
type: "sym",
sig: identity,
topics: [topic],
key: keyid
});
// post message
var message = {
type: "sym",
sig: identity,
topic: topic,
payload: payload,
ttl: 20,
key: keyid
};
var err = shh.post(message)
if (err !== null) {
throw 'message not sent: ' + message;
}
var filterName = '` + whisperMessage3 + `';
var filterId = filter.filterId;
if (!filterId) {
throw 'filter not installed properly';
}
`,
true,
},
{
"test 4: anonymous broadcast (From == nil && To == nil)",
`
var topic = makeTopic();
var payload = '` + whisperMessage4 + `';
// generate symmetric key
var keyid = shh.generateSymmetricKey();
if (!shh.hasSymmetricKey(keyid)) {
throw new Error('key not found');
}
// start watching for messages
var filter = shh.filter({
type: "sym",
topics: [topic],
key: keyid
});
// post message
var message = {
type: "sym",
topic: topic,
payload: payload,
ttl: 20,
key: keyid
};
var err = shh.post(message)
if (err !== null) {
throw 'message not sent: ' + err;
}
var filterName = '` + whisperMessage4 + `';
var filterId = filter.filterId;
if (!filterId) {
throw 'filter not installed properly';
}
`,
true,
},
{
"test 5: encrypted anonymous message (From == nil && To != nil)",
`
var identity = '` + accountKey2Hex + `';
if (!web3.shh.hasKeyPair(identity)) {
throw 'idenitity "` + accountKey2Hex + `" not found in whisper';
}
var topic = makeTopic();
var payload = '` + whisperMessage5 + `';
// start watching for messages
var filter = shh.filter({
type: "asym",
key: identity,
topics: [topic],
});
// post message
var message = {
type: "asym",
key: identity,
topic: topic,
payload: payload,
ttl: 20
};
var err = shh.post(message)
if (err !== null) {
throw 'message not sent: ' + message;
}
var filterName = '` + whisperMessage5 + `';
var filterId = filter.filterId;
if (!filterId) {
throw 'filter not installed properly';
}
`,
true,
},
{
"test 6: encrypted signed response to us (From != nil && To != nil)",
`
var identity1 = '` + accountKey1Hex + `';
if (!web3.shh.hasKeyPair(identity1)) {
throw 'idenitity "` + accountKey1Hex + `" not found in whisper';
}
var identity2 = '` + accountKey2Hex + `';
if (!web3.shh.hasKeyPair(identity2)) {
throw 'idenitity "` + accountKey2Hex + `" not found in whisper';
}
var topic = makeTopic();
var payload = '` + whisperMessage6 + `';
// start watching for messages
var filter = shh.filter({
type: "asym",
sig: identity2,
key: identity1,
topics: [topic]
});
// post message
var message = {
type: "asym",
sig: identity2,
key: identity1,
topic: topic,
payload: payload,
ttl: 20
};
var err = shh.post(message)
if (err !== null) {
throw 'message not sent: ' + message;
}
var filterName = '` + whisperMessage6 + `';
var filterId = filter.filterId;
if (!filterId) {
throw 'filter not installed properly';
}
`,
true,
},
}
for _, testCase := range testCases {
s.T().Log(testCase.name)
testCaseKey := crypto.Keccak256Hash([]byte(testCase.name)).Hex()
jailInstance.Parse(testCaseKey, `
var shh = web3.shh;
var makeTopic = function () {
var min = 1;
var max = Math.pow(16, 8);
var randInt = Math.floor(Math.random() * (max - min + 1)) + min;
return web3.toHex(randInt);
};
`)
vm, err := jailInstance.JailCellVM(testCaseKey)
require.NoError(err, "cannot get VM")
// post messages
if _, err := vm.Run(testCase.testCode); err != nil {
require.Fail(err.Error())
return
}
if !testCase.useFilter {
continue
}
// update installed filters
filterId, err := vm.Get("filterId")
require.NoError(err, "cannot get filterId")
filterName, err := vm.Get("filterName")
require.NoError(err, "cannot get filterName")
if _, ok := installedFilters[filterName.String()]; !ok {
require.FailNow("unrecognized filter")
}
installedFilters[filterName.String()] = filterId.String()
}
time.Sleep(2 * time.Second) // allow whisper to poll
for testKey, filter := range installedFilters {
if filter != "" {
s.T().Logf("filter found: %v", filter)
for _, message := range whisperAPI.GetNewSubscriptionMessages(filter) {
s.T().Logf("message found: %s", gethcommon.FromHex(message.Payload))
passedTests[testKey] = true
}
}
}
for testName, passedTest := range passedTests {
if !passedTest {
s.Fail(fmt.Sprintf("test not passed: %v", testName))
}
}
}

218
geth/api/backend_test.go Normal file
View File

@ -0,0 +1,218 @@
package api_test
import (
"testing"
"time"
"github.com/ethereum/go-ethereum/les"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/api"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
"github.com/stretchr/testify/suite"
)
func TestBackendTestSuite(t *testing.T) {
suite.Run(t, new(BackendTestSuite))
}
type BackendTestSuite struct {
BaseTestSuite
backend *api.StatusBackend
}
func (s *BackendTestSuite) SetupTest() {
require := s.Require()
backend := api.NewStatusBackend()
require.NotNil(backend)
require.IsType(&api.StatusBackend{}, backend)
s.backend = backend
s.NodeManager = backend.NodeManager()
}
func (s *BackendTestSuite) StartTestBackend(networkID int) {
require := s.Require()
require.NotNil(s.backend)
nodeConfig, err := MakeTestNodeConfig(networkID)
require.NoError(err)
// import account keys
require.NoError(common.ImportTestAccount(nodeConfig.KeyStoreDir, "test-account1.pk"))
require.NoError(common.ImportTestAccount(nodeConfig.KeyStoreDir, "test-account2.pk"))
require.False(s.backend.IsNodeRunning())
nodeStarted, err := s.backend.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted
require.True(s.backend.IsNodeRunning())
}
func (s *BackendTestSuite) StopTestBackend() {
require := s.Require()
require.NotNil(s.backend)
require.True(s.backend.IsNodeRunning())
require.NoError(s.backend.StopNode())
require.False(s.backend.IsNodeRunning())
}
func (s *BackendTestSuite) WhisperService() *whisper.Whisper {
require := s.Require()
require.NotNil(s.backend)
whisperService, err := s.backend.NodeManager().WhisperService()
require.NoError(err)
require.NotNil(whisperService)
return whisperService
}
func (s *BackendTestSuite) LightEthereumService() *les.LightEthereum {
require := s.Require()
require.NotNil(s.backend)
lightEthereum, err := s.backend.NodeManager().LightEthereumService()
require.NoError(err)
require.NotNil(lightEthereum)
return lightEthereum
}
func (s *BackendTestSuite) RestartTestNode() {
require := s.Require()
require.NotNil(s.backend)
require.True(s.backend.IsNodeRunning())
nodeRestarted, err := s.backend.RestartNode()
require.NoError(err)
require.NotNil(nodeRestarted)
require.True(s.backend.IsNodeRunning())
<-nodeRestarted
}
func (s *BackendTestSuite) TestNewBackend() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
}
func (s *BackendTestSuite) TestNodeStartStop() {
require := s.Require()
require.NotNil(s.backend)
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
require.NoError(err)
// try stopping non-started node
require.False(s.backend.IsNodeRunning())
err = s.backend.StopNode()
if s.Error(err) {
require.IsType(node.ErrNoRunningNode, err)
}
require.False(s.backend.IsNodeRunning())
nodeStarted, err := s.backend.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted // wait till node is started
require.True(s.backend.IsNodeRunning())
// try starting another node (w/o stopping the previously started node)
_, err = s.backend.StartNode(nodeConfig)
if s.Error(err) {
require.IsType(node.ErrNodeAlreadyExists, err)
}
// now stop node, and make sure that a new node, on different network can be started
err = s.backend.StopNode()
require.NoError(err)
// start new node with exactly the same config
require.False(s.backend.IsNodeRunning())
nodeStarted, err = s.backend.StartNode(nodeConfig)
require.NoError(err)
defer s.StopTestNode()
<-nodeStarted
require.True(s.backend.IsNodeRunning())
}
func (s *BackendTestSuite) TestNetworkSwitching() {
require := s.Require()
require.NotNil(s.backend)
// get Ropsten config
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
require.NoError(err)
require.False(s.backend.IsNodeRunning())
nodeStarted, err := s.backend.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted // wait till node is started
require.True(s.backend.IsNodeRunning())
s.FirstBlockHash("0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d")
// now stop node, and make sure that a new node, on different network can be started
err = s.backend.StopNode()
require.NoError(err)
// start new node with completely different config
nodeConfig, err = MakeTestNodeConfig(params.RinkebyNetworkID)
require.NoError(err)
require.False(s.backend.IsNodeRunning())
nodeStarted, err = s.backend.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted
require.True(s.backend.IsNodeRunning())
// make sure we are on another network indeed
s.FirstBlockHash("0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177")
require.NoError(s.backend.StopNode())
}
func (s *BackendTestSuite) TestResetChainData() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
time.Sleep(2 * time.Second) // allow to sync for some time
s.True(s.backend.IsNodeRunning())
nodeReady, err := s.backend.ResetChainData()
require.NoError(err)
<-nodeReady
s.True(s.backend.IsNodeRunning()) // new node, with previous config should be running
// make sure we can read the first byte, and it is valid (for Rinkeby)
s.FirstBlockHash("0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177")
}
func (s *BackendTestSuite) TestRestartNode() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RinkebyNetworkID)
defer s.StopTestBackend()
s.FirstBlockHash("0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177")
s.True(s.backend.IsNodeRunning())
nodeRestarted, err := s.backend.RestartNode()
require.NoError(err)
<-nodeRestarted
s.True(s.backend.IsNodeRunning()) // new node, with previous config should be running
// make sure we can read the first byte, and it is valid (for Rinkeby)
s.FirstBlockHash("0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177")
}

View File

@ -0,0 +1,663 @@
package api_test
import (
"encoding/json"
"fmt"
"math/big"
"reflect"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/les/status"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
)
func (s *BackendTestSuite) TestSendContractTx() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// create an account
sampleAddress, _, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 10)
common.PanicAfter(20*time.Second, completeQueuedTransaction, s.T().Name())
// replace transaction notification handler
var txHash = gethcommon.Hash{}
var txHashCheck = gethcommon.Hash{}
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint :dupl
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string))
// the first call will fail (we are not logged in, but trying to complete tx)
log.Info("trying to complete with no user logged in")
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.EqualError(err, node.ErrNoAccountSelected.Error(),
fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]))
// the second call will also fail (we are logged in as different user)
log.Info("trying to complete with invalid user")
err = s.backend.SelectAccount(sampleAddress, TestConfig.Account1.Password)
s.NoError(err)
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.EqualError(err, status.ErrInvalidCompleteTxSender.Error(),
fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]))
// the third call will work as expected (as we are logged in with correct credentials)
log.Info("trying to complete with correct user, this should suceed")
s.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"]))
log.Info("contract transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex())
close(completeQueuedTransaction)
return
}
})
// this call blocks, up until Complete Transaction is called
byteCode, err := hexutil.Decode(`0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029`)
require.NoError(err)
// send transaction
txHashCheck, err = backend.SendTransaction(nil, status.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: nil, // marker, contract creation is expected
//Value: (*hexutil.Big)(new(big.Int).Mul(big.NewInt(1), gethcommon.Ether)),
Gas: (*hexutil.Big)(big.NewInt(params.DefaultGas)),
Data: byteCode,
})
s.NoError(err, "cannot send transaction")
<-completeQueuedTransaction
s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid")
s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed")
s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point")
}
func (s *BackendTestSuite) TestSendEtherTx() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// create an account
sampleAddress, _, _, err := s.backend.CreateAccount(TestConfig.Account1.Password)
require.NoError(err)
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
common.PanicAfter(20*time.Second, completeQueuedTransaction, s.T().Name())
// replace transaction notification handler
var txHash = gethcommon.Hash{}
var txHashCheck = gethcommon.Hash{}
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint: dupl
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string))
// the first call will fail (we are not logged in, but trying to complete tx)
log.Info("trying to complete with no user logged in")
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.EqualError(err, node.ErrNoAccountSelected.Error(),
fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]))
// the second call will also fail (we are logged in as different user)
log.Info("trying to complete with invalid user")
err = s.backend.SelectAccount(sampleAddress, TestConfig.Account1.Password)
s.NoError(err)
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.EqualError(err, status.ErrInvalidCompleteTxSender.Error(),
fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]))
// the third call will work as expected (as we are logged in with correct credentials)
log.Info("trying to complete with correct user, this should suceed")
s.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"]))
log.Info("contract transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex())
close(completeQueuedTransaction)
return
}
})
// this call blocks, up until Complete Transaction is called
txHashCheck, err = backend.SendTransaction(nil, status.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
s.NoError(err, "cannot send transaction")
<-completeQueuedTransaction
s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid")
s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed")
s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point")
}
func (s *BackendTestSuite) TestDoubleCompleteQueuedTransactions() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// log into account from which transactions will be sent
require.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
common.PanicAfter(20*time.Second, completeQueuedTransaction, s.T().Name())
// replace transaction notification handler
var txID string
txFailedEventCalled := false
txHash := gethcommon.Hash{}
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
log.Info("transaction queued (will be failed and completed on the second call)", "id", txID)
// try with wrong password
// make sure that tx is NOT removed from the queue (by re-trying with the correct password)
_, err = s.backend.CompleteTransaction(txID, TestConfig.Account1.Password+"wrong")
s.EqualError(err, keystore.ErrDecrypt.Error())
s.Equal(1, backend.TransactionQueue().Count(), "txqueue cannot be empty, as tx has failed")
// now try to complete transaction, but with the correct password
txHash, err = s.backend.CompleteTransaction(event["id"].(string), TestConfig.Account1.Password)
s.NoError(err)
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
s.Equal(0, backend.TransactionQueue().Count(), "txqueue must be empty, as tx has completed")
log.Info("transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex())
close(completeQueuedTransaction)
}
if envelope.Type == node.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
log.Info("transaction return event received", "id", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := "could not decrypt key with given passphrase"
s.Equal(receivedErrMessage, expectedErrMessage)
receivedErrCode := event["error_code"].(string)
s.Equal("2", receivedErrCode)
txFailedEventCalled = true
}
})
// this call blocks, and should return on *second* attempt to CompleteTransaction (w/ the correct password)
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
s.NoError(err, "cannot send transaction")
<-completeQueuedTransaction
s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid")
s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed")
s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point")
s.True(txFailedEventCalled, "expected tx failure signal is not received")
}
func (s *BackendTestSuite) TestDiscardQueuedTransaction() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// reset queue
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
require.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
common.PanicAfter(20*time.Second, completeQueuedTransaction, s.T().Name())
// replace transaction notification handler
var txID string
txFailedEventCalled := false
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
log.Info("transaction queued (will be discarded soon)", "id", txID)
s.True(backend.TransactionQueue().Has(status.QueuedTxID(txID)), "txqueue should still have test tx")
// discard
err := s.backend.DiscardTransaction(txID)
s.NoError(err, "cannot discard tx")
// try completing discarded transaction
_, err = s.backend.CompleteTransaction(txID, TestConfig.Account1.Password)
s.EqualError(err, "transaction hash not found", "expects tx not found, but call to CompleteTransaction succeeded")
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
s.False(backend.TransactionQueue().Has(status.QueuedTxID(txID)),
fmt.Sprintf("txqueue should not have test tx at this point (it should be discarded): %s", txID))
close(completeQueuedTransaction)
}
if envelope.Type == node.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
log.Info("transaction return event received", "id", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := status.ErrQueuedTxDiscarded.Error()
s.Equal(receivedErrMessage, expectedErrMessage)
receivedErrCode := event["error_code"].(string)
s.Equal("4", receivedErrCode)
txFailedEventCalled = true
}
})
// this call blocks, and should return when DiscardQueuedTransaction() is called
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
s.EqualError(err, status.ErrQueuedTxDiscarded.Error(), "transaction is expected to be discarded")
<-completeQueuedTransaction
s.True(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction returned hash, while it shouldn't")
s.Zero(backend.TransactionQueue().Count(), "tx queue must be empty at this point")
s.True(txFailedEventCalled, "expected tx failure signal is not received")
}
func (s *BackendTestSuite) TestCompleteMultipleQueuedTransactions() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// reset queue
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
require.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
// make sure you panic if transaction complete doesn't return
testTxCount := 3
txIDs := make(chan string, testTxCount)
allTestTxCompleted := make(chan struct{}, 1)
// replace transaction notification handler
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
log.Info("transaction queued (will be completed in a single call, once aggregated)", "id", txID)
txIDs <- txID
}
})
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
s.NoError(err, "cannot send transaction")
s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction returned empty hash")
}
// wait for transactions, and complete them in a single call
completeTxs := func(txIDStrings string) {
var parsedIDs []string
err := json.Unmarshal([]byte(txIDStrings), &parsedIDs)
s.NoError(err)
parsedIDs = append(parsedIDs, "invalid-tx-id")
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
// complete
results := s.backend.CompleteTransactions(string(updatedTxIDStrings), TestConfig.Account1.Password)
if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error.Error() != "transaction hash not found" {
s.Fail(fmt.Sprintf("cannot complete txs: %v", results))
return
}
for txID, txResult := range results {
if txResult.Error != nil && txID != "invalid-tx-id" {
s.Fail(fmt.Sprintf("invalid error for %s", txID))
return
}
if txResult.Hash.Hex() == "0x0000000000000000000000000000000000000000000000000000000000000000" && txID != "invalid-tx-id" {
s.Fail(fmt.Sprintf("invalid hash (expected non empty hash): %s", txID))
return
}
if txResult.Hash.Hex() != "0x0000000000000000000000000000000000000000000000000000000000000000" {
log.Info("transaction complete", "URL", "https://rinkeby.etherscan.io/tx/%s"+txResult.Hash.Hex())
}
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
for _, txID := range parsedIDs {
s.False(backend.TransactionQueue().Has(status.QueuedTxID(txID)),
"txqueue should not have test tx at this point (it should be completed)")
}
}
go func() {
var txIDStrings []string
for i := 0; i < testTxCount; i++ {
txIDStrings = append(txIDStrings, <-txIDs)
}
txIDJSON, _ := json.Marshal(txIDStrings)
completeTxs(string(txIDJSON))
allTestTxCompleted <- struct{}{}
}()
// send multiple transactions
for i := 0; i < testTxCount; i++ {
go sendTx()
}
select {
case <-allTestTxCompleted:
// pass
case <-time.After(20 * time.Second):
s.Fail("test timed out")
return
}
if backend.TransactionQueue().Count() != 0 {
s.Fail("tx queue must be empty at this point")
return
}
}
func (s *BackendTestSuite) TestDiscardMultipleQueuedTransactions() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
time.Sleep(TestConfig.Node.SyncSeconds * time.Second) // allow to sync
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// reset queue
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
require.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
// make sure you panic if transaction complete doesn't return
testTxCount := 3
txIDs := make(chan string, testTxCount)
allTestTxDiscarded := make(chan struct{}, 1)
// replace transaction notification handler
txFailedEventCallCount := 0
node.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope node.SignalEnvelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err)
if envelope.Type == node.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
log.Info("transaction queued (will be discarded soon)", "id", txID)
s.True(backend.TransactionQueue().Has(status.QueuedTxID(txID)), "txqueue should still have test tx")
txIDs <- txID
}
if envelope.Type == node.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
log.Info("transaction return event received", "id", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := status.ErrQueuedTxDiscarded.Error()
s.Equal(receivedErrMessage, expectedErrMessage)
receivedErrCode := event["error_code"].(string)
s.Equal("4", receivedErrCode)
txFailedEventCallCount++
if txFailedEventCallCount == testTxCount {
allTestTxDiscarded <- struct{}{}
}
}
})
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
s.EqualError(err, status.ErrQueuedTxDiscarded.Error())
s.True(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction returned hash, while it shouldn't")
}
// wait for transactions, and discard immediately
discardTxs := func(txIDStrings string) {
var parsedIDs []string
err := json.Unmarshal([]byte(txIDStrings), &parsedIDs)
s.NoError(err)
parsedIDs = append(parsedIDs, "invalid-tx-id")
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
// discard
discardResults := s.backend.DiscardTransactions(string(updatedTxIDStrings))
if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error.Error() != "transaction hash not found" {
s.Fail(fmt.Sprintf("cannot discard txs: %v", discardResults))
return
}
// try completing discarded transaction
completeResults := s.backend.CompleteTransactions(string(updatedTxIDStrings), TestConfig.Account1.Password)
if len(completeResults) != (testTxCount + 1) {
s.Fail(fmt.Sprint("unexpected number of errors (call to CompleteTransaction should not succeed)"))
}
for _, txResult := range completeResults {
if txResult.Error.Error() != "transaction hash not found" {
s.Fail(fmt.Sprintf("invalid error for %s", txResult.Hash.Hex()))
return
}
if txResult.Hash.Hex() != "0x0000000000000000000000000000000000000000000000000000000000000000" {
s.Fail(fmt.Sprintf("invalid hash (expected zero): %s", txResult.Hash.Hex()))
return
}
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
for _, txID := range parsedIDs {
if backend.TransactionQueue().Has(status.QueuedTxID(txID)) {
s.Fail(fmt.Sprintf("txqueue should not have test tx at this point (it should be discarded): %s", txID))
return
}
}
}
go func() {
var txIDStrings []string
for i := 0; i < testTxCount; i++ {
txIDStrings = append(txIDStrings, <-txIDs)
}
txIDJSON, _ := json.Marshal(txIDStrings)
discardTxs(string(txIDJSON))
}()
// send multiple transactions
for i := 0; i < testTxCount; i++ {
go sendTx()
}
select {
case <-allTestTxDiscarded:
// pass
case <-time.After(20 * time.Second):
s.Fail("test timed out")
return
}
if backend.TransactionQueue().Count() != 0 {
s.Fail("tx queue must be empty at this point")
return
}
}
func (s *BackendTestSuite) TestNonExistentQueuedTransactions() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// log into account from which transactions will be sent
require.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
// replace transaction notification handler
node.SetDefaultNodeNotificationHandler(func(string) {})
// try completing non-existing transaction
_, err := s.backend.CompleteTransaction("some-bad-transaction-id", TestConfig.Account1.Password)
s.Error(err, "error expected and not received")
s.EqualError(err, status.ErrQueuedTxIDNotFound.Error())
}
func (s *BackendTestSuite) TestEvictionOfQueuedTransactions() {
require := s.Require()
require.NotNil(s.backend)
s.StartTestBackend(params.RopstenNetworkID)
defer s.StopTestBackend()
backend := s.LightEthereumService().StatusBackend
require.NotNil(backend)
// reset queue
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
require.NoError(s.backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
txQueue := backend.TransactionQueue()
var i = 0
txIDs := [status.DefaultTxQueueCap + 5 + 10]status.QueuedTxID{}
backend.SetTransactionQueueHandler(func(queuedTx status.QueuedTx) {
log.Info("tx enqueued", "i", i + 1, "queue size", txQueue.Count(), "id", queuedTx.ID)
txIDs[i] = queuedTx.ID
i++
})
s.Zero(txQueue.Count(), "transaction count should be zero")
for i := 0; i < 10; i++ {
go backend.SendTransaction(nil, status.SendTxArgs{}) // nolint: errcheck
}
time.Sleep(1 * time.Second)
log.Info(fmt.Sprintf("Number of transactions queued: %d. Queue size (shouldn't be more than %d): %d",
i, status.DefaultTxQueueCap, txQueue.Count()))
s.Equal(10, txQueue.Count(), "transaction count should be 10")
for i := 0; i < status.DefaultTxQueueCap+5; i++ { // stress test by hitting with lots of goroutines
go backend.SendTransaction(nil, status.SendTxArgs{}) // nolint: errcheck
}
time.Sleep(3 * time.Second)
if txQueue.Count() > status.DefaultTxQueueCap {
s.Fail(fmt.Sprintf("transaction count should be %d (or %d): got %d", status.DefaultTxQueueCap, status.DefaultTxQueueCap-1, txQueue.Count()))
return
}
for _, txID := range txIDs {
txQueue.Remove(txID)
}
if txQueue.Count() != 0 {
s.Fail(fmt.Sprintf("transaction count should be zero: %d", txQueue.Count()))
return
}
}

View File

@ -1,4 +1,4 @@
package params
package common
import (
"os"
@ -6,13 +6,14 @@ import (
"sync"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/geth/params"
)
// Logger is wrapper for custom logging
type Logger struct {
origHandler log.Handler
handler log.Handler
config *NodeConfig
config *params.NodeConfig
}
var (
@ -21,7 +22,7 @@ var (
)
// SetupLogger configs logger using parameters in config
func SetupLogger(config *NodeConfig) (*Logger, error) {
func SetupLogger(config *params.NodeConfig) (*Logger, error) {
if !config.LogEnabled {
return nil, nil
}

View File

@ -1,4 +1,4 @@
package params_test
package common_test
import (
"io/ioutil"
@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/geth/common"
)
func TestLogger(t *testing.T) {
@ -22,7 +23,7 @@ func TestLogger(t *testing.T) {
if err != nil {
t.Fatal("cannot create config object")
}
nodeLogger, err := params.SetupLogger(nodeConfig)
nodeLogger, err := common.SetupLogger(nodeConfig)
if err != nil {
t.Fatal("cannot create logger object")
}
@ -32,7 +33,7 @@ func TestLogger(t *testing.T) {
nodeConfig.LogEnabled = true
nodeConfig.LogToStderr = false // just capture logs to file
nodeLogger, err = params.SetupLogger(nodeConfig)
nodeLogger, err = common.SetupLogger(nodeConfig)
if err != nil {
t.Fatal("cannot create logger object")
}

252
geth/common/types.go Normal file
View File

@ -0,0 +1,252 @@
package common
import (
"encoding/json"
"errors"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/les/status"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/static"
)
var (
ErrDeprecatedMethod = errors.New("Method is depricated and will be removed in future release")
)
// SelectedExtKey is a container for currently selected (logged in) account
type SelectedExtKey struct {
Address common.Address
AccountKey *keystore.Key
SubAccounts []accounts.Account
}
// Hex dumps address of a given extended key as hex string
func (k *SelectedExtKey) Hex() string {
if k == nil {
return "0x0"
}
return k.Address.Hex()
}
// NodeManager defines expected methods for managing Status node
type NodeManager interface {
// StartNode start Status node, fails if node is already started
StartNode(config *params.NodeConfig) (<-chan struct{}, error)
// StopNode stop the running Status node.
// Stopped node cannot be resumed, one starts a new node instead.
StopNode() error
// RestartNode restart running Status node, fails if node is not running
RestartNode() (<-chan struct{}, error)
// ResetChainData remove chain data from data directory.
// Node is stopped, and new node is started, with clean data directory.
ResetChainData() (<-chan struct{}, error)
// IsNodeRunning confirm that node is running
IsNodeRunning() bool
// NodeConfig returns reference to running node's configuration
NodeConfig() (*params.NodeConfig, error)
// Node returns underlying Status node
Node() (*node.Node, error)
// PopulateStaticPeers populates node's list of static bootstrap peers
PopulateStaticPeers() error
// AddPeer adds URL of static peer
AddPeer(url string) error
// LightEthereumService exposes reference to LES service running on top of the node
LightEthereumService() (*les.LightEthereum, error)
// WhisperService returns reference to running Whisper service
WhisperService() (*whisper.Whisper, error)
// AccountManager returns reference to node's account manager
AccountManager() (*accounts.Manager, error)
// AccountKeyStore returns reference to account manager's keystore
AccountKeyStore() (*keystore.KeyStore, error)
// RPCClient exposes reference to RPC client connected to the running node
RPCClient() (*rpc.Client, error)
}
// AccountManager defines expected methods for managing Status accounts
type AccountManager interface {
// CreateAccount creates an internal geth account
// BIP44-compatible keys are generated: CKD#1 is stored as account key, CKD#2 stored as sub-account root
// Public key of CKD#1 is returned, with CKD#2 securely encoded into account key file (to be used for
// sub-account derivations)
CreateAccount(password string) (address, pubKey, mnemonic string, err error)
// CreateChildAccount creates sub-account for an account identified by parent address.
// CKD#2 is used as root for master accounts (when parentAddress is "").
// Otherwise (when parentAddress != ""), child is derived directly from parent.
CreateChildAccount(parentAddress, password string) (address, pubKey string, err error)
// RecoverAccount re-creates master key using given details.
// Once master key is re-generated, it is inserted into keystore (if not already there).
RecoverAccount(password, mnemonic string) (address, pubKey string, err error)
// VerifyAccountPassword tries to decrypt a given account key file, with a provided password.
// If no error is returned, then account is considered verified.
VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error)
// SelectAccount selects current account, by verifying that address has corresponding account which can be decrypted
// using provided password. Once verification is done, decrypted key is injected into Whisper (as a single identity,
// all previous identities are removed).
SelectAccount(address, password string) error
// ReSelectAccount selects previously selected account, often, after node restart.
ReSelectAccount() error
// SelectedAccount returns currently selected account
SelectedAccount() (*SelectedExtKey, error)
// Logout clears whisper identities
Logout() error
// AccountsListRequestHandler returns handler to process account list request
AccountsListRequestHandler() func(entities []common.Address) []common.Address
// AddressToDecryptedAccount tries to load decrypted key for a given account.
// The running node, has a keystore directory which is loaded on start. Key file
// for a given address is expected to be in that directory prior to node start.
AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error)
}
// RawCompleteTransactionResult is a JSON returned from transaction complete function (used internally)
type RawCompleteTransactionResult struct {
Hash common.Hash
Error error
}
// RawDiscardTransactionResult is list of results from CompleteTransactions() (used internally)
type RawDiscardTransactionResult struct {
Error error
}
// TxQueueManager defines expected methods for managing transaction queue
type TxQueueManager interface {
// TransactionQueueHandler returns handler that processes incoming tx queue requests
TransactionQueueHandler() func(queuedTx status.QueuedTx)
// TransactionReturnHandler returns handler that processes responses from internal tx manager
TransactionReturnHandler() func(queuedTx *status.QueuedTx, err error)
// CompleteTransaction instructs backend to complete sending of a given transaction
CompleteTransaction(id, password string) (common.Hash, error)
// CompleteTransactions instructs backend to complete sending of multiple transactions
CompleteTransactions(ids, password string) map[string]RawCompleteTransactionResult
// DiscardTransaction discards a given transaction from transaction queue
DiscardTransaction(id string) error
// DiscardTransactions discards given multiple transactions from transaction queue
DiscardTransactions(ids string) map[string]RawDiscardTransactionResult
}
// JailCell represents single jail cell, which is basically a JavaScript VM.
type JailCell interface {
CellVM() *otto.Otto
}
// JailManager defines methods for managing jailed environments
type JailManager interface {
// Parse creates a new jail cell context, with the given chatID as identifier.
// New context executes provided JavaScript code, right after the initialization.
Parse(chatID string, js string) string
// Call executes given JavaScript function w/i a jail cell context identified by the chatID.
// Jail cell is clonned before call is executed i.e. all calls execute w/i their own contexts.
Call(chatID string, path string, args string) string
// NewJailCell initializes and returns jail cell
NewJailCell(id string) JailCell
// JailCellVM returns instance of Otto VM (which is persisted w/i jail cell) by chatID
JailCellVM(chatID string) (*otto.Otto, error)
// BaseJS allows to setup initial JavaScript to be loaded on each jail.Parse()
BaseJS(js string)
}
// APIResponse generic response from API
type APIResponse struct {
Error string `json:"error"`
}
// AccountInfo represents account's info
type AccountInfo struct {
Address string `json:"address"`
PubKey string `json:"pubkey"`
Mnemonic string `json:"mnemonic"`
Error string `json:"error"`
}
// CompleteTransactionResult is a JSON returned from transaction complete function (used in exposed method)
type CompleteTransactionResult struct {
ID string `json:"id"`
Hash string `json:"hash"`
Error string `json:"error"`
}
// CompleteTransactionsResult is list of results from CompleteTransactions() (used in exposed method)
type CompleteTransactionsResult struct {
Results map[string]CompleteTransactionResult `json:"results"`
}
// DiscardTransactionResult is a JSON returned from transaction discard function
type DiscardTransactionResult struct {
ID string `json:"id"`
Error string `json:"error"`
}
// DiscardTransactionsResult is a list of results from DiscardTransactions()
type DiscardTransactionsResult struct {
Results map[string]DiscardTransactionResult `json:"results"`
}
// TestConfig contains shared (among different test packages) parameters
type TestConfig struct {
Node struct {
SyncSeconds time.Duration
HTTPPort int
WSPort int
}
Account1 struct {
Address string
Password string
}
Account2 struct {
Address string
Password string
}
}
// LoadTestConfig loads test configuration values from disk
func LoadTestConfig() (*TestConfig, error) {
var testConfig TestConfig
configData := string(static.MustAsset("config/test-data.json"))
if err := json.Unmarshal([]byte(configData), &testConfig); err != nil {
return nil, err
}
return &testConfig, nil
}

145
geth/common/utils.go Normal file
View File

@ -0,0 +1,145 @@
package common
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"runtime/debug"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/static"
)
const (
// MessageIDKey is a key for message ID
// This ID is required to track from which chat a given send transaction request is coming.
MessageIDKey = contextKey("message_id")
)
type contextKey string // in order to make sure that our context key does not collide with keys from other packages
// errors
var (
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
)
// ParseAccountString parses hex encoded string and returns is as accounts.Account.
func ParseAccountString(account string) (accounts.Account, error) {
// valid address, convert to account
if common.IsHexAddress(account) {
return accounts.Account{Address: common.HexToAddress(account)}, nil
}
return accounts.Account{}, ErrInvalidAccountAddressOrKey
}
// FromAddress converts account address from string to common.Address.
// The function is useful to format "From" field of send transaction struct.
func FromAddress(accountAddress string) common.Address {
from, err := ParseAccountString(accountAddress)
if err != nil {
return common.Address{}
}
return from.Address
}
// ToAddress converts account address from string to *common.Address.
// The function is useful to format "To" field of send transaction struct.
func ToAddress(accountAddress string) *common.Address {
to, err := ParseAccountString(accountAddress)
if err != nil {
return nil
}
return &to.Address
}
// ImportTestAccount checks if test account exists in keystore, and if not
// tries to import it (from static resources, see "static/keys" folder)
func ImportTestAccount(keystoreDir, accountFile string) error {
// make sure that keystore folder exists
if _, err := os.Stat(keystoreDir); os.IsNotExist(err) {
os.MkdirAll(keystoreDir, os.ModePerm) // nolint: errcheck
}
dst := filepath.Join(keystoreDir, accountFile)
if _, err := os.Stat(dst); os.IsNotExist(err) {
err = ioutil.WriteFile(dst, static.MustAsset("keys/"+accountFile), 0644)
if err != nil {
log.Warn("cannot copy test account PK", "error", err)
return err
}
}
return nil
}
// PanicAfter throws panic() after waitSeconds, unless abort channel receives notification
func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) {
go func() {
select {
case <-abort:
return
case <-time.After(waitSeconds):
panic("whatever you were doing takes toooo long: " + desc)
}
}()
}
// MessageIDFromContext returns message id from context (if exists)
func MessageIDFromContext(ctx context.Context) string {
if ctx == nil {
return ""
}
if messageID, ok := ctx.Value(MessageIDKey).(string); ok {
return messageID
}
return ""
}
// ParseJSONArray parses JSON array into Go array of string
func ParseJSONArray(items string) ([]string, error) {
var parsedItems []string
err := json.Unmarshal([]byte(items), &parsedItems)
if err != nil {
return nil, err
}
return parsedItems, nil
}
// Fatalf is used to halt the execution.
// When called the function prints stack end exits.
// Failure is logged into both StdErr and StdOut.
func Fatalf(reason interface{}, args ...interface{}) {
// decide on output stream
w := io.MultiWriter(os.Stdout, os.Stderr)
outf, _ := os.Stdout.Stat()
errf, _ := os.Stderr.Stat()
if outf != nil && errf != nil && os.SameFile(outf, errf) {
w = os.Stderr
}
// find out whether error or string has been passed as a reason
r := reflect.ValueOf(reason)
if r.Kind() == reflect.String {
fmt.Fprintf(w, "Fatal Failure: "+reason.(string)+"\n", args)
} else {
fmt.Fprintf(w, "Fatal Failure: %v\n", reason.(error))
}
debug.PrintStack()
os.Exit(1)
}

View File

@ -2,7 +2,7 @@ package jail
import (
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/node"
)
const (
@ -64,7 +64,7 @@ func makeSendHandler(jail *Jail, chatID string) func(call otto.FunctionCall) (re
// makeJethIsConnectedHandler returns jeth.isConnected() handler
func makeJethIsConnectedHandler(jail *Jail) func(call otto.FunctionCall) (response otto.Value) {
return func(call otto.FunctionCall) otto.Value {
client, err := jail.RPCClient()
client, err := jail.requestManager.RPCClient()
if err != nil {
return newErrorResponse(call, -32603, err.Error(), nil)
}
@ -75,13 +75,19 @@ func makeJethIsConnectedHandler(jail *Jail) func(call otto.FunctionCall) (respon
}
if !netListeningResult {
return newErrorResponse(call, -32603, geth.ErrInvalidGethNode.Error(), nil)
return newErrorResponse(call, -32603, node.ErrNoRunningNode.Error(), nil)
}
return newResultResponse(call, true)
}
}
// LocalStorageSetEvent is a signal sent whenever local storage Set method is called
type LocalStorageSetEvent struct {
ChatID string `json:"chat_id"`
Data string `json:"data"`
}
// makeLocalStorageSetHandler returns localStorage.set() handler
func makeLocalStorageSetHandler(chatID string) func(call otto.FunctionCall) (response otto.Value) {
return func(call otto.FunctionCall) otto.Value {
@ -90,9 +96,9 @@ func makeLocalStorageSetHandler(chatID string) func(call otto.FunctionCall) (res
data = data[:LocalStorageMaxDataLen]
}
geth.SendSignal(geth.SignalEnvelope{
node.SendSignal(node.SignalEnvelope{
Type: EventLocalStorageSet,
Event: geth.LocalStorageSetEvent{
Event: LocalStorageSetEvent{
ChatID: chatID,
Data: data,
},

View File

@ -11,102 +11,92 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/static"
)
const (
// JailedRuntimeRequestTimeout seconds before jailed request times out
JailedRuntimeRequestTimeout = time.Second * 60
// JailCellRequestTimeout seconds before jailed request times out
JailCellRequestTimeout = 60
)
var web3JSCode = static.MustAsset("scripts/web3.js")
// errors
var (
ErrInvalidJail = errors.New("jail environment is not properly initialized")
)
// Jail represents jailed environment inside of which we hold
// multiple cells. Each cell is separate JavaScript VM.
type Jail struct {
sync.RWMutex
client *rpc.Client // lazy inited on the first call
cells map[string]*JailedRuntime // jail supports running many isolated instances of jailed runtime
statusJS string
requestQueue *geth.JailedRequestQueue
}
// JailedRuntime represents single jail cell, which is JavaScript VM.
type JailedRuntime struct {
// JailCell represents single jail cell, which is basically a JavaScript VM.
type JailCell struct {
id string
vm *otto.Otto
sem *semaphore.Semaphore
}
var web3JS = static.MustAsset("scripts/web3.js")
var jailInstance *Jail
var once sync.Once
// New returns singleton jail environment
func New() *Jail {
once.Do(func() {
jailInstance = &Jail{
cells: make(map[string]*JailedRuntime),
}
})
return jailInstance
// Jail represents jailed environment inside of which we hold multiple cells.
// Each cell is a separate JavaScript VM.
type Jail struct {
sync.RWMutex
requestManager *RequestManager
cells map[string]common.JailCell // jail supports running many isolated instances of jailed runtime
baseJSCode string // JavaScript used to initialize all new cells with
}
// Init allows to setup initial JavaScript to be loaded on each jail.Parse()
func Init(js string) *Jail {
jailInstance = New() // singleton, we will always get the same reference
jailInstance.statusJS = js
return jailInstance
func (cell *JailCell) CellVM() *otto.Otto {
return cell.vm
}
// GetInstance returns singleton jail environment instance
func GetInstance() *Jail {
return New() // singleton, we will always get the same reference
// New returns new Jail environment
func New(nodeManager common.NodeManager) *Jail {
return &Jail{
requestManager: NewRequestManager(nodeManager),
cells: make(map[string]common.JailCell),
}
}
// NewJailedRuntime initializes and returns jail cell
func NewJailedRuntime(id string) *JailedRuntime {
return &JailedRuntime{
// BaseJS allows to setup initial JavaScript to be loaded on each jail.Parse()
func (jail *Jail) BaseJS(js string) {
jail.baseJSCode = js
}
// NewJailCell initializes and returns jail cell
func (jail *Jail) NewJailCell(id string) common.JailCell {
return &JailCell{
id: id,
vm: otto.New(),
sem: semaphore.New(1, JailedRuntimeRequestTimeout),
sem: semaphore.New(1, JailCellRequestTimeout*time.Second),
}
}
// Parse creates a new jail cell context, with the given chatID as identifier
// Parse creates a new jail cell context, with the given chatID as identifier.
// New context executes provided JavaScript code, right after the initialization.
func (jail *Jail) Parse(chatID string, js string) string {
var err error
if jail == nil {
return printError(ErrInvalidJail.Error())
return makeError(ErrInvalidJail.Error())
}
jail.Lock()
defer jail.Unlock()
jail.cells[chatID] = NewJailedRuntime(chatID)
vm := jail.cells[chatID].vm
jail.cells[chatID] = jail.NewJailCell(chatID)
vm := jail.cells[chatID].CellVM()
initJjs := jail.statusJS + ";"
initJjs := jail.baseJSCode + ";"
if _, err = vm.Run(initJjs); err != nil {
return printError(err.Error())
return makeError(err.Error())
}
// init jeth and its handlers
if err = vm.Set("jeth", struct{}{}); err != nil {
return printError(err.Error())
return makeError(err.Error())
}
if err = registerHandlers(jail, vm, chatID); err != nil {
return printError(err.Error())
return makeError(err.Error())
}
jjs := string(web3JS) + `
jjs := string(web3JSCode) + `
var Web3 = require('web3');
var web3 = new Web3(jeth);
var Bignumber = require("bignumber.js");
@ -115,40 +105,36 @@ func (jail *Jail) Parse(chatID string, js string) string {
}
` + js + "; var catalog = JSON.stringify(_status_catalog);"
if _, err = vm.Run(jjs); err != nil {
return printError(err.Error())
return makeError(err.Error())
}
res, err := vm.Get("catalog")
if err != nil {
return printError(err.Error())
return makeError(err.Error())
}
return printResult(res.String(), err)
return makeResult(res.String(), err)
}
// Call executes given JavaScript function w/i a jail cell context identified by the chatID
// Call executes given JavaScript function w/i a jail cell context identified by the chatID.
// Jail cell is clonned before call is executed i.e. all calls execute w/i their own contexts.
func (jail *Jail) Call(chatID string, path string, args string) string {
_, err := jail.RPCClient()
if err != nil {
return printError(err.Error())
}
jail.RLock()
cell, ok := jail.cells[chatID]
if !ok {
jail.RUnlock()
return printError(fmt.Sprintf("Cell[%s] doesn't exist.", chatID))
return makeError(fmt.Sprintf("Cell[%s] doesn't exist.", chatID))
}
jail.RUnlock()
vm := cell.vm.Copy() // isolate VM to allow concurrent access
vm := cell.CellVM().Copy() // isolate VM to allow concurrent access
res, err := vm.Call("call", nil, path, args)
return printResult(res.String(), err)
return makeResult(res.String(), err)
}
// GetVM returns instance of Otto VM (which is persisted w/i jail cell) by chatID
func (jail *Jail) GetVM(chatID string) (*otto.Otto, error) {
// JailCellVM returns instance of Otto VM (which is persisted w/i jail cell) by chatID
func (jail *Jail) JailCellVM(chatID string) (*otto.Otto, error) {
if jail == nil {
return nil, ErrInvalidJail
}
@ -161,18 +147,13 @@ func (jail *Jail) GetVM(chatID string) (*otto.Otto, error) {
return nil, fmt.Errorf("cell[%s] doesn't exist", chatID)
}
return cell.vm, nil
return cell.CellVM(), nil
}
// Send will serialize the first argument, send it to the node and returns the response.
// nolint: errcheck, unparam
func (jail *Jail) Send(chatID string, call otto.FunctionCall) (response otto.Value) {
client, err := jail.RPCClient()
if err != nil {
return newErrorResponse(call, -32603, err.Error(), nil)
}
requestQueue, err := jail.RequestQueue()
client, err := jail.requestManager.RPCClient()
if err != nil {
return newErrorResponse(call, -32603, err.Error(), nil)
}
@ -185,7 +166,7 @@ func (jail *Jail) Send(chatID string, call otto.FunctionCall) (response otto.Val
}
var (
rawReq = []byte(reqVal.String())
reqs []geth.RPCCall
reqs []RPCCall
batch bool
)
if rawReq[0] == '[' {
@ -193,7 +174,7 @@ func (jail *Jail) Send(chatID string, call otto.FunctionCall) (response otto.Val
json.Unmarshal(rawReq, &reqs)
} else {
batch = false
reqs = make([]geth.RPCCall, 1)
reqs = make([]RPCCall, 1)
json.Unmarshal(rawReq, &reqs[0])
}
@ -205,8 +186,8 @@ func (jail *Jail) Send(chatID string, call otto.FunctionCall) (response otto.Val
var result json.RawMessage
// execute directly w/o RPC call to node
if req.Method == geth.SendTransactionRequest {
txHash, err := requestQueue.ProcessSendTransactionRequest(call.Otto, req)
if req.Method == SendTransactionRequest {
txHash, err := jail.requestManager.ProcessSendTransactionRequest(call.Otto, req)
resp.Set("result", txHash.Hex())
if err != nil {
resp = newErrorResponse(call, -32603, err.Error(), &req.ID).Object()
@ -218,7 +199,7 @@ func (jail *Jail) Send(chatID string, call otto.FunctionCall) (response otto.Val
// do extra request pre processing (persist message id)
// within function semaphore will be acquired and released,
// so that no more than one client (per cell) can enter
messageID, err := requestQueue.PreProcessRequest(call.Otto, req)
messageID, err := jail.requestManager.PreProcessRequest(call.Otto, req)
if err != nil {
return newErrorResponse(call, -32603, err.Error(), nil)
}
@ -256,7 +237,7 @@ func (jail *Jail) Send(chatID string, call otto.FunctionCall) (response otto.Val
resps.Call("push", resp)
// do extra request post processing (setting back tx context)
requestQueue.PostProcessRequest(call.Otto, req, messageID)
jail.requestManager.PostProcessRequest(call.Otto, req, messageID)
}
// Return the responses either to the callback (if supplied)
@ -273,59 +254,6 @@ func (jail *Jail) Send(chatID string, call otto.FunctionCall) (response otto.Val
return response
}
// RPCClient returns RPC client instance, creating it if necessary.
// Returned instance is cached, so successive calls receive the same one.
// nolint: dupl
func (jail *Jail) RPCClient() (*rpc.Client, error) {
if jail == nil {
return nil, ErrInvalidJail
}
if jail.client != nil {
return jail.client, nil
}
nodeManager := geth.NodeManagerInstance()
if !nodeManager.NodeInited() {
return nil, geth.ErrInvalidGethNode
}
// obtain RPC client from running node
client, err := nodeManager.RPCClient()
if err != nil {
return nil, err
}
jail.client = client
return jail.client, nil
}
// RequestQueue returns request queue instance, creating it if necessary.
// Returned instance is cached, so successive calls receive the same one.
// nolint: dupl
func (jail *Jail) RequestQueue() (*geth.JailedRequestQueue, error) {
if jail == nil {
return nil, ErrInvalidJail
}
if jail.requestQueue != nil {
return jail.requestQueue, nil
}
nodeManager := geth.NodeManagerInstance()
if !nodeManager.NodeInited() {
return nil, geth.ErrInvalidGethNode
}
requestQueue, err := nodeManager.JailedRequestQueue()
if err != nil {
return nil, err
}
jail.requestQueue = requestQueue
return jail.requestQueue, nil
}
func newErrorResponse(call otto.FunctionCall, code int, msg string, id interface{}) otto.Value {
// Bundle the error into a JSON RPC call response
m := map[string]interface{}{"jsonrpc": "2.0", "id": id, "error": map[string]interface{}{"code": code, msg: msg}}
@ -351,18 +279,23 @@ func throwJSException(msg interface{}) otto.Value {
panic(val)
}
func printError(error string) string {
str := geth.JSONError{
// JSONError is wrapper around errors, that are sent upwards
type JSONError struct {
Error string `json:"error"`
}
func makeError(error string) string {
str := JSONError{
Error: error,
}
outBytes, _ := json.Marshal(&str)
return string(outBytes)
}
func printResult(res string, err error) string {
func makeResult(res string, err error) string {
var out string
if err != nil {
out = printError(err.Error())
out = makeError(err.Error())
} else {
if "undefined" == res {
res = "null"

File diff suppressed because it is too large Load Diff

233
geth/jail/requests.go Normal file
View File

@ -0,0 +1,233 @@
package jail
import (
"context"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/les/status"
"github.com/ethereum/go-ethereum/rpc"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/common"
)
const (
// SendTransactionRequest is triggered on send transaction request
SendTransactionRequest = "eth_sendTransaction"
)
// RequestManager represents interface to manage jailed requests.
// Whenever some request passed to a Jail, needs to be pre/post processed,
// request manager is the right place for that.
type RequestManager struct {
nodeManager common.NodeManager
}
func NewRequestManager(nodeManager common.NodeManager) *RequestManager {
return &RequestManager{
nodeManager: nodeManager,
}
}
// PreProcessRequest pre-processes a given RPC call to a given Otto VM
func (m *RequestManager) PreProcessRequest(vm *otto.Otto, req RPCCall) (string, error) {
messageID := currentMessageID(vm.Context())
return messageID, nil
}
// PostProcessRequest post-processes a given RPC call to a given Otto VM
func (m *RequestManager) PostProcessRequest(vm *otto.Otto, req RPCCall, messageID string) {
if len(messageID) > 0 {
vm.Call("addContext", nil, messageID, common.MessageIDKey, messageID) // nolint: errcheck
}
// set extra markers for queued transaction requests
if req.Method == SendTransactionRequest {
vm.Call("addContext", nil, messageID, SendTransactionRequest, true) // nolint: errcheck
}
}
// ProcessSendTransactionRequest processes send transaction request.
// Both pre and post processing happens within this function. Pre-processing
// happens before transaction is send to backend, and post processing occurs
// when backend notifies that transaction sending is complete (either successfully
// or with error)
func (m *RequestManager) ProcessSendTransactionRequest(vm *otto.Otto, req RPCCall) (gethcommon.Hash, error) {
lightEthereum, err := m.nodeManager.LightEthereumService()
if err != nil {
return gethcommon.Hash{}, err
}
backend := lightEthereum.StatusBackend
messageID, err := m.PreProcessRequest(vm, req)
if err != nil {
return gethcommon.Hash{}, err
}
// onSendTransactionRequest() will use context to obtain and release ticket
ctx := context.Background()
ctx = context.WithValue(ctx, common.MessageIDKey, messageID)
// this call blocks, up until Complete Transaction is called
txHash, err := backend.SendTransaction(ctx, sendTxArgsFromRPCCall(req))
if err != nil {
return gethcommon.Hash{}, err
}
// invoke post processing
m.PostProcessRequest(vm, req, messageID)
return txHash, nil
}
// RPCClient returns RPC client instance, creating it if necessary.
func (m *RequestManager) RPCClient() (*rpc.Client, error) {
return m.nodeManager.RPCClient()
}
// RPCCall represents RPC call parameters
type RPCCall struct {
ID int64
Method string
Params []interface{}
}
func sendTxArgsFromRPCCall(req RPCCall) status.SendTxArgs {
if req.Method != SendTransactionRequest { // no need to persist extra state for other requests
return status.SendTxArgs{}
}
return status.SendTxArgs{
From: req.parseFromAddress(),
To: req.parseToAddress(),
Value: req.parseValue(),
Data: req.parseData(),
Gas: req.parseGas(),
GasPrice: req.parseGasPrice(),
}
}
func (r RPCCall) parseFromAddress() gethcommon.Address {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return gethcommon.HexToAddress("0x")
}
from, ok := params["from"].(string)
if !ok {
from = "0x"
}
return gethcommon.HexToAddress(from)
}
func (r RPCCall) parseToAddress() *gethcommon.Address {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
}
to, ok := params["to"].(string)
if !ok {
return nil
}
address := gethcommon.HexToAddress(to)
return &address
}
func (r RPCCall) parseData() hexutil.Bytes {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return hexutil.Bytes("0x")
}
data, ok := params["data"].(string)
if !ok {
data = "0x"
}
byteCode, err := hexutil.Decode(data)
if err != nil {
byteCode = hexutil.Bytes(data)
}
return byteCode
}
// nolint: dupl
func (r RPCCall) parseValue() *hexutil.Big {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
//return (*hexutil.Big)(big.NewInt("0x0"))
}
inputValue, ok := params["value"].(string)
if !ok {
return nil
}
parsedValue, err := hexutil.DecodeBig(inputValue)
if err != nil {
return nil
}
return (*hexutil.Big)(parsedValue)
}
// nolint: dupl
func (r RPCCall) parseGas() *hexutil.Big {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
}
inputValue, ok := params["gas"].(string)
if !ok {
return nil
}
parsedValue, err := hexutil.DecodeBig(inputValue)
if err != nil {
return nil
}
return (*hexutil.Big)(parsedValue)
}
// nolint: dupl
func (r RPCCall) parseGasPrice() *hexutil.Big {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
}
inputValue, ok := params["gasPrice"].(string)
if !ok {
return nil
}
parsedValue, err := hexutil.DecodeBig(inputValue)
if err != nil {
return nil
}
return (*hexutil.Big)(parsedValue)
}
// currentMessageID looks for `status.message_id` variable in current JS context
func currentMessageID(ctx otto.Context) string {
if statusObj, ok := ctx.Symbols["status"]; ok {
messageID, err := statusObj.Object().Get("message_id")
if err != nil {
return ""
}
if messageID, err := messageID.ToString(); err == nil {
return messageID
}
}
return ""
}

View File

@ -1,4 +1,4 @@
package geth
package node
import (
"bytes"
@ -10,9 +10,10 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/extkeys"
"github.com/status-im/status-go/geth/common"
)
// errors
@ -23,29 +24,41 @@ var (
ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities")
ErrNoAccountSelected = errors.New("no account has been selected, please login")
ErrInvalidMasterKeyCreated = errors.New("can not create master extended key")
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
)
// AccountManager represents account manager interface
type AccountManager struct {
nodeManager common.NodeManager
selectedAccount *common.SelectedExtKey // account that was processed during the last call to SelectAccount()
}
// NewAccountManager returns new node account manager
func NewAccountManager(nodeManager common.NodeManager) *AccountManager {
return &AccountManager{
nodeManager: nodeManager,
}
}
// CreateAccount creates an internal geth account
// BIP44-compatible keys are generated: CKD#1 is stored as account key, CKD#2 stored as sub-account root
// Public key of CKD#1 is returned, with CKD#2 securely encoded into account key file (to be used for
// sub-account derivations)
func CreateAccount(password string) (address, pubKey, mnemonic string, err error) {
func (m *AccountManager) CreateAccount(password string) (address, pubKey, mnemonic string, err error) {
// generate mnemonic phrase
m := extkeys.NewMnemonic(extkeys.Salt)
mnemonic, err = m.MnemonicPhrase(128, extkeys.EnglishLanguage)
mn := extkeys.NewMnemonic(extkeys.Salt)
mnemonic, err = mn.MnemonicPhrase(128, extkeys.EnglishLanguage)
if err != nil {
return "", "", "", fmt.Errorf("can not create mnemonic seed: %v", err)
}
// generate extended master key (see BIP32)
extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt))
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt))
if err != nil {
return "", "", "", fmt.Errorf("can not create master extended key: %v", err)
}
// import created key into account keystore
address, pubKey, err = importExtendedKey(extKey, password)
address, pubKey, err = m.importExtendedKey(extKey, password)
if err != nil {
return "", "", "", err
}
@ -56,22 +69,21 @@ func CreateAccount(password string) (address, pubKey, mnemonic string, err error
// CreateChildAccount creates sub-account for an account identified by parent address.
// CKD#2 is used as root for master accounts (when parentAddress is "").
// Otherwise (when parentAddress != ""), child is derived directly from parent.
func CreateChildAccount(parentAddress, password string) (address, pubKey string, err error) {
nodeManager := NodeManagerInstance()
keyStore, err := nodeManager.AccountKeyStore()
func (m *AccountManager) CreateChildAccount(parentAddress, password string) (address, pubKey string, err error) {
keyStore, err := m.nodeManager.AccountKeyStore()
if err != nil {
return "", "", err
}
if parentAddress == "" && nodeManager.SelectedAccount != nil { // derive from selected account by default
parentAddress = nodeManager.SelectedAccount.Address.Hex()
if parentAddress == "" && m.selectedAccount != nil { // derive from selected account by default
parentAddress = m.selectedAccount.Address.Hex()
}
if parentAddress == "" {
return "", "", ErrNoAccountSelected
}
account, err := ParseAccountString(parentAddress)
account, err := common.ParseAccountString(parentAddress)
if err != nil {
return "", "", ErrAddressToAccountMappingFailure
}
@ -98,14 +110,14 @@ func CreateChildAccount(parentAddress, password string) (address, pubKey string,
accountKey.SubAccountIndex++
// import derived key into account keystore
address, pubKey, err = importExtendedKey(childKey, password)
address, pubKey, err = m.importExtendedKey(childKey, password)
if err != nil {
return
}
// update in-memory selected account
if nodeManager.SelectedAccount != nil {
nodeManager.SelectedAccount.AccountKey = accountKey
if m.selectedAccount != nil {
m.selectedAccount.AccountKey = accountKey
}
return address, pubKey, nil
@ -113,16 +125,16 @@ func CreateChildAccount(parentAddress, password string) (address, pubKey string,
// RecoverAccount re-creates master key using given details.
// Once master key is re-generated, it is inserted into keystore (if not already there).
func RecoverAccount(password, mnemonic string) (address, pubKey string, err error) {
func (m *AccountManager) RecoverAccount(password, mnemonic string) (address, pubKey string, err error) {
// re-create extended key (see BIP32)
m := extkeys.NewMnemonic(extkeys.Salt)
extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt))
mn := extkeys.NewMnemonic(extkeys.Salt)
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt))
if err != nil {
return "", "", ErrInvalidMasterKeyCreated
}
// import re-created key into account keystore
address, pubKey, err = importExtendedKey(extKey, password)
address, pubKey, err = m.importExtendedKey(extKey, password)
if err != nil {
return
}
@ -132,11 +144,11 @@ func RecoverAccount(password, mnemonic string) (address, pubKey string, err erro
// VerifyAccountPassword tries to decrypt a given account key file, with a provided password.
// If no error is returned, then account is considered verified.
func VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error) {
func (m *AccountManager) VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error) {
var err error
var keyJSON []byte
addressObj := common.BytesToAddress(common.FromHex(address))
addressObj := gethcommon.BytesToAddress(gethcommon.FromHex(address))
checkAccountKey := func(path string, fileInfo os.FileInfo) error {
if len(keyJSON) > 0 || fileInfo.IsDir() {
return nil
@ -183,14 +195,13 @@ func VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key
// SelectAccount selects current account, by verifying that address has corresponding account which can be decrypted
// using provided password. Once verification is done, decrypted key is injected into Whisper (as a single identity,
// all previous identities are removed).
func SelectAccount(address, password string) error {
nodeManager := NodeManagerInstance()
keyStore, err := nodeManager.AccountKeyStore()
func (m *AccountManager) SelectAccount(address, password string) error {
keyStore, err := m.nodeManager.AccountKeyStore()
if err != nil {
return err
}
account, err := ParseAccountString(address)
account, err := common.ParseAccountString(address)
if err != nil {
return ErrAddressToAccountMappingFailure
}
@ -200,7 +211,7 @@ func SelectAccount(address, password string) error {
return fmt.Errorf("%s: %v", ErrAccountToKeyMappingFailure.Error(), err)
}
whisperService, err := nodeManager.WhisperService()
whisperService, err := m.nodeManager.WhisperService()
if err != nil {
return err
}
@ -210,11 +221,11 @@ func SelectAccount(address, password string) error {
}
// persist account key for easier recovery of currently selected key
subAccounts, err := findSubAccounts(accountKey.ExtendedKey, accountKey.SubAccountIndex)
subAccounts, err := m.findSubAccounts(accountKey.ExtendedKey, accountKey.SubAccountIndex)
if err != nil {
return err
}
nodeManager.SelectedAccount = &SelectedExtKey{
m.selectedAccount = &common.SelectedExtKey{
Address: account.Address,
AccountKey: accountKey,
SubAccounts: subAccounts,
@ -223,16 +234,22 @@ func SelectAccount(address, password string) error {
return nil
}
// ReSelectAccount selects previously selected account, often, after node restart.
func ReSelectAccount() error {
nodeManager := NodeManagerInstance()
// SelectedAccount returns currently selected account
func (m *AccountManager) SelectedAccount() (*common.SelectedExtKey, error) {
if m.selectedAccount == nil {
return nil, ErrNoAccountSelected
}
return m.selectedAccount, nil
}
selectedAccount := nodeManager.SelectedAccount
// ReSelectAccount selects previously selected account, often, after node restart.
func (m *AccountManager) ReSelectAccount() error {
selectedAccount := m.selectedAccount
if selectedAccount == nil {
return nil
}
whisperService, err := nodeManager.WhisperService()
whisperService, err := m.nodeManager.WhisperService()
if err != nil {
return err
}
@ -245,9 +262,8 @@ func ReSelectAccount() error {
}
// Logout clears whisper identities
func Logout() error {
nodeManager := NodeManagerInstance()
whisperService, err := nodeManager.WhisperService()
func (m *AccountManager) Logout() error {
whisperService, err := m.nodeManager.WhisperService()
if err != nil {
return err
}
@ -257,15 +273,15 @@ func Logout() error {
return fmt.Errorf("%s: %v", ErrWhisperClearIdentitiesFailure, err)
}
nodeManager.SelectedAccount = nil
m.selectedAccount = nil
return nil
}
// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key.
// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file.
func importExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
keyStore, err := NodeManagerInstance().AccountKeyStore()
func (m *AccountManager) importExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
keyStore, err := m.nodeManager.AccountKeyStore()
if err != nil {
return "", "", err
}
@ -282,28 +298,28 @@ func importExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, p
if err != nil {
return address, "", err
}
pubKey = common.ToHex(crypto.FromECDSAPub(&key.PrivateKey.PublicKey))
pubKey = gethcommon.ToHex(crypto.FromECDSAPub(&key.PrivateKey.PublicKey))
return
}
func onAccountsListRequest(entities []common.Address) []common.Address {
nodeManager := NodeManagerInstance()
if nodeManager.SelectedAccount == nil {
return []common.Address{}
// AccountsListRequestHandler returns handler to process account list request
func (m *AccountManager) AccountsListRequestHandler() func(entities []gethcommon.Address) []gethcommon.Address {
return func(entities []gethcommon.Address) []gethcommon.Address {
if m.selectedAccount == nil {
return []gethcommon.Address{}
}
refreshSelectedAccount()
m.refreshSelectedAccount()
filtered := make([]common.Address, 0)
filtered := make([]gethcommon.Address, 0)
for _, account := range entities {
// main account
if nodeManager.SelectedAccount.Address.Hex() == account.Hex() {
if m.selectedAccount.Address.Hex() == account.Hex() {
filtered = append(filtered, account)
} else {
// sub accounts
for _, subAccount := range nodeManager.SelectedAccount.SubAccounts {
for _, subAccount := range m.selectedAccount.SubAccounts {
if subAccount.Address.Hex() == account.Hex() {
filtered = append(filtered, account)
}
@ -313,28 +329,27 @@ func onAccountsListRequest(entities []common.Address) []common.Address {
return filtered
}
}
// refreshSelectedAccount re-populates list of sub-accounts of the currently selected account (if any)
func refreshSelectedAccount() {
nodeManager := NodeManagerInstance()
if nodeManager.SelectedAccount == nil {
func (m *AccountManager) refreshSelectedAccount() {
if m.selectedAccount == nil {
return
}
accountKey := nodeManager.SelectedAccount.AccountKey
accountKey := m.selectedAccount.AccountKey
if accountKey == nil {
return
}
// re-populate list of sub-accounts
subAccounts, err := findSubAccounts(accountKey.ExtendedKey, accountKey.SubAccountIndex)
subAccounts, err := m.findSubAccounts(accountKey.ExtendedKey, accountKey.SubAccountIndex)
if err != nil {
return
}
nodeManager.SelectedAccount = &SelectedExtKey{
Address: nodeManager.SelectedAccount.Address,
AccountKey: nodeManager.SelectedAccount.AccountKey,
m.selectedAccount = &common.SelectedExtKey{
Address: m.selectedAccount.Address,
AccountKey: m.selectedAccount.AccountKey,
SubAccounts: subAccounts,
}
}
@ -342,9 +357,8 @@ func refreshSelectedAccount() {
// findSubAccounts traverses cached accounts and adds as a sub-accounts any
// that belong to the currently selected account.
// The extKey is CKD#2 := root of sub-accounts of the main account
func findSubAccounts(extKey *extkeys.ExtendedKey, subAccountIndex uint32) ([]accounts.Account, error) {
nodeManager := NodeManagerInstance()
keyStore, err := nodeManager.AccountKeyStore()
func (m *AccountManager) findSubAccounts(extKey *extkeys.ExtendedKey, subAccountIndex uint32) ([]accounts.Account, error) {
keyStore, err := m.nodeManager.AccountKeyStore()
if err != nil {
return []accounts.Account{}, err
}
@ -352,7 +366,7 @@ func findSubAccounts(extKey *extkeys.ExtendedKey, subAccountIndex uint32) ([]acc
subAccounts := make([]accounts.Account, 0)
if extKey.Depth == 5 { // CKD#2 level
// gather possible sub-account addresses
subAccountAddresses := make([]common.Address, 0)
subAccountAddresses := make([]gethcommon.Address, 0)
for i := uint32(0); i < subAccountIndex; i++ {
childKey, err := extKey.Child(i)
if err != nil {
@ -373,3 +387,20 @@ func findSubAccounts(extKey *extkeys.ExtendedKey, subAccountIndex uint32) ([]acc
return subAccounts, nil
}
// AddressToDecryptedAccount tries to load decrypted key for a given account.
// The running node, has a keystore directory which is loaded on start. Key file
// for a given address is expected to be in that directory prior to node start.
func (m *AccountManager) AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) {
keyStore, err := m.nodeManager.AccountKeyStore()
if err != nil {
return accounts.Account{}, nil, err
}
account, err := common.ParseAccountString(address)
if err != nil {
return accounts.Account{}, nil, ErrAddressToAccountMappingFailure
}
return keyStore.AccountDecryptedKey(account, password)
}

114
geth/node/accounts_test.go Normal file
View File

@ -0,0 +1,114 @@
package node_test
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node"
. "github.com/status-im/status-go/geth/testing"
"github.com/stretchr/testify/suite"
)
func TestAccountsTestSuite(t *testing.T) {
suite.Run(t, new(AccountsTestSuite))
}
type AccountsTestSuite struct {
BaseTestSuite
}
func (s *AccountsTestSuite) SetupTest() {
require := s.Require()
s.NodeManager = node.NewNodeManager()
require.NotNil(s.NodeManager)
require.IsType(&node.NodeManager{}, s.NodeManager)
}
func (s *AccountsTestSuite) TestVerifyAccountPassword() {
require := s.Require()
require.NotNil(s.NodeManager)
accountManager := node.NewAccountManager(nil)
require.NotNil(accountManager)
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
require.NoError(err)
defer os.RemoveAll(keyStoreDir) // nolint: errcheck
emptyKeyStoreDir, err := ioutil.TempDir(os.TempDir(), "empty")
require.NoError(err)
defer os.RemoveAll(emptyKeyStoreDir) // nolint: errcheck
// import account keys
require.NoError(common.ImportTestAccount(keyStoreDir, "test-account1.pk"))
require.NoError(common.ImportTestAccount(keyStoreDir, "test-account2.pk"))
account1Address := gethcommon.BytesToAddress(gethcommon.FromHex(TestConfig.Account1.Address))
testCases := []struct {
name string
keyPath string
address string
password string
expectedError error
}{
{
"correct address, correct password (decrypt should succeed)",
keyStoreDir,
TestConfig.Account1.Address,
TestConfig.Account1.Password,
nil,
},
{
"correct address, correct password, non-existent key store",
filepath.Join(keyStoreDir, "non-existent-folder"),
TestConfig.Account1.Address,
TestConfig.Account1.Password,
fmt.Errorf("cannot traverse key store folder: lstat %s/non-existent-folder: no such file or directory", keyStoreDir),
},
{
"correct address, correct password, empty key store (pk is not there)",
emptyKeyStoreDir,
TestConfig.Account1.Address,
TestConfig.Account1.Password,
fmt.Errorf("cannot locate account for address: %x", account1Address),
},
{
"wrong address, correct password",
keyStoreDir,
"0x79791d3e8f2daa1f7fec29649d152c0ada3cc535",
TestConfig.Account1.Password,
fmt.Errorf("cannot locate account for address: %s", "79791d3e8f2daa1f7fec29649d152c0ada3cc535"),
},
{
"correct address, wrong password",
keyStoreDir,
TestConfig.Account1.Address,
"wrong password", // wrong password
errors.New("could not decrypt key with given passphrase"),
},
}
for _, testCase := range testCases {
s.T().Log(testCase.name)
accountKey, err := accountManager.VerifyAccountPassword(testCase.keyPath, testCase.address, testCase.password)
if !reflect.DeepEqual(err, testCase.expectedError) {
s.FailNow(fmt.Sprintf("unexpected error: expected \n'%v', got \n'%v'", testCase.expectedError, err))
}
if err == nil {
if accountKey == nil {
s.T().Error("no error reported, but account key is missing")
}
accountAddress := gethcommon.BytesToAddress(gethcommon.FromHex(testCase.address))
if accountKey.Address != accountAddress {
s.T().Fatalf("account mismatch: have %x, want %x", accountKey.Address, accountAddress)
}
}
}
}

View File

@ -1,6 +1,6 @@
// +build darwin,cgo
package geth
package node
/*
#cgo CFLAGS: -x objective-c

498
geth/node/manager.go Normal file
View File

@ -0,0 +1,498 @@
package node
import (
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"sync"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/rpc"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/params"
)
// errors
var (
ErrNodeAlreadyExists = errors.New("there is a running node already, stop it before starting another one")
ErrNoRunningNode = errors.New("there is no running node")
ErrInvalidNodeManager = errors.New("node manager is not properly initialized")
ErrInvalidWhisperService = errors.New("whisper service is unavailable")
ErrInvalidLightEthereumService = errors.New("LES service is unavailable")
ErrInvalidAccountManager = errors.New("could not retrieve account manager")
ErrAccountKeyStoreMissing = errors.New("account key store is not set")
ErrInvalidRPCClient = errors.New("RPC service is unavailable")
)
// NodeManager manages Status node (which abstracts contained geth node)
type NodeManager struct {
sync.RWMutex
config *params.NodeConfig // Status node configuration
node *node.Node // reference to Geth P2P stack/node
whisperService *whisper.Whisper // reference to Whisper service
lesService *les.LightEthereum // reference to LES service
rpcClient *rpc.Client // reference to RPC client
}
// NewNodeManager makes new instance of node manager
func NewNodeManager() *NodeManager {
m := &NodeManager{}
// allow interrupting running nodes
go func() {
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt)
defer signal.Stop(sigc)
<-sigc
if m.node == nil {
return
}
log.Info("Got interrupt, shutting down...")
go m.node.Stop() // nolint: errcheck
for i := 3; i > 0; i-- {
<-sigc
if i > 1 {
log.Info(fmt.Sprintf("Already shutting down, interrupt %d more times for panic.", i-1))
}
}
panic("interrupted!")
}()
return m
}
// StartNode start Status node, fails if node is already started
func (m *NodeManager) StartNode(config *params.NodeConfig) (<-chan struct{}, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.Lock()
defer m.Unlock()
if m.node != nil {
return nil, ErrNodeAlreadyExists
}
return m.startNode(config)
}
// startNode start Status node, fails if node is already started
func (m *NodeManager) startNode(config *params.NodeConfig) (<-chan struct{}, error) {
if m.node != nil {
return nil, ErrNodeAlreadyExists
}
var err error
m.node, err = MakeNode(config)
if err != nil {
return nil, err
}
m.config = config // preserve config of successfully created node
nodeStarted := make(chan struct{})
go func() {
defer HaltOnPanic()
if err := m.node.Start(); err != nil {
m.Lock() // TODO potential deadlock (add test case to prove otherwise)
m.config = nil
m.lesService = nil
m.whisperService = nil
m.rpcClient = nil
m.node = nil
m.Unlock()
SendSignal(SignalEnvelope{
Type: EventNodeCrashed,
Event: NodeCrashEvent{
Error: fmt.Errorf("%v: %v", ErrNodeStartFailure, err).Error(),
},
})
close(nodeStarted)
return
}
// node is ready, use it
m.onNodeStarted(nodeStarted)
}()
return nodeStarted, nil
}
// onNodeStarted extra processing once we have running node
func (m *NodeManager) onNodeStarted(nodeStarted chan struct{}) {
// post-start processing
if err := m.populateStaticPeers(); err != nil {
log.Error("Static peers population", "error", err)
}
// obtain node info
var nodeInfo *p2p.NodeInfo
if server := m.node.Server(); server != nil {
if nodeInfo = server.NodeInfo(); nodeInfo != nil {
log.Info("Node is ready", "enode", nodeInfo.Enode)
}
}
// notify all subscribers that node is started
SendSignal(SignalEnvelope{
Type: EventNodeStarted,
Event: struct{}{},
})
close(nodeStarted)
// wait up until node is stopped
m.node.Wait()
SendSignal(SignalEnvelope{
Type: EventNodeStopped,
Event: struct{}{},
})
log.Info("Node is stopped", "enode", nodeInfo.Enode)
}
// IsNodeRunning confirm that node is running
func (m *NodeManager) IsNodeRunning() bool {
if m == nil {
return false
}
m.RLock()
defer m.RUnlock()
return m.node != nil
}
// StopNode stop Status node. Stopped node cannot be resumed.
func (m *NodeManager) StopNode() error {
if m == nil {
return ErrInvalidNodeManager
}
m.Lock()
defer m.Unlock()
return m.stopNode()
}
// stopNode stop Status node. Stopped node cannot be resumed.
func (m *NodeManager) stopNode() error {
if m.node == nil {
return ErrNoRunningNode
}
if err := m.node.Stop(); err != nil {
return err
}
m.config = nil
m.lesService = nil
m.whisperService = nil
m.rpcClient = nil
m.node = nil
return nil
}
// Node returns underlying Status node
func (m *NodeManager) Node() (*node.Node, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
return m.node, nil
}
// PopulateStaticPeers connects current node with our publicly available LES/SHH/Swarm cluster
func (m *NodeManager) PopulateStaticPeers() error {
if m == nil {
return ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
return m.populateStaticPeers()
}
// populateStaticPeers connects current node with our publicly available LES/SHH/Swarm cluster
func (m *NodeManager) populateStaticPeers() error {
if m.node == nil {
return ErrNoRunningNode
}
if !m.config.BootClusterConfig.Enabled {
log.Info("Boot cluster is disabled")
return nil
}
enodes, err := m.config.LoadBootClusterNodes()
if err != nil {
log.Warn("Can not load boot nodes", "error", err)
}
for _, enode := range enodes {
err := m.addPeer(enode)
if err != nil {
log.Warn("Boot node addition failed", "error", err)
continue
}
log.Info("Boot node added", "enode", enode)
}
return nil
}
// AddPeer adds new static peer node
func (m *NodeManager) AddPeer(url string) error {
if m == nil {
return ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
return m.addPeer(url)
}
// addPeer adds new static peer node
func (m *NodeManager) addPeer(url string) error {
if m == nil || m.node == nil {
return ErrNoRunningNode
}
server := m.node.Server()
if server == nil {
return ErrNoRunningNode
}
// Try to add the url as a static peer and return
parsedNode, err := discover.ParseNode(url)
if err != nil {
return err
}
server.AddPeer(parsedNode)
return nil
}
// ResetChainData remove chain data from data directory.
// Node is stopped, and new node is started, with clean data directory.
func (m *NodeManager) ResetChainData() (<-chan struct{}, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.Lock()
defer m.Unlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
prevConfig := *m.config
if err := m.stopNode(); err != nil {
return nil, err
}
chainDataDir := filepath.Join(prevConfig.DataDir, prevConfig.Name, "lightchaindata")
if _, err := os.Stat(chainDataDir); os.IsNotExist(err) {
return nil, err
}
if err := os.RemoveAll(chainDataDir); err != nil {
return nil, err
}
// send signal up to native app
SendSignal(SignalEnvelope{
Type: EventChainDataRemoved,
Event: struct{}{},
})
log.Info("chaindata removed", "dir", chainDataDir)
return m.startNode(&prevConfig)
}
// RestartNode restart running Status node, fails if node is not running
func (m *NodeManager) RestartNode() (<-chan struct{}, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.Lock()
defer m.Unlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
prevConfig := *m.config
if err := m.stopNode(); err != nil {
return nil, err
}
return m.startNode(&prevConfig)
}
// NodeConfig exposes reference to running node's configuration
func (m *NodeManager) NodeConfig() (*params.NodeConfig, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
return m.config, nil
}
// LightEthereumService exposes reference to LES service running on top of the node
func (m *NodeManager) LightEthereumService() (*les.LightEthereum, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
if m.lesService == nil {
if err := m.node.Service(&m.lesService); err != nil {
log.Warn("Cannot obtain LES service", "error", err)
return nil, ErrInvalidLightEthereumService
}
}
if m.lesService == nil {
return nil, ErrInvalidLightEthereumService
}
return m.lesService, nil
}
// WhisperService exposes reference to Whisper service running on top of the node
func (m *NodeManager) WhisperService() (*whisper.Whisper, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
if m.whisperService == nil {
if err := m.node.Service(&m.whisperService); err != nil {
log.Warn("Cannot obtain whisper service", "error", err)
return nil, ErrInvalidWhisperService
}
}
if m.whisperService == nil {
return nil, ErrInvalidWhisperService
}
return m.whisperService, nil
}
// AccountManager exposes reference to node's accounts manager
func (m *NodeManager) AccountManager() (*accounts.Manager, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
accountManager := m.node.AccountManager()
if accountManager == nil {
return nil, ErrInvalidAccountManager
}
return accountManager, nil
}
// AccountKeyStore exposes reference to accounts key store
func (m *NodeManager) AccountKeyStore() (*keystore.KeyStore, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
accountManager := m.node.AccountManager()
if accountManager == nil {
return nil, ErrInvalidAccountManager
}
backends := accountManager.Backends(keystore.KeyStoreType)
if len(backends) == 0 {
return nil, ErrAccountKeyStoreMissing
}
keyStore, ok := backends[0].(*keystore.KeyStore)
if !ok {
return nil, ErrAccountKeyStoreMissing
}
return keyStore, nil
}
// RPCClient exposes reference to RPC client connected to the running node
func (m *NodeManager) RPCClient() (*rpc.Client, error) {
if m == nil {
return nil, ErrInvalidNodeManager
}
m.RLock()
defer m.RUnlock()
if m.node == nil {
return nil, ErrNoRunningNode
}
if m.rpcClient == nil {
var err error
m.rpcClient, err = m.node.Attach()
if err != nil {
log.Error("Cannot attach RPC client to node", "error", err)
return nil, ErrInvalidRPCClient
}
}
if m.rpcClient == nil {
return nil, ErrInvalidRPCClient
}
return m.rpcClient, nil
}

312
geth/node/manager_test.go Normal file
View File

@ -0,0 +1,312 @@
package node_test
import (
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/les"
gethnode "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
"github.com/stretchr/testify/suite"
)
func TestManagerTestSuite(t *testing.T) {
suite.Run(t, new(ManagerTestSuite))
}
type ManagerTestSuite struct {
BaseTestSuite
}
func (s *ManagerTestSuite) SetupTest() {
s.NodeManager = node.NewNodeManager()
s.Require().NotNil(s.NodeManager)
s.Require().IsType(&node.NodeManager{}, s.NodeManager)
}
func (s *ManagerTestSuite) TestGettingReferencedServices() {
s.Require().NotNil(s.NodeManager)
var nilNodeManager *node.NodeManager
// test for nil values of nodeManager
var noNodeTests = []struct {
name string
initFn func() (interface{}, error)
expectedErr error
}{
{
"null manager, get NodeConfig",
func() (interface{}, error) {
return nilNodeManager.NodeConfig()
},
node.ErrInvalidNodeManager,
},
{
"null manager, get Node",
func() (interface{}, error) {
return nilNodeManager.Node()
},
node.ErrInvalidNodeManager,
},
{
"null manager, get LES",
func() (interface{}, error) {
return nilNodeManager.LightEthereumService()
},
node.ErrInvalidNodeManager,
},
{
"null manager, get Whisper",
func() (interface{}, error) {
return nilNodeManager.WhisperService()
},
node.ErrInvalidNodeManager,
},
{
"null manager, get AccountManager",
func() (interface{}, error) {
return nilNodeManager.AccountManager()
},
node.ErrInvalidNodeManager,
},
{
"null manager, get AccountKeyStore",
func() (interface{}, error) {
return nilNodeManager.AccountKeyStore()
},
node.ErrInvalidNodeManager,
},
{
"null manager, get RPC Client",
func() (interface{}, error) {
return nilNodeManager.RPCClient()
},
node.ErrInvalidNodeManager,
},
{
"non-null manager, no running node, get NodeConfig",
func() (interface{}, error) {
return s.NodeManager.NodeConfig()
},
node.ErrNoRunningNode,
},
{
"non-null manager, no running node, get Node",
func() (interface{}, error) {
return s.NodeManager.Node()
},
node.ErrNoRunningNode,
},
{
"non-null manager, no running node, get LES",
func() (interface{}, error) {
return s.NodeManager.LightEthereumService()
},
node.ErrNoRunningNode,
},
{
"non-null manager, no running node, get Whisper",
func() (interface{}, error) {
return s.NodeManager.WhisperService()
},
node.ErrNoRunningNode,
},
{
"non-null manager, no running node, get AccountManager",
func() (interface{}, error) {
return s.NodeManager.AccountManager()
},
node.ErrNoRunningNode,
},
{
"non-null manager, no running node, get AccountKeyStore",
func() (interface{}, error) {
return s.NodeManager.AccountKeyStore()
},
node.ErrNoRunningNode,
},
{
"non-null manager, no running node, get RPC Client",
func() (interface{}, error) {
return s.NodeManager.RPCClient()
},
node.ErrNoRunningNode,
},
}
for _, testCase := range noNodeTests {
s.T().Log(testCase.name)
obj, err := testCase.initFn()
s.Nil(obj)
s.EqualError(err, testCase.expectedErr.Error())
}
// test with node fully started
s.StartTestNode(params.RinkebyNetworkID)
defer s.StopTestNode()
var nodeReadyTestCases = []struct {
name string
initFn func() (interface{}, error)
expectedType interface{}
}{
{
"node is running, get NodeConfig",
func() (interface{}, error) {
return s.NodeManager.NodeConfig()
},
&params.NodeConfig{},
},
{
"node is running, get Node",
func() (interface{}, error) {
return s.NodeManager.Node()
},
&gethnode.Node{},
},
{
"node is running, get LES",
func() (interface{}, error) {
return s.NodeManager.LightEthereumService()
},
&les.LightEthereum{},
},
{
"node is running, get Whisper",
func() (interface{}, error) {
return s.NodeManager.WhisperService()
},
&whisper.Whisper{},
},
{
"node is running, get AccountManager",
func() (interface{}, error) {
return s.NodeManager.AccountManager()
},
&accounts.Manager{},
},
{
"node is running, get AccountKeyStore",
func() (interface{}, error) {
return s.NodeManager.AccountKeyStore()
},
&keystore.KeyStore{},
},
{
"node is running, get RPC Client",
func() (interface{}, error) {
return s.NodeManager.RPCClient()
},
&rpc.Client{},
},
}
for _, testCase := range nodeReadyTestCases {
obj, err := testCase.initFn()
s.T().Log(testCase.name)
s.NoError(err)
s.NotNil(obj)
s.IsType(testCase.expectedType, obj)
}
}
func (s *ManagerTestSuite) TestNodeStartStop() {
require := s.Require()
require.NotNil(s.NodeManager)
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
require.NoError(err)
// try stopping non-started node
require.False(s.NodeManager.IsNodeRunning())
err = s.NodeManager.StopNode()
if s.Error(err) {
require.IsType(node.ErrNoRunningNode, err)
}
require.False(s.NodeManager.IsNodeRunning())
nodeStarted, err := s.NodeManager.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted // wait till node is started
require.True(s.NodeManager.IsNodeRunning())
// try starting another node (w/o stopping the previously started node)
_, err = s.NodeManager.StartNode(nodeConfig)
if s.Error(err) {
require.IsType(node.ErrNodeAlreadyExists, err)
}
// now stop node, and make sure that a new node, on different network can be started
err = s.NodeManager.StopNode()
require.NoError(err)
// start new node with exactly the same config
require.False(s.NodeManager.IsNodeRunning())
nodeStarted, err = s.NodeManager.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted
require.True(s.NodeManager.IsNodeRunning())
s.StopTestNode()
}
func (s *ManagerTestSuite) TestNetworkSwitching() {
require := s.Require()
require.NotNil(s.NodeManager)
// get Ropsten config
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
require.NoError(err)
require.False(s.NodeManager.IsNodeRunning())
nodeStarted, err := s.NodeManager.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted // wait till node is started
require.True(s.NodeManager.IsNodeRunning())
s.FirstBlockHash("0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d")
// now stop node, and make sure that a new node, on different network can be started
err = s.NodeManager.StopNode()
require.NoError(err)
// start new node with completely different config
nodeConfig, err = MakeTestNodeConfig(params.RinkebyNetworkID)
require.NoError(err)
require.False(s.NodeManager.IsNodeRunning())
nodeStarted, err = s.NodeManager.StartNode(nodeConfig)
require.NoError(err)
<-nodeStarted
require.True(s.NodeManager.IsNodeRunning())
// make sure we are on another network indeed
s.FirstBlockHash("0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177")
s.StopTestNode()
}
func (s *ManagerTestSuite) TestResetChainData() {
require := s.Require()
require.NotNil(s.NodeManager)
s.StartTestNode(params.RinkebyNetworkID)
defer s.StopTestNode()
time.Sleep(2 * time.Second) // allow to sync for some time
s.True(s.NodeManager.IsNodeRunning())
require.NoError(s.NodeManager.ResetChainData())
s.True(s.NodeManager.IsNodeRunning()) // new node, with previous config should be running
// make sure we can read the first byte, and it is valid (for Rinkeby)
s.FirstBlockHash("0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177")
}

View File

@ -1,19 +1,18 @@
package geth
package node
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"reflect"
"runtime/debug"
"strconv"
"strings"
"syscall"
"time"
"github.com/ethereum/go-ethereum/common"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth"
@ -29,56 +28,35 @@ import (
"github.com/ethereum/go-ethereum/whisper/mailserver"
"github.com/ethereum/go-ethereum/whisper/notifications"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/params"
)
const (
// EventNodeStarted is triggered when underlying node is fully started
EventNodeStarted = "node.started"
// EventNodeCrashed is triggered when node crashes
EventNodeCrashed = "node.crashed"
)
// node-related errors
var (
ErrEthServiceRegistrationFailure = errors.New("failed to register the Ethereum service")
ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service")
ErrLightEthRegistrationFailure = errors.New("failed to register the LES service")
ErrNodeMakeFailure = errors.New("error creating p2p node")
ErrNodeRunFailure = errors.New("error running p2p node")
ErrNodeStartFailure = errors.New("error starting p2p node")
)
// Node represents running node (serves as a wrapper around P2P node)
type Node struct {
config *params.NodeConfig // configuration used to create Status node
geth *node.Node // reference to the running Geth node
gethConfig *node.Config // configuration used to create P2P node
started chan struct{} // channel to wait for node to start
}
// Inited checks whether status node has been properly initialized
func (n *Node) Inited() bool {
return n != nil && n.geth != nil
}
// GethStack returns reference to Geth stack
func (n *Node) GethStack() *node.Node {
return n.geth
}
// MakeNode create a geth node entity
func MakeNode(config *params.NodeConfig) *Node {
func MakeNode(config *params.NodeConfig) (*node.Node, error) {
// make sure data directory exists
if err := os.MkdirAll(filepath.Join(config.DataDir), os.ModePerm); err != nil {
Fatalf(err)
return nil, err
}
// make sure keys directory exists
if err := os.MkdirAll(filepath.Join(config.KeyStoreDir), os.ModePerm); err != nil {
Fatalf(err)
return nil, err
}
// setup logging
if _, err := params.SetupLogger(config); err != nil {
Fatalf(err)
if _, err := common.SetupLogger(config); err != nil {
return nil, err
}
// configure required node (should you need to update node's config, e.g. add bootstrap nodes, see node.Config)
@ -108,25 +86,20 @@ func MakeNode(config *params.NodeConfig) *Node {
stack, err := node.New(stackConfig)
if err != nil {
Fatalf(ErrNodeMakeFailure)
return nil, ErrNodeMakeFailure
}
// start Ethereum service
if err := activateEthService(stack, config); err != nil {
Fatalf(fmt.Errorf("%v: %v", ErrEthServiceRegistrationFailure, err))
return nil, fmt.Errorf("%v: %v", ErrEthServiceRegistrationFailure, err)
}
// start Whisper service
if err := activateShhService(stack, config); err != nil {
Fatalf(fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err))
return nil, fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
}
return &Node{
geth: stack,
gethConfig: stackConfig,
started: make(chan struct{}),
config: config,
}
return stack, nil
}
// defaultEmbeddedNodeConfig returns default stack configuration for mobile client node
@ -135,6 +108,7 @@ func defaultEmbeddedNodeConfig(config *params.NodeConfig) *node.Config {
DataDir: config.DataDir,
KeyStoreDir: config.KeyStoreDir,
UseLightweightKDF: true,
NoUSB: true,
Name: config.Name,
Version: config.Version,
P2P: p2p.Config{
@ -162,26 +136,90 @@ func defaultEmbeddedNodeConfig(config *params.NodeConfig) *node.Config {
// updateCHT changes trusted canonical hash trie root
func updateCHT(eth *les.LightEthereum, config *params.NodeConfig) {
// 0xabaa042dec1ee30e0e8323d010a9c7d9a09b848631acdf66f66e966903b67755
bc := eth.BlockChain()
// TODO: Remove this thing as this is an ugly hack.
// Once CHT sync sub-protocol is working in LES, we will rely on it, as it provides
// decentralized solution. For now, in order to avoid forcing users to long sync times
// we use central static resource
type MsgCHTRoot struct {
GenesisHash string `json:"net"`
Number uint64 `json:"number"`
Prod string `json:"prod"`
Dev string `json:"dev"`
}
loadCHTLists := func() ([]MsgCHTRoot, error) {
url := "https://gist.githubusercontent.com/farazdagi/a8d36e2818b3b2b6074d691da63a0c36/raw/?u=" + strconv.Itoa(int(time.Now().Unix()))
client := &http.Client{Timeout: 5 * time.Second}
r, err := client.Get(url)
if err != nil {
return nil, err
}
defer r.Body.Close()
var roots []MsgCHTRoot
err = json.NewDecoder(r.Body).Decode(&roots)
if err != nil {
return nil, err
}
return roots, nil
}
if roots, err := loadCHTLists(); err == nil {
for _, root := range roots {
if bc.Genesis().Hash().Hex() == root.GenesisHash {
log.Info("Loaded root", "root", root)
if root.Number == 0 {
continue
}
chtRoot := root.Prod
if config.DevMode {
chtRoot = root.Dev
}
eth.WriteTrustedCht(light.TrustedCht{
Number: root.Number,
Root: gethcommon.HexToHash(chtRoot),
})
log.Info("Loaded CHT from net", "CHT", chtRoot, "number", root.Number)
return
}
}
}
// resort to manually updated
log.Info("Loading CHT from net failed, setting manually")
if bc.Genesis().Hash() == params.MainNetGenesisHash {
eth.WriteTrustedCht(light.TrustedCht{
Number: 805,
Root: common.HexToHash("85e4286fe0a730390245c49de8476977afdae0eb5530b277f62a52b12313d50f"),
Root: gethcommon.HexToHash("85e4286fe0a730390245c49de8476977afdae0eb5530b277f62a52b12313d50f"),
})
log.Info("Added trusted CHT for mainnet")
}
if bc.Genesis().Hash() == params.RopstenNetGenesisHash {
root := "28bcafd5504326a34995efc36d3a9ba0b6a22f5832e8e58bacb646b54cb8911a"
root := "fa851b5252cc48ab55f375833b0344cc5c7cacea69be7e2a57976c38d3bb3aef"
if config.DevMode {
root = "abaa042dec1ee30e0e8323d010a9c7d9a09b848631acdf66f66e966903b67755"
root = "f2f862314509b22a773eedaaa7fa6452474eb71a3b72525a97dbf5060cbea88f"
}
eth.WriteTrustedCht(light.TrustedCht{
Number: 226,
Root: common.HexToHash(root),
Number: 239,
Root: gethcommon.HexToHash(root),
})
log.Info("Added trusted CHT for Ropsten", "CHT", root)
}
//if bc.Genesis().Hash() == params.RinkebyNetGenesisHash {
// root := "0xb100882d00a09292f15e712707649b24d019a47a509e83a00530ac542425c3bd"
// if config.DevMode {
// root = "0xb100882d00a09292f15e712707649b24d019a47a509e83a00530ac542425c3bd"
// }
// eth.WriteTrustedCht(light.TrustedCht{
// Number: 55,
// Root: gethcommon.HexToHash(root),
// })
// log.Info("Added trusted CHT for Rinkeby", "CHT", root)
//}
}
// activateEthService configures and registers the eth.Ethereum service with a given node.
@ -205,7 +243,6 @@ func activateEthService(stack *node.Node, config *params.NodeConfig) error {
ethConf.NetworkId = config.NetworkID
ethConf.DatabaseCache = config.LightEthConfig.DatabaseCache
ethConf.MaxPeers = config.MaxPeers
ethConf.DatabaseHandles = makeDatabaseHandles()
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
lightEth, err := les.New(ctx, &ethConf)
if err == nil {
@ -273,37 +310,6 @@ func makeWSHost(config *params.NodeConfig) string {
return config.WSHost
}
// makeDatabaseHandles makes sure that enough file descriptors are available to the process
// (and returns half of them for node's database to use)
func makeDatabaseHandles() int {
// current limit
var limit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
Fatalf(err)
}
// increase limit
limit.Cur = limit.Max
if limit.Cur > params.DefaultFileDescriptorLimit {
limit.Cur = params.DefaultFileDescriptorLimit
}
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
Fatalf(err)
}
// re-query limit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
Fatalf(err)
}
// cap limit
if limit.Cur > params.DefaultFileDescriptorLimit {
limit.Cur = params.DefaultFileDescriptorLimit
}
return int(limit.Cur) / 2
}
// makeBootstrapNodes returns default (hence bootstrap) list of peers
func makeBootstrapNodes() []*discover.Node {
// on desktops params.TestnetBootnodes and params.MainBootnodes,
@ -331,45 +337,3 @@ func makeBootstrapNodesV5() []*discv5.Node {
return bootstapNodes
}
// Fatalf is used to halt the execution.
// When called the function prints stack end exits.
// Failure is logged into both StdErr and StdOut.
func Fatalf(reason interface{}, args ...interface{}) {
// decide on output stream
w := io.MultiWriter(os.Stdout, os.Stderr)
outf, _ := os.Stdout.Stat()
errf, _ := os.Stderr.Stat()
if outf != nil && errf != nil && os.SameFile(outf, errf) {
w = os.Stderr
}
// find out whether error or string has been passed as a reason
r := reflect.ValueOf(reason)
if r.Kind() == reflect.String {
fmt.Fprintf(w, "Fatal Failure: "+reason.(string)+"\n", args)
} else {
fmt.Fprintf(w, "Fatal Failure: %v\n", reason.(error))
}
debug.PrintStack()
os.Exit(1)
}
// HaltOnPanic recovers from panic, logs issue, sends upward notification, and exits
func HaltOnPanic() {
if r := recover(); r != nil {
err := fmt.Errorf("%v: %v", ErrNodeRunFailure, r)
// send signal up to native app
SendSignal(SignalEnvelope{
Type: EventNodeCrashed,
Event: NodeCrashEvent{
Error: err.Error(),
},
})
Fatalf(err) // os.exit(1) is called internally
}
}

73
geth/node/signals.go Normal file
View File

@ -0,0 +1,73 @@
package node
/*
#include <stddef.h>
#include <stdbool.h>
extern bool StatusServiceSignalEvent(const char *jsonEvent);
*/
import "C"
import (
"encoding/json"
"github.com/ethereum/go-ethereum/log"
)
const (
// EventNodeStarted is triggered when underlying node is started
EventNodeStarted = "node.started"
// EventNodeReady is triggered when underlying node is fully ready
// (consider backend to be fully registered)
EventNodeReady = "node.ready"
// EventNodeStopped is triggered when underlying node is fully stopped
EventNodeStopped = "node.stopped"
// EventNodeCrashed is triggered when node crashes
EventNodeCrashed = "node.crashed"
// EventChainDataRemoved is triggered when node's chain data is removed
EventChainDataRemoved = "chaindata.removed"
)
// SignalEnvelope is a general signal sent upward from node to RN app
type SignalEnvelope struct {
Type string `json:"type"`
Event interface{} `json:"event"`
}
// NodeCrashEvent is special kind of error, used to report node crashes
type NodeCrashEvent struct {
Error string `json:"error"`
}
// NodeNotificationHandler defines a handler able to process incoming node events.
// Events are encoded as JSON strings.
type NodeNotificationHandler func(jsonEvent string)
var notificationHandler NodeNotificationHandler = TriggerDefaultNodeNotificationHandler
// SetDefaultNodeNotificationHandler sets notification handler to invoke on SendSignal
func SetDefaultNodeNotificationHandler(fn NodeNotificationHandler) {
notificationHandler = fn
}
// TriggerDefaultNodeNotificationHandler triggers default notification handler (helpful in tests)
func TriggerDefaultNodeNotificationHandler(jsonEvent string) {
log.Info("Notification received", "event", jsonEvent)
}
// SendSignal sends application signal (JSON, normally) upwards to application (via default notification handler)
func SendSignal(signal SignalEnvelope) {
data, _ := json.Marshal(&signal)
C.StatusServiceSignalEvent(C.CString(string(data)))
}
//export NotifyNode
func NotifyNode(jsonEvent *C.char) { // nolint: golint
notificationHandler(C.GoString(jsonEvent))
}
//export TriggerTestSignal
func TriggerTestSignal() { // nolint: golint
C.StatusServiceSignalEvent(C.CString(`{"answer": 42}`))
}

194
geth/node/txqueue.go Normal file
View File

@ -0,0 +1,194 @@
package node
import (
"context"
"github.com/ethereum/go-ethereum/accounts/keystore"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les/status"
"github.com/status-im/status-go/geth/common"
)
const (
// EventTransactionQueued is triggered when send transaction request is queued
EventTransactionQueued = "transaction.queued"
// EventTransactionFailed is triggered when send transaction request fails
EventTransactionFailed = "transaction.failed"
// SendTxDefaultErrorCode is sent by default, when error is not nil, but type is unknown/unexpected.
SendTxDefaultErrorCode = SendTransactionDefaultErrorCode
)
// Send transaction response codes
const (
SendTransactionNoErrorCode = "0"
SendTransactionDefaultErrorCode = "1"
SendTransactionPasswordErrorCode = "2"
SendTransactionTimeoutErrorCode = "3"
SendTransactionDiscardedErrorCode = "4"
)
var txReturnCodes = map[error]string{ // deliberately strings, in case more meaningful codes are to be returned
nil: SendTransactionNoErrorCode,
keystore.ErrDecrypt: SendTransactionPasswordErrorCode,
status.ErrQueuedTxTimedOut: SendTransactionTimeoutErrorCode,
status.ErrQueuedTxDiscarded: SendTransactionDiscardedErrorCode,
}
// TxQueueManager provides means to manage internal Status Backend (injected into LES)
type TxQueueManager struct {
nodeManager common.NodeManager
accountManager common.AccountManager
}
func NewTxQueueManager(nodeManager common.NodeManager, accountManager common.AccountManager) *TxQueueManager {
return &TxQueueManager{
nodeManager: nodeManager,
accountManager: accountManager,
}
}
// CompleteTransaction instructs backend to complete sending of a given transaction
func (m *TxQueueManager) CompleteTransaction(id, password string) (gethcommon.Hash, error) {
lightEthereum, err := m.nodeManager.LightEthereumService()
if err != nil {
return gethcommon.Hash{}, err
}
backend := lightEthereum.StatusBackend
selectedAccount, err := m.accountManager.SelectedAccount()
if err != nil {
return gethcommon.Hash{}, err
}
ctx := context.Background()
ctx = context.WithValue(ctx, status.SelectedAccountKey, selectedAccount.Hex())
return backend.CompleteQueuedTransaction(ctx, status.QueuedTxID(id), password)
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func (m *TxQueueManager) CompleteTransactions(ids, password string) map[string]common.RawCompleteTransactionResult {
results := make(map[string]common.RawCompleteTransactionResult)
parsedIDs, err := common.ParseJSONArray(ids)
if err != nil {
results["none"] = common.RawCompleteTransactionResult{
Error: err,
}
return results
}
for _, txID := range parsedIDs {
txHash, txErr := m.CompleteTransaction(txID, password)
results[txID] = common.RawCompleteTransactionResult{
Hash: txHash,
Error: txErr,
}
}
return results
}
// DiscardTransaction discards a given transaction from transaction queue
func (m *TxQueueManager) DiscardTransaction(id string) error {
lightEthereum, err := m.nodeManager.LightEthereumService()
if err != nil {
return err
}
backend := lightEthereum.StatusBackend
return backend.DiscardQueuedTransaction(status.QueuedTxID(id))
}
// DiscardTransactions discards given multiple transactions from transaction queue
func (m *TxQueueManager) DiscardTransactions(ids string) map[string]common.RawDiscardTransactionResult {
var parsedIDs []string
results := make(map[string]common.RawDiscardTransactionResult)
parsedIDs, err := common.ParseJSONArray(ids)
if err != nil {
results["none"] = common.RawDiscardTransactionResult{
Error: err,
}
return results
}
for _, txID := range parsedIDs {
err := m.DiscardTransaction(txID)
if err != nil {
results[txID] = common.RawDiscardTransactionResult{
Error: err,
}
}
}
return results
}
// SendTransactionEvent is a signal sent on a send transaction request
type SendTransactionEvent struct {
ID string `json:"id"`
Args status.SendTxArgs `json:"args"`
MessageID string `json:"message_id"`
}
// TransactionQueueHandler returns handler that processes incoming tx queue requests
func (m *TxQueueManager) TransactionQueueHandler() func(queuedTx status.QueuedTx) {
return func(queuedTx status.QueuedTx) {
SendSignal(SignalEnvelope{
Type: EventTransactionQueued,
Event: SendTransactionEvent{
ID: string(queuedTx.ID),
Args: queuedTx.Args,
MessageID: common.MessageIDFromContext(queuedTx.Context),
},
})
}
}
// ReturnSendTransactionEvent is a JSON returned whenever transaction send is returned
type ReturnSendTransactionEvent struct {
ID string `json:"id"`
Args status.SendTxArgs `json:"args"`
MessageID string `json:"message_id"`
ErrorMessage string `json:"error_message"`
ErrorCode string `json:"error_code"`
}
// TransactionReturnHandler returns handler that processes responses from internal tx manager
func (m *TxQueueManager) TransactionReturnHandler() func(queuedTx *status.QueuedTx, err error) {
return func(queuedTx *status.QueuedTx, err error) {
if err == nil {
return
}
// discard notifications with empty tx
if queuedTx == nil {
return
}
// error occurred, signal up to application
SendSignal(SignalEnvelope{
Type: EventTransactionFailed,
Event: ReturnSendTransactionEvent{
ID: string(queuedTx.ID),
Args: queuedTx.Args,
MessageID: common.MessageIDFromContext(queuedTx.Context),
ErrorMessage: err.Error(),
ErrorCode: m.sendTransactionErrorCode(err),
},
})
}
}
func (m *TxQueueManager) sendTransactionErrorCode(err error) string {
if code, ok := txReturnCodes[err]; ok {
return code
}
return SendTxDefaultErrorCode
}

24
geth/node/utils.go Normal file
View File

@ -0,0 +1,24 @@
package node
import (
"fmt"
"github.com/status-im/status-go/geth/common"
)
// HaltOnPanic recovers from panic, logs issue, sends upward notification, and exits
func HaltOnPanic() {
if r := recover(); r != nil {
err := fmt.Errorf("%v: %v", ErrNodeRunFailure, r)
// send signal up to native app
SendSignal(SignalEnvelope{
Type: EventNodeCrashed,
Event: NodeCrashEvent{
Error: err.Error(),
},
})
common.Fatalf(err) // os.exit(1) is called internally
}
}

98
geth/node/whisper_test.go Normal file
View File

@ -0,0 +1,98 @@
package node_test
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
"github.com/stretchr/testify/suite"
)
func TestWhisperTestSuite(t *testing.T) {
suite.Run(t, new(WhisperTestSuite))
}
type WhisperTestSuite struct {
BaseTestSuite
}
func (s *WhisperTestSuite) SetupTest() {
s.NodeManager = node.NewNodeManager()
s.Require().NotNil(s.NodeManager)
s.Require().IsType(&node.NodeManager{}, s.NodeManager)
}
func (s *WhisperTestSuite) TestWhisperFilterRace() {
require := s.Require()
require.NotNil(s.NodeManager)
s.StartTestNode(params.RinkebyNetworkID)
defer s.StopTestNode()
whisperService, err := s.NodeManager.WhisperService()
require.NoError(err)
require.NotNil(whisperService)
whisperAPI := whisper.NewPublicWhisperAPI(whisperService)
require.NotNil(whisperAPI)
accountManager := node.NewAccountManager(s.NodeManager)
require.NotNil(accountManager)
// account1
_, accountKey1, err := accountManager.AddressToDecryptedAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
require.NoError(err)
accountKey1Hex := common.ToHex(crypto.FromECDSAPub(&accountKey1.PrivateKey.PublicKey))
_, err = whisperService.AddKeyPair(accountKey1.PrivateKey)
require.NoError(err)
ok, err := whisperAPI.HasKeyPair(accountKey1Hex)
require.NoError(err)
require.True(ok, "identity not injected")
// account2
_, accountKey2, err := accountManager.AddressToDecryptedAccount(TestConfig.Account2.Address, TestConfig.Account2.Password)
require.NoError(err)
accountKey2Hex := common.ToHex(crypto.FromECDSAPub(&accountKey2.PrivateKey.PublicKey))
_, err = whisperService.AddKeyPair(accountKey2.PrivateKey)
require.NoError(err)
ok, err = whisperAPI.HasKeyPair(accountKey2Hex)
require.NoError(err)
require.True(ok, "identity not injected")
// race filter addition
filterAdded := make(chan struct{})
allFiltersAdded := make(chan struct{})
go func() {
counter := 10
for range filterAdded {
counter--
if counter <= 0 {
break
}
}
close(allFiltersAdded)
}()
for i := 0; i < 10; i++ {
go func() {
// nolint: errcheck
whisperAPI.Subscribe(whisper.WhisperFilterArgs{
Sig: accountKey1Hex,
Key: accountKey2Hex,
Topics: [][]byte{
{0x4e, 0x03, 0x65, 0x7a}, {0x34, 0x60, 0x7c, 0x9b}, {0x21, 0x41, 0x7d, 0xf9},
},
})
filterAdded <- struct{}{}
}()
}
<-allFiltersAdded
}

View File

@ -1,438 +0,0 @@
package geth
import (
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"sync"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/rpc"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/params"
)
// SelectedExtKey is a container for currently selected (logged in) account
type SelectedExtKey struct {
Address common.Address
AccountKey *keystore.Key
SubAccounts []accounts.Account
}
// NodeManager manages Status node (which abstracts contained geth node)
type NodeManager struct {
node *Node // reference to Status node
services *NodeServiceStack // default stack of services running on geth node
api *node.PrivateAdminAPI // exposes collection of administrative API methods
SelectedAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
}
// NodeServiceStack contains "standard" node services (which are always available)
type NodeServiceStack struct {
lightEthereum *les.LightEthereum // LES service
whisperService *whisper.Whisper // Whisper service
rpcClient *rpc.Client // RPC client
jailedRequestQueue *JailedRequestQueue // bridge via which jail notifies node of incoming requests
}
// errors
var (
ErrInvalidGethNode = errors.New("no running geth node detected")
ErrInvalidAccountManager = errors.New("could not retrieve account manager")
ErrInvalidWhisperService = errors.New("whisper service is unavailable")
ErrInvalidLightEthereumService = errors.New("can not retrieve LES service")
ErrInvalidClient = errors.New("RPC client is not properly initialized")
ErrInvalidJailedRequestQueue = errors.New("jailed request queue is not properly initialized")
ErrNodeMakeFailure = errors.New("error creating p2p node")
ErrNodeStartFailure = errors.New("error starting p2p node")
ErrNodeRunFailure = errors.New("error running p2p node")
ErrInvalidNodeAPI = errors.New("no node API connected")
ErrAccountKeyStoreMissing = errors.New("account key store is not set")
)
var (
nodeManagerInstance *NodeManager
createOnce sync.Once
)
// CreateAndRunNode creates and starts running Geth node locally (exposing given RPC port along the way)
func CreateAndRunNode(config *params.NodeConfig) error {
defer HaltOnPanic()
nodeManager := NewNodeManager(config)
if nodeManager.NodeInited() {
nodeManager.RunNode()
nodeManager.WaitNodeStarted()
return nil
}
return ErrNodeStartFailure
}
// NewNodeManager makes new instance of node manager
func NewNodeManager(config *params.NodeConfig) *NodeManager {
createOnce.Do(func() {
nodeManagerInstance = &NodeManager{
services: &NodeServiceStack{
jailedRequestQueue: NewJailedRequestsQueue(),
},
}
nodeManagerInstance.node = MakeNode(config)
})
return nodeManagerInstance
}
// NodeManagerInstance exposes node manager instance
func NodeManagerInstance() *NodeManager {
return nodeManagerInstance
}
// RunNode starts Geth node
func (m *NodeManager) RunNode() {
go func() {
defer HaltOnPanic()
m.StartNode()
if _, err := m.AccountManager(); err != nil {
log.Warn(ErrInvalidAccountManager.Error())
}
if err := m.node.geth.Service(&m.services.whisperService); err != nil {
log.Warn("cannot get whisper service", "error", err)
}
if err := m.node.geth.Service(&m.services.lightEthereum); err != nil {
log.Warn("cannot get light ethereum service", "error", err)
}
// setup handlers
if lightEthereum, err := m.LightEthereumService(); err == nil {
lightEthereum.StatusBackend.SetTransactionQueueHandler(onSendTransactionRequest)
lightEthereum.StatusBackend.SetAccountsFilterHandler(onAccountsListRequest)
lightEthereum.StatusBackend.SetTransactionReturnHandler(onSendTransactionReturn)
}
var err error
m.services.rpcClient, err = m.node.geth.Attach()
if err != nil {
log.Warn("cannot get RPC client service", "error", ErrInvalidClient)
}
// expose API
m.api = node.NewPrivateAdminAPI(m.node.geth)
m.PopulateStaticPeers()
m.onNodeStarted() // node started, notify listeners
m.node.geth.Wait()
log.Info("node stopped")
}()
}
// StartNode starts running P2P node
func (m *NodeManager) StartNode() {
if m == nil || !m.NodeInited() {
panic(ErrInvalidGethNode)
}
if err := m.node.geth.Start(); err != nil {
panic(fmt.Sprintf("%v: %v", ErrNodeStartFailure, err))
}
if server := m.node.geth.Server(); server != nil {
if nodeInfo := server.NodeInfo(); nodeInfo != nil {
log.Info(nodeInfo.Enode)
}
}
// allow interrupting running nodes
go func() {
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt)
defer signal.Stop(sigc)
<-sigc
log.Info("Got interrupt, shutting down...")
go m.node.geth.Stop() // nolint: errcheck
for i := 3; i > 0; i-- {
<-sigc
if i > 1 {
log.Info(fmt.Sprintf("Already shutting down, interrupt %d more times for panic.", i-1))
}
}
panic("interrupted!")
}()
}
// StopNode stops running P2P node
func (m *NodeManager) StopNode() error {
if m == nil || !m.NodeInited() {
return ErrInvalidGethNode
}
if err := m.node.geth.Stop(); err != nil {
return err
}
m.node.started = make(chan struct{})
return nil
}
// RestartNode restarts P2P node
func (m *NodeManager) RestartNode() error {
if m == nil || !m.NodeInited() {
return ErrInvalidGethNode
}
if err := m.StopNode(); err != nil {
return err
}
m.RunNode()
m.WaitNodeStarted()
return nil
}
// ResumeNode resumes previously stopped P2P node
func (m *NodeManager) ResumeNode() error {
if m == nil || !m.NodeInited() {
return ErrInvalidGethNode
}
m.RunNode()
m.WaitNodeStarted()
return ReSelectAccount()
}
// ResetChainData purges chain data (by removing data directory). Safe to apply on running P2P node.
func (m *NodeManager) ResetChainData() error {
if m == nil || !m.NodeInited() {
return ErrInvalidGethNode
}
if err := m.StopNode(); err != nil {
return err
}
chainDataDir := filepath.Join(m.node.gethConfig.DataDir, m.node.gethConfig.Name, "lightchaindata")
if _, err := os.Stat(chainDataDir); os.IsNotExist(err) {
return err
}
if err := os.RemoveAll(chainDataDir); err != nil {
return err
}
log.Info("chaindata removed", "dir", chainDataDir)
return m.ResumeNode()
}
// StartNodeRPCServer starts HTTP RPC server
func (m *NodeManager) StartNodeRPCServer() (bool, error) {
if m == nil || !m.NodeInited() {
return false, ErrInvalidGethNode
}
if m.api == nil {
return false, ErrInvalidNodeAPI
}
config := m.node.gethConfig
modules := strings.Join(config.HTTPModules, ",")
cors := strings.Join(config.HTTPCors, ",")
return m.api.StartRPC(&config.HTTPHost, &config.HTTPPort, &cors, &modules)
}
// StopNodeRPCServer stops HTTP RPC server attached to node
func (m *NodeManager) StopNodeRPCServer() (bool, error) {
if m == nil || !m.NodeInited() {
return false, ErrInvalidGethNode
}
if m.api == nil {
return false, ErrInvalidNodeAPI
}
return m.api.StopRPC()
}
// NodeInited checks whether manager has initialized node attached
func (m *NodeManager) NodeInited() bool {
if m == nil || !m.node.Inited() {
return false
}
return true
}
// Node returns attached node if it has been initialized
func (m *NodeManager) Node() *Node {
if !m.NodeInited() {
return nil
}
return m.node
}
// AccountManager exposes reference to accounts manager
func (m *NodeManager) AccountManager() (*accounts.Manager, error) {
if m == nil || !m.NodeInited() {
return nil, ErrInvalidGethNode
}
return m.node.geth.AccountManager(), nil
}
// AccountKeyStore exposes reference to accounts key store
func (m *NodeManager) AccountKeyStore() (*keystore.KeyStore, error) {
if m == nil || !m.NodeInited() {
return nil, ErrInvalidGethNode
}
accountManager, err := m.AccountManager()
if err != nil {
return nil, err
}
backends := accountManager.Backends(keystore.KeyStoreType)
if len(backends) == 0 {
return nil, ErrAccountKeyStoreMissing
}
keyStore, ok := backends[0].(*keystore.KeyStore)
if !ok {
return nil, ErrAccountKeyStoreMissing
}
return keyStore, nil
}
// LightEthereumService exposes LES
// nolint: dupl
func (m *NodeManager) LightEthereumService() (*les.LightEthereum, error) {
if m == nil || !m.NodeInited() {
return nil, ErrInvalidGethNode
}
if m.services.lightEthereum == nil {
return nil, ErrInvalidLightEthereumService
}
return m.services.lightEthereum, nil
}
// WhisperService exposes Whisper service
// nolint: dupl
func (m *NodeManager) WhisperService() (*whisper.Whisper, error) {
if m == nil || !m.NodeInited() {
return nil, ErrInvalidGethNode
}
if m.services.whisperService == nil {
return nil, ErrInvalidWhisperService
}
return m.services.whisperService, nil
}
// RPCClient exposes Geth's RPC client
// nolint: dupl
func (m *NodeManager) RPCClient() (*rpc.Client, error) {
if m == nil || !m.NodeInited() {
return nil, ErrInvalidGethNode
}
if m.services.rpcClient == nil {
return nil, ErrInvalidClient
}
return m.services.rpcClient, nil
}
// JailedRequestQueue exposes reference to queue of jailed requests
func (m *NodeManager) JailedRequestQueue() (*JailedRequestQueue, error) {
if m == nil || !m.NodeInited() {
return nil, ErrInvalidGethNode
}
if m.services.jailedRequestQueue == nil {
return nil, ErrInvalidJailedRequestQueue
}
return m.services.jailedRequestQueue, nil
}
// AddPeer adds new peer node
func (m *NodeManager) AddPeer(url string) (bool, error) {
if m == nil || !m.NodeInited() {
return false, ErrInvalidGethNode
}
server := m.node.geth.Server()
if server == nil {
return false, ErrInvalidGethNode
}
// Try to add the url as a static peer and return
parsedNode, err := discover.ParseNode(url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
server.AddPeer(parsedNode)
return true, nil
}
// WaitNodeStarted blocks until node is started (start channel gets notified)
func (m *NodeManager) WaitNodeStarted() {
<-m.node.started // block until node is started
}
// onNodeStarted sends upward notification letting the app know that Geth node is ready to be used
func (m *NodeManager) onNodeStarted() {
// notify local listener
m.node.started <- struct{}{}
close(m.node.started)
// send signal up to native app
SendSignal(SignalEnvelope{
Type: EventNodeStarted,
Event: struct{}{},
})
}
// PopulateStaticPeers connects current node with our publicly available LES/SHH/Swarm cluster
func (m *NodeManager) PopulateStaticPeers() {
if !m.node.config.BootClusterConfig.Enabled {
log.Info("Boot cluster is disabled")
return
}
enodes, err := m.node.config.LoadBootClusterNodes()
if err != nil {
log.Warn("Can not load boot nodes", "error", err)
}
for _, enode := range enodes {
m.AddPeer(enode) // nolint: errcheck
log.Info("Boot node added", "enode", enode)
}
}
// Hex dumps address of a given extended key as hex string
func (k *SelectedExtKey) Hex() string {
if k == nil {
return "0x0"
}
return k.Address.Hex()
}

View File

@ -1,45 +0,0 @@
package geth_test
import (
"os"
"testing"
"time"
"github.com/status-im/status-go/geth"
)
var testConfig *geth.TestConfig
func TestMain(m *testing.M) {
// load shared test configuration
var err error
testConfig, err = geth.LoadTestConfig()
if err != nil {
panic(err)
}
// run tests
retCode := m.Run()
//time.Sleep(25 * time.Second) // to give some time to propagate txs to the rest of the network
os.Exit(retCode)
}
func TestResetChainData(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
if err := geth.NodeManagerInstance().ResetChainData(); err != nil {
t.Error(err)
return
}
// allow some time to re-sync
time.Sleep(testConfig.Node.SyncSeconds * time.Second)
// now make sure that everything is intact
TestQueuedTransactions(t)
}

View File

@ -109,7 +109,7 @@ type SwarmConfig struct {
Enabled bool
}
// BootCluster holds configuration for supporting boot cluster, which is a temporary
// BootClusterConfig holds configuration for supporting boot cluster, which is a temporary
// means for mobile devices to get connected to Ethereum network (UDP-based discovery
// may not be available, so we need means to discover the network manually).
type BootClusterConfig struct {
@ -329,7 +329,7 @@ func (c *NodeConfig) LoadBootClusterNodes() ([]string, error) {
}
// parse JSON
if err := json.Unmarshal([]byte(configData), &bootnodes); err != nil {
if err := json.Unmarshal(configData, &bootnodes); err != nil {
return nil, err
}
return bootnodes, nil

View File

@ -11,8 +11,8 @@ import (
"github.com/ethereum/go-ethereum/core"
gethparams "github.com/ethereum/go-ethereum/params"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/params"
. "github.com/status-im/status-go/geth/testing"
)
var loadConfigTestCases = []struct {
@ -312,7 +312,7 @@ var loadConfigTestCases = []struct {
params.BootClusterConfigFile, nodeConfig.BootClusterConfig.ConfigFile)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("boot cluster is expected to be enabled by default")
}
@ -349,7 +349,7 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.BootClusterConfig.Enabled != false {
if nodeConfig.BootClusterConfig.Enabled {
t.Fatal("boot cluster is expected to be disabled")
}
},
@ -588,11 +588,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != true {
if !nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", true, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -614,11 +614,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != false {
if nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", false, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -641,11 +641,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != true {
if !nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", true, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -668,11 +668,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != false {
if nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", false, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -694,11 +694,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != true {
if !nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", true, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -721,11 +721,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != false {
if nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", false, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -747,11 +747,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != true {
if !nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", true, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -774,11 +774,11 @@ var loadConfigTestCases = []struct {
t.Fatalf("unexpected error: %v", err)
}
if nodeConfig.DevMode != false {
if nodeConfig.DevMode {
t.Fatalf("unexpected dev mode: expected: %v, got: %v", false, nodeConfig.DevMode)
}
if nodeConfig.BootClusterConfig.Enabled != true {
if !nodeConfig.BootClusterConfig.Enabled {
t.Fatal("expected boot cluster to be enabled")
}
@ -835,7 +835,7 @@ func TestConfigWriteRead(t *testing.T) {
t.Fatalf("cannot read configuration from disk: %v", err)
}
refConfigData := geth.LoadFromFile(refFile)
refConfigData := LoadFromFile(refFile)
refConfigData = strings.Replace(refConfigData, "$TMPDIR", nodeConfig.DataDir, -1)
refConfigData = strings.Replace(refConfigData, "$VERSION", params.Version, -1)

View File

@ -49,7 +49,7 @@ const (
DefaultFileDescriptorLimit = uint64(2048)
// DatabaseCache is memory (in MBs) allocated to internal caching (min 16MB / database forced)
DatabaseCache = 128
DatabaseCache = 16
// LogFile defines where to write logs to
LogFile = "geth.log"
@ -57,6 +57,10 @@ const (
// LogLevel defines the minimum log level to report
LogLevel = "INFO"
// LogLevelSuccinct defines the log level when only errors are reported.
// Useful when the default INFO level becomes too verbose.
LogLevelSuccinct = "ERROR"
// LogToStderr defines whether logged info should also be output to os.Stderr
LogToStderr = true
@ -89,6 +93,7 @@ const (
)
var (
RopstenNetGenesisHash = common.HexToHash("0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d") // Testnet genesis hash to enforce below configs on
MainNetGenesisHash = common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") // Mainnet genesis hash to enforce below configs on
RopstenNetGenesisHash = common.HexToHash("0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d")
RinkebyNetGenesisHash = common.HexToHash("0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177")
MainNetGenesisHash = common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

140
geth/testing/testing.go Normal file
View File

@ -0,0 +1,140 @@
package testing
import (
"bytes"
"context"
"io"
"os"
"path/filepath"
"strconv"
"strings"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/params"
"github.com/stretchr/testify/suite"
)
var (
TestConfig *common.TestConfig
// RootDir is the main application directory
RootDir string
// TestDataDir is data directory used for tests
TestDataDir string
// TestNetworkNames network ID to name mapping
TestNetworkNames = map[int]string{
params.MainNetworkID: "Mainnet",
params.RopstenNetworkID: "Ropsten",
params.RinkebyNetworkID: "Rinkeby",
}
)
func init() {
pwd, err := os.Getwd()
if err != nil {
panic(err)
}
// setup root directory
RootDir = filepath.Dir(pwd)
if strings.HasSuffix(RootDir, "geth") || strings.HasSuffix(RootDir, "cmd") { // we need to hop one more level
RootDir = filepath.Join(RootDir, "..")
}
// setup auxiliary directories
TestDataDir = filepath.Join(RootDir, ".ethereumtest")
TestConfig, err = common.LoadTestConfig()
if err != nil {
panic(err)
}
}
type BaseTestSuite struct {
suite.Suite
NodeManager common.NodeManager
}
func (s *BaseTestSuite) StartTestNode(networkID int) {
require := s.Require()
require.NotNil(s.NodeManager)
nodeConfig, err := MakeTestNodeConfig(networkID)
require.NoError(err)
keyStoreDir := filepath.Join(TestDataDir, TestNetworkNames[networkID], "keystore")
require.NoError(common.ImportTestAccount(keyStoreDir, "test-account1.pk"))
require.NoError(common.ImportTestAccount(keyStoreDir, "test-account2.pk"))
require.False(s.NodeManager.IsNodeRunning())
nodeStarted, err := s.NodeManager.StartNode(nodeConfig)
require.NoError(err)
require.NotNil(nodeStarted)
<-nodeStarted
require.True(s.NodeManager.IsNodeRunning())
}
func (s *BaseTestSuite) StopTestNode() {
require := s.Require()
require.NotNil(s.NodeManager)
require.True(s.NodeManager.IsNodeRunning())
require.NoError(s.NodeManager.StopNode())
require.False(s.NodeManager.IsNodeRunning())
}
func (s *BaseTestSuite) FirstBlockHash(expectedHash string) {
require := s.Require()
require.NotNil(s.NodeManager)
var firstBlock struct {
Hash gethcommon.Hash `json:"hash"`
}
// obtain RPC client for running node
runningNode, err := s.NodeManager.Node()
require.NoError(err)
require.NotNil(runningNode)
rpcClient, err := runningNode.Attach()
require.NoError(err)
// get first block
err = rpcClient.CallContext(context.Background(), &firstBlock, "eth_getBlockByNumber", "0x0", true)
require.NoError(err)
s.Equal(expectedHash, firstBlock.Hash.Hex())
}
func MakeTestNodeConfig(networkID int) (*params.NodeConfig, error) {
configJSON := `{
"NetworkId": ` + strconv.Itoa(networkID) + `,
"DataDir": "` + filepath.Join(TestDataDir, TestNetworkNames[networkID]) + `",
"HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `,
"WSPort": ` + strconv.Itoa(TestConfig.Node.WSPort) + `,
"LogEnabled": true,
"LogLevel": "ERROR"
}`
nodeConfig, err := params.LoadNodeConfig(configJSON)
if err != nil {
return nil, err
}
return nodeConfig, nil
}
// LoadFromFile is useful for loading test data, from testdata/filename into a variable
// nolint: errcheck
func LoadFromFile(filename string) string {
f, err := os.Open(filename)
if err != nil {
return ""
}
buf := bytes.NewBuffer(nil)
io.Copy(buf, f)
f.Close()
return string(buf.Bytes())
}

View File

@ -1,385 +0,0 @@
package geth
import (
"context"
"encoding/json"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/les/status"
"github.com/robertkrimen/otto"
)
const (
// EventTransactionQueued is triggered whan send transaction request is queued
EventTransactionQueued = "transaction.queued"
// EventTransactionFailed is triggered when send transaction request fails
EventTransactionFailed = "transaction.failed"
// SendTransactionRequest is triggered on send transaction request
SendTransactionRequest = "eth_sendTransaction"
// MessageIDKey is a key for message ID
// This ID is required to track from which chat a given send transaction request is coming.
MessageIDKey = contextKey("message_id")
)
type contextKey string // in order to make sure that our context key does not collide with keys from other packages
// Send transaction response codes
const (
SendTransactionNoErrorCode = "0"
SendTransactionDefaultErrorCode = "1"
SendTransactionPasswordErrorCode = "2"
SendTransactionTimeoutErrorCode = "3"
SendTransactionDiscardedErrorCode = "4"
)
func onSendTransactionRequest(queuedTx status.QueuedTx) {
SendSignal(SignalEnvelope{
Type: EventTransactionQueued,
Event: SendTransactionEvent{
ID: string(queuedTx.ID),
Args: queuedTx.Args,
MessageID: messageIDFromContext(queuedTx.Context),
},
})
}
func onSendTransactionReturn(queuedTx *status.QueuedTx, err error) {
if err == nil {
return
}
// discard notifications with empty tx
if queuedTx == nil {
return
}
// error occurred, signal up to application
SendSignal(SignalEnvelope{
Type: EventTransactionFailed,
Event: ReturnSendTransactionEvent{
ID: string(queuedTx.ID),
Args: queuedTx.Args,
MessageID: messageIDFromContext(queuedTx.Context),
ErrorMessage: err.Error(),
ErrorCode: sendTransactionErrorCode(err),
},
})
}
func sendTransactionErrorCode(err error) string {
if err == nil {
return SendTransactionNoErrorCode
}
switch err {
case keystore.ErrDecrypt:
return SendTransactionPasswordErrorCode
case status.ErrQueuedTxTimedOut:
return SendTransactionTimeoutErrorCode
case status.ErrQueuedTxDiscarded:
return SendTransactionDiscardedErrorCode
default:
return SendTransactionDefaultErrorCode
}
}
// CompleteTransaction instructs backend to complete sending of a given transaction
func CompleteTransaction(id, password string) (common.Hash, error) {
lightEthereum, err := NodeManagerInstance().LightEthereumService()
if err != nil {
return common.Hash{}, err
}
backend := lightEthereum.StatusBackend
ctx := context.Background()
ctx = context.WithValue(ctx, status.SelectedAccountKey, NodeManagerInstance().SelectedAccount.Hex())
return backend.CompleteQueuedTransaction(ctx, status.QueuedTxID(id), password)
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func CompleteTransactions(ids, password string) map[string]RawCompleteTransactionResult {
results := make(map[string]RawCompleteTransactionResult)
parsedIDs, err := parseJSONArray(ids)
if err != nil {
results["none"] = RawCompleteTransactionResult{
Error: err,
}
return results
}
for _, txID := range parsedIDs {
txHash, txErr := CompleteTransaction(txID, password)
results[txID] = RawCompleteTransactionResult{
Hash: txHash,
Error: txErr,
}
}
return results
}
// DiscardTransaction discards a given transaction from transaction queue
func DiscardTransaction(id string) error {
lightEthereum, err := NodeManagerInstance().LightEthereumService()
if err != nil {
return err
}
backend := lightEthereum.StatusBackend
return backend.DiscardQueuedTransaction(status.QueuedTxID(id))
}
// DiscardTransactions discards given multiple transactions from transaction queue
func DiscardTransactions(ids string) map[string]RawDiscardTransactionResult {
var parsedIDs []string
results := make(map[string]RawDiscardTransactionResult)
parsedIDs, err := parseJSONArray(ids)
if err != nil {
results["none"] = RawDiscardTransactionResult{
Error: err,
}
return results
}
for _, txID := range parsedIDs {
err := DiscardTransaction(txID)
if err != nil {
results[txID] = RawDiscardTransactionResult{
Error: err,
}
}
}
return results
}
func messageIDFromContext(ctx context.Context) string {
if ctx == nil {
return ""
}
if messageID, ok := ctx.Value(MessageIDKey).(string); ok {
return messageID
}
return ""
}
// JailedRequestQueue is used for allowing request pre and post processing.
// Such processing may include validation, injection of params (like message ID) etc
type JailedRequestQueue struct{}
// NewJailedRequestsQueue returns new instance of request queue
func NewJailedRequestsQueue() *JailedRequestQueue {
return &JailedRequestQueue{}
}
// PreProcessRequest pre-processes a given RPC call to a given Otto VM
func (q *JailedRequestQueue) PreProcessRequest(vm *otto.Otto, req RPCCall) (string, error) {
messageID := currentMessageID(vm.Context())
return messageID, nil
}
// PostProcessRequest post-processes a given RPC call to a given Otto VM
func (q *JailedRequestQueue) PostProcessRequest(vm *otto.Otto, req RPCCall, messageID string) {
if len(messageID) > 0 {
vm.Call("addContext", nil, messageID, MessageIDKey, messageID) // nolint: errcheck
}
// set extra markers for queued transaction requests
if req.Method == SendTransactionRequest {
vm.Call("addContext", nil, messageID, SendTransactionRequest, true) // nolint: errcheck
}
}
// ProcessSendTransactionRequest processes send transaction request.
// Both pre and post processing happens within this function. Pre-processing
// happens before transaction is send to backend, and post processing occurs
// when backend notifies that transaction sending is complete (either successfully
// or with error)
func (q *JailedRequestQueue) ProcessSendTransactionRequest(vm *otto.Otto, req RPCCall) (common.Hash, error) {
// obtain status backend from LES service
lightEthereum, err := NodeManagerInstance().LightEthereumService()
if err != nil {
return common.Hash{}, err
}
backend := lightEthereum.StatusBackend
messageID, err := q.PreProcessRequest(vm, req)
if err != nil {
return common.Hash{}, err
}
// onSendTransactionRequest() will use context to obtain and release ticket
ctx := context.Background()
ctx = context.WithValue(ctx, MessageIDKey, messageID)
// this call blocks, up until Complete Transaction is called
txHash, err := backend.SendTransaction(ctx, sendTxArgsFromRPCCall(req))
if err != nil {
return common.Hash{}, err
}
// invoke post processing
q.PostProcessRequest(vm, req, messageID)
return txHash, nil
}
// currentMessageID looks for `status.message_id` variable in current JS context
func currentMessageID(ctx otto.Context) string {
if statusObj, ok := ctx.Symbols["status"]; ok {
messageID, err := statusObj.Object().Get("message_id")
if err != nil {
return ""
}
if messageID, err := messageID.ToString(); err == nil {
return messageID
}
}
return ""
}
func sendTxArgsFromRPCCall(req RPCCall) status.SendTxArgs {
if req.Method != SendTransactionRequest { // no need to persist extra state for other requests
return status.SendTxArgs{}
}
return status.SendTxArgs{
From: req.parseFromAddress(),
To: req.parseToAddress(),
Value: req.parseValue(),
Data: req.parseData(),
Gas: req.parseGas(),
GasPrice: req.parseGasPrice(),
}
}
func (r RPCCall) parseFromAddress() common.Address {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return common.HexToAddress("0x")
}
from, ok := params["from"].(string)
if !ok {
from = "0x"
}
return common.HexToAddress(from)
}
func (r RPCCall) parseToAddress() *common.Address {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
}
to, ok := params["to"].(string)
if !ok {
return nil
}
address := common.HexToAddress(to)
return &address
}
func (r RPCCall) parseData() hexutil.Bytes {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return hexutil.Bytes("0x")
}
data, ok := params["data"].(string)
if !ok {
data = "0x"
}
byteCode, err := hexutil.Decode(data)
if err != nil {
byteCode = hexutil.Bytes(data)
}
return byteCode
}
// nolint: dupl
func (r RPCCall) parseValue() *hexutil.Big {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
//return (*hexutil.Big)(big.NewInt("0x0"))
}
inputValue, ok := params["value"].(string)
if !ok {
return nil
}
parsedValue, err := hexutil.DecodeBig(inputValue)
if err != nil {
return nil
}
return (*hexutil.Big)(parsedValue)
}
// nolint: dupl
func (r RPCCall) parseGas() *hexutil.Big {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
}
inputValue, ok := params["gas"].(string)
if !ok {
return nil
}
parsedValue, err := hexutil.DecodeBig(inputValue)
if err != nil {
return nil
}
return (*hexutil.Big)(parsedValue)
}
// nolint: dupl
func (r RPCCall) parseGasPrice() *hexutil.Big {
params, ok := r.Params[0].(map[string]interface{})
if !ok {
return nil
}
inputValue, ok := params["gasPrice"].(string)
if !ok {
return nil
}
parsedValue, err := hexutil.DecodeBig(inputValue)
if err != nil {
return nil
}
return (*hexutil.Big)(parsedValue)
}
func parseJSONArray(items string) ([]string, error) {
var parsedItems []string
err := json.Unmarshal([]byte(items), &parsedItems)
if err != nil {
return nil, err
}
return parsedItems, nil
}

View File

@ -1,887 +0,0 @@
package geth_test
import (
"encoding/json"
"math/big"
"reflect"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/les/status"
"github.com/status-im/status-go/geth"
"github.com/status-im/status-go/geth/params"
)
func TestQueuedContracts(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return
}
backend := lightEthereum.StatusBackend
// create an account
sampleAddress, _, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
if err = geth.Logout(); err != nil {
t.Fatal(err)
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
geth.PanicAfter(5*time.Second, completeQueuedTransaction, "TestQueuedContracts")
// replace transaction notification handler
var txHash = common.Hash{}
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint :dupl
var envelope geth.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
// the first call will fail (we are not logged in, but trying to complete tx)
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != status.ErrInvalidCompleteTxSender {
t.Errorf("expected error on queued transaction[%v] not thrown: expected %v, got %v", event["id"], status.ErrInvalidCompleteTxSender, err)
return
}
// the second call will also fail (we are logged in as different user)
if err := geth.SelectAccount(sampleAddress, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", sampleAddress)
return
}
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != status.ErrInvalidCompleteTxSender {
t.Errorf("expected error on queued transaction[%v] not thrown: expected %v, got %v", event["id"], status.ErrInvalidCompleteTxSender, err)
return
}
// the third call will work as expected (as we are logged in with correct credentials)
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != nil {
t.Errorf("cannot complete queued transaction[%v]: %v", event["id"], err)
return
}
t.Logf("contract transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex())
close(completeQueuedTransaction)
}
})
// this call blocks, up until Complete Transaction is called
byteCode, err := hexutil.Decode(`0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029`)
if err != nil {
t.Error(err)
return
}
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: nil, // marker, contract creation is expected
//Value: (*hexutil.Big)(new(big.Int).Mul(big.NewInt(1), common.Ether)),
Gas: (*hexutil.Big)(big.NewInt(params.DefaultGas)),
Data: byteCode,
})
if err != nil {
t.Errorf("Test failed: cannot send transaction: %v", err)
}
if txHashCheck.Hex() != txHash.Hex() {
t.Errorf("Transaction hash returned from SendTransaction is invalid: expected %s, got %s", txHashCheck.Hex(), txHash.Hex())
return
}
<-completeQueuedTransaction
if reflect.DeepEqual(txHashCheck, common.Hash{}) {
t.Error("Test failed: transaction was never queued or completed")
return
}
if backend.TransactionQueue().Count() != 0 {
t.Error("tx queue must be empty at this point")
return
}
}
func TestQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return
}
backend := lightEthereum.StatusBackend
// create an account
sampleAddress, _, _, err := geth.CreateAccount(testConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
if err = geth.Logout(); err != nil {
t.Fatal(err)
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
geth.PanicAfter(5*time.Second, completeQueuedTransaction, "TestQueuedTransactions")
// replace transaction notification handler
var txHash = common.Hash{}
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint: dupl
var envelope geth.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
// the first call will fail (we are not logged in, but trying to complete tx)
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != status.ErrInvalidCompleteTxSender {
t.Errorf("expected error on queued transaction[%v] not thrown: expected %v, got %v", event["id"], status.ErrInvalidCompleteTxSender, err)
return
}
// the second call will also fail (we are logged in as different user)
if err := geth.SelectAccount(sampleAddress, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", sampleAddress)
return
}
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != status.ErrInvalidCompleteTxSender {
t.Errorf("expected error on queued transaction[%v] not thrown: expected %v, got %v", event["id"], status.ErrInvalidCompleteTxSender, err)
return
}
// the third call will work as expected (as we are logged in with correct credentials)
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != nil {
t.Errorf("cannot complete queued transaction[%v]: %v", event["id"], err)
return
}
t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex())
close(completeQueuedTransaction)
}
})
// this call blocks, up until Complete Transaction is called
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("Test failed: cannot send transaction: %v", err)
}
if txHashCheck.Hex() != txHash.Hex() {
t.Errorf("Transaction hash returned from SendTransaction is invalid: expected %s, got %s", txHashCheck.Hex(), txHash.Hex())
return
}
<-completeQueuedTransaction
if reflect.DeepEqual(txHashCheck, common.Hash{}) {
t.Error("Test failed: transaction was never queued or completed")
return
}
if backend.TransactionQueue().Count() != 0 {
t.Error("tx queue must be empty at this point")
return
}
}
func TestDoubleCompleteQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return
}
backend := lightEthereum.StatusBackend
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
geth.PanicAfter(20*time.Second, completeQueuedTransaction, "TestQueuedTransactions")
// replace transaction notification handler
var txID string
txFailedEventCalled := false
txHash := common.Hash{}
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope geth.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be failed and completed on the second call): {id: %s}\n", txID)
// try with wrong password
// make sure that tx is NOT removed from the queue (by re-trying with the correct password)
if _, err = geth.CompleteTransaction(txID, testConfig.Account1.Password+"wrong"); err != keystore.ErrDecrypt {
t.Errorf("expects wrong password error, but call succeeded (or got another error: %v)", err)
return
}
if txCount := backend.TransactionQueue().Count(); txCount != 1 {
t.Errorf("txqueue cannot be empty, as tx has failed: expected = 1, got = %d", txCount)
return
}
// now try to complete transaction, but with the correct password
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != nil {
t.Errorf("cannot complete queued transaction[%v]: %v", event["id"], err)
return
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
if txCount := backend.TransactionQueue().Count(); txCount != 0 {
t.Errorf("txqueue must be empty, as tx has completed: expected = 0, got = %d", txCount)
return
}
t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex())
completeQueuedTransaction <- struct{}{} // so that timeout is aborted
}
if envelope.Type == geth.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := "could not decrypt key with given passphrase"
if receivedErrMessage != expectedErrMessage {
t.Errorf("unexpected error message received: got %v", receivedErrMessage)
return
}
receivedErrCode := event["error_code"].(string)
if receivedErrCode != geth.SendTransactionPasswordErrorCode {
t.Errorf("unexpected error code received: got %v", receivedErrCode)
return
}
txFailedEventCalled = true
}
})
// this call blocks, and should return on *second* attempt to CompleteTransaction (w/ the correct password)
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("cannot send transaction: %v", err)
return
}
if txHashCheck.Hex() != txHash.Hex() {
t.Errorf("tx hash returned from SendTransaction is invalid: expected %s, got %s", txHashCheck.Hex(), txHash.Hex())
return
}
if reflect.DeepEqual(txHashCheck, common.Hash{}) {
t.Error("transaction was never queued or completed")
return
}
if backend.TransactionQueue().Count() != 0 {
t.Error("tx queue must be empty at this point")
return
}
if !txFailedEventCalled {
t.Error("expected tx failure signal is not received")
return
}
}
func TestDiscardQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return
}
backend := lightEthereum.StatusBackend
// reset queue
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
geth.PanicAfter(20*time.Second, completeQueuedTransaction, "TestDiscardQueuedTransactions")
// replace transaction notification handler
var txID string
txFailedEventCalled := false
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope geth.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
if !backend.TransactionQueue().Has(status.QueuedTxID(txID)) {
t.Errorf("txqueue should still have test tx: %s", txID)
return
}
// discard
err := geth.DiscardTransaction(txID)
if err != nil {
t.Errorf("cannot discard tx: %v", err)
return
}
// try completing discarded transaction
_, err = geth.CompleteTransaction(txID, testConfig.Account1.Password)
if err.Error() != "transaction hash not found" {
t.Error("expects tx not found, but call to CompleteTransaction succeeded")
return
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
if backend.TransactionQueue().Has(status.QueuedTxID(txID)) {
t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID)
return
}
completeQueuedTransaction <- struct{}{} // so that timeout is aborted
}
if envelope.Type == geth.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := status.ErrQueuedTxDiscarded.Error()
if receivedErrMessage != expectedErrMessage {
t.Errorf("unexpected error message received: got %v", receivedErrMessage)
return
}
receivedErrCode := event["error_code"].(string)
if receivedErrCode != geth.SendTransactionDiscardedErrorCode {
t.Errorf("unexpected error code received: got %v", receivedErrCode)
return
}
txFailedEventCalled = true
}
})
// this call blocks, and should return when DiscardQueuedTransaction() is called
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != status.ErrQueuedTxDiscarded {
t.Errorf("expected error not thrown: %v", err)
return
}
if !reflect.DeepEqual(txHashCheck, common.Hash{}) {
t.Error("transaction returned hash, while it shouldn't")
return
}
if backend.TransactionQueue().Count() != 0 {
t.Error("tx queue must be empty at this point")
return
}
if !txFailedEventCalled {
t.Error("expected tx failure signal is not received")
return
}
}
func TestCompleteMultipleQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return
}
backend := lightEthereum.StatusBackend
// reset queue
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
// make sure you panic if transaction complete doesn't return
testTxCount := 3
txIDs := make(chan string, testTxCount)
allTestTxCompleted := make(chan struct{}, 1)
// replace transaction notification handler
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope geth.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be completed in a single call, once aggregated): {id: %s}\n", txID)
txIDs <- txID
}
})
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("unexpected error thrown: %v", err)
return
}
if reflect.DeepEqual(txHashCheck, common.Hash{}) {
t.Error("transaction returned empty hash")
return
}
}
// wait for transactions, and complete them in a single call
completeTxs := func(txIDStrings string) {
var parsedIDs []string
if err := json.Unmarshal([]byte(txIDStrings), &parsedIDs); err != nil {
t.Error(err)
return
}
parsedIDs = append(parsedIDs, "invalid-tx-id")
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
// complete
results := geth.CompleteTransactions(string(updatedTxIDStrings), testConfig.Account1.Password)
if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error.Error() != "transaction hash not found" {
t.Errorf("cannot complete txs: %v", results)
return
}
for txID, txResult := range results {
if txResult.Error != nil && txID != "invalid-tx-id" {
t.Errorf("invalid error for %s", txID)
return
}
if txResult.Hash.Hex() == "0x0000000000000000000000000000000000000000000000000000000000000000" && txID != "invalid-tx-id" {
t.Errorf("invalid hash (expected non empty hash): %s", txID)
return
}
if txResult.Hash.Hex() != "0x0000000000000000000000000000000000000000000000000000000000000000" {
t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txResult.Hash.Hex())
}
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
for _, txID := range parsedIDs {
if backend.TransactionQueue().Has(status.QueuedTxID(txID)) {
t.Errorf("txqueue should not have test tx at this point (it should be completed): %s", txID)
return
}
}
}
go func() {
var txIDStrings []string
for i := 0; i < testTxCount; i++ {
txIDStrings = append(txIDStrings, <-txIDs)
}
txIDJSON, _ := json.Marshal(txIDStrings)
completeTxs(string(txIDJSON))
allTestTxCompleted <- struct{}{}
}()
// send multiple transactions
for i := 0; i < testTxCount; i++ {
go sendTx()
}
select {
case <-allTestTxCompleted:
// pass
case <-time.After(20 * time.Second):
t.Error("test timed out")
return
}
if backend.TransactionQueue().Count() != 0 {
t.Error("tx queue must be empty at this point")
return
}
}
func TestDiscardMultipleQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return
}
backend := lightEthereum.StatusBackend
// reset queue
backend.TransactionQueue().Reset()
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
// make sure you panic if transaction complete doesn't return
testTxCount := 3
txIDs := make(chan string, testTxCount)
allTestTxDiscarded := make(chan struct{}, 1)
// replace transaction notification handler
txFailedEventCallCount := 0
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope geth.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
if !backend.TransactionQueue().Has(status.QueuedTxID(txID)) {
t.Errorf("txqueue should still have test tx: %s", txID)
return
}
txIDs <- txID
}
if envelope.Type == geth.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := status.ErrQueuedTxDiscarded.Error()
if receivedErrMessage != expectedErrMessage {
t.Errorf("unexpected error message received: got %v", receivedErrMessage)
return
}
receivedErrCode := event["error_code"].(string)
if receivedErrCode != geth.SendTransactionDiscardedErrorCode {
t.Errorf("unexpected error code received: got %v", receivedErrCode)
return
}
txFailedEventCallCount++
if txFailedEventCallCount == testTxCount {
allTestTxDiscarded <- struct{}{}
}
}
})
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: geth.FromAddress(testConfig.Account1.Address),
To: geth.ToAddress(testConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != status.ErrQueuedTxDiscarded {
t.Errorf("expected error not thrown: %v", err)
return
}
if !reflect.DeepEqual(txHashCheck, common.Hash{}) {
t.Error("transaction returned hash, while it shouldn't")
return
}
}
// wait for transactions, and discard immediately
discardTxs := func(txIDStrings string) {
var parsedIDs []string
if err := json.Unmarshal([]byte(txIDStrings), &parsedIDs); err != nil {
t.Error(err)
return
}
parsedIDs = append(parsedIDs, "invalid-tx-id")
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
// discard
discardResults := geth.DiscardTransactions(string(updatedTxIDStrings))
if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error.Error() != "transaction hash not found" {
t.Errorf("cannot discard txs: %v", discardResults)
return
}
// try completing discarded transaction
completeResults := geth.CompleteTransactions(string(updatedTxIDStrings), testConfig.Account1.Password)
if len(completeResults) != (testTxCount + 1) {
t.Error("unexpected number of errors (call to CompleteTransaction should not succeed)")
}
for _, txResult := range completeResults {
if txResult.Error.Error() != "transaction hash not found" {
t.Errorf("invalid error for %s", txResult.Hash.Hex())
return
}
if txResult.Hash.Hex() != "0x0000000000000000000000000000000000000000000000000000000000000000" {
t.Errorf("invalid hash (expected zero): %s", txResult.Hash.Hex())
return
}
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
for _, txID := range parsedIDs {
if backend.TransactionQueue().Has(status.QueuedTxID(txID)) {
t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID)
return
}
}
}
go func() {
var txIDStrings []string
for i := 0; i < testTxCount; i++ {
txIDStrings = append(txIDStrings, <-txIDs)
}
txIDJSON, _ := json.Marshal(txIDStrings)
discardTxs(string(txIDJSON))
}()
// send multiple transactions
for i := 0; i < testTxCount; i++ {
go sendTx()
}
select {
case <-allTestTxDiscarded:
// pass
case <-time.After(20 * time.Second):
t.Error("test timed out")
return
}
if backend.TransactionQueue().Count() != 0 {
t.Error("tx queue must be empty at this point")
return
}
}
func TestNonExistentQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
geth.PanicAfter(20*time.Second, completeQueuedTransaction, "TestQueuedTransactions")
// replace transaction notification handler
geth.SetDefaultNodeNotificationHandler(func(string) {})
// try completing non-existing transaction
if _, err = geth.CompleteTransaction("some-bad-transaction-id", testConfig.Account1.Password); err == nil {
t.Error("error expected and not received")
return
}
if err != status.ErrQueuedTxIDNotFound {
t.Errorf("unexpected error received: expected '%s', got: '%s'", status.ErrQueuedTxIDNotFound.Error(), err.Error())
return
}
}
func TestEvictionOfQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
// obtain reference to status backend
lightEthereum, err := geth.NodeManagerInstance().LightEthereumService()
if err != nil {
t.Errorf("Test failed: LES service is not running: %v", err)
return
}
backend := lightEthereum.StatusBackend
// log into account from which transactions will be sent
if err := geth.SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
geth.PanicAfter(20*time.Second, completeQueuedTransaction, "TestQueuedTransactions")
// replace transaction notification handler
var txHash = common.Hash{}
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope geth.SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == geth.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
t.Logf("Transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
// next call is the very same one, but with the correct password
if txHash, err = geth.CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != nil {
t.Errorf("cannot complete queued transaction[%v]: %v", event["id"], err)
return
}
t.Logf("Transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex())
completeQueuedTransaction <- struct{}{} // so that timeout is aborted
}
})
txQueue := backend.TransactionQueue()
var i = 0
txIDs := [status.DefaultTxQueueCap + 5 + 10]status.QueuedTxID{}
backend.SetTransactionQueueHandler(func(queuedTx status.QueuedTx) {
t.Logf("%d. Transaction queued (queue size: %d): {id: %v}\n", i, txQueue.Count(), queuedTx.ID)
txIDs[i] = queuedTx.ID
i++
})
if txQueue.Count() != 0 {
t.Errorf("transaction count should be zero: %d", txQueue.Count())
return
}
for i := 0; i < 10; i++ {
go backend.SendTransaction(nil, status.SendTxArgs{}) // nolint: errcheck
}
time.Sleep(3 * time.Second)
t.Logf("Number of transactions queued: %d. Queue size (shouldn't be more than %d): %d", i, status.DefaultTxQueueCap, txQueue.Count())
if txQueue.Count() != 10 {
t.Errorf("transaction count should be 10: got %d", txQueue.Count())
return
}
for i := 0; i < status.DefaultTxQueueCap+5; i++ { // stress test by hitting with lots of goroutines
go backend.SendTransaction(nil, status.SendTxArgs{}) // nolint: errcheck
}
time.Sleep(5 * time.Second)
if txQueue.Count() > status.DefaultTxQueueCap {
t.Errorf("transaction count should be %d (or %d): got %d", status.DefaultTxQueueCap, status.DefaultTxQueueCap-1, txQueue.Count())
return
}
for _, txID := range txIDs {
txQueue.Remove(txID)
}
if txQueue.Count() != 0 {
t.Errorf("transaction count should be zero: %d", txQueue.Count())
return
}
}

View File

@ -1,109 +0,0 @@
package geth
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les/status"
)
// SignalEnvelope is a general signal sent upward from node to RN app
type SignalEnvelope struct {
Type string `json:"type"`
Event interface{} `json:"event"`
}
// AccountInfo represents account's info
type AccountInfo struct {
Address string `json:"address"`
PubKey string `json:"pubkey"`
Mnemonic string `json:"mnemonic"`
Error string `json:"error"`
}
// JSONError is wrapper around errors, that are sent upwards
type JSONError struct {
Error string `json:"error"`
}
// NodeCrashEvent is special kind of error, used to report node crashes
type NodeCrashEvent struct {
Error string `json:"error"`
}
// AddPeerResult is a JSON returned as a response to AddPeer() request
type AddPeerResult struct {
Success bool `json:"success"`
Error string `json:"error"`
}
// WhisperMessageEvent is a signal sent on incoming Whisper message
type WhisperMessageEvent struct {
Payload string `json:"payload"`
To string `json:"to"`
From string `json:"from"`
Sent int64 `json:"sent"`
TTL int64 `json:"ttl"`
Hash string `json:"hash"`
}
// SendTransactionEvent is a signal sent on a send transaction request
type SendTransactionEvent struct {
ID string `json:"id"`
Args status.SendTxArgs `json:"args"`
MessageID string `json:"message_id"`
}
// ReturnSendTransactionEvent is a JSON returned whenever transaction send is returned
type ReturnSendTransactionEvent struct {
ID string `json:"id"`
Args status.SendTxArgs `json:"args"`
MessageID string `json:"message_id"`
ErrorMessage string `json:"error_message"`
ErrorCode string `json:"error_code"`
}
// CompleteTransactionResult is a JSON returned from transaction complete function (used in exposed method)
type CompleteTransactionResult struct {
ID string `json:"id"`
Hash string `json:"hash"`
Error string `json:"error"`
}
// RawCompleteTransactionResult is a JSON returned from transaction complete function (used internally)
type RawCompleteTransactionResult struct {
Hash common.Hash
Error error
}
// CompleteTransactionsResult is list of results from CompleteTransactions() (used in exposed method)
type CompleteTransactionsResult struct {
Results map[string]CompleteTransactionResult `json:"results"`
}
// RawDiscardTransactionResult is list of results from CompleteTransactions() (used internally)
type RawDiscardTransactionResult struct {
Error error
}
// DiscardTransactionResult is a JSON returned from transaction discard function
type DiscardTransactionResult struct {
ID string `json:"id"`
Error string `json:"error"`
}
// DiscardTransactionsResult is a list of results from DiscardTransactions()
type DiscardTransactionsResult struct {
Results map[string]DiscardTransactionResult `json:"results"`
}
// LocalStorageSetEvent is a signal sent whenever local storage Set method is called
type LocalStorageSetEvent struct {
ChatID string `json:"chat_id"`
Data string `json:"data"`
}
// RPCCall represents RPC call parameters
type RPCCall struct {
ID int64
Method string
Params []interface{}
}

View File

@ -1,320 +0,0 @@
package geth
/*
#include <stddef.h>
#include <stdbool.h>
extern bool StatusServiceSignalEvent(const char *jsonEvent);
*/
import "C"
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/static"
)
var (
muPrepareTestNode sync.Mutex
// RootDir is the main application directory
RootDir string
// TestDataDir is data directory used for tests
TestDataDir string
)
func init() {
pwd, err := os.Getwd()
if err != nil {
panic(err)
}
// setup root directory
RootDir = filepath.Dir(pwd)
if strings.HasSuffix(RootDir, "geth") || strings.HasSuffix(RootDir, "cmd") { // we need to hop one more level
RootDir = filepath.Join(RootDir, "..")
}
// setup auxiliary directories
TestDataDir = filepath.Join(RootDir, ".ethereumtest")
}
// NodeNotificationHandler defines a handler able to process incoming node events.
// Events are encoded as JSON strings.
type NodeNotificationHandler func(jsonEvent string)
var notificationHandler NodeNotificationHandler = TriggerDefaultNodeNotificationHandler
// SetDefaultNodeNotificationHandler sets notification handler to invoke on SendSignal
func SetDefaultNodeNotificationHandler(fn NodeNotificationHandler) {
notificationHandler = fn
}
// TriggerDefaultNodeNotificationHandler triggers default notification handler (helpful in tests)
func TriggerDefaultNodeNotificationHandler(jsonEvent string) {
log.Info("notification received (default notification handler)", "event", jsonEvent)
}
// SendSignal sends application signal (JSON, normally) upwards to application (via default notification handler)
func SendSignal(signal SignalEnvelope) {
data, _ := json.Marshal(&signal)
C.StatusServiceSignalEvent(C.CString(string(data)))
}
//export NotifyNode
func NotifyNode(jsonEvent *C.char) { // nolint: golint
notificationHandler(C.GoString(jsonEvent))
}
//export TriggerTestSignal
func TriggerTestSignal() { // nolint: golint
C.StatusServiceSignalEvent(C.CString(`{"answer": 42}`))
}
// TestConfig contains shared (among different test packages) parameters
type TestConfig struct {
Node struct {
SyncSeconds time.Duration
HTTPPort int
WSPort int
}
Account1 struct {
Address string
Password string
}
Account2 struct {
Address string
Password string
}
}
// LoadTestConfig loads test configuration values from disk
func LoadTestConfig() (*TestConfig, error) {
var testConfig TestConfig
configData := string(static.MustAsset("config/test-data.json"))
if err := json.Unmarshal([]byte(configData), &testConfig); err != nil {
return nil, err
}
return &testConfig, nil
}
// LoadFromFile is useful for loading test data, from testdata/filename into a variable
// nolint: errcheck
func LoadFromFile(filename string) string {
f, err := os.Open(filename)
if err != nil {
return ""
}
buf := bytes.NewBuffer(nil)
io.Copy(buf, f)
f.Close()
return string(buf.Bytes())
}
// PrepareTestNode initializes node manager and start a test node (only once!)
func PrepareTestNode() (err error) {
muPrepareTestNode.Lock()
defer muPrepareTestNode.Unlock()
manager := NodeManagerInstance()
if manager.NodeInited() {
return nil
}
defer HaltOnPanic()
testConfig, err := LoadTestConfig()
if err != nil {
return err
}
syncRequired := false
if _, err = os.Stat(TestDataDir); os.IsNotExist(err) {
syncRequired = true
}
// prepare node directory
if err = os.MkdirAll(filepath.Join(TestDataDir, "keystore"), os.ModePerm); err != nil {
log.Warn("make node failed", "error", err)
return err
}
// import test accounts (with test ether on it)
if err = ImportTestAccount(filepath.Join(TestDataDir, "keystore"), "test-account1.pk"); err != nil {
panic(err)
}
if err = ImportTestAccount(filepath.Join(TestDataDir, "keystore"), "test-account2.pk"); err != nil {
panic(err)
}
// start geth node and wait for it to initialize
config, err := params.NewNodeConfig(filepath.Join(TestDataDir, "data"), params.RopstenNetworkID, true)
if err != nil {
return err
}
config.KeyStoreDir = filepath.Join(TestDataDir, "keystore")
config.HTTPPort = testConfig.Node.HTTPPort // to avoid conflicts with running app, using different port in tests
config.WSPort = testConfig.Node.WSPort // ditto
config.LogEnabled = true
err = CreateAndRunNode(config)
if err != nil {
panic(err)
}
manager = NodeManagerInstance()
if !manager.NodeInited() {
panic(ErrInvalidGethNode)
}
if service, err := manager.RPCClient(); err != nil || service == nil {
panic(ErrInvalidGethNode)
}
if service, err := manager.WhisperService(); err != nil || service == nil {
panic(ErrInvalidGethNode)
}
if service, err := manager.LightEthereumService(); err != nil || service == nil {
panic(ErrInvalidGethNode)
}
if syncRequired {
log.Warn("Sync is required", "duration", testConfig.Node.SyncSeconds)
time.Sleep(testConfig.Node.SyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done
}
return nil
}
// MakeTestCompleteTxHandler returns node notification handler to be used in test
// basically notification handler completes a transaction (that is enqueued after
// the handler has been installed)
func MakeTestCompleteTxHandler(t *testing.T, txHash *common.Hash, completed chan struct{}) (handler func(jsonEvent string), err error) {
testConfig, err := LoadTestConfig()
if err != nil {
return
}
handler = func(jsonEvent string) {
var envelope SignalEnvelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
t.Logf("Transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
if err := SelectAccount(testConfig.Account1.Address, testConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", testConfig.Account1.Address)
return
}
var err error
if *txHash, err = CompleteTransaction(event["id"].(string), testConfig.Account1.Password); err != nil {
t.Errorf("cannot complete queued transaction[%v]: %v", event["id"], err)
return
}
t.Logf("Contract created: https://testnet.etherscan.io/tx/%s", txHash.Hex())
close(completed) // so that timeout is aborted
}
}
return
}
// PanicAfter throws panic() after waitSeconds, unless abort channel receives notification
func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) {
go func() {
select {
case <-abort:
return
case <-time.After(waitSeconds):
panic("whatever you were doing takes toooo long: " + desc)
}
}()
}
// FromAddress converts account address from string to common.Address.
// The function is useful to format "From" field of send transaction struct.
func FromAddress(accountAddress string) common.Address {
from, err := ParseAccountString(accountAddress)
if err != nil {
return common.Address{}
}
return from.Address
}
// ToAddress converts account address from string to *common.Address.
// The function is useful to format "To" field of send transaction struct.
func ToAddress(accountAddress string) *common.Address {
to, err := ParseAccountString(accountAddress)
if err != nil {
return nil
}
return &to.Address
}
// ParseAccountString parses hex encoded string and returns is as accounts.Account.
func ParseAccountString(account string) (accounts.Account, error) {
// valid address, convert to account
if common.IsHexAddress(account) {
return accounts.Account{Address: common.HexToAddress(account)}, nil
}
return accounts.Account{}, ErrInvalidAccountAddressOrKey
}
// AddressToDecryptedAccount tries to load and decrypt account with a given password
func AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) {
nodeManager := NodeManagerInstance()
keyStore, err := nodeManager.AccountKeyStore()
if err != nil {
return accounts.Account{}, nil, err
}
account, err := ParseAccountString(address)
if err != nil {
return accounts.Account{}, nil, ErrAddressToAccountMappingFailure
}
return keyStore.AccountDecryptedKey(account, password)
}
// ImportTestAccount checks if test account exists in keystore, and if not
// tries to import it (from static resources, see "static/keys" folder)
func ImportTestAccount(keystoreDir, accountFile string) error {
// make sure that keystore folder exists
if _, err := os.Stat(keystoreDir); os.IsNotExist(err) {
os.MkdirAll(keystoreDir, os.ModePerm) // nolint: errcheck
}
dst := filepath.Join(keystoreDir, accountFile)
if _, err := os.Stat(dst); os.IsNotExist(err) {
err = ioutil.WriteFile(dst, static.MustAsset("keys/"+accountFile), 0644)
if err != nil {
log.Warn("cannot copy test account PK", "error", err)
return err
}
}
return nil
}

View File

@ -1 +0,0 @@
package geth

View File

@ -1,85 +0,0 @@
package geth_test
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth"
)
func TestWhisperFilterRace(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
whisperService, err := geth.NodeManagerInstance().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
whisperAPI := whisper.NewPublicWhisperAPI(whisperService)
// account1
_, accountKey1, err := geth.AddressToDecryptedAccount(testConfig.Account1.Address, testConfig.Account1.Password)
if err != nil {
t.Fatal(err)
}
accountKey1Hex := common.ToHex(crypto.FromECDSAPub(&accountKey1.PrivateKey.PublicKey))
if _, err = whisperService.AddKeyPair(accountKey1.PrivateKey); err != nil {
t.Fatal(err)
}
if ok, err := whisperAPI.HasKeyPair(accountKey1Hex); err != nil || !ok {
t.Fatalf("identity not injected: %v", accountKey1Hex)
}
// account2
_, accountKey2, err := geth.AddressToDecryptedAccount(testConfig.Account2.Address, testConfig.Account2.Password)
if err != nil {
t.Fatal(err)
}
accountKey2Hex := common.ToHex(crypto.FromECDSAPub(&accountKey2.PrivateKey.PublicKey))
if _, err = whisperService.AddKeyPair(accountKey2.PrivateKey); err != nil {
t.Fatal(err)
}
if ok, err := whisperAPI.HasKeyPair(accountKey2Hex); err != nil || !ok {
t.Fatalf("identity not injected: %v", accountKey2Hex)
}
// race filter addition
filterAdded := make(chan struct{})
allFiltersAdded := make(chan struct{})
go func() {
counter := 10
for range filterAdded {
counter--
if counter <= 0 {
break
}
}
close(allFiltersAdded)
}()
for i := 0; i < 10; i++ {
go func() {
// nolint: errcheck
whisperAPI.Subscribe(whisper.WhisperFilterArgs{
Sig: accountKey1Hex,
Key: accountKey2Hex,
Topics: [][]byte{
{0x4e, 0x03, 0x65, 0x7a}, {0x34, 0x60, 0x7c, 0x9b}, {0x21, 0x41, 0x7d, 0xf9},
},
})
filterAdded <- struct{}{}
}()
}
<-allFiltersAdded
}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"Node": {
"SyncSeconds": 90,
"SyncSeconds": 10,
"HTTPPort": 8645,
"WSPort": 8646
},

View File

@ -1,4 +1,4 @@
// Package static embeds static (JS, HTML) resources right into the binaries
package static
//go:generate go-bindata -pkg static -o bindata.go scripts/ bootcluster/ config/ keys/
//go:generate go-bindata -pkg static -o bindata.go scripts/ bootcluster/ config/ keys/ testdata/...

View File

@ -3,6 +3,7 @@ package status
import (
"errors"
"sync"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
@ -106,13 +107,18 @@ func (q *TxQueue) Stop() {
// evictionLoop frees up queue to accommodate another transaction item
func (q *TxQueue) evictionLoop() {
evict := func() {
if len(q.transactions) >= DefaultTxQueueCap { // eviction is required to accommodate another/last item
q.Remove(<-q.evictableIDs)
}
}
for {
select {
case <-q.enqueueTicker:
if len(q.transactions) >= (DefaultTxQueueCap - 1) { // eviction is required to accommodate another/last item
q.Remove(<-q.evictableIDs)
q.enqueueTicker <- struct{}{} // in case we pulled already removed item
}
case <-time.After(250 * time.Millisecond): // do not wait for manual ticks, check queue regularly
evict()
case <-q.enqueueTicker: // when manually requested
evict()
case <-q.stopped:
log.Info("StatusIM: transaction queue's eviction loop stopped")
q.stoppedGroup.Done()
@ -127,8 +133,9 @@ func (q *TxQueue) enqueueLoop() {
for {
select {
case queuedTx := <-q.incomingPool:
log.Info("StatusIM: transaction enqueued", "tx", queuedTx.ID)
log.Info("StatusIM: transaction enqueued requested", "tx", queuedTx.ID)
q.Enqueue(queuedTx)
log.Info("StatusIM: transaction enqueued", "tx", queuedTx.ID)
case <-q.stopped:
log.Info("StatusIM: transaction queue's enqueue loop stopped")
q.stoppedGroup.Done()

View File

@ -96,6 +96,7 @@ func VerifyProof(rootHash common.Hash, key []byte, proof []rlp.RawValue) (value
sha.Reset()
sha.Write(buf)
if !bytes.Equal(sha.Sum(nil), wantHash) {
log.Error("Invalid CHT Proof", "hash", common.ToHex(sha.Sum(nil)), "node", i)
return nil, fmt.Errorf("bad proof node %d: hash mismatch", i)
}
n, err := decodeNode(wantHash, buf, 0)