github.com/arieschain/arieschain@v0.0.0-20191023063405-37c074544356/signer/rules/rules_test.go (about)

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