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