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 }