123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632 |
- // Copyright 2018 The go-ethereum Authors
- // This file is part of go-ethereum.
- //
- // go-ethereum is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // go-ethereum is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
- //
- package rules
- import (
- "fmt"
- "math/big"
- "strings"
- "testing"
- "github.com/ethereum/go-ethereum/accounts"
- "github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/common/hexutil"
- "github.com/ethereum/go-ethereum/core/types"
- "github.com/ethereum/go-ethereum/internal/ethapi"
- "github.com/ethereum/go-ethereum/signer/core"
- "github.com/ethereum/go-ethereum/signer/storage"
- )
- const JS = `
- /**
- This is an example implementation of a Javascript rule file.
- When the signer receives a request over the external API, the corresponding method is evaluated.
- Three things can happen:
- 1. The method returns "Approve". This means the operation is permitted.
- 2. The method returns "Reject". This means the operation is rejected.
- 3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
- that the operation will continue to manual processing, via the regular UI method chosen by the user.
- [*] Note: Future version of the ruleset may use more complex json-based returnvalues, making it possible to not
- only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
- accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
- **/
- function ApproveListing(request){
- console.log("In js approve listing");
- console.log(request.accounts[3].Address)
- console.log(request.meta.Remote)
- return "Approve"
- }
- function ApproveTx(request){
- console.log("test");
- console.log("from");
- return "Reject";
- }
- function test(thing){
- console.log(thing.String())
- }
- `
- func mixAddr(a string) (*common.MixedcaseAddress, error) {
- return common.NewMixedcaseAddressFromString(a)
- }
- type alwaysDenyUI struct{}
- func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
- }
- func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
- return core.SignTxResponse{Transaction: request.Transaction, Approved: false, Password: ""}, nil
- }
- func (alwaysDenyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
- return core.SignDataResponse{Approved: false, Password: ""}, nil
- }
- func (alwaysDenyUI) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
- return core.ExportResponse{Approved: false}, nil
- }
- func (alwaysDenyUI) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
- return core.ImportResponse{Approved: false, OldPassword: "", NewPassword: ""}, nil
- }
- func (alwaysDenyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
- return core.ListResponse{Accounts: nil}, nil
- }
- func (alwaysDenyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
- return core.NewAccountResponse{Approved: false, Password: ""}, nil
- }
- func (alwaysDenyUI) ShowError(message string) {
- panic("implement me")
- }
- func (alwaysDenyUI) ShowInfo(message string) {
- panic("implement me")
- }
- func (alwaysDenyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
- panic("implement me")
- }
- func initRuleEngine(js string) (*rulesetUI, error) {
- r, err := NewRuleEvaluator(&alwaysDenyUI{}, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
- if err != nil {
- return nil, fmt.Errorf("failed to create js engine: %v", err)
- }
- if err = r.Init(js); err != nil {
- return nil, fmt.Errorf("failed to load bootstrap js: %v", err)
- }
- return r, nil
- }
- func TestListRequest(t *testing.T) {
- accs := make([]core.Account, 5)
- for i := range accs {
- addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
- acc := core.Account{
- Address: common.BytesToAddress(common.Hex2Bytes(addr)),
- URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
- }
- accs[i] = acc
- }
- js := `function ApproveListing(){ return "Approve" }`
- r, err := initRuleEngine(js)
- if err != nil {
- t.Errorf("Couldn't create evaluator %v", err)
- return
- }
- resp, err := r.ApproveListing(&core.ListRequest{
- Accounts: accs,
- Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
- })
- if len(resp.Accounts) != len(accs) {
- t.Errorf("Expected check to resolve to 'Approve'")
- }
- }
- func TestSignTxRequest(t *testing.T) {
- js := `
- function ApproveTx(r){
- console.log("transaction.from", r.transaction.from);
- console.log("transaction.to", r.transaction.to);
- console.log("transaction.value", r.transaction.value);
- console.log("transaction.nonce", r.transaction.nonce);
- if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
- if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
- }`
- r, err := initRuleEngine(js)
- if err != nil {
- t.Errorf("Couldn't create evaluator %v", err)
- return
- }
- to, err := mixAddr("000000000000000000000000000000000000dead")
- if err != nil {
- t.Error(err)
- return
- }
- from, err := mixAddr("0000000000000000000000000000000000001337")
- if err != nil {
- t.Error(err)
- return
- }
- fmt.Printf("to %v", to.Address().String())
- resp, err := r.ApproveTx(&core.SignTxRequest{
- Transaction: core.SendTxArgs{
- From: *from,
- To: to},
- Callinfo: nil,
- Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
- })
- if err != nil {
- t.Errorf("Unexpected error %v", err)
- }
- if !resp.Approved {
- t.Errorf("Expected check to resolve to 'Approve'")
- }
- }
- type dummyUI struct {
- calls []string
- }
- func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
- d.calls = append(d.calls, "ApproveTx")
- return core.SignTxResponse{}, core.ErrRequestDenied
- }
- func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
- d.calls = append(d.calls, "ApproveSignData")
- return core.SignDataResponse{}, core.ErrRequestDenied
- }
- func (d *dummyUI) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
- d.calls = append(d.calls, "ApproveExport")
- return core.ExportResponse{}, core.ErrRequestDenied
- }
- func (d *dummyUI) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
- d.calls = append(d.calls, "ApproveImport")
- return core.ImportResponse{}, core.ErrRequestDenied
- }
- func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
- d.calls = append(d.calls, "ApproveListing")
- return core.ListResponse{}, core.ErrRequestDenied
- }
- func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
- d.calls = append(d.calls, "ApproveNewAccount")
- return core.NewAccountResponse{}, core.ErrRequestDenied
- }
- func (d *dummyUI) ShowError(message string) {
- d.calls = append(d.calls, "ShowError")
- }
- func (d *dummyUI) ShowInfo(message string) {
- d.calls = append(d.calls, "ShowInfo")
- }
- func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
- d.calls = append(d.calls, "OnApprovedTx")
- }
- func (d *dummyUI) OnSignerStartup(info core.StartupInfo) {
- }
- //TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
- func TestForwarding(t *testing.T) {
- js := ""
- ui := &dummyUI{make([]string, 0)}
- jsBackend := storage.NewEphemeralStorage()
- credBackend := storage.NewEphemeralStorage()
- r, err := NewRuleEvaluator(ui, jsBackend, credBackend)
- if err != nil {
- t.Fatalf("Failed to create js engine: %v", err)
- }
- if err = r.Init(js); err != nil {
- t.Fatalf("Failed to load bootstrap js: %v", err)
- }
- r.ApproveSignData(nil)
- r.ApproveTx(nil)
- r.ApproveImport(nil)
- r.ApproveNewAccount(nil)
- r.ApproveListing(nil)
- r.ApproveExport(nil)
- r.ShowError("test")
- r.ShowInfo("test")
- //This one is not forwarded
- r.OnApprovedTx(ethapi.SignTransactionResult{})
- expCalls := 8
- if len(ui.calls) != expCalls {
- t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ","))
- }
- }
- func TestMissingFunc(t *testing.T) {
- r, err := initRuleEngine(JS)
- if err != nil {
- t.Errorf("Couldn't create evaluator %v", err)
- return
- }
- _, err = r.execute("MissingMethod", "test")
- if err == nil {
- t.Error("Expected error")
- }
- approved, err := r.checkApproval("MissingMethod", nil, nil)
- if err == nil {
- t.Errorf("Expected missing method to yield error'")
- }
- if approved {
- t.Errorf("Expected missing method to cause non-approval")
- }
- fmt.Printf("Err %v", err)
- }
- func TestStorage(t *testing.T) {
- js := `
- function testStorage(){
- storage.Put("mykey", "myvalue")
- a = storage.Get("mykey")
- storage.Put("mykey", ["a", "list"]) // Should result in "a,list"
- a += storage.Get("mykey")
- storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]"
- a += storage.Get("mykey")
- storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
- a += storage.Get("mykey")
- a += storage.Get("missingkey") //Missing keys should result in empty string
- storage.Put("","missing key==noop") // Can't store with 0-length key
- a += storage.Get("") // Should result in ''
- var b = new BigNumber(2)
- var c = new BigNumber(16)//"0xf0",16)
- var d = b.plus(c)
- console.log(d)
- return a
- }
- `
- r, err := initRuleEngine(js)
- if err != nil {
- t.Errorf("Couldn't create evaluator %v", err)
- return
- }
- v, err := r.execute("testStorage", nil)
- if err != nil {
- t.Errorf("Unexpected error %v", err)
- }
- retval, err := v.ToString()
- if err != nil {
- t.Errorf("Unexpected error %v", err)
- }
- exp := `myvaluea,list[object Object]{"an":"object"}`
- if retval != exp {
- t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
- }
- fmt.Printf("Err %v", err)
- }
- const ExampleTxWindow = `
- function big(str){
- if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
- return new BigNumber(str)
- }
- // Time window: 1 week
- var window = 1000* 3600*24*7;
- // Limit : 1 ether
- var limit = new BigNumber("1e18");
- function isLimitOk(transaction){
- var value = big(transaction.value)
- // Start of our window function
- var windowstart = new Date().getTime() - window;
- var txs = [];
- var stored = storage.Get('txs');
- if(stored != ""){
- txs = JSON.parse(stored)
- }
- // First, remove all that have passed out of the time-window
- var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
- console.log(txs, newtxs.length);
- // Secondly, aggregate the current sum
- sum = new BigNumber(0)
- sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
- console.log("ApproveTx > Sum so far", sum);
- console.log("ApproveTx > Requested", value.toNumber());
- // Would we exceed weekly limit ?
- return sum.plus(value).lt(limit)
- }
- function ApproveTx(r){
- console.log(r)
- console.log(typeof(r))
- if (isLimitOk(r.transaction)){
- return "Approve"
- }
- return "Nope"
- }
- /**
- * OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
- * 'response_str' contains the return value that will be sent to the external caller.
- * The return value from this method is ignore - the reason for having this callback is to allow the
- * ruleset to keep track of approved transactions.
- *
- * When implementing rate-limited rules, this callback should be used.
- * If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
- * then accepts the transaction, this method will be called.
- *
- * TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
- */
- function OnApprovedTx(resp){
- var value = big(resp.tx.value)
- var txs = []
- // Load stored transactions
- var stored = storage.Get('txs');
- if(stored != ""){
- txs = JSON.parse(stored)
- }
- // Add this to the storage
- txs.push({tstamp: new Date().getTime(), value: value});
- storage.Put("txs", JSON.stringify(txs));
- }
- `
- func dummyTx(value hexutil.Big) *core.SignTxRequest {
- to, _ := mixAddr("000000000000000000000000000000000000dead")
- from, _ := mixAddr("000000000000000000000000000000000000dead")
- n := hexutil.Uint64(3)
- gas := hexutil.Uint64(21000)
- gasPrice := hexutil.Big(*big.NewInt(2000000))
- return &core.SignTxRequest{
- Transaction: core.SendTxArgs{
- From: *from,
- To: to,
- Value: value,
- Nonce: n,
- GasPrice: gasPrice,
- Gas: gas,
- },
- Callinfo: []core.ValidationInfo{
- {Typ: "Warning", Message: "All your base are bellong to us"},
- },
- Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
- }
- }
- func dummyTxWithV(value uint64) *core.SignTxRequest {
- v := big.NewInt(0).SetUint64(value)
- h := hexutil.Big(*v)
- return dummyTx(h)
- }
- func dummySigned(value *big.Int) *types.Transaction {
- to := common.HexToAddress("000000000000000000000000000000000000dead")
- gas := uint64(21000)
- gasPrice := big.NewInt(2000000)
- data := make([]byte, 0)
- return types.NewTransaction(3, to, value, gas, gasPrice, data)
- }
- func TestLimitWindow(t *testing.T) {
- r, err := initRuleEngine(ExampleTxWindow)
- if err != nil {
- t.Errorf("Couldn't create evaluator %v", err)
- return
- }
- // 0.3 ether: 429D069189E0000 wei
- v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
- h := hexutil.Big(*v)
- // The first three should succeed
- for i := 0; i < 3; i++ {
- unsigned := dummyTx(h)
- resp, err := r.ApproveTx(unsigned)
- if err != nil {
- t.Errorf("Unexpected error %v", err)
- }
- if !resp.Approved {
- t.Errorf("Expected check to resolve to 'Approve'")
- }
- // Create a dummy signed transaction
- response := ethapi.SignTransactionResult{
- Tx: dummySigned(v),
- Raw: common.Hex2Bytes("deadbeef"),
- }
- r.OnApprovedTx(response)
- }
- // Fourth should fail
- resp, err := r.ApproveTx(dummyTx(h))
- if resp.Approved {
- t.Errorf("Expected check to resolve to 'Reject'")
- }
- }
- // dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
- type dontCallMe struct {
- t *testing.T
- }
- func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
- }
- func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
- d.t.Fatalf("Did not expect next-handler to be called")
- return core.SignTxResponse{}, core.ErrRequestDenied
- }
- func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
- d.t.Fatalf("Did not expect next-handler to be called")
- return core.SignDataResponse{}, core.ErrRequestDenied
- }
- func (d *dontCallMe) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) {
- d.t.Fatalf("Did not expect next-handler to be called")
- return core.ExportResponse{}, core.ErrRequestDenied
- }
- func (d *dontCallMe) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) {
- d.t.Fatalf("Did not expect next-handler to be called")
- return core.ImportResponse{}, core.ErrRequestDenied
- }
- func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
- d.t.Fatalf("Did not expect next-handler to be called")
- return core.ListResponse{}, core.ErrRequestDenied
- }
- func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
- d.t.Fatalf("Did not expect next-handler to be called")
- return core.NewAccountResponse{}, core.ErrRequestDenied
- }
- func (d *dontCallMe) ShowError(message string) {
- d.t.Fatalf("Did not expect next-handler to be called")
- }
- func (d *dontCallMe) ShowInfo(message string) {
- d.t.Fatalf("Did not expect next-handler to be called")
- }
- func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) {
- d.t.Fatalf("Did not expect next-handler to be called")
- }
- //TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
- // if it does, that would be bad since developers may rely on that to store data,
- // instead of using the disk-based data storage
- func TestContextIsCleared(t *testing.T) {
- js := `
- function ApproveTx(){
- if (typeof foobar == 'undefined') {
- foobar = "Approve"
- }
- console.log(foobar)
- if (foobar == "Approve"){
- foobar = "Reject"
- }else{
- foobar = "Approve"
- }
- return foobar
- }
- `
- ui := &dontCallMe{t}
- r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
- if err != nil {
- t.Fatalf("Failed to create js engine: %v", err)
- }
- if err = r.Init(js); err != nil {
- t.Fatalf("Failed to load bootstrap js: %v", err)
- }
- tx := dummyTxWithV(0)
- r1, err := r.ApproveTx(tx)
- r2, err := r.ApproveTx(tx)
- if r1.Approved != r2.Approved {
- t.Errorf("Expected execution context to be cleared between executions")
- }
- }
- func TestSignData(t *testing.T) {
- js := `function ApproveListing(){
- return "Approve"
- }
- function ApproveSignData(r){
- if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
- {
- if(r.message.indexOf("bazonk") >= 0){
- return "Approve"
- }
- return "Reject"
- }
- // Otherwise goes to manual processing
- }`
- r, err := initRuleEngine(js)
- if err != nil {
- t.Errorf("Couldn't create evaluator %v", err)
- return
- }
- message := []byte("baz bazonk foo")
- hash, msg := core.SignHash(message)
- raw := hexutil.Bytes(message)
- addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
- fmt.Printf("address %v %v\n", addr.String(), addr.Original())
- resp, err := r.ApproveSignData(&core.SignDataRequest{
- Address: *addr,
- Message: msg,
- Hash: hash,
- Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
- Rawdata: raw,
- })
- if err != nil {
- t.Fatalf("Unexpected error %v", err)
- }
- if !resp.Approved {
- t.Fatalf("Expected approved")
- }
- }
|