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