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