github.com/Unheilbar/quorum@v1.0.0/signer/rules/rules_test.go (about)

     1  // Copyright 2018 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package rules
    18  
    19  import (
    20  	"fmt"
    21  	"math/big"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/ethereum/go-ethereum/accounts"
    26  	"github.com/ethereum/go-ethereum/common"
    27  	"github.com/ethereum/go-ethereum/common/hexutil"
    28  	"github.com/ethereum/go-ethereum/core/types"
    29  	"github.com/ethereum/go-ethereum/internal/ethapi"
    30  	"github.com/ethereum/go-ethereum/signer/core"
    31  	"github.com/ethereum/go-ethereum/signer/storage"
    32  )
    33  
    34  const JS = `
    35  /**
    36  This is an example implementation of a Javascript rule file.
    37  
    38  When the signer receives a request over the external API, the corresponding method is evaluated.
    39  Three things can happen:
    40  
    41  1. The method returns "Approve". This means the operation is permitted.
    42  2. The method returns "Reject". This means the operation is rejected.
    43  3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
    44  that the operation will continue to manual processing, via the regular UI method chosen by the user.
    45  
    46  [*] Note: Future version of the ruleset may use more complex json-based returnvalues, making it possible to not
    47  only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
    48  accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
    49  
    50  **/
    51  
    52  function ApproveListing(request){
    53  	console.log("In js approve listing");
    54  	console.log(request.accounts[3].Address)
    55  	console.log(request.meta.Remote)
    56  	return "Approve"
    57  }
    58  
    59  function ApproveTx(request){
    60  	console.log("test");
    61  	console.log("from");
    62  	return "Reject";
    63  }
    64  
    65  function test(thing){
    66  	console.log(thing.String())
    67  }
    68  
    69  `
    70  
    71  func mixAddr(a string) (*common.MixedcaseAddress, error) {
    72  	return common.NewMixedcaseAddressFromString(a)
    73  }
    74  
    75  type alwaysDenyUI struct{}
    76  
    77  func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
    78  	return core.UserInputResponse{}, nil
    79  }
    80  func (alwaysDenyUI) RegisterUIServer(api *core.UIServerAPI) {
    81  }
    82  
    83  func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
    84  }
    85  
    86  func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
    87  	return core.SignTxResponse{Transaction: request.Transaction, Approved: false}, nil
    88  }
    89  
    90  func (alwaysDenyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
    91  	return core.SignDataResponse{Approved: false}, nil
    92  }
    93  
    94  func (alwaysDenyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
    95  	return core.ListResponse{Accounts: nil}, nil
    96  }
    97  
    98  func (alwaysDenyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
    99  	return core.NewAccountResponse{Approved: false}, nil
   100  }
   101  
   102  func (alwaysDenyUI) ShowError(message string) {
   103  	panic("implement me")
   104  }
   105  
   106  func (alwaysDenyUI) ShowInfo(message string) {
   107  	panic("implement me")
   108  }
   109  
   110  func (alwaysDenyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
   111  	panic("implement me")
   112  }
   113  
   114  func initRuleEngine(js string) (*rulesetUI, error) {
   115  	r, err := NewRuleEvaluator(&alwaysDenyUI{}, storage.NewEphemeralStorage())
   116  	if err != nil {
   117  		return nil, fmt.Errorf("failed to create js engine: %v", err)
   118  	}
   119  	if err = r.Init(js); err != nil {
   120  		return nil, fmt.Errorf("failed to load bootstrap js: %v", err)
   121  	}
   122  	return r, nil
   123  }
   124  
   125  func TestListRequest(t *testing.T) {
   126  	accs := make([]accounts.Account, 5)
   127  
   128  	for i := range accs {
   129  		addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
   130  		acc := accounts.Account{
   131  			Address: common.BytesToAddress(common.Hex2Bytes(addr)),
   132  			URL:     accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
   133  		}
   134  		accs[i] = acc
   135  	}
   136  
   137  	js := `function ApproveListing(){ return "Approve" }`
   138  
   139  	r, err := initRuleEngine(js)
   140  	if err != nil {
   141  		t.Errorf("Couldn't create evaluator %v", err)
   142  		return
   143  	}
   144  	resp, _ := r.ApproveListing(&core.ListRequest{
   145  		Accounts: accs,
   146  		Meta:     core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   147  	})
   148  	if len(resp.Accounts) != len(accs) {
   149  		t.Errorf("Expected check to resolve to 'Approve'")
   150  	}
   151  }
   152  
   153  func TestSignTxRequest(t *testing.T) {
   154  	js := `
   155  	function ApproveTx(r){
   156  		console.log("transaction.from", r.transaction.from);
   157  		console.log("transaction.to", r.transaction.to);
   158  		console.log("transaction.value", r.transaction.value);
   159  		console.log("transaction.nonce", r.transaction.nonce);
   160  		if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
   161  		if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
   162  	}`
   163  
   164  	r, err := initRuleEngine(js)
   165  	if err != nil {
   166  		t.Errorf("Couldn't create evaluator %v", err)
   167  		return
   168  	}
   169  	to, err := mixAddr("000000000000000000000000000000000000dead")
   170  	if err != nil {
   171  		t.Error(err)
   172  		return
   173  	}
   174  	from, err := mixAddr("0000000000000000000000000000000000001337")
   175  
   176  	if err != nil {
   177  		t.Error(err)
   178  		return
   179  	}
   180  	t.Logf("to %v", to.Address().String())
   181  	resp, err := r.ApproveTx(&core.SignTxRequest{
   182  		Transaction: core.SendTxArgs{
   183  			From: *from,
   184  			To:   to},
   185  		Callinfo: nil,
   186  		Meta:     core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   187  	})
   188  	if err != nil {
   189  		t.Errorf("Unexpected error %v", err)
   190  	}
   191  	if !resp.Approved {
   192  		t.Errorf("Expected check to resolve to 'Approve'")
   193  	}
   194  }
   195  
   196  type dummyUI struct {
   197  	calls []string
   198  }
   199  
   200  func (d *dummyUI) RegisterUIServer(api *core.UIServerAPI) {
   201  	panic("implement me")
   202  }
   203  
   204  func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
   205  	d.calls = append(d.calls, "OnInputRequired")
   206  	return core.UserInputResponse{}, nil
   207  }
   208  
   209  func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
   210  	d.calls = append(d.calls, "ApproveTx")
   211  	return core.SignTxResponse{}, core.ErrRequestDenied
   212  }
   213  
   214  func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
   215  	d.calls = append(d.calls, "ApproveSignData")
   216  	return core.SignDataResponse{}, core.ErrRequestDenied
   217  }
   218  
   219  func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
   220  	d.calls = append(d.calls, "ApproveListing")
   221  	return core.ListResponse{}, core.ErrRequestDenied
   222  }
   223  
   224  func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
   225  	d.calls = append(d.calls, "ApproveNewAccount")
   226  	return core.NewAccountResponse{}, core.ErrRequestDenied
   227  }
   228  
   229  func (d *dummyUI) ShowError(message string) {
   230  	d.calls = append(d.calls, "ShowError")
   231  }
   232  
   233  func (d *dummyUI) ShowInfo(message string) {
   234  	d.calls = append(d.calls, "ShowInfo")
   235  }
   236  
   237  func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
   238  	d.calls = append(d.calls, "OnApprovedTx")
   239  }
   240  
   241  func (d *dummyUI) OnSignerStartup(info core.StartupInfo) {
   242  }
   243  
   244  // TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
   245  func TestForwarding(t *testing.T) {
   246  	js := ""
   247  	ui := &dummyUI{make([]string, 0)}
   248  	jsBackend := storage.NewEphemeralStorage()
   249  	r, err := NewRuleEvaluator(ui, jsBackend)
   250  	if err != nil {
   251  		t.Fatalf("Failed to create js engine: %v", err)
   252  	}
   253  	if err = r.Init(js); err != nil {
   254  		t.Fatalf("Failed to load bootstrap js: %v", err)
   255  	}
   256  	r.ApproveSignData(nil)
   257  	r.ApproveTx(nil)
   258  	r.ApproveNewAccount(nil)
   259  	r.ApproveListing(nil)
   260  	r.ShowError("test")
   261  	r.ShowInfo("test")
   262  
   263  	//This one is not forwarded
   264  	r.OnApprovedTx(ethapi.SignTransactionResult{})
   265  
   266  	expCalls := 6
   267  	if len(ui.calls) != expCalls {
   268  		t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ","))
   269  	}
   270  }
   271  
   272  func TestMissingFunc(t *testing.T) {
   273  	r, err := initRuleEngine(JS)
   274  	if err != nil {
   275  		t.Errorf("Couldn't create evaluator %v", err)
   276  		return
   277  	}
   278  
   279  	_, err = r.execute("MissingMethod", "test")
   280  
   281  	if err == nil {
   282  		t.Error("Expected error")
   283  	}
   284  
   285  	approved, err := r.checkApproval("MissingMethod", nil, nil)
   286  	if err == nil {
   287  		t.Errorf("Expected missing method to yield error'")
   288  	}
   289  	if approved {
   290  		t.Errorf("Expected missing method to cause non-approval")
   291  	}
   292  	t.Logf("Err %v", err)
   293  }
   294  func TestStorage(t *testing.T) {
   295  	js := `
   296  	function testStorage(){
   297  		storage.put("mykey", "myvalue")
   298  		a = storage.get("mykey")
   299  
   300  		storage.put("mykey", ["a", "list"])  	// Should result in "a,list"
   301  		a += storage.get("mykey")
   302  
   303  
   304  		storage.put("mykey", {"an": "object"}) 	// Should result in "[object Object]"
   305  		a += storage.get("mykey")
   306  
   307  
   308  		storage.put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
   309  		a += storage.get("mykey")
   310  
   311  		a += storage.get("missingkey")		//Missing keys should result in empty string
   312  		storage.put("","missing key==noop") // Can't store with 0-length key
   313  		a += storage.get("")				// Should result in ''
   314  
   315  		var b = new BigNumber(2)
   316  		var c = new BigNumber(16)//"0xf0",16)
   317  		var d = b.plus(c)
   318  		console.log(d)
   319  		return a
   320  	}
   321  `
   322  	r, err := initRuleEngine(js)
   323  	if err != nil {
   324  		t.Errorf("Couldn't create evaluator %v", err)
   325  		return
   326  	}
   327  
   328  	v, err := r.execute("testStorage", nil)
   329  
   330  	if err != nil {
   331  		t.Errorf("Unexpected error %v", err)
   332  	}
   333  	retval := v.ToString().String()
   334  
   335  	if err != nil {
   336  		t.Errorf("Unexpected error %v", err)
   337  	}
   338  	exp := `myvaluea,list[object Object]{"an":"object"}`
   339  	if retval != exp {
   340  		t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
   341  	}
   342  	t.Logf("Err %v", err)
   343  }
   344  
   345  const ExampleTxWindow = `
   346  	function big(str){
   347  		if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
   348  		return new BigNumber(str)
   349  	}
   350  
   351  	// Time window: 1 week
   352  	var window = 1000* 3600*24*7;
   353  
   354  	// Limit : 1 ether
   355  	var limit = new BigNumber("1e18");
   356  
   357  	function isLimitOk(transaction){
   358  		var value = big(transaction.value)
   359  		// Start of our window function
   360  		var windowstart = new Date().getTime() - window;
   361  
   362  		var txs = [];
   363  		var stored = storage.get('txs');
   364  
   365  		if(stored != ""){
   366  			txs = JSON.parse(stored)
   367  		}
   368  		// First, remove all that have passed out of the time-window
   369  		var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
   370  		console.log(txs, newtxs.length);
   371  
   372  		// Secondly, aggregate the current sum
   373  		sum = new BigNumber(0)
   374  
   375  		sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
   376  		console.log("ApproveTx > Sum so far", sum);
   377  		console.log("ApproveTx > Requested", value.toNumber());
   378  
   379  		// Would we exceed weekly limit ?
   380  		return sum.plus(value).lt(limit)
   381  
   382  	}
   383  	function ApproveTx(r){
   384  		console.log(r)
   385  		console.log(typeof(r))
   386  		if (isLimitOk(r.transaction)){
   387  			return "Approve"
   388  		}
   389  		return "Nope"
   390  	}
   391  
   392  	/**
   393  	* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
   394   	* 'response_str' contains the return value that will be sent to the external caller.
   395  	* The return value from this method is ignore - the reason for having this callback is to allow the
   396  	* ruleset to keep track of approved transactions.
   397  	*
   398  	* When implementing rate-limited rules, this callback should be used.
   399  	* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
   400  	* then accepts the transaction, this method will be called.
   401  	*
   402  	* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
   403  	*/
   404   	function OnApprovedTx(resp){
   405  		var value = big(resp.tx.value)
   406  		var txs = []
   407  		// Load stored transactions
   408  		var stored = storage.get('txs');
   409  		if(stored != ""){
   410  			txs = JSON.parse(stored)
   411  		}
   412  		// Add this to the storage
   413  		txs.push({tstamp: new Date().getTime(), value: value});
   414  		storage.put("txs", JSON.stringify(txs));
   415  	}
   416  
   417  `
   418  
   419  func dummyTx(value hexutil.Big) *core.SignTxRequest {
   420  	to, _ := mixAddr("000000000000000000000000000000000000dead")
   421  	from, _ := mixAddr("000000000000000000000000000000000000dead")
   422  	n := hexutil.Uint64(3)
   423  	gas := hexutil.Uint64(21000)
   424  	gasPrice := hexutil.Big(*big.NewInt(2000000))
   425  
   426  	return &core.SignTxRequest{
   427  		Transaction: core.SendTxArgs{
   428  			From:     *from,
   429  			To:       to,
   430  			Value:    value,
   431  			Nonce:    n,
   432  			GasPrice: gasPrice,
   433  			Gas:      gas,
   434  		},
   435  		Callinfo: []core.ValidationInfo{
   436  			{Typ: "Warning", Message: "All your base are bellong to us"},
   437  		},
   438  		Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   439  	}
   440  }
   441  
   442  func dummyTxWithV(value uint64) *core.SignTxRequest {
   443  	v := big.NewInt(0).SetUint64(value)
   444  	h := hexutil.Big(*v)
   445  	return dummyTx(h)
   446  }
   447  
   448  func dummySigned(value *big.Int) *types.Transaction {
   449  	to := common.HexToAddress("000000000000000000000000000000000000dead")
   450  	gas := uint64(21000)
   451  	gasPrice := big.NewInt(2000000)
   452  	data := make([]byte, 0)
   453  	return types.NewTransaction(3, to, value, gas, gasPrice, data)
   454  }
   455  
   456  func TestLimitWindow(t *testing.T) {
   457  	r, err := initRuleEngine(ExampleTxWindow)
   458  	if err != nil {
   459  		t.Errorf("Couldn't create evaluator %v", err)
   460  		return
   461  	}
   462  	// 0.3 ether: 429D069189E0000 wei
   463  	v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
   464  	h := hexutil.Big(*v)
   465  	// The first three should succeed
   466  	for i := 0; i < 3; i++ {
   467  		unsigned := dummyTx(h)
   468  		resp, err := r.ApproveTx(unsigned)
   469  		if err != nil {
   470  			t.Errorf("Unexpected error %v", err)
   471  		}
   472  		if !resp.Approved {
   473  			t.Errorf("Expected check to resolve to 'Approve'")
   474  		}
   475  		// Create a dummy signed transaction
   476  
   477  		response := ethapi.SignTransactionResult{
   478  			Tx:  dummySigned(v),
   479  			Raw: common.Hex2Bytes("deadbeef"),
   480  		}
   481  		r.OnApprovedTx(response)
   482  	}
   483  	// Fourth should fail
   484  	resp, _ := r.ApproveTx(dummyTx(h))
   485  	if resp.Approved {
   486  		t.Errorf("Expected check to resolve to 'Reject'")
   487  	}
   488  }
   489  
   490  // dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
   491  type dontCallMe struct {
   492  	t *testing.T
   493  }
   494  
   495  func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
   496  	d.t.Fatalf("Did not expect next-handler to be called")
   497  	return core.UserInputResponse{}, nil
   498  }
   499  
   500  func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) {
   501  }
   502  
   503  func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
   504  }
   505  
   506  func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
   507  	d.t.Fatalf("Did not expect next-handler to be called")
   508  	return core.SignTxResponse{}, core.ErrRequestDenied
   509  }
   510  
   511  func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
   512  	d.t.Fatalf("Did not expect next-handler to be called")
   513  	return core.SignDataResponse{}, core.ErrRequestDenied
   514  }
   515  
   516  func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
   517  	d.t.Fatalf("Did not expect next-handler to be called")
   518  	return core.ListResponse{}, core.ErrRequestDenied
   519  }
   520  
   521  func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
   522  	d.t.Fatalf("Did not expect next-handler to be called")
   523  	return core.NewAccountResponse{}, core.ErrRequestDenied
   524  }
   525  
   526  func (d *dontCallMe) ShowError(message string) {
   527  	d.t.Fatalf("Did not expect next-handler to be called")
   528  }
   529  
   530  func (d *dontCallMe) ShowInfo(message string) {
   531  	d.t.Fatalf("Did not expect next-handler to be called")
   532  }
   533  
   534  func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) {
   535  	d.t.Fatalf("Did not expect next-handler to be called")
   536  }
   537  
   538  // TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
   539  // if it does, that would be bad since developers may rely on that to store data,
   540  // instead of using the disk-based data storage
   541  func TestContextIsCleared(t *testing.T) {
   542  	js := `
   543  	function ApproveTx(){
   544  		if (typeof foobar == 'undefined') {
   545  			foobar = "Approve"
   546   		}
   547  		console.log(foobar)
   548  		if (foobar == "Approve"){
   549  			foobar = "Reject"
   550  		}else{
   551  			foobar = "Approve"
   552  		}
   553  		return foobar
   554  	}
   555  	`
   556  	ui := &dontCallMe{t}
   557  	r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage())
   558  	if err != nil {
   559  		t.Fatalf("Failed to create js engine: %v", err)
   560  	}
   561  	if err = r.Init(js); err != nil {
   562  		t.Fatalf("Failed to load bootstrap js: %v", err)
   563  	}
   564  	tx := dummyTxWithV(0)
   565  	r1, _ := r.ApproveTx(tx)
   566  	r2, _ := r.ApproveTx(tx)
   567  	if r1.Approved != r2.Approved {
   568  		t.Errorf("Expected execution context to be cleared between executions")
   569  	}
   570  }
   571  
   572  func TestSignData(t *testing.T) {
   573  	js := `function ApproveListing(){
   574      return "Approve"
   575  }
   576  function ApproveSignData(r){
   577      if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
   578      {
   579          if(r.messages[0].value.indexOf("bazonk") >= 0){
   580              return "Approve"
   581          }
   582          return "Reject"
   583      }
   584      // Otherwise goes to manual processing
   585  }`
   586  	r, err := initRuleEngine(js)
   587  	if err != nil {
   588  		t.Errorf("Couldn't create evaluator %v", err)
   589  		return
   590  	}
   591  	message := "baz bazonk foo"
   592  	hash, rawdata := accounts.TextAndHash([]byte(message))
   593  	addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
   594  
   595  	t.Logf("address %v %v\n", addr.String(), addr.Original())
   596  
   597  	nvt := []*core.NameValueType{
   598  		{
   599  			Name:  "message",
   600  			Typ:   "text/plain",
   601  			Value: message,
   602  		},
   603  	}
   604  	resp, err := r.ApproveSignData(&core.SignDataRequest{
   605  		Address:  *addr,
   606  		Messages: nvt,
   607  		Hash:     hash,
   608  		Meta:     core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   609  		Rawdata:  []byte(rawdata),
   610  	})
   611  	if err != nil {
   612  		t.Fatalf("Unexpected error %v", err)
   613  	}
   614  	if !resp.Approved {
   615  		t.Fatalf("Expected approved")
   616  	}
   617  }