diff --git a/.gitignore b/.gitignore index c1b6a5eb5..f5f106634 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,6 @@ .ethtest */**/*tx_database* */**/*dapps* -Godeps/_workspace/pkg -Godeps/_workspace/bin #* .#* @@ -35,8 +33,9 @@ profile.cov .vagrant # tests -src/.ethereumtest/ +.ethereumtest/ # # golang -cover.out -cover.html +coverage.out +coverage-all.out +coverage.html diff --git a/Makefile b/Makefile index 7acdf8817..65a32a292 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ GOBIN = build/bin GO ?= latest statusgo: - build/env.sh go build -i -o $(GOBIN)/statusgo -v $(shell build/flags.sh) ./src + build/env.sh go build -i -o $(GOBIN)/statusgo -v $(shell build/flags.sh) ./cmd/status @echo "status go compilation done." @echo "Run \"build/bin/statusgo\" to view available commands" @@ -14,25 +14,48 @@ statusgo-cross: statusgo-android statusgo-ios @ls -ld $(GOBIN)/statusgo-* statusgo-android: xgo - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=android-16/aar -v $(shell build/flags.sh) ./src + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=android-16/aar -v $(shell build/flags.sh) ./cmd/status @echo "Android cross compilation done:" statusgo-ios: xgo - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/flags.sh) ./src + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/flags.sh) ./cmd/status @echo "iOS framework cross compilation done:" statusgo-ios-simulator: xgo - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo-ios-simulator --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/flags.sh) ./src + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo-ios-simulator --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/flags.sh) ./cmd/status @echo "iOS framework cross compilation done:" xgo: build/env.sh go get github.com/karalabe/xgo -test: - build/env.sh go test -v -coverprofile=cover.out ./src +test-all: + @build/env.sh echo "mode: set" > coverage-all.out + build/env.sh go test -coverprofile=coverage.out -covermode=set ./geth + @build/env.sh tail -n +2 coverage.out >> coverage-all.out + build/env.sh go test -coverprofile=coverage.out -covermode=set ./jail + @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 tool cover -html=coverage-all.out -o coverage.html + @build/env.sh go tool cover -func=coverage-all.out -test-cover: test - build/env.sh go tool cover -html=cover.out -o cover.html +test: test-all + +test-geth: + build/env.sh go test -v -coverprofile=coverage.out ./geth + @build/env.sh go tool cover -html=coverage.out -o coverage.html + @build/env.sh go tool cover -func=coverage.out + +test-jail: + build/env.sh go test -v -coverprofile=coverage.out ./jail + @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 + @build/env.sh go tool cover -func=coverage.out clean: rm -fr build/bin/* + rm coverage.out coverage-all.out coverage.html diff --git a/build/env.sh b/build/env.sh index 89e174e65..8591d6e8e 100755 --- a/build/env.sh +++ b/build/env.sh @@ -16,7 +16,7 @@ WS2="$ROOT/build/_workspace/project" if [ ! -d "$WS1/src" ]; then mkdir -p "$WS1" cd "$WS1" - ln -s "$ROOT/src/vendor" src + ln -s "$ROOT/vendor" src cd "$ROOT" fi diff --git a/src/library.go b/cmd/status/library.go similarity index 62% rename from src/library.go rename to cmd/status/library.go index b5739fcad..466d2a2d7 100644 --- a/src/library.go +++ b/cmd/status/library.go @@ -1,29 +1,39 @@ package main +/* +#include +#include +extern bool StatusServiceSignalEvent(const char *jsonEvent); +*/ import "C" import ( "encoding/json" "fmt" - "github.com/ethereum/go-ethereum/whisper" "os" + + "github.com/ethereum/go-ethereum/whisper" + "github.com/status-im/status-go/geth" + "github.com/status-im/status-go/jail" ) -var emptyError = "" +// export TriggerTestSignal +func TriggerTestSignal() { + C.StatusServiceSignalEvent(C.CString(`{"answer": 42}`)) +} //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 := createAccount(C.GoString(password)) + address, pubKey, mnemonic, err := geth.CreateAccount(C.GoString(password)) - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := AccountInfo{ + out := geth.AccountInfo{ Address: address, PubKey: pubKey, Mnemonic: mnemonic, @@ -37,15 +47,15 @@ func CreateAccount(password *C.char) *C.char { //export CreateChildAccount func CreateChildAccount(parentAddress, password *C.char) *C.char { - address, pubKey, err := createChildAccount(C.GoString(parentAddress), C.GoString(password)) + address, pubKey, err := geth.CreateChildAccount(C.GoString(parentAddress), C.GoString(password)) - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := AccountInfo{ + out := geth.AccountInfo{ Address: address, PubKey: pubKey, Error: errString, @@ -58,15 +68,15 @@ func CreateChildAccount(parentAddress, password *C.char) *C.char { //export RecoverAccount func RecoverAccount(password, mnemonic *C.char) *C.char { - address, pubKey, err := recoverAccount(C.GoString(password), C.GoString(mnemonic)) + address, pubKey, err := geth.RecoverAccount(C.GoString(password), C.GoString(mnemonic)) - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := AccountInfo{ + out := geth.AccountInfo{ Address: address, PubKey: pubKey, Mnemonic: C.GoString(mnemonic), @@ -81,15 +91,15 @@ func RecoverAccount(password, mnemonic *C.char) *C.char { 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 := selectAccount(C.GoString(address), C.GoString(password)) + err := geth.SelectAccount(C.GoString(address), C.GoString(password)) - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := JSONError{ + out := geth.JSONError{ Error: errString, } outBytes, _ := json.Marshal(&out) @@ -101,15 +111,15 @@ func Login(address, password *C.char) *C.char { func Logout() *C.char { // This is equivalent to clearing whisper identities - err := logout() + err := geth.Logout() - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := JSONError{ + out := geth.JSONError{ Error: errString, } outBytes, _ := json.Marshal(&out) @@ -123,15 +133,15 @@ func UnlockAccount(address, password *C.char, seconds int) *C.char { // This is equivalent to unlocking an account from the command line, // just modified to unlock the account for the currently running geth node // based on the provided arguments - err := unlockAccount(C.GoString(address), C.GoString(password), seconds) + err := geth.UnlockAccount(C.GoString(address), C.GoString(password), seconds) - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := JSONError{ + out := geth.JSONError{ Error: errString, } outBytes, _ := json.Marshal(&out) @@ -141,15 +151,15 @@ func UnlockAccount(address, password *C.char, seconds int) *C.char { //export CompleteTransaction func CompleteTransaction(id, password *C.char) *C.char { - txHash, err := completeTransaction(C.GoString(id), C.GoString(password)) + txHash, err := geth.CompleteTransaction(C.GoString(id), C.GoString(password)) - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := CompleteTransactionResult{ + out := geth.CompleteTransactionResult{ Hash: txHash.Hex(), Error: errString, } @@ -160,17 +170,16 @@ func CompleteTransaction(id, password *C.char) *C.char { //export StartNode func StartNode(datadir *C.char) *C.char { - // This starts a geth node with the given datadir - err := createAndStartNode(C.GoString(datadir)) + err := geth.CreateAndRunNode(C.GoString(datadir), geth.RPCPort) - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := JSONError{ + out := geth.JSONError{ Error: errString, } outBytes, _ := json.Marshal(&out) @@ -178,33 +187,33 @@ func StartNode(datadir *C.char) *C.char { return C.CString(string(outBytes)) } -//export parse -func parse(chatId *C.char, js *C.char) *C.char { - res := Parse(C.GoString(chatId), C.GoString(js)) +//export InitJail +func InitJail(js *C.char) { + jail.Init(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)) return C.CString(res) } -//export call -func call(chatId *C.char, path *C.char, params *C.char) *C.char { - res := Call(C.GoString(chatId), C.GoString(path), C.GoString(params)) +//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)) return C.CString(res) } -//export initJail -func initJail(js *C.char) { - Init(C.GoString(js)) -} - -//export addPeer -func addPeer(url *C.char) *C.char { - success, err := doAddPeer(C.GoString(url)) - errString := emptyError +//export AddPeer +func AddPeer(url *C.char) *C.char { + success, err := geth.GetNodeManager().AddPeer(C.GoString(url)) + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := AddPeerResult{ + out := geth.AddPeerResult{ Success: success, Error: errString, } @@ -213,24 +222,24 @@ func addPeer(url *C.char) *C.char { return C.CString(string(outBytes)) } -//export addWhisperFilter -func addWhisperFilter(filterJson *C.char) *C.char { +//export AddWhisperFilter +func AddWhisperFilter(filterJson *C.char) *C.char { var id int var filter whisper.NewFilterArgs err := json.Unmarshal([]byte(C.GoString(filterJson)), &filter) if err == nil { - id = doAddWhisperFilter(filter) + id = geth.AddWhisperFilter(filter) } - errString := emptyError + errString := "" if err != nil { fmt.Fprintln(os.Stderr, err) errString = err.Error() } - out := AddWhisperFilterResult{ + out := geth.AddWhisperFilterResult{ Id: id, Error: errString, } @@ -240,14 +249,12 @@ func addWhisperFilter(filterJson *C.char) *C.char { } -//export removeWhisperFilter -func removeWhisperFilter(idFilter int) { - - doRemoveWhisperFilter(idFilter) +//export RemoveWhisperFilter +func RemoveWhisperFilter(idFilter int) { + geth.RemoveWhisperFilter(idFilter) } -//export clearWhisperFilters -func clearWhisperFilters() { - - doClearWhisperFilters() +//export ClearWhisperFilters +func ClearWhisperFilters() { + geth.ClearWhisperFilters() } diff --git a/src/main.go b/cmd/status/main.go similarity index 100% rename from src/main.go rename to cmd/status/main.go diff --git a/common/utils.go b/common/utils.go deleted file mode 100644 index 23354ee0e..000000000 --- a/common/utils.go +++ /dev/null @@ -1,26 +0,0 @@ -package common - -import ( - "io" - "os" -) - -func copyFile(dst, src string) error { - s, err := os.Open(src) - if err != nil { - return err - } - defer s.Close() - - d, err := os.Create(dst) - if err != nil { - return err - } - defer d.Close() - - if _, err := io.Copy(d, s); err != nil { - return err - } - - return nil -} diff --git a/src/data/static-nodes.json b/data/static-nodes.json similarity index 100% rename from src/data/static-nodes.json rename to data/static-nodes.json diff --git a/src/data/test-account.pk b/data/test-account.pk similarity index 100% rename from src/data/test-account.pk rename to data/test-account.pk diff --git a/extkeys/mnemonic_test.go b/extkeys/mnemonic_test.go index ca25ba168..6ac93f242 100644 --- a/extkeys/mnemonic_test.go +++ b/extkeys/mnemonic_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "testing" + "github.com/status-im/status-go/extkeys" ) diff --git a/src/gethdep.go b/geth/accounts.go similarity index 59% rename from src/gethdep.go rename to geth/accounts.go index f200eaf17..a5bf351ba 100644 --- a/src/gethdep.go +++ b/geth/accounts.go @@ -1,30 +1,16 @@ -package main - -/* -#include -#include -extern bool GethServiceSignalEvent( const char *jsonEvent ); -*/ -import "C" +package geth import ( - "encoding/json" "errors" "fmt" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/les/status" - "github.com/ethereum/go-ethereum/p2p/discover" - errextra "github.com/pkg/errors" - "github.com/status-im/status-go/src/extkeys" + "github.com/status-im/status-go/extkeys" ) var ( - ErrInvalidGethNode = errors.New("no running node detected for account unlock") - ErrInvalidWhisperService = errors.New("whisper service is unavailable") - ErrInvalidAccountManager = errors.New("could not retrieve account manager") ErrAddressToAccountMappingFailure = errors.New("cannot retreive a valid account for a given address") ErrAccountToKeyMappingFailure = errors.New("cannot retreive a valid key for a given account") ErrUnlockCalled = errors.New("no need to unlock accounts, login instead") @@ -32,32 +18,25 @@ var ( ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities") ErrWhisperNoIdentityFound = errors.New("failed to locate identity previously injected into Whisper") ErrNoAccountSelected = errors.New("no account has been selected, please login") + ErrInvalidMasterKeyCreated = errors.New("can not create master extended key") ) // 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) { - if currentNode == nil { - return "", "", "", ErrInvalidGethNode - } - - if accountManager == nil { - return "", "", "", ErrInvalidAccountManager - } - +func CreateAccount(password string) (address, pubKey, mnemonic string, err error) { // generate mnemonic phrase - m := extkeys.NewMnemonic() + m := extkeys.NewMnemonic(extkeys.Salt) mnemonic, err = m.MnemonicPhrase(128, extkeys.EnglishLanguage) if err != nil { - return "", "", "", errextra.Wrap(err, "Can not create mnemonic seed") + 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)) if err != nil { - return "", "", "", errextra.Wrap(err, "Can not create master extended key") + return "", "", "", fmt.Errorf("can not create master extended key: %v", err) } // import created key into account keystore @@ -72,17 +51,15 @@ 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) { - if currentNode == nil { - return "", "", ErrInvalidGethNode - } - - if accountManager == nil { - return "", "", ErrInvalidAccountManager +func CreateChildAccount(parentAddress, password string) (address, pubKey string, err error) { + nodeManager := GetNodeManager() + accountManager, err := nodeManager.AccountManager() + if err != nil { + return "", "", err } if parentAddress == "" { // by default derive from currently selected account - parentAddress = selectedAddress + parentAddress = nodeManager.SelectedAddress } if parentAddress == "" { @@ -123,20 +100,12 @@ 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) { - if currentNode == nil { - return "", "", ErrInvalidGethNode - } - - if accountManager == nil { - return "", "", ErrInvalidAccountManager - } - +func RecoverAccount(password, mnemonic string) (address, pubKey string, err error) { // re-create extended key (see BIP32) - m := extkeys.NewMnemonic() + m := extkeys.NewMnemonic(extkeys.Salt) extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) if err != nil { - return "", "", errextra.Wrap(err, "Can not create master extended key") + return "", "", ErrInvalidMasterKeyCreated } // import re-created key into account keystore @@ -151,14 +120,13 @@ func recoverAccount(password, mnemonic string) (address, pubKey string, err erro // 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 { - if currentNode == nil { - return ErrInvalidGethNode +func SelectAccount(address, password string) error { + nodeManager := GetNodeManager() + accountManager, err := nodeManager.AccountManager() + if err != nil { + return err } - if accountManager == nil { - return ErrInvalidAccountManager - } account, err := utils.MakeAddress(accountManager, address) if err != nil { return ErrAddressToAccountMappingFailure @@ -169,34 +137,35 @@ func selectAccount(address, password string) error { return fmt.Errorf("%s: %v", ErrAccountToKeyMappingFailure.Error(), err) } - if whisperService == nil { - return ErrInvalidWhisperService + whisperService, err := nodeManager.WhisperService() + if err != nil { + return err } + if err := whisperService.InjectIdentity(accountKey.PrivateKey); err != nil { return ErrWhisperIdentityInjectionFailure } // persist address for easier recovery of currently selected key (from Whisper) - selectedAddress = address + nodeManager.SelectedAddress = address return nil } // logout clears whisper identities -func logout() error { - if currentNode == nil { - return ErrInvalidGethNode - } - if whisperService == nil { - return ErrInvalidWhisperService +func Logout() error { + nodeManager := GetNodeManager() + whisperService, err := nodeManager.WhisperService() + if err != nil { + return err } - err := whisperService.ClearIdentities() + err = whisperService.ClearIdentities() if err != nil { return fmt.Errorf("%s: %v", ErrWhisperClearIdentitiesFailure, err) } - selectedAddress = "" + nodeManager.SelectedAddress = "" return nil } @@ -204,85 +173,31 @@ func logout() error { // unlockAccount unlocks an existing account for a certain duration and // inject the account as a whisper identity if the account was created as // a whisper enabled account -func unlockAccount(address, password string, seconds int) error { - if currentNode == nil { - return ErrInvalidGethNode - } - +func UnlockAccount(address, password string, seconds int) error { return ErrUnlockCalled } // 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) { + accountManager, err := GetNodeManager().AccountManager() + if err != nil { + return "", "", err + } + // imports extended key, create key file (if necessary) account, err := accountManager.ImportExtendedKey(extKey, password) if err != nil { - return "", "", errextra.Wrap(err, "Account manager could not create the account") + return "", "", err } address = fmt.Sprintf("%x", account.Address) // obtain public key to return account, key, err := accountManager.AccountDecryptedKey(account, password) if err != nil { - return address, "", errextra.Wrap(err, "Could not recover the key") + return address, "", err } pubKey = common.ToHex(crypto.FromECDSAPub(&key.PrivateKey.PublicKey)) return } - -// createAndStartNode creates a node entity and starts the -// node running locally -func createAndStartNode(inputDir string) error { - - currentNode = MakeNode(inputDir) - if currentNode != nil { - RunNode(currentNode) - return nil - } - - return errors.New("Could not create the in-memory node object") - -} - -func doAddPeer(url string) (bool, error) { - server := currentNode.Server() - if server == nil { - return false, errors.New("node not started") - } - // Try to add the url as a static peer and return - node, err := discover.ParseNode(url) - if err != nil { - return false, fmt.Errorf("invalid enode: %v", err) - } - server.AddPeer(node) - return true, nil -} - -func onSendTransactionRequest(queuedTx status.QueuedTx) { - event := GethEvent{ - Type: "sendTransactionQueued", - Event: SendTransactionEvent{ - Id: string(queuedTx.Id), - Args: queuedTx.Args, - }, - } - - body, _ := json.Marshal(&event) - C.GethServiceSignalEvent(C.CString(string(body))) -} - -func completeTransaction(id, password string) (common.Hash, error) { - if currentNode != nil { - if lightEthereum != nil { - backend := lightEthereum.StatusBackend - - return backend.CompleteQueuedTransaction(status.QueuedTxId(id), password) - } - - return common.Hash{}, errors.New("can not retrieve LES service") - } - - return common.Hash{}, errors.New("can not complete transaction: no running node detected") -} diff --git a/geth/accounts_test.go b/geth/accounts_test.go new file mode 100644 index 000000000..2892d6a72 --- /dev/null +++ b/geth/accounts_test.go @@ -0,0 +1,308 @@ +package geth_test + +import ( + "errors" + "fmt" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/status-im/status-go/geth" +) + +func TestCreateChildAccount(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + accountManager, err := geth.GetNodeManager().AccountManager() + if err != nil { + t.Error(err) + return + } + + // create an account + address, pubKey, mnemonic, err := geth.CreateAccount(newAccountPassword) + if err != nil { + t.Errorf("could not create account: %v", err) + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic) + + account, err := utils.MakeAddress(accountManager, 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 + account, key, err := accountManager.AccountDecryptedKey(account, newAccountPassword) + 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("", newAccountPassword) + 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, newAccountPassword) + 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 retreive 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("", newAccountPassword) + 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("", newAccountPassword) + 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, newAccountPassword) + 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 TestRecoverAccount(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + accountManager, _ := geth.GetNodeManager().AccountManager() + + // create an account + address, pubKey, mnemonic, err := geth.CreateAccount(newAccountPassword) + if err != nil { + t.Errorf("could not create account: %v", err) + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic) + + // try recovering using password + mnemonic + addressCheck, pubKeyCheck, err := geth.RecoverAccount(newAccountPassword, 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 := utils.MakeAddress(accountManager, address) + if err != nil { + t.Errorf("can not get account from address: %v", err) + } + + account, key, err := accountManager.AccountDecryptedKey(account, newAccountPassword) + if err != nil { + t.Errorf("can not obtain decrypted account key: %v", err) + return + } + extChild2String := key.ExtendedKey.String() + + if err := accountManager.DeleteAccount(account, newAccountPassword); err != nil { + t.Errorf("cannot remove account: %v", err) + } + + addressCheck, pubKeyCheck, err = geth.RecoverAccount(newAccountPassword, 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 + account, key, err = accountManager.AccountDecryptedKey(account, newAccountPassword) + 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(newAccountPassword, 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.GetNodeManager().WhisperService() + if err != nil { + t.Errorf("whisper service not running: %v", err) + } + + // make sure that identity is not (yet injected) + if whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKeyCheck))) { + t.Error("identity already present in whisper") + } + err = geth.SelectAccount(addressCheck, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + if !whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKeyCheck))) { + t.Errorf("identity not injected into whisper: %v", err) + } +} + +func TestAccountSelect(t *testing.T) { + + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + // test to see if the account was injected in whisper + whisperService, err := geth.GetNodeManager().WhisperService() + if err != nil { + t.Errorf("whisper service not running: %v", err) + } + + // create an account + address1, pubKey1, _, err := geth.CreateAccount(newAccountPassword) + if err != nil { + t.Errorf("could not create account: %v", err) + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address1, pubKey1) + + address2, pubKey2, _, err := geth.CreateAccount(newAccountPassword) + if err != nil { + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address2, pubKey2) + + // make sure that identity is not (yet injected) + if whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(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, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + if !whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(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.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) { + t.Error("identity already present in whisper") + } + err = geth.SelectAccount(address2, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + if !whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) { + t.Errorf("identity not injected into whisper: %v", err) + } + if whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) { + t.Error("identity should be removed, but it is still present in whisper") + } +} + +func TestAccountLogout(t *testing.T) { + + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + whisperService, err := geth.GetNodeManager().WhisperService() + if err != nil { + t.Errorf("whisper service not running: %v", err) + } + + // create an account + address, pubKey, _, err := geth.CreateAccount(newAccountPassword) + if err != nil { + t.Errorf("could not create account: %v", err) + return + } + + // make sure that identity doesn't exist (yet) in Whisper + if whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey))) { + t.Error("identity already present in whisper") + } + + // select/login + err = geth.SelectAccount(address, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + if !whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey))) { + t.Error("identity not injected into whisper") + } + + err = geth.Logout() + if err != nil { + t.Errorf("cannot logout: %v", err) + } + + // now, logout and check if identity is removed indeed + if whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey))) { + t.Error("identity not cleared from whisper") + } +} diff --git a/geth/node.go b/geth/node.go new file mode 100644 index 000000000..7fbde6b32 --- /dev/null +++ b/geth/node.go @@ -0,0 +1,281 @@ +package geth + +import ( + "errors" + "flag" + "fmt" + "runtime" + "sync" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/les" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/release" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/whisper" + "gopkg.in/urfave/cli.v1" +) + +const ( + clientIdentifier = "Geth" // Client identifier to advertise over the network + versionMajor = 1 // Major version component of the current release + versionMinor = 5 // Minor version component of the current release + versionPatch = 0 // Patch version component of the current release + versionMeta = "unstable" // Version metadata to append to the version string + + versionOracle = "0xfa7b9770ca4cb04296cac84f37736d4041251cdf" // Ethereum address of the Geth release oracle + + RPCPort = 8545 // RPC port (replaced in unit tests) +) + +var ( + ErrDataDirPreprocessingFailed = errors.New("failed to pre-process data directory") + 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") + ErrNodeStartFailure = errors.New("could not create the in-memory node object") +) + +type NodeNotificationHandler func(jsonEvent string) + +type NodeManager struct { + currentNode *node.Node // currently running geth node + ctx *cli.Context // the CLI context used to start the geth node + lightEthereum *les.LightEthereum // LES service + accountManager *accounts.Manager // the account manager attached to the currentNode + SelectedAddress string // address of the account that was processed during the last call to SelectAccount() + whisperService *whisper.Whisper // Whisper service + client *rpc.ClientRestartWrapper // RPC client + notificationHandler NodeNotificationHandler // internal signal handler (used in tests) +} + +var ( + nodeManagerInstance *NodeManager + createOnce sync.Once +) + +func NewNodeManager(datadir string, rpcport int) *NodeManager { + createOnce.Do(func() { + nodeManagerInstance = &NodeManager{} + nodeManagerInstance.MakeNode(datadir, rpcport) + nodeManagerInstance.SetNotificationHandler(func(jsonEvent string) { + glog.V(logger.Info).Infof("internal notification received: %s\n", jsonEvent) + }) + }) + + return nodeManagerInstance +} + +func GetNodeManager() *NodeManager { + return nodeManagerInstance +} + +// createAndStartNode creates a node entity and starts the +// node running locally +func CreateAndRunNode(datadir string, rpcport int) error { + nodeManager := NewNodeManager(datadir, rpcport) + + if nodeManager.HasNode() { + nodeManager.RunNode() + return nil + } + + return ErrNodeStartFailure +} + +// MakeNode create a geth node entity +func (m *NodeManager) MakeNode(datadir string, rpcport int) *node.Node { + // TODO remove admin rpcapi flag + set := flag.NewFlagSet("test", 0) + set.Bool("lightkdf", true, "Reduce key-derivation RAM & CPU usage at some expense of KDF strength") + set.Bool("shh", true, "whisper") + set.Bool("light", true, "disable eth") + set.Bool("testnet", true, "light test network") + set.Bool("rpc", true, "enable rpc") + set.String("rpcaddr", "localhost", "host for RPC") + set.Int("rpcport", rpcport, "rpc port") + set.String("rpccorsdomain", "*", "allow all domains") + set.String("verbosity", "3", "verbosity level") + set.String("rpcapi", "db,eth,net,web3,shh,personal,admin", "rpc api(s)") + set.String("datadir", datadir, "data directory for geth") + set.String("logdir", datadir, "log dir for glog") + m.ctx = cli.NewContext(nil, set, nil) + + // Construct the textual version string from the individual components + vString := fmt.Sprintf("%d.%d.%d", versionMajor, versionMinor, versionPatch) + + // Construct the version release oracle configuration + var rConfig release.Config + rConfig.Oracle = common.HexToAddress(versionOracle) + + rConfig.Major = uint32(versionMajor) + rConfig.Minor = uint32(versionMinor) + rConfig.Patch = uint32(versionPatch) + + utils.DebugSetup(m.ctx) + + // create node and start requested protocols + m.currentNode = utils.MakeNode(m.ctx, clientIdentifier, vString) + utils.RegisterEthService(m.ctx, m.currentNode, rConfig, makeDefaultExtra()) + + // Whisper must be explicitly enabled, but is auto-enabled in --dev mode. + shhEnabled := m.ctx.GlobalBool(utils.WhisperEnabledFlag.Name) + shhAutoEnabled := !m.ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && m.ctx.GlobalIsSet(utils.DevModeFlag.Name) + if shhEnabled || shhAutoEnabled { + utils.RegisterShhService(m.currentNode) + } + + m.accountManager = m.currentNode.AccountManager() + + return m.currentNode +} + +// StartNode starts a geth node entity +func (m *NodeManager) RunNode() { + utils.StartNode(m.currentNode) + + if m.currentNode.AccountManager() == nil { + glog.V(logger.Warn).Infoln("cannot get account manager") + } + if err := m.currentNode.Service(&m.whisperService); err != nil { + glog.V(logger.Warn).Infoln("cannot get whisper service:", err) + } + if err := m.currentNode.Service(&m.lightEthereum); err != nil { + glog.V(logger.Warn).Infoln("cannot get light ethereum service:", err) + } + m.lightEthereum.StatusBackend.SetTransactionQueueHandler(onSendTransactionRequest) + + m.client = rpc.NewClientRestartWrapper(func() *rpc.Client { + client, err := m.currentNode.Attach() + if err != nil { + return nil + } + return client + }) + m.currentNode.Wait() +} + +func (m *NodeManager) AddPeer(url string) (bool, error) { + if m == nil || !m.HasNode() { + return false, ErrInvalidGethNode + } + + server := m.currentNode.Server() + if server == nil { + return false, errors.New("node not started") + } + // 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 +} + +func (m *NodeManager) HasNode() bool { + return m != nil && m.currentNode != nil +} + +func (m *NodeManager) HasAccountManager() bool { + return m.accountManager != nil +} + +func (m *NodeManager) AccountManager() (*accounts.Manager, error) { + if m == nil || !m.HasNode() { + return nil, ErrInvalidGethNode + } + + if !m.HasAccountManager() { + return nil, ErrInvalidAccountManager + } + + return m.accountManager, nil +} + +func (m *NodeManager) HasWhisperService() bool { + return m.whisperService != nil +} + +func (m *NodeManager) WhisperService() (*whisper.Whisper, error) { + if m == nil || !m.HasNode() { + return nil, ErrInvalidGethNode + } + + if !m.HasWhisperService() { + return nil, ErrInvalidWhisperService + } + + return m.whisperService, nil +} + +func (m *NodeManager) HasLightEthereumService() bool { + return m.lightEthereum != nil +} + +func (m *NodeManager) LightEthereumService() (*les.LightEthereum, error) { + if m == nil || !m.HasNode() { + return nil, ErrInvalidGethNode + } + + if !m.HasLightEthereumService() { + return nil, ErrInvalidLightEthereumService + } + + return m.lightEthereum, nil +} + +func (m *NodeManager) HasClientRestartWrapper() bool { + return m.client != nil +} + +func (m *NodeManager) ClientRestartWrapper() (*rpc.ClientRestartWrapper, error) { + if m == nil || !m.HasNode() { + return nil, ErrInvalidGethNode + } + + if !m.HasClientRestartWrapper() { + return nil, ErrInvalidClient + } + + return m.client, nil +} + +func (m *NodeManager) SetNotificationHandler(fn NodeNotificationHandler) { + m.notificationHandler = fn +} + +func (m *NodeManager) NotificationHandler() NodeNotificationHandler { + return m.notificationHandler +} + +func makeDefaultExtra() []byte { + var clientInfo = struct { + Version uint + Name string + GoVersion string + Os string + }{uint(versionMajor<<16 | versionMinor<<8 | versionPatch), clientIdentifier, runtime.Version(), runtime.GOOS} + extra, err := rlp.EncodeToBytes(clientInfo) + if err != nil { + glog.V(logger.Warn).Infoln("error setting canonical miner information:", err) + } + + if uint64(len(extra)) > params.MaximumExtraDataSize.Uint64() { + glog.V(logger.Warn).Infoln("error setting canonical miner information: extra exceeds", params.MaximumExtraDataSize) + glog.V(logger.Debug).Infof("extra: %x\n", extra) + return nil + } + + return extra +} diff --git a/geth/node_test.go b/geth/node_test.go new file mode 100644 index 000000000..aa0c7b86a --- /dev/null +++ b/geth/node_test.go @@ -0,0 +1,26 @@ +package geth_test + +import ( + "github.com/status-im/status-go/geth" + "testing" +) + +const ( + testAddress = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" + testAddressPassword = "asdf" + newAccountPassword = "badpassword" + + whisperMessage1 = "test message 1 (K1 -> K1)" + whisperMessage2 = "test message 2 (K1 -> '')" + whisperMessage3 = "test message 3 ('' -> '')" + whisperMessage4 = "test message 4 ('' -> K1)" + whisperMessage5 = "test message 5 (K2 -> K1)" +) + +func TestNodeSetup(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } +} diff --git a/geth/signals.c b/geth/signals.c new file mode 100644 index 000000000..cb9a7c7e9 --- /dev/null +++ b/geth/signals.c @@ -0,0 +1,181 @@ +#if defined(IOS_DEPLOYMENT) +// ====================================================================================== +// iOS framework compilation using xgo +// ====================================================================================== + +#include +#include + +bool StatusServiceSignalEvent(const char *jsonEvent) { + // code for sending JSON notification up to iOS app + return true; +} + +#elif defined(ANDROID_DEPLOYMENT) +// ====================================================================================== +// Android archive compilation using xgo +// ====================================================================================== + +#include +#include +#include + +bool StatusServiceSignalEvent(const char *jsonEvent); + +static JavaVM *gJavaVM = NULL; +static jclass JavaClassPtr_StatusService = NULL; +static jmethodID JavaMethodPtr_signalEvent = NULL; + +static bool JniLibraryInit(JNIEnv *env); + +/*! + * @brief Get interface to JNI. + * + * @return true if thread should be detached from JNI. + */ +static bool JniAttach(JNIEnv **env) { + jint status; + + if (gJavaVM == NULL) { + env = NULL; + } + + status = (*gJavaVM)->GetEnv(gJavaVM, (void **)env, JNI_VERSION_1_6); + if (status == JNI_EDETACHED) { + // attach thread to JNI + //(*gJavaVM)->AttachCurrentThread( gJavaVM, (void **)env, NULL ); // Oracle JNI API + (*gJavaVM)->AttachCurrentThread(gJavaVM, env, NULL); // Android JNI API + return true; + } else if (status != JNI_OK) { + return false; + } + + return false; +} + +/*! + * @brief The VM calls JNI_OnLoad when the native library is loaded. + */ +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + bool detach; + JNIEnv *env; + int result = JNI_VERSION_1_6; + + gJavaVM = vm; + + // attach thread to JNI + detach = JniAttach(&env); + if (env == NULL) { + // failed + gJavaVM = NULL; + return 0; + } + + if (!JniLibraryInit(env)) { + // fail loading of JNI library + result = 0; + } + + if (detach) { + // detach thread from JNI + (*gJavaVM)->DetachCurrentThread(gJavaVM); + } + + if (result != JNI_VERSION_1_6) { + gJavaVM = NULL; + } + + return result; +} + +/*! + * @brief Initialize library. + */ +bool JniLibraryInit(JNIEnv *env) { + int i; + + JavaClassPtr_StatusService = (*env)->FindClass(env, "com/statusim/module/StatusService"); + if (JavaClassPtr_StatusService == NULL) return false; + + JavaClassPtr_StatusService = (jclass)(*env)->NewGlobalRef(env, JavaClassPtr_StatusService); + if (JavaClassPtr_StatusService == NULL) return false; + + struct { + bool bStatic; + jclass classPtr; + jmethodID *methodPtr; + const char *methodId; + const char *params; + } javaMethodDescriptors[] = { + { + true, + JavaClassPtr_StatusService, + &JavaMethodPtr_signalEvent, // &JavaMethodPtr_someNonStaticMethod + "signalEvent", // someNonStaticMethod + "(Ljava/lang/String;)V" + }, + }; + + for (i = 0; i < sizeof(javaMethodDescriptors) / sizeof(javaMethodDescriptors[0]); i++) { + if (javaMethodDescriptors[i].bStatic) { + *(javaMethodDescriptors[i].methodPtr) = (*env)->GetStaticMethodID( + env, javaMethodDescriptors[i].classPtr, javaMethodDescriptors[i].methodId, javaMethodDescriptors[i].params); + } else { + *(javaMethodDescriptors[i].methodPtr) = (*env)->GetMethodID( + env, javaMethodDescriptors[i].classPtr, javaMethodDescriptors[i].methodId, javaMethodDescriptors[i].params); + } + + if (*(javaMethodDescriptors[i].methodPtr) == NULL) return false; + } + + return true; +} + +/*! + * @brief Calls static method signalEvent of class com.statusim.module.StatusService. + * + * @param jsonEvent - UTF8 string + */ +bool StatusServiceSignalEvent(const char *jsonEvent) { + bool detach; + JNIEnv *env; + + // attach thread to JNI + detach = JniAttach( &env ); + if (env == NULL) { // failed + return false; + } + + jstring javaJsonEvent = NULL; + if (jsonEvent != NULL) { + javaJsonEvent = (*env)->NewStringUTF(env, jsonEvent); + } + + (*env)->CallStaticVoidMethod(env, JavaClassPtr_StatusService, JavaMethodPtr_signalEvent, javaJsonEvent); + + if (javaJsonEvent != NULL) (*env)->DeleteLocalRef(env, javaJsonEvent); + + if (detach) { // detach thread from JNI + (*gJavaVM)->DetachCurrentThread(gJavaVM); + } + + return true; +} + +#else +// ====================================================================================== +// cgo compilation (for local tests) +// ====================================================================================== + +#include +#include +#include +#include "_cgo_export.h" + +bool StatusServiceSignalEvent(const char *jsonEvent) { + NotifyNode((char *)jsonEvent); // re-send notification back to status node + + return true; +} + +#endif diff --git a/geth/txqueue.go b/geth/txqueue.go new file mode 100644 index 000000000..090ba2816 --- /dev/null +++ b/geth/txqueue.go @@ -0,0 +1,43 @@ +package geth + +/* +#include +#include +extern bool StatusServiceSignalEvent( const char *jsonEvent ); +*/ +import "C" + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/les/status" +) + +const ( + EventTransactionQueued = "transaction.queued" +) + +func onSendTransactionRequest(queuedTx status.QueuedTx) { + event := GethEvent{ + Type: EventTransactionQueued, + Event: SendTransactionEvent{ + Id: string(queuedTx.Id), + Args: queuedTx.Args, + }, + } + + body, _ := json.Marshal(&event) + C.StatusServiceSignalEvent(C.CString(string(body))) +} + +func CompleteTransaction(id, password string) (common.Hash, error) { + lightEthereum, err := GetNodeManager().LightEthereumService() + if err != nil { + return common.Hash{}, err + } + + backend := lightEthereum.StatusBackend + + return backend.CompleteQueuedTransaction(status.QueuedTxId(id), password) +} diff --git a/geth/txqueue_test.go b/geth/txqueue_test.go new file mode 100644 index 000000000..10c2fb520 --- /dev/null +++ b/geth/txqueue_test.go @@ -0,0 +1,149 @@ +package geth_test + +import ( + "encoding/json" + "math/big" + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/les/status" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/rpc" + "github.com/status-im/status-go/geth" +) + +func TestQueuedTransactions(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + accountManager, err := geth.GetNodeManager().AccountManager() + if err != nil { + t.Errorf(err.Error()) + return + } + + // create an account + address, _, _, err := geth.CreateAccount(newAccountPassword) + if err != nil { + t.Errorf("could not create account: %v", err) + return + } + + // test transaction queueing + lightEthereum, err := geth.GetNodeManager().LightEthereumService() + if err != nil { + t.Errorf("Test failed: LES service is not running: %v", err) + return + } + backend := lightEthereum.StatusBackend + + // make sure you panic if transaction complete doesn't return + completeQueuedTransaction := make(chan bool, 1) + geth.PanicAfter(20*time.Second, completeQueuedTransaction) + + // replace transaction notification handler + var txHash = common.Hash{} + geth.GetNodeManager().SetNotificationHandler(func(jsonEvent string) { + var envelope geth.GethEvent + 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{}) + glog.V(logger.Info).Infof("Transaction queued (will be completed in 5 secs): {id: %s}\n", event["id"].(string)) + time.Sleep(5 * time.Second) + + if txHash, err = geth.CompleteTransaction(event["id"].(string), testAddressPassword); err != nil { + t.Errorf("cannot complete queued transation[%v]: %v", event["id"], err) + return + } + + glog.V(logger.Info).Infof("Transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex()) + completeQueuedTransaction <- true // so that timeout is aborted + } + }) + + // try completing non-existing transaction + if _, err := geth.CompleteTransaction("some-bad-transaction-id", testAddressPassword); err == nil { + t.Error("error expected and not recieved") + return + } + + // send normal transaction + from, err := utils.MakeAddress(accountManager, testAddress) + if err != nil { + t.Errorf("could not retrieve account from address: %v", err) + return + } + + to, err := utils.MakeAddress(accountManager, address) + if err != nil { + t.Errorf("could not retrieve account from address: %v", err) + return + } + + // this call blocks, up until Complete Transaction is called + txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + From: from.Address, + To: &to.Address, + Value: rpc.NewHexNumber(big.NewInt(1000000000000)), + }) + if err != nil { + t.Errorf("Test failed: cannot send transaction: %v", err) + } + + if !reflect.DeepEqual(txHash, txHashCheck) { + t.Error("Transaction hash returned from SendTransaction is invalid") + return + } + + time.Sleep(10 * time.Second) + + if reflect.DeepEqual(txHashCheck, common.Hash{}) { + t.Error("Test failed: transaction was never queued or completed") + return + } + + // now test eviction queue + txQueue := backend.TransactionQueue() + var i = 0 + backend.SetTransactionQueueHandler(func(queuedTx status.QueuedTx) { + //glog.V(logger.Info).Infof("%d. Transaction queued (queue size: %d): {id: %v}\n", i, txQueue.Count(), 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{}) + } + 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{}) + } + time.Sleep(5 * time.Second) + + if txQueue.Count() != status.DefaultTxQueueCap && txQueue.Count() != (status.DefaultTxQueueCap-1) { + t.Errorf("transaction count should be %d (or %d): got %d", status.DefaultTxQueueCap, status.DefaultTxQueueCap-1, txQueue.Count()) + return + } +} diff --git a/common/types.go b/geth/types.go similarity index 94% rename from common/types.go rename to geth/types.go index d9d7c35ef..94a499d23 100644 --- a/common/types.go +++ b/geth/types.go @@ -1,4 +1,4 @@ -package common +package geth import ( "github.com/ethereum/go-ethereum/les/status" @@ -35,7 +35,7 @@ type WhisperMessageEvent struct { } type SendTransactionEvent struct { - Id string `json:"hash"` + Id string `json:"id"` Args status.SendTxArgs `json:"args"` } diff --git a/geth/utils.go b/geth/utils.go new file mode 100644 index 000000000..8e698e1e5 --- /dev/null +++ b/geth/utils.go @@ -0,0 +1,161 @@ +package geth + +import "C" +import ( + "bytes" + "io" + "os" + "path" + "path/filepath" + "sync" + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +var muPrepareTestNode sync.Mutex + +const ( + testDataDir = "../.ethereumtest" + testNodeSyncSeconds = 300 +) + +//export NotifyNode +func NotifyNode(jsonEvent *C.char) { + GetNodeManager().NotificationHandler()(C.GoString(jsonEvent)) +} + +func CopyFile(dst, src string) error { + s, err := os.Open(src) + if err != nil { + return err + } + defer s.Close() + + d, err := os.Create(dst) + if err != nil { + return err + } + defer d.Close() + + if _, err := io.Copy(d, s); err != nil { + return err + } + + return nil +} + +// LoadFromFile is usefull for loading test data, from testdata/filename into a variable +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()) +} + +func PrepareTestNode() (err error) { + muPrepareTestNode.Lock() + defer muPrepareTestNode.Unlock() + + manager := GetNodeManager() + if manager.HasNode() { + return nil + } + + syncRequired := false + if _, err := os.Stat(testDataDir); os.IsNotExist(err) { + syncRequired = true + } + + // prepare node directory + dataDir, err := PreprocessDataDir(testDataDir) + if err != nil { + glog.V(logger.Warn).Infoln("make node failed:", err) + return err + } + + // import test account (with test ether on it) + dst := filepath.Join(testDataDir, "testnet", "keystore", "test-account.pk") + if _, err := os.Stat(dst); os.IsNotExist(err) { + err = CopyFile(dst, filepath.Join("../data", "test-account.pk")) + if err != nil { + glog.V(logger.Warn).Infof("cannot copy test account PK: %v", err) + return err + } + } + + // start geth node and wait for it to initialize + // internally once.Do() is used, so call below is thread-safe + go CreateAndRunNode(dataDir, 8546) // to avoid conflicts with running react-native app, run on different port + time.Sleep(3 * time.Second) + + manager = GetNodeManager() + if !manager.HasNode() { + panic("could not obtain geth node") + } + + manager.AddPeer("enode://409772c7dea96fa59a912186ad5bcdb5e51b80556b3fe447d940f99d9eaadb51d4f0ffedb68efad232b52475dd7bd59b51cee99968b3cc79e2d5684b33c4090c@139.162.166.59:30303") + + if syncRequired { + glog.V(logger.Warn).Infof("Sync is required, it will take %d seconds", testNodeSyncSeconds) + time.Sleep(testNodeSyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done + } else { + time.Sleep(5 * time.Second) + } + + return nil +} + +func RemoveTestNode() { + err := os.RemoveAll(testDataDir) + if err != nil { + glog.V(logger.Warn).Infof("could not clean up temporary datadir") + } +} + +func PreprocessDataDir(dataDir string) (string, error) { + testDataDir := path.Join(dataDir, "testnet", "keystore") + if _, err := os.Stat(testDataDir); os.IsNotExist(err) { + if err := os.MkdirAll(testDataDir, 0755); err != nil { + return dataDir, ErrDataDirPreprocessingFailed + } + } + + // copy over static peer nodes list (LES auto-discovery is not stable yet) + dst := filepath.Join(dataDir, "testnet", "static-nodes.json") + if _, err := os.Stat(dst); os.IsNotExist(err) { + src := filepath.Join("../data", "static-nodes.json") + if err := CopyFile(dst, src); err != nil { + return dataDir, err + } + } + + return dataDir, nil +} + +// PanicAfter throws panic() after waitSeconds, unless abort channel receives notification +func PanicAfter(waitSeconds time.Duration, abort chan bool) { + // panic if function takes too long + timeout := make(chan bool, 1) + + go func() { + time.Sleep(waitSeconds) + timeout <- true + }() + + go func() { + select { + case <-abort: + return + case <-timeout: + panic("function takes to long, which generally means we are stuck") + } + }() +} diff --git a/geth/whisper.go b/geth/whisper.go new file mode 100644 index 000000000..4548108e2 --- /dev/null +++ b/geth/whisper.go @@ -0,0 +1,69 @@ +package geth + +/* +#include +#include +extern bool StatusServiceSignalEvent( const char *jsonEvent ); +*/ +import "C" +import ( + "encoding/json" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/whisper" +) + +var ( + whisperFilters []int +) + +func onWhisperMessage(message *whisper.Message) { + event := GethEvent{ + Type: "whisper", + Event: WhisperMessageEvent{ + Payload: string(message.Payload), + From: common.ToHex(crypto.FromECDSAPub(message.Recover())), + To: common.ToHex(crypto.FromECDSAPub(message.To)), + Sent: message.Sent.Unix(), + TTL: int64(message.TTL / time.Second), + Hash: common.ToHex(message.Hash.Bytes()), + }, + } + body, _ := json.Marshal(&event) + C.StatusServiceSignalEvent(C.CString(string(body))) +} + +func AddWhisperFilter(args whisper.NewFilterArgs) int { + whisperService, err := GetNodeManager().WhisperService() + if err != nil { + return -1 + } + + filter := whisper.Filter{ + To: crypto.ToECDSAPub(common.FromHex(args.To)), + From: crypto.ToECDSAPub(common.FromHex(args.From)), + Topics: whisper.NewFilterTopics(args.Topics...), + Fn: onWhisperMessage, + } + + id := whisperService.Watch(filter) + whisperFilters = append(whisperFilters, id) + return id +} + +func RemoveWhisperFilter(idFilter int) { + whisperService, err := GetNodeManager().WhisperService() + if err != nil { + return + } + whisperService.Unwatch(idFilter) +} + +func ClearWhisperFilters() { + for _, idFilter := range whisperFilters { + RemoveWhisperFilter(idFilter) + } + whisperFilters = nil +} diff --git a/geth/whisper_test.go b/geth/whisper_test.go new file mode 100644 index 000000000..5e17e722b --- /dev/null +++ b/geth/whisper_test.go @@ -0,0 +1,164 @@ +package geth_test + +import ( + "fmt" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/whisper" + "github.com/status-im/status-go/geth" +) + +func TestWhisperMessaging(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + whisperService, err := geth.GetNodeManager().WhisperService() + if err != nil { + t.Errorf("whisper service not running: %v", err) + } + + whisperAPI := whisper.NewPublicWhisperAPI(whisperService) + + // prepare message + postArgs := whisper.PostArgs{ + From: "", + To: "", + TTL: 10, + Topics: [][]byte{[]byte("test topic")}, + Payload: "test message", + } + + // create an accounts + address1, pubKey1, _, err := geth.CreateAccount(newAccountPassword) + if err != nil { + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address1, pubKey1) + + address2, pubKey2, _, err := geth.CreateAccount(newAccountPassword) + if err != nil { + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address2, pubKey2) + + // start watchers + var receivedMessages = map[string]bool{ + whisperMessage1: false, + whisperMessage2: false, + whisperMessage3: false, + whisperMessage4: false, + whisperMessage5: false, + } + whisperService.Watch(whisper.Filter{ + //From: crypto.ToECDSAPub(common.FromHex(pubKey1)), + //To: crypto.ToECDSAPub(common.FromHex(pubKey2)), + Fn: func(msg *whisper.Message) { + //glog.V(logger.Info).Infof("Whisper message received: %s", msg.Payload) + receivedMessages[string(msg.Payload)] = true + }, + }) + + // inject key of newly created account into Whisper, as identity + if whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) { + t.Error("identity already present in whisper") + } + err = geth.SelectAccount(address1, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + identitySucceess := whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) + if !identitySucceess || err != nil { + t.Errorf("identity not injected into whisper: %v", err) + } + if whisperService.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) { // ensure that second id is not injected + t.Error("identity already present in whisper") + } + + // double selecting (shouldn't be a problem) + err = geth.SelectAccount(address1, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + + // TEST 0: From != nil && To != nil: encrypted signed message (but we cannot decrypt it - so watchers will not report this) + postArgs.From = pubKey1 + postArgs.To = pubKey2 // owner of that public key will be able to decrypt it + postSuccess, err := whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 1: From != nil && To != nil: encrypted signed message (to self) + postArgs.From = pubKey1 + postArgs.To = pubKey1 + postArgs.Payload = whisperMessage1 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // send from account that is not in Whisper identity list + postArgs.From = pubKey2 + postSuccess, err = whisperAPI.Post(postArgs) + if err == nil || err.Error() != fmt.Sprintf("unknown identity to send from: %s", pubKey2) { + t.Error("expected error not voiced: we are sending from non-injected whisper identity") + } + + // TEST 2: From != nil && To == nil: signed broadcast (known sender) + postArgs.From = pubKey1 + postArgs.To = "" + postArgs.Payload = whisperMessage2 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 3: From == nil && To == nil: anonymous broadcast + postArgs.From = "" + postArgs.To = "" + postArgs.Payload = whisperMessage3 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 4: From == nil && To != nil: encrypted anonymous message + postArgs.From = "" + postArgs.To = pubKey1 + postArgs.Payload = whisperMessage4 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 5: From != nil && To != nil: encrypted and signed response + postArgs.From = "" + postArgs.To = pubKey1 + postArgs.Payload = whisperMessage5 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + time.Sleep(2 * time.Second) // allow whisper to poll + for message, status := range receivedMessages { + if !status { + t.Errorf("Expected message not received: %s", message) + } + } + +} diff --git a/jail/jail.go b/jail/jail.go new file mode 100644 index 000000000..01ec53fdc --- /dev/null +++ b/jail/jail.go @@ -0,0 +1,259 @@ +package jail + +import ( + "encoding/json" + "errors" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/rpc" + "github.com/robertkrimen/otto" + "github.com/status-im/status-go/geth" +) + +var ( + ErrInvalidJail = errors.New("jail environment is not properly initialized") +) + +type Jail struct { + client *rpc.ClientRestartWrapper // lazy inited on the first call to jail.ClientRestartWrapper() + VMs map[string]*otto.Otto + statusJS string +} + +var jailInstance *Jail +var once sync.Once + +func New() *Jail { + once.Do(func() { + jailInstance = &Jail{ + VMs: make(map[string]*otto.Otto), + } + }) + + return jailInstance +} + +func Init(js string) *Jail { + jailInstance = New() // singleton, we will always get the same reference + jailInstance.statusJS = js + + return jailInstance +} + +func GetInstance() *Jail { + return New() // singleton, we will always get the same reference +} + +func (jail *Jail) Parse(chatId string, js string) string { + if jail == nil { + return printError(ErrInvalidJail.Error()) + } + + vm := otto.New() + initJjs := jail.statusJS + ";" + jail.VMs[chatId] = vm + _, err := vm.Run(initJjs) + vm.Set("jeth", struct{}{}) + + jethObj, _ := vm.Get("jeth") + jethObj.Object().Set("send", jail.Send) + jethObj.Object().Set("sendAsync", jail.Send) + + jjs := Web3_JS + ` + var Web3 = require('web3'); + var web3 = new Web3(jeth); + var Bignumber = require("bignumber.js"); + function bn(val){ + return new Bignumber(val); + } + ` + js + "; var catalog = JSON.stringify(_status_catalog);" + vm.Run(jjs) + + res, _ := vm.Get("catalog") + + return printResult(res.String(), err) +} + +func (jail *Jail) Call(chatId string, path string, args string) string { + _, err := jail.ClientRestartWrapper() + if err != nil { + return printError(err.Error()) + } + + vm, ok := jail.VMs[chatId] + if !ok { + return printError(fmt.Sprintf("VM[%s] doesn't exist.", chatId)) + } + + res, err := vm.Call("call", nil, path, args) + + return printResult(res.String(), err) +} + +func (jail *Jail) GetVM(chatId string) (*otto.Otto, error) { + if jail == nil { + return nil, ErrInvalidJail + } + + vm, ok := jail.VMs[chatId] + if !ok { + return nil, fmt.Errorf("VM[%s] doesn't exist.", chatId) + } + + return vm, nil +} + +type jsonrpcCall struct { + Id int64 + Method string + Params []interface{} +} + +// Send will serialize the first argument, send it to the node and returns the response. +func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { + clientFactory, err := jail.ClientRestartWrapper() + if err != nil { + return newErrorResponse(call, -32603, err.Error(), nil) + } + + // Remarshal the request into a Go value. + JSON, _ := call.Otto.Object("JSON") + reqVal, err := JSON.Call("stringify", call.Argument(0)) + if err != nil { + throwJSException(err.Error()) + } + var ( + rawReq = []byte(reqVal.String()) + reqs []jsonrpcCall + batch bool + ) + if rawReq[0] == '[' { + batch = true + json.Unmarshal(rawReq, &reqs) + } else { + batch = false + reqs = make([]jsonrpcCall, 1) + json.Unmarshal(rawReq, &reqs[0]) + } + + // Execute the requests. + resps, _ := call.Otto.Object("new Array()") + for _, req := range reqs { + resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) + resp.Set("id", req.Id) + var result json.RawMessage + + client := clientFactory.Client() + errc := make(chan error, 1) + errc2 := make(chan error) + go func() { + errc2 <- <-errc + }() + errc <- client.Call(&result, req.Method, req.Params...) + err = <-errc2 + + switch err := err.(type) { + case nil: + if result == nil { + // Special case null because it is decoded as an empty + // raw message for some reason. + resp.Set("result", otto.NullValue()) + } else { + resultVal, err := JSON.Call("parse", string(result)) + if err != nil { + resp = newErrorResponse(call, -32603, err.Error(), &req.Id).Object() + } else { + resp.Set("result", resultVal) + } + } + case rpc.Error: + resp.Set("error", map[string]interface{}{ + "code": err.ErrorCode(), + "message": err.Error(), + }) + default: + resp = newErrorResponse(call, -32603, err.Error(), &req.Id).Object() + } + resps.Call("push", resp) + } + + // Return the responses either to the callback (if supplied) + // or directly as the return value. + if batch { + response = resps.Value() + } else { + response, _ = resps.Get("0") + } + if fn := call.Argument(1); fn.Class() == "Function" { + fn.Call(otto.NullValue(), otto.NullValue(), response) + return otto.UndefinedValue() + } + return response +} + +func (jail *Jail) ClientRestartWrapper() (*rpc.ClientRestartWrapper, error) { + if jail == nil { + return nil, ErrInvalidJail + } + + if jail.client != nil { + return jail.client, nil + } + + nodeManager := geth.GetNodeManager() + if !nodeManager.HasNode() { + return nil, geth.ErrInvalidGethNode + } + + // obtain RPC client from running node + client, err := nodeManager.ClientRestartWrapper() + if err != nil { + return nil, err + } + jail.client = client + + return jail.client, 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{}{"version": "2.0", "id": id, "error": map[string]interface{}{"code": code, msg: msg}} + res, _ := json.Marshal(m) + val, _ := call.Otto.Run("(" + string(res) + ")") + return val +} + +// throwJSException panics on an otto.Value. The Otto VM will recover from the +// Go panic and throw msg as a JavaScript error. +func throwJSException(msg interface{}) otto.Value { + val, err := otto.ToValue(msg) + if err != nil { + glog.V(logger.Error).Infof("Failed to serialize JavaScript exception %v: %v", msg, err) + } + panic(val) +} + +func printError(error string) string { + str := geth.JSONError{ + Error: error, + } + outBytes, _ := json.Marshal(&str) + return string(outBytes) +} + +func printResult(res string, err error) string { + var out string + if err != nil { + out = printError(err.Error()) + } else { + if "undefined" == res { + res = "null" + } + out = fmt.Sprintf(`{"result": %s}`, res) + } + + return out +} diff --git a/jail/jail_test.go b/jail/jail_test.go new file mode 100644 index 000000000..58b74de3b --- /dev/null +++ b/jail/jail_test.go @@ -0,0 +1,242 @@ +package jail_test + +import ( + "reflect" + "testing" + + "github.com/status-im/status-go/geth" + "github.com/status-im/status-go/jail" +) + +const ( + TEST_ADDRESS = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" + CHAT_ID_INIT = "CHAT_ID_INIT_TEST" + CHAT_ID_CALL = "CHAT_ID_CALL_TEST" + CHAT_ID_NON_EXISTENT = "CHAT_IDNON_EXISTENT" + + TESTDATA_STATUS_JS = "testdata/status.js" +) + +func TestJailUnInited(t *testing.T) { + errorWrapper := func(err error) string { + return `{"error":"` + err.Error() + `"}` + } + + expectedError := errorWrapper(jail.ErrInvalidJail) + + var jailInstance *jail.Jail + response := jailInstance.Parse(CHAT_ID_CALL, ``) + if response != expectedError { + t.Errorf("error expected, but got: %v", response) + } + + response = jailInstance.Call(CHAT_ID_CALL, `["commands", "testCommand"]`, `{"val": 12}`) + if response != expectedError { + t.Errorf("error expected, but got: %v", response) + } + + _, err := jailInstance.GetVM(CHAT_ID_CALL) + if err != jail.ErrInvalidJail { + t.Errorf("error expected, but got: %v", err) + } + + _, err = jailInstance.ClientRestartWrapper() + if err != jail.ErrInvalidJail { + t.Errorf("error expected, but got: %v", err) + } + + // now make sure that if Init is called, then Parse doesn't produce any error + jailInstance = jail.Init(``) + if jailInstance == nil { + t.Error("jail instance shouldn't be nil at this point") + return + } + statusJS := geth.LoadFromFile(TESTDATA_STATUS_JS) + `; + _status_catalog.commands["testCommand"] = function (params) { + return params.val * params.val; + };` + response = jailInstance.Parse(CHAT_ID_CALL, statusJS) + expectedResponse := `{"result": {"commands":{},"responses":{}}}` + if response != expectedResponse { + t.Errorf("unexpected response received: %v", response) + } + + // however, we still expect issue voiced if somebody tries to execute code with Call + response = jailInstance.Call(CHAT_ID_CALL, `["commands", "testCommand"]`, `{"val": 12}`) + if response != errorWrapper(geth.ErrInvalidGethNode) { + t.Errorf("error expected, but got: %v", response) + } + + // make sure that Call() succeeds when node is started + err = geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + response = jailInstance.Call(CHAT_ID_CALL, `["commands", "testCommand"]`, `{"val": 12}`) + expectedResponse = `{"result": 144}` + if response != expectedResponse { + t.Errorf("expected response is not returned: expected %s, got %s", expectedResponse, response) + return + } +} + +func TestJailInit(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + initCode := ` + var _status_catalog = { + foo: 'bar' + }; + ` + jailInstance := jail.Init(initCode) + + extraCode := ` + var extraFunc = function (x) { + return x * x; + }; + ` + response := jailInstance.Parse(CHAT_ID_INIT, extraCode) + + expectedResponse := `{"result": {"foo":"bar"}}` + + if !reflect.DeepEqual(expectedResponse, response) { + t.Error("Expected output not returned from jail.Parse()") + return + } +} + +func TestJailFunctionCall(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + jailInstance := jail.Init("") + + // load Status JS and add test command to it + statusJS := geth.LoadFromFile(TESTDATA_STATUS_JS) + `; + _status_catalog.commands["testCommand"] = function (params) { + return params.val * params.val; + };` + jailInstance.Parse(CHAT_ID_CALL, statusJS) + + // call with wrong chat id + response := jailInstance.Call(CHAT_ID_NON_EXISTENT, "", "") + expectedError := `{"error":"VM[CHAT_IDNON_EXISTENT] doesn't exist."}` + if response != expectedError { + t.Errorf("expected error is not returned: expected %s, got %s", expectedError, response) + return + } + + // call extraFunc() + response = jailInstance.Call(CHAT_ID_CALL, `["commands", "testCommand"]`, `{"val": 12}`) + expectedResponse := `{"result": 144}` + if response != expectedResponse { + t.Errorf("expected response is not returned: expected %s, got %s", expectedResponse, response) + return + } +} + +func TestJailRPCSend(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + jailInstance := jail.Init("") + + // load Status JS and add test command to it + statusJS := geth.LoadFromFile(TESTDATA_STATUS_JS) + jailInstance.Parse(CHAT_ID_CALL, statusJS) + + // obtain VM for a given chat (to send custom JS to jailed version of Send()) + vm, err := jailInstance.GetVM(CHAT_ID_CALL) + if err != nil { + t.Errorf("cannot get VM: %v", err) + return + } + + _, err = vm.Run(` + var data = {"jsonrpc":"2.0","method":"eth_getBalance","params":["` + TEST_ADDRESS + `", "latest"],"id":1}; + var sendResult = web3.currentProvider.send(data) + console.log(JSON.stringify(sendResult)) + var sendResult = web3.fromWei(sendResult.result, "ether") + `) + if err != nil { + t.Errorf("cannot run custom code on VM: %v", err) + return + } + + value, err := vm.Get("sendResult") + if err != nil { + t.Errorf("cannot obtain result of balance check operation: %v", err) + return + } + + balance, err := value.ToFloat() + if err != nil { + t.Errorf("cannot obtain result of balance check operation: %v", err) + return + } + + if balance < 90 || balance > 100 { + t.Error("wrong balance (there should be lots of test Ether on that account)") + return + } + + t.Logf("Balance of %.2f ETH found on '%s' account", balance, TEST_ADDRESS) +} + +func TestJailMultipleInitSingletonJail(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + jailInstance1 := jail.Init("") + jailInstance2 := jail.Init("") + jailInstance3 := jail.New() + jailInstance4 := jail.GetInstance() + + if !reflect.DeepEqual(jailInstance1, jailInstance2) { + t.Error("singleton property of jail instance is violated") + } + if !reflect.DeepEqual(jailInstance2, jailInstance3) { + t.Error("singleton property of jail instance is violated") + } + if !reflect.DeepEqual(jailInstance3, jailInstance4) { + t.Error("singleton property of jail instance is violated") + } +} + +func TestJailGetVM(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + jailInstance := jail.Init("") + + expectedError := `VM[` + CHAT_ID_NON_EXISTENT + `] doesn't exist.` + _, err = jailInstance.GetVM(CHAT_ID_NON_EXISTENT) + if err == nil || err.Error() != expectedError { + t.Error("expected error, but call succeeded") + } + + // now let's create VM.. + jailInstance.Parse(CHAT_ID_CALL, ``) + // ..and see if VM becomes available + _, err = jailInstance.GetVM(CHAT_ID_CALL) + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/jail/testdata/commands.js b/jail/testdata/commands.js new file mode 100644 index 000000000..4a8b7eb21 --- /dev/null +++ b/jail/testdata/commands.js @@ -0,0 +1,320 @@ +status.command({ + name: "location", + description: "Send location", + color: "#9a5dcf", + preview: function (params) { + var text = status.components.text( + { + style: { + marginTop: 5, + marginHorizontal: 0, + fontSize: 14, + fontFamily: "font", + color: "black" + } + }, params.value); + var uri = "https://maps.googleapis.com/maps/api/staticmap?center=" + + params.value + + "&size=100x100&maptype=roadmap&key=AIzaSyBNsj1qoQEYPb3IllmWMAscuXW0eeuYqAA&language=en" + + "&markers=size:mid%7Ccolor:0xff0000%7Clabel:%7C" + + params.value; + + var image = status.components.image( + { + source: {uri: uri}, + style: { + width: 100, + height: 100 + } + } + ); + + return status.components.view({}, [text, image]); + } +}).param({ + name: "address", + type: status.types.TEXT, + placeholder: "Address" +}); + +var phones = [ + { + number: "89171111111", + description: "Number format 1" + }, + { + number: "89371111111", + description: "Number format 1" + }, + { + number: "+79171111111", + description: "Number format 2" + }, + { + number: "9171111111", + description: "Number format 3" + } +]; + +function suggestionsContainerStyle(suggestionsCount) { + return { + marginVertical: 1, + marginHorizontal: 0, + height: Math.min(150, (56 * suggestionsCount)), + backgroundColor: "white", + borderRadius: 5 + }; +} + +var suggestionContainerStyle = { + paddingLeft: 16, + backgroundColor: "white" +}; + +var suggestionSubContainerStyle = { + height: 56, + borderBottomWidth: 1, + borderBottomColor: "#0000001f" +}; + +var valueStyle = { + marginTop: 9, + fontSize: 14, + fontFamily: "font", + color: "#000000de" +}; + +var descriptionStyle = { + marginTop: 1.5, + fontSize: 14, + fontFamily: "font", + color: "#838c93de" +}; + +function startsWith(str1, str2) { + // String.startsWith(...) doesn't work in otto + return str1.lastIndexOf(str2, 0) == 0 && str1 != str2; +} + +function phoneSuggestions(params) { + var ph, suggestions; + if (!params.value || params.value == "") { + ph = phones; + } else { + ph = phones.filter(function (phone) { + return startsWith(phone.number, params.value); + }); + } + + if (ph.length == 0) { + return; + } + + suggestions = ph.map(function (phone) { + return status.components.touchable( + {onPress: [status.events.SET_VALUE, phone.number]}, + status.components.view(suggestionContainerStyle, + [status.components.view(suggestionSubContainerStyle, + [ + status.components.text( + {style: valueStyle}, + phone.number + ), + status.components.text( + {style: descriptionStyle}, + phone.description + ) + ])]) + ); + }); + + var view = status.components.scrollView( + suggestionsContainerStyle(ph.length), + suggestions + ); + + return {markup: view}; +} + +var phoneConfig = { + name: "phone", + description: "Send phone number", + color: "#5fc48d", + validator: function (params) { + return { + validationHandler: "phone", + parameters: [params.value] + }; + }, + params: [{ + name: "phone", + type: status.types.PHONE, + suggestions: phoneSuggestions, + placeholder: "Phone number" + }], + handler: function (params) { + return { + event: "sign-up", + params: [params.value] + }; + } +}; +status.response(phoneConfig); +status.command(phoneConfig); + +status.command({ + name: "help", + description: "Help", + color: "#7099e6", + /* Validator example + validator: function (params) { + if (params.value != "3") { + var error = status.components.view( + {backgroundColor: "red"}, + [status.components.text({}, "ooops :(")] + ); + return {errors: [error]} + } + },*/ + params: [{ + name: "query", + type: status.types.TEXT + }] +}); + +status.response({ + name: "confirmation-code", + color: "#7099e6", + description: "Confirmation code", + params: [{ + name: "code", + type: status.types.NUMBER + }], + handler: function (params) { + return { + event: "confirm-sign-up", + params: [params.value] + }; + }, + validator: function(params){ + if(!/^[\d]{4}$/.test(params.value)){ + var error = status.components.validationMessage( + "Confirmation code", + "Wrong format" + ); + + return {errors: [error]} + } + } +}); + +status.response({ + name: "keypair", + color: "#7099e6", + description: "Keypair password", + icon: "icon_lock_white", + params: [{ + name: "password", + type: status.types.PASSWORD + }], + handler: function (params) { + return { + event: "save-password", + params: [params.value] + }; + }, + preview: function (params) { + return status.components.text( + { + style: { + marginTop: 5, + marginHorizontal: 0, + fontSize: 14, + fontFamily: "font", + color: "black" + } + }, "*****"); + } +}); + +function walletView(params) { + if (params.value != "") { + var url = params.value; + if (!/^[a-zA-Z-_]+:/.test(url)) { + url = 'http://' + url; + } + + return {webViewUrl: url}; + } +} + +status.command({ + name: "browse", + description: "browser", + color: "#ffa500", + fullscreen: true, + suggestionsTrigger: 'on-send', + params: [{ + name: "webpage", + suggestions: walletView, + type: status.types.TEXT + }] +}); + +function validateBalance(params) { + try { + var val = web3.toWei(params.value, "ether"); + } catch (err) { + return { + errors: [ + status.components.validationMessage( + "Amount", + "Amount is not valid number"//err.message + ) + ] + }; + } + + var balance = web3.eth.getBalance(params.command.address); + if (bn(val).greaterThan(bn(balance))) { + return { + errors: [ + status.components.validationMessage( + "Amount", + "Not enough ETH on balance (" + + web3.fromWei(balance, "ether") + + " ETH)" + ) + ] + }; + } +} + +function sendTransaction(params) { + var data = { + from: params.command.from, + to: params.command.to, + value: web3.toWei(params.value, "ether") + }; + var hash = web3.eth.sendTransaction(data); + + return {"transaction-hash": hash}; +} + +status.command({ + name: "send", + color: "#5fc48d", + description: "Send transaction", + params: [{ + name: "amount", + type: status.types.NUMBER + }], + preview: function (params) { + return status.components.text( + {}, + params.value + " ETH" + ); + }, + handler: sendTransaction, + validator: validateBalance +}); diff --git a/jail/testdata/status.js b/jail/testdata/status.js new file mode 100644 index 000000000..7b75533ec --- /dev/null +++ b/jail/testdata/status.js @@ -0,0 +1,155 @@ +var _status_catalog = { + commands: {}, + responses: {} +}; + +function Command() { +} +function Response() { +} + +Command.prototype.addToCatalog = function () { + _status_catalog.commands[this.name] = this; +}; + +Command.prototype.param = function (parameter) { + this.params.push(parameter); + + return this; +}; + +Command.prototype.create = function (com) { + this.name = com.name; + this.description = com.description; + this.handler = com.handler; + this["has-handler"] = com.handler != null; + this.validator = com.validator; + this.color = com.color; + this.icon = com.icon; + this.params = com.params || []; + this.preview = com.preview; + this["suggestions-trigger"] = com.suggestionsTrigger || "on-change"; + this.fullscreen = com.fullscreen; + this.addToCatalog(); + + return this; +}; + + +Response.prototype = Object.create(Command.prototype); +Response.prototype.addToCatalog = function () { + _status_catalog.responses[this.name] = this; +}; +Response.prototype.onReceiveResponse = function (handler) { + this.onReceive = handler; +}; + +function call(pathStr, paramsStr) { + var params = JSON.parse(paramsStr), + path = JSON.parse(pathStr), + fn, res; + + fn = path.reduce(function (catalog, name) { + if (catalog && catalog[name]) { + return catalog[name]; + } + }, + _status_catalog + ); + + if (!fn) { + return null; + } + + res = fn(params); + + return JSON.stringify(res); +} + +function text(options, s) { + return ['text', options, s]; +} + +function view(options, elements) { + return ['view', options].concat(elements); +} + +function image(options) { + return ['image', options]; +} + +function touchable(options, element) { + return ['touchable', options, element]; +} + +function scrollView(options, elements) { + return ['scroll-view', options].concat(elements); +} + +function webView(url) { + return ['web-view', { + source: { + uri: url + }, + javaScriptEnabled: true + }]; +} + +function validationMessage(titleText, descriptionText) { + var titleStyle = { + color: "white", + fontSize: 12, + fontFamily: "sans-serif" + }; + var title = status.components.text(titleStyle, titleText); + + var descriptionStyle = { + color: "white", + fontSize: 12, + fontFamily: "sans-serif", + opacity: 0.9 + }; + var description = status.components.text(descriptionStyle, descriptionText); + + return status.components.view( + { + backgroundColor: "red", + height: 61, + paddingLeft: 16, + paddingTop: 14, + }, + [title, description] + ); +} + +var status = { + command: function (h) { + var command = new Command(); + return command.create(h); + }, + response: function (h) { + var response = new Response(); + return response.create(h); + }, + autorun: function (commandName) { + _status_catalog.autorun = commandName; + }, + types: { + TEXT: 'text', + NUMBER: 'number', + PHONE: 'phone', + PASSWORD: 'password' + }, + events: { + SET_VALUE: 'set-value' + }, + components: { + view: view, + text: text, + image: image, + touchable: touchable, + scrollView: scrollView, + webView: webView, + validationMessage: validationMessage + } +}; \ No newline at end of file diff --git a/src/web3.go b/jail/web3.go similarity index 99% rename from src/web3.go rename to jail/web3.go index 2ea7f0ab7..3e97eda37 100644 --- a/src/web3.go +++ b/jail/web3.go @@ -1,4 +1,4 @@ -package main +package jail const Web3_JS = ` require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -#include - -#ifdef IOS_DEPLOYMENT -#else -#include -#endif - - - -bool GethServiceSignalEvent( const char *jsonEvent ); - -#ifdef IOS_DEPLOYMENT - -bool GethServiceSignalEvent( const char *jsonEvent ) -{ - return true; -} - -#else - -static JavaVM *gJavaVM = NULL; -static jclass JavaClassPtr_GethService = NULL; -static jmethodID JavaMethodPtr_signalEvent = NULL; - -static bool JniLibraryInit( JNIEnv *env ); - -/*! - * @brief Get interface to JNI. - * - * @return true if thread should be detached from JNI. - */ -static bool JniAttach( JNIEnv **env ) -{ - jint status; - - if (gJavaVM == NULL) - { - env = NULL; - } - - status = (*gJavaVM)->GetEnv( gJavaVM, (void **)env, JNI_VERSION_1_6); - if (status == JNI_EDETACHED) - { - // attach thread to JNI - //(*gJavaVM)->AttachCurrentThread( gJavaVM, (void **)env, NULL ); // Oracle JNI API - (*gJavaVM)->AttachCurrentThread( gJavaVM, env, NULL ); // Android JNI API - return true; - } - else if (status != JNI_OK) - { - return false; - } - - return false; -} - -/*! - * @brief The VM calls JNI_OnLoad when the native library is loaded. - */ -JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) -{ - bool detach; - JNIEnv *env; - int result = JNI_VERSION_1_6; - - gJavaVM = vm; - - // attach thread to JNI - detach = JniAttach( &env ); - if (env == NULL) - { - // failed - gJavaVM = NULL; - return 0; - } - - if (!JniLibraryInit( env )) - { - // fail loading of JNI library - result = 0; - } - - if (detach) - { - // detach thread from JNI - (*gJavaVM)->DetachCurrentThread( gJavaVM ); - } - - if (result != JNI_VERSION_1_6) - { - gJavaVM = NULL; - } - - return result; -} - -/*! - * @brief Initialize library. - */ -bool JniLibraryInit( JNIEnv *env ) -{ - int i; - - JavaClassPtr_GethService = (*env)->FindClass( env, "com/statusim/geth/service/GethService" ); - if (JavaClassPtr_GethService == NULL) return false; - JavaClassPtr_GethService = (jclass)(*env)->NewGlobalRef( env, JavaClassPtr_GethService ); - if (JavaClassPtr_GethService == NULL) return false; - - struct { bool bStatic; jclass classPtr; jmethodID *methodPtr; const char *methodId; const char *params; } javaMethodDescriptors[] = - { - { true, JavaClassPtr_GethService, &JavaMethodPtr_signalEvent, "signalEvent", "(Ljava/lang/String;)V" }, - -// { false, JavaClassPtr_GethService, &JavaMethodPtr_someNonStaticMethod, "someNonStaticMethod", "(Ljava/lang/String;)V" }, - }; - - for (i = 0; i < sizeof(javaMethodDescriptors) / sizeof(javaMethodDescriptors[0]); i++) - { - if (javaMethodDescriptors[i].bStatic) - { - *(javaMethodDescriptors[i].methodPtr) = (*env)->GetStaticMethodID( env, javaMethodDescriptors[i].classPtr, javaMethodDescriptors[i].methodId, javaMethodDescriptors[i].params ); - } - else - { - *(javaMethodDescriptors[i].methodPtr) = (*env)->GetMethodID( env, javaMethodDescriptors[i].classPtr, javaMethodDescriptors[i].methodId, javaMethodDescriptors[i].params ); - } - - if (*(javaMethodDescriptors[i].methodPtr) == NULL) return false; - } - - return true; -} - -/*! - * @brief Calls static method signalEvent of class com.statusim.GethService. - * - * @param jsonEvent - UTF8 string - */ -bool GethServiceSignalEvent( const char *jsonEvent ) -{ - bool detach; - JNIEnv *env; - - // attach thread to JNI - detach = JniAttach( &env ); - if (env == NULL) - { - // failed - return false; - } - - jstring javaJsonEvent = NULL; - if (jsonEvent != NULL) - { - javaJsonEvent = (*env)->NewStringUTF( env, jsonEvent ); - } - - (*env)->CallStaticVoidMethod( env, JavaClassPtr_GethService, JavaMethodPtr_signalEvent, javaJsonEvent ); - - if (javaJsonEvent != NULL) (*env)->DeleteLocalRef( env, javaJsonEvent ); - - if (detach) - { - // detach thread from JNI - (*gJavaVM)->DetachCurrentThread( gJavaVM ); - } - - return true; -} -#endif diff --git a/src/node.go b/src/node.go deleted file mode 100644 index c1af626c5..000000000 --- a/src/node.go +++ /dev/null @@ -1,162 +0,0 @@ -package main - -import ( - "errors" - "flag" - "fmt" - "os" - "path" - "path/filepath" - "runtime" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/les" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/release" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/whisper" - "gopkg.in/urfave/cli.v1" -) - -const ( - clientIdentifier = "Geth" // Client identifier to advertise over the network - versionMajor = 1 // Major version component of the current release - versionMinor = 5 // Minor version component of the current release - versionPatch = 0 // Patch version component of the current release - versionMeta = "unstable" // Version metadata to append to the version string - - versionOracle = "0xfa7b9770ca4cb04296cac84f37736d4041251cdf" // Ethereum address of the Geth release oracle -) - -var ( - vString string // Combined textual representation of the version - rConfig release.Config // Structured version information and release oracle config - currentNode *node.Node // currently running geth node - c *cli.Context // the CLI context used to start the geth node - lightEthereum *les.LightEthereum // LES service - accountManager *accounts.Manager // the account manager attached to the currentNode - selectedAddress string // address of the account that was processed during the last call to SelectAccount() - whisperService *whisper.Whisper // whisper service - datadir string // data directory for geth - rpcport int = 8545 // RPC port (replaced in unit tests) - client *rpc.Client -) - -var ( - ErrDataDirPreprocessingFailed = errors.New("Failed to pre-process data directory") -) - -// MakeNode create a geth node entity -func MakeNode(inputDir string) *node.Node { - - datadir := inputDir - - // TODO remove admin rpcapi flag - set := flag.NewFlagSet("test", 0) - set.Bool("lightkdf", true, "Reduce key-derivation RAM & CPU usage at some expense of KDF strength") - set.Bool("shh", true, "whisper") - set.Bool("light", true, "disable eth") - set.Bool("testnet", true, "light test network") - set.Bool("rpc", true, "enable rpc") - set.String("rpcaddr", "localhost", "host for RPC") - set.Int("rpcport", rpcport, "rpc port") - set.String("rpccorsdomain", "*", "allow all domains") - set.String("verbosity", "3", "verbosity level") - set.String("rpcapi", "db,eth,net,web3,shh,personal,admin", "rpc api(s)") - set.String("datadir", datadir, "data directory for geth") - set.String("logdir", datadir, "log dir for glog") - c = cli.NewContext(nil, set, nil) - - // Construct the textual version string from the individual components - vString = fmt.Sprintf("%d.%d.%d", versionMajor, versionMinor, versionPatch) - - // Construct the version release oracle configuration - rConfig.Oracle = common.HexToAddress(versionOracle) - - rConfig.Major = uint32(versionMajor) - rConfig.Minor = uint32(versionMinor) - rConfig.Patch = uint32(versionPatch) - - utils.DebugSetup(c) - currentNode = makeNode(c, clientIdentifier, vString) - - return currentNode - -} - -func makeNode(ctx *cli.Context, name, version string) *node.Node { - nodeIn := utils.MakeNode(ctx, name, version) - utils.RegisterEthService(ctx, nodeIn, rConfig, makeDefaultExtra()) - // Whisper must be explicitly enabled, but is auto-enabled in --dev mode. - shhEnabled := ctx.GlobalBool(utils.WhisperEnabledFlag.Name) - shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name) - if shhEnabled || shhAutoEnabled { - utils.RegisterShhService(nodeIn) - } - return nodeIn -} - -// StartNode starts a geth node entity -func RunNode(nodeIn *node.Node) { - utils.StartNode(nodeIn) - - if err := nodeIn.Service(&accountManager); err != nil { - glog.V(logger.Warn).Infoln("cannot get account manager:", err) - } - if err := nodeIn.Service(&whisperService); err != nil { - glog.V(logger.Warn).Infoln("cannot get whisper service:", err) - } - if err := nodeIn.Service(&lightEthereum); err != nil { - glog.V(logger.Warn).Infoln("cannot get light ethereum service:", err) - } - lightEthereum.StatusBackend.SetTransactionQueueHandler(onSendTransactionRequest) - - client, _ = nodeIn.Attach() - nodeIn.Wait() -} - -func makeDefaultExtra() []byte { - var clientInfo = struct { - Version uint - Name string - GoVersion string - Os string - }{uint(versionMajor<<16 | versionMinor<<8 | versionPatch), clientIdentifier, runtime.Version(), runtime.GOOS} - extra, err := rlp.EncodeToBytes(clientInfo) - if err != nil { - glog.V(logger.Warn).Infoln("error setting canonical miner information:", err) - } - - if uint64(len(extra)) > params.MaximumExtraDataSize.Uint64() { - glog.V(logger.Warn).Infoln("error setting canonical miner information: extra exceeds", params.MaximumExtraDataSize) - glog.V(logger.Debug).Infof("extra: %x\n", extra) - return nil - } - return extra -} - -func preprocessDataDir(dataDir string) (string, error) { - testDataDir := path.Join(dataDir, "testnet", "keystore") - if _, err := os.Stat(testDataDir); os.IsNotExist(err) { - if err := os.MkdirAll(testDataDir, 0755); err != nil { - return dataDir, ErrDataDirPreprocessingFailed - } - } - - // copy over static peer nodes list (LES auto-discovery is not stable yet) - dst := filepath.Join(dataDir, "testnet", "static-nodes.json") - if _, err := os.Stat(dst); os.IsNotExist(err) { - src := filepath.Join("data", "static-nodes.json") - if err := copyFile(dst, src); err != nil { - return dataDir, err - } - } - - return dataDir, nil -} diff --git a/src/whisper.go b/src/whisper.go deleted file mode 100644 index 036194bf3..000000000 --- a/src/whisper.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -/* -#include -#include -extern bool GethServiceSignalEvent( const char *jsonEvent ); -*/ -import "C" -import ( - "encoding/json" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/whisper" -) - -var( - whisperFilters []int -) - - -func onWhisperMessage(message *whisper.Message) { - event := GethEvent{ - Type: "whisper", - Event: WhisperMessageEvent{ - Payload: string(message.Payload), - From: common.ToHex(crypto.FromECDSAPub(message.Recover())), - To: common.ToHex(crypto.FromECDSAPub(message.To)), - Sent: message.Sent.Unix(), - TTL: int64(message.TTL / time.Second), - Hash: common.ToHex(message.Hash.Bytes()), - }, - } - body, _ := json.Marshal(&event) - C.GethServiceSignalEvent(C.CString(string(body))) -} - -func doAddWhisperFilter(args whisper.NewFilterArgs) int { - var id int - filter := whisper.Filter{ - To: crypto.ToECDSAPub(common.FromHex(args.To)), - From: crypto.ToECDSAPub(common.FromHex(args.From)), - Topics: whisper.NewFilterTopics(args.Topics...), - Fn: onWhisperMessage, - } - - id = whisperService.Watch(filter) - whisperFilters = append(whisperFilters, id) - return id -} - -func doRemoveWhisperFilter(idFilter int) { - whisperService.Unwatch(idFilter) -} - -func doClearWhisperFilters() { - for _, idFilter := range whisperFilters { - doRemoveWhisperFilter(idFilter) - } - whisperFilters = nil -} \ No newline at end of file diff --git a/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go b/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go index da2862b17..a295bcee5 100644 --- a/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go +++ b/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go @@ -34,7 +34,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - "github.com/status-im/status-go/src/extkeys" + "github.com/status-im/status-go/extkeys" ) var ( diff --git a/vendor/github.com/ethereum/go-ethereum/accounts/key.go b/vendor/github.com/ethereum/go-ethereum/accounts/key.go index f56d32eac..f01154311 100644 --- a/vendor/github.com/ethereum/go-ethereum/accounts/key.go +++ b/vendor/github.com/ethereum/go-ethereum/accounts/key.go @@ -33,7 +33,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/pborman/uuid" - "github.com/status-im/status-go/src/extkeys" + "github.com/status-im/status-go/extkeys" ) const ( diff --git a/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go b/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go index a054f0006..700a4eadc 100644 --- a/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go +++ b/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go @@ -39,7 +39,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/randentropy" "github.com/pborman/uuid" - "github.com/status-im/status-go/src/extkeys" + "github.com/status-im/status-go/extkeys" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/scrypt" ) diff --git a/xgo/base/Dockerfile b/xgo/base/Dockerfile index 426c82640..3ddfc00ae 100644 --- a/xgo/base/Dockerfile +++ b/xgo/base/Dockerfile @@ -1,6 +1,6 @@ FROM karalabe/xgo-latest -# Inject the container entry point, the build script (patched for Status bindings conditional builds of library.c) +# Inject the container entry point, the build script (patched for Status bindings conditional builds of C code) ADD build.sh /build.sh ENV BUILD /build.sh RUN chmod +x $BUILD diff --git a/xgo/base/build.sh b/xgo/base/build.sh index ebe46681e..625e0e4f8 100644 --- a/xgo/base/build.sh +++ b/xgo/base/build.sh @@ -159,6 +159,7 @@ for TARGET in $TARGETS; do if [ "$PLATFORM" == "" ] || [ "$PLATFORM" == "." ] || [ "$PLATFORM" == "android" ]; then PLATFORM=16 # Jelly Bean 4.0.0 fi + CGO_STATUS_IM="-D ANDROID_DEPLOYMENT" if [ "$PLATFORM" -ge 16 ]; then CGO_CCPIE="-fPIE" CGO_LDPIE="-fPIE" @@ -190,11 +191,11 @@ for TARGET in $TARGETS; do if [ $XGOARCH == "." ] || [ $XGOARCH == "arm" ]; then CC=arm-linux-androideabi-gcc CXX=arm-linux-androideabi-g++ GOOS=android GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go get $V $X "${T[@]}" --ldflags="$V $LD" -d ./$PACK - CC=arm-linux-androideabi-gcc CXX=arm-linux-androideabi-g++ GOOS=android GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go build $V $X "${T[@]}" --ldflags="$V $EXT_LDPIE $EXT_LDAMD $LD" $BM -o "/build/$NAME-android-$PLATFORM-arm`extension android`" ./$PACK + CGO_CFLAGS="-D ANDROID_DEPLOYMENT" CC=arm-linux-androideabi-gcc CXX=arm-linux-androideabi-g++ GOOS=android GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE $CGO_STATUS_IM" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go build $V $X "${T[@]}" --ldflags="$V $EXT_LDPIE $EXT_LDAMD $LD" $BM -o "/build/$NAME-android-$PLATFORM-arm`extension android`" ./$PACK fi if [ $XGOARCH == "." ] || [ $XGOARCH == "aar" ]; then CC=arm-linux-androideabi-gcc CXX=arm-linux-androideabi-g++ GOOS=android GOARCH=arm GOARM=7 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d ./$PACK - CC=arm-linux-androideabi-gcc CXX=arm-linux-androideabi-g++ GOOS=android GOARCH=arm GOARM=7 CGO_ENABLED=1 go build $V $X "${T[@]}" --ldflags="$V $EXT_LDAMD $LD" --buildmode=c-shared -o "/build-android-aar/$NAME-android-$PLATFORM-arm.so" ./$PACK + CC=arm-linux-androideabi-gcc CXX=arm-linux-androideabi-g++ GOOS=android GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${T[@]}" --ldflags="$V $EXT_LDAMD $LD" --buildmode=c-shared -o "/build-android-aar/$NAME-android-$PLATFORM-arm.so" ./$PACK fi fi fi @@ -214,11 +215,11 @@ for TARGET in $TARGETS; do if [ $XGOARCH == "." ] || [ $XGOARCH == "386" ]; then CC=i686-linux-android-gcc CXX=i686-linux-android-g++ GOOS=android GOARCH=386 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go get $V $X "${T[@]}" --ldflags="$V $LD" -d ./$PACK - CC=i686-linux-android-gcc CXX=i686-linux-android-g++ GOOS=android GOARCH=386 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go build $V $X "${T[@]}" --ldflags="$V $EXT_LDPIE $LD" $BM -o "/build/$NAME-android-$PLATFORM-386`extension android`" ./$PACK + CC=i686-linux-android-gcc CXX=i686-linux-android-g++ GOOS=android GOARCH=386 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE $CGO_STATUS_IM" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go build $V $X "${T[@]}" --ldflags="$V $EXT_LDPIE $LD" $BM -o "/build/$NAME-android-$PLATFORM-386`extension android`" ./$PACK fi if [ $XGOARCH == "." ] || [ $XGOARCH == "aar" ]; then CC=i686-linux-android-gcc CXX=i686-linux-android-g++ GOOS=android GOARCH=386 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d ./$PACK - CC=i686-linux-android-gcc CXX=i686-linux-android-g++ GOOS=android GOARCH=386 CGO_ENABLED=1 go build $V $X "${T[@]}" --ldflags="$V $LD" --buildmode=c-shared -o "/build-android-aar/$NAME-android-$PLATFORM-386.so" ./$PACK + CC=i686-linux-android-gcc CXX=i686-linux-android-g++ GOOS=android GOARCH=386 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${T[@]}" --ldflags="$V $LD" --buildmode=c-shared -o "/build-android-aar/$NAME-android-$PLATFORM-386.so" ./$PACK fi fi if [ "$PLATFORM" -ge 21 ] && ([ $XGOARCH == "." ] || [ $XGOARCH == "arm64" ] || [ $XGOARCH == "aar" ]); then @@ -234,14 +235,15 @@ for TARGET in $TARGETS; do if [ $XGOARCH == "." ] || [ $XGOARCH == "arm64" ]; then CC=aarch64-linux-android-gcc CXX=aarch64-linux-android-g++ GOOS=android GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go get $V $X "${T[@]}" --ldflags="$V $LD" -d ./$PACK - CC=aarch64-linux-android-gcc CXX=aarch64-linux-android-g++ GOOS=android GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go build $V $X "${T[@]}" --ldflags="$V $EXT_LDPIE $LD" $BM -o "/build/$NAME-android-$PLATFORM-arm64`extension android`" ./$PACK + CC=aarch64-linux-android-gcc CXX=aarch64-linux-android-g++ GOOS=android GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_CCPIE $CGO_STATUS_IM" CGO_CXXFLAGS="$CGO_CCPIE" CGO_LDFLAGS="$CGO_LDPIE" go build $V $X "${T[@]}" --ldflags="$V $EXT_LDPIE $LD" $BM -o "/build/$NAME-android-$PLATFORM-arm64`extension android`" ./$PACK fi if [ $XGOARCH == "." ] || [ $XGOARCH == "aar" ]; then CC=aarch64-linux-android-gcc CXX=aarch64-linux-android-g++ GOOS=android GOARCH=arm64 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d ./$PACK - CC=aarch64-linux-android-gcc CXX=aarch64-linux-android-g++ GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build $V $X "${T[@]}" --ldflags="$V $LD" --buildmode=c-shared -o "/build-android-aar/$NAME-android-$PLATFORM-arm64.so" ./$PACK + CC=aarch64-linux-android-gcc CXX=aarch64-linux-android-g++ GOOS=android GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${T[@]}" --ldflags="$V $LD" --buildmode=c-shared -o "/build-android-aar/$NAME-android-$PLATFORM-arm64.so" ./$PACK fi fi fi + unset CGO_STATUS_IM # good to let that extra var go away # Assemble the Android Archive from the built shared libraries if [ $XGOARCH == "." ] || [ $XGOARCH == "aar" ]; then title=${NAME^} @@ -491,7 +493,7 @@ for TARGET in $TARGETS; do if [ "$GO_VERSION" -lt 160 ]; then LDSTRIP="-s" fi - STATUS_IM_CFLAGS="-D IOS_DEPLOYMENT" + CGO_STATUS_IM="-D IOS_DEPLOYMENT" # Cross compile to all available iOS and simulator platforms if [ -d "$IOS_NDK_ARM_7" ] && ([ $XGOARCH == "." ] || [ $XGOARCH == "arm-7" ] || [ $XGOARCH == "framework" ]); then echo "Bootstrapping ios-$PLATFORM/arm-7..." @@ -505,7 +507,7 @@ for TARGET in $TARGETS; do CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm GOARM=7 CGO_ENABLED=1 go build $V $X "${IOSTAGS[@]}" --ldflags="$LDSTRIP $V $LD" $BM -o "/build/$NAME-ios-$PLATFORM-armv7`extension darwin`" ./$PACK fi if [ $XGOARCH == "." ] || [ $XGOARCH == "framework" ]; then - CGO_CFLAGS="-D IOS_DEPLOYMENT" CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm GOARM=7 CGO_ENABLED=1 go build $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" --buildmode=c-archive -o "/build-ios-fw/$NAME-ios-$PLATFORM-armv7.a" ./$PACK + CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" --buildmode=c-archive -o "/build-ios-fw/$NAME-ios-$PLATFORM-armv7.a" ./$PACK fi echo "Cleaning up Go runtime for ios-$PLATFORM/arm-7..." rm -rf /usr/local/go/pkg/darwin_arm @@ -519,10 +521,10 @@ for TARGET in $TARGETS; do CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ HOST=arm-apple-darwin11 PREFIX=/usr/local $BUILD_DEPS /deps ${DEPS_ARGS[@]} CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go get $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" -d ./$PACK if [ $XGOARCH == "." ] || [ $XGOARCH == "arm64" ]; then - CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build $V $X "${IOSTAGS[@]}" --ldflags="$LDSTRIP $V $LD" $BM -o "/build/$NAME-ios-$PLATFORM-arm64`extension darwin`" ./$PACK + CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${IOSTAGS[@]}" --ldflags="$LDSTRIP $V $LD" $BM -o "/build/$NAME-ios-$PLATFORM-arm64`extension darwin`" ./$PACK fi if [ $XGOARCH == "." ] || [ $XGOARCH == "framework" ]; then - CGO_CFLAGS="-D IOS_DEPLOYMENT" CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" --buildmode=c-archive -o "/build-ios-fw/$NAME-ios-$PLATFORM-arm64.a" ./$PACK + CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" --buildmode=c-archive -o "/build-ios-fw/$NAME-ios-$PLATFORM-arm64.a" ./$PACK fi echo "Cleaning up Go runtime for ios-$PLATFORM/arm64..." rm -rf /usr/local/go/pkg/darwin_arm64 @@ -537,15 +539,16 @@ for TARGET in $TARGETS; do CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ HOST=arm-apple-darwin11 PREFIX=/usr/local $BUILD_DEPS /deps ${DEPS_ARGS[@]} CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go get $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" -d ./$PACK if [ $XGOARCH == "." ] || [ $XGOARCH == "amd64" ]; then - CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build $V $X "${IOSTAGS[@]}" --ldflags="$LDSTRIP $V $LD" $BM -o "/build/$NAME-ios-$PLATFORM-x86_64`extension darwin`" ./$PACK + CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${IOSTAGS[@]}" --ldflags="$LDSTRIP $V $LD" $BM -o "/build/$NAME-ios-$PLATFORM-x86_64`extension darwin`" ./$PACK fi if [ $XGOARCH == "." ] || [ $XGOARCH == "framework" ]; then - CGO_CFLAGS="-D IOS_DEPLOYMENT" CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" --buildmode=c-archive -o "/build-ios-fw/$NAME-ios-$PLATFORM-x86_64.a" ./$PACK + CC=arm-apple-darwin11-clang CXX=arm-apple-darwin11-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_STATUS_IM" go build $V $X "${IOSTAGS[@]}" --ldflags="$V $LD" --buildmode=c-archive -o "/build-ios-fw/$NAME-ios-$PLATFORM-x86_64.a" ./$PACK fi echo "Cleaning up Go runtime for ios-$PLATFORM/amd64..." rm -rf /usr/local/go/pkg/darwin_amd64 mv /usr/local/go/pkg/darwin_amd64_bak /usr/local/go/pkg/darwin_amd64 fi + unset CGO_STATUS_IM # Assemble the iOS framework from the built binaries if [ $XGOARCH == "." ] || [ $XGOARCH == "framework" ]; then title=${NAME^}