github.com/daeglee/go-ethereum@v0.0.0-20190504220456-cad3e8d18e9b/signer/rules/rules_test.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU 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 // go-ethereum 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 General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. 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 155 js := ` 156 function ApproveTx(r){ 157 console.log("transaction.from", r.transaction.from); 158 console.log("transaction.to", r.transaction.to); 159 console.log("transaction.value", r.transaction.value); 160 console.log("transaction.nonce", r.transaction.nonce); 161 if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"} 162 if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"} 163 }` 164 165 r, err := initRuleEngine(js) 166 if err != nil { 167 t.Errorf("Couldn't create evaluator %v", err) 168 return 169 } 170 to, err := mixAddr("000000000000000000000000000000000000dead") 171 if err != nil { 172 t.Error(err) 173 return 174 } 175 from, err := mixAddr("0000000000000000000000000000000000001337") 176 177 if err != nil { 178 t.Error(err) 179 return 180 } 181 fmt.Printf("to %v", to.Address().String()) 182 resp, err := r.ApproveTx(&core.SignTxRequest{ 183 Transaction: core.SendTxArgs{ 184 From: *from, 185 To: to}, 186 Callinfo: nil, 187 Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, 188 }) 189 if err != nil { 190 t.Errorf("Unexpected error %v", err) 191 } 192 if !resp.Approved { 193 t.Errorf("Expected check to resolve to 'Approve'") 194 } 195 } 196 197 type dummyUI struct { 198 calls []string 199 } 200 201 func (d *dummyUI) RegisterUIServer(api *core.UIServerAPI) { 202 panic("implement me") 203 } 204 205 func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { 206 d.calls = append(d.calls, "OnInputRequired") 207 return core.UserInputResponse{}, nil 208 } 209 210 func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { 211 d.calls = append(d.calls, "ApproveTx") 212 return core.SignTxResponse{}, core.ErrRequestDenied 213 } 214 215 func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { 216 d.calls = append(d.calls, "ApproveSignData") 217 return core.SignDataResponse{}, core.ErrRequestDenied 218 } 219 220 func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { 221 d.calls = append(d.calls, "ApproveListing") 222 return core.ListResponse{}, core.ErrRequestDenied 223 } 224 225 func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { 226 d.calls = append(d.calls, "ApproveNewAccount") 227 return core.NewAccountResponse{}, core.ErrRequestDenied 228 } 229 230 func (d *dummyUI) ShowError(message string) { 231 d.calls = append(d.calls, "ShowError") 232 } 233 234 func (d *dummyUI) ShowInfo(message string) { 235 d.calls = append(d.calls, "ShowInfo") 236 } 237 238 func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) { 239 d.calls = append(d.calls, "OnApprovedTx") 240 } 241 242 func (d *dummyUI) OnSignerStartup(info core.StartupInfo) { 243 } 244 245 //TestForwarding tests that the rule-engine correctly dispatches requests to the next caller 246 func TestForwarding(t *testing.T) { 247 248 js := "" 249 ui := &dummyUI{make([]string, 0)} 250 jsBackend := storage.NewEphemeralStorage() 251 r, err := NewRuleEvaluator(ui, jsBackend) 252 if err != nil { 253 t.Fatalf("Failed to create js engine: %v", err) 254 } 255 if err = r.Init(js); err != nil { 256 t.Fatalf("Failed to load bootstrap js: %v", err) 257 } 258 r.ApproveSignData(nil) 259 r.ApproveTx(nil) 260 r.ApproveNewAccount(nil) 261 r.ApproveListing(nil) 262 r.ShowError("test") 263 r.ShowInfo("test") 264 265 //This one is not forwarded 266 r.OnApprovedTx(ethapi.SignTransactionResult{}) 267 268 expCalls := 6 269 if len(ui.calls) != expCalls { 270 271 t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ",")) 272 273 } 274 275 } 276 277 func TestMissingFunc(t *testing.T) { 278 r, err := initRuleEngine(JS) 279 if err != nil { 280 t.Errorf("Couldn't create evaluator %v", err) 281 return 282 } 283 284 _, err = r.execute("MissingMethod", "test") 285 286 if err == nil { 287 t.Error("Expected error") 288 } 289 290 approved, err := r.checkApproval("MissingMethod", nil, nil) 291 if err == nil { 292 t.Errorf("Expected missing method to yield error'") 293 } 294 if approved { 295 t.Errorf("Expected missing method to cause non-approval") 296 } 297 fmt.Printf("Err %v", err) 298 299 } 300 func TestStorage(t *testing.T) { 301 302 js := ` 303 function testStorage(){ 304 storage.Put("mykey", "myvalue") 305 a = storage.Get("mykey") 306 307 storage.Put("mykey", ["a", "list"]) // Should result in "a,list" 308 a += storage.Get("mykey") 309 310 311 storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]" 312 a += storage.Get("mykey") 313 314 315 storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}' 316 a += storage.Get("mykey") 317 318 a += storage.Get("missingkey") //Missing keys should result in empty string 319 storage.Put("","missing key==noop") // Can't store with 0-length key 320 a += storage.Get("") // Should result in '' 321 322 var b = new BigNumber(2) 323 var c = new BigNumber(16)//"0xf0",16) 324 var d = b.plus(c) 325 console.log(d) 326 return a 327 } 328 ` 329 r, err := initRuleEngine(js) 330 if err != nil { 331 t.Errorf("Couldn't create evaluator %v", err) 332 return 333 } 334 335 v, err := r.execute("testStorage", nil) 336 337 if err != nil { 338 t.Errorf("Unexpected error %v", err) 339 } 340 341 retval, err := v.ToString() 342 343 if err != nil { 344 t.Errorf("Unexpected error %v", err) 345 } 346 exp := `myvaluea,list[object Object]{"an":"object"}` 347 if retval != exp { 348 t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval) 349 } 350 fmt.Printf("Err %v", err) 351 352 } 353 354 const ExampleTxWindow = ` 355 function big(str){ 356 if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)} 357 return new BigNumber(str) 358 } 359 360 // Time window: 1 week 361 var window = 1000* 3600*24*7; 362 363 // Limit : 1 ether 364 var limit = new BigNumber("1e18"); 365 366 function isLimitOk(transaction){ 367 var value = big(transaction.value) 368 // Start of our window function 369 var windowstart = new Date().getTime() - window; 370 371 var txs = []; 372 var stored = storage.Get('txs'); 373 374 if(stored != ""){ 375 txs = JSON.parse(stored) 376 } 377 // First, remove all that have passed out of the time-window 378 var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart}); 379 console.log(txs, newtxs.length); 380 381 // Secondly, aggregate the current sum 382 sum = new BigNumber(0) 383 384 sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum); 385 console.log("ApproveTx > Sum so far", sum); 386 console.log("ApproveTx > Requested", value.toNumber()); 387 388 // Would we exceed weekly limit ? 389 return sum.plus(value).lt(limit) 390 391 } 392 function ApproveTx(r){ 393 console.log(r) 394 console.log(typeof(r)) 395 if (isLimitOk(r.transaction)){ 396 return "Approve" 397 } 398 return "Nope" 399 } 400 401 /** 402 * OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter 403 * 'response_str' contains the return value that will be sent to the external caller. 404 * The return value from this method is ignore - the reason for having this callback is to allow the 405 * ruleset to keep track of approved transactions. 406 * 407 * When implementing rate-limited rules, this callback should be used. 408 * If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user 409 * then accepts the transaction, this method will be called. 410 * 411 * TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx. 412 */ 413 function OnApprovedTx(resp){ 414 var value = big(resp.tx.value) 415 var txs = [] 416 // Load stored transactions 417 var stored = storage.Get('txs'); 418 if(stored != ""){ 419 txs = JSON.parse(stored) 420 } 421 // Add this to the storage 422 txs.push({tstamp: new Date().getTime(), value: value}); 423 storage.Put("txs", JSON.stringify(txs)); 424 } 425 426 ` 427 428 func dummyTx(value hexutil.Big) *core.SignTxRequest { 429 430 to, _ := mixAddr("000000000000000000000000000000000000dead") 431 from, _ := mixAddr("000000000000000000000000000000000000dead") 432 n := hexutil.Uint64(3) 433 gas := hexutil.Uint64(21000) 434 gasPrice := hexutil.Big(*big.NewInt(2000000)) 435 436 return &core.SignTxRequest{ 437 Transaction: core.SendTxArgs{ 438 From: *from, 439 To: to, 440 Value: value, 441 Nonce: n, 442 GasPrice: gasPrice, 443 Gas: gas, 444 }, 445 Callinfo: []core.ValidationInfo{ 446 {Typ: "Warning", Message: "All your base are bellong to us"}, 447 }, 448 Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, 449 } 450 } 451 func dummyTxWithV(value uint64) *core.SignTxRequest { 452 453 v := big.NewInt(0).SetUint64(value) 454 h := hexutil.Big(*v) 455 return dummyTx(h) 456 } 457 func dummySigned(value *big.Int) *types.Transaction { 458 to := common.HexToAddress("000000000000000000000000000000000000dead") 459 gas := uint64(21000) 460 gasPrice := big.NewInt(2000000) 461 data := make([]byte, 0) 462 return types.NewTransaction(3, to, value, gas, gasPrice, data) 463 464 } 465 func TestLimitWindow(t *testing.T) { 466 467 r, err := initRuleEngine(ExampleTxWindow) 468 if err != nil { 469 t.Errorf("Couldn't create evaluator %v", err) 470 return 471 } 472 473 // 0.3 ether: 429D069189E0000 wei 474 v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000")) 475 h := hexutil.Big(*v) 476 // The first three should succeed 477 for i := 0; i < 3; i++ { 478 unsigned := dummyTx(h) 479 resp, err := r.ApproveTx(unsigned) 480 if err != nil { 481 t.Errorf("Unexpected error %v", err) 482 } 483 if !resp.Approved { 484 t.Errorf("Expected check to resolve to 'Approve'") 485 } 486 // Create a dummy signed transaction 487 488 response := ethapi.SignTransactionResult{ 489 Tx: dummySigned(v), 490 Raw: common.Hex2Bytes("deadbeef"), 491 } 492 r.OnApprovedTx(response) 493 } 494 // Fourth should fail 495 resp, _ := r.ApproveTx(dummyTx(h)) 496 if resp.Approved { 497 t.Errorf("Expected check to resolve to 'Reject'") 498 } 499 500 } 501 502 // dontCallMe is used as a next-handler that does not want to be called - it invokes test failure 503 type dontCallMe struct { 504 t *testing.T 505 } 506 507 func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { 508 d.t.Fatalf("Did not expect next-handler to be called") 509 return core.UserInputResponse{}, nil 510 } 511 func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) { 512 } 513 514 func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) { 515 } 516 517 func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { 518 d.t.Fatalf("Did not expect next-handler to be called") 519 return core.SignTxResponse{}, core.ErrRequestDenied 520 } 521 522 func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { 523 d.t.Fatalf("Did not expect next-handler to be called") 524 return core.SignDataResponse{}, core.ErrRequestDenied 525 } 526 527 func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { 528 d.t.Fatalf("Did not expect next-handler to be called") 529 return core.ListResponse{}, core.ErrRequestDenied 530 } 531 532 func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { 533 d.t.Fatalf("Did not expect next-handler to be called") 534 return core.NewAccountResponse{}, core.ErrRequestDenied 535 } 536 537 func (d *dontCallMe) ShowError(message string) { 538 d.t.Fatalf("Did not expect next-handler to be called") 539 } 540 541 func (d *dontCallMe) ShowInfo(message string) { 542 d.t.Fatalf("Did not expect next-handler to be called") 543 } 544 545 func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) { 546 d.t.Fatalf("Did not expect next-handler to be called") 547 } 548 549 //TestContextIsCleared tests that the rule-engine does not retain variables over several requests. 550 // if it does, that would be bad since developers may rely on that to store data, 551 // instead of using the disk-based data storage 552 func TestContextIsCleared(t *testing.T) { 553 554 js := ` 555 function ApproveTx(){ 556 if (typeof foobar == 'undefined') { 557 foobar = "Approve" 558 } 559 console.log(foobar) 560 if (foobar == "Approve"){ 561 foobar = "Reject" 562 }else{ 563 foobar = "Approve" 564 } 565 return foobar 566 } 567 ` 568 ui := &dontCallMe{t} 569 r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage()) 570 if err != nil { 571 t.Fatalf("Failed to create js engine: %v", err) 572 } 573 if err = r.Init(js); err != nil { 574 t.Fatalf("Failed to load bootstrap js: %v", err) 575 } 576 tx := dummyTxWithV(0) 577 r1, _ := r.ApproveTx(tx) 578 r2, _ := r.ApproveTx(tx) 579 if r1.Approved != r2.Approved { 580 t.Errorf("Expected execution context to be cleared between executions") 581 } 582 } 583 584 func TestSignData(t *testing.T) { 585 586 js := `function ApproveListing(){ 587 return "Approve" 588 } 589 function ApproveSignData(r){ 590 if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") 591 { 592 if(r.message[0].value.indexOf("bazonk") >= 0){ 593 return "Approve" 594 } 595 return "Reject" 596 } 597 // Otherwise goes to manual processing 598 }` 599 r, err := initRuleEngine(js) 600 if err != nil { 601 t.Errorf("Couldn't create evaluator %v", err) 602 return 603 } 604 message := "baz bazonk foo" 605 hash, rawdata := accounts.TextAndHash([]byte(message)) 606 addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa") 607 608 fmt.Printf("address %v %v\n", addr.String(), addr.Original()) 609 610 nvt := []*core.NameValueType{ 611 { 612 Name: "message", 613 Typ: "text/plain", 614 Value: message, 615 }, 616 } 617 resp, err := r.ApproveSignData(&core.SignDataRequest{ 618 Address: *addr, 619 Message: nvt, 620 Hash: hash, 621 Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, 622 Rawdata: []byte(rawdata), 623 }) 624 if err != nil { 625 t.Fatalf("Unexpected error %v", err) 626 } 627 if !resp.Approved { 628 t.Fatalf("Expected approved") 629 } 630 }