gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/contractor/host_integration_test.go (about) 1 package contractor 2 3 import ( 4 "bytes" 5 "errors" 6 "net" 7 "os" 8 "path/filepath" 9 "testing" 10 "time" 11 12 "gitlab.com/NebulousLabs/fastrand" 13 "gitlab.com/SiaPrime/SiaPrime/build" 14 "gitlab.com/SiaPrime/SiaPrime/crypto" 15 "gitlab.com/SiaPrime/SiaPrime/encoding" 16 "gitlab.com/SiaPrime/SiaPrime/modules" 17 "gitlab.com/SiaPrime/SiaPrime/modules/consensus" 18 "gitlab.com/SiaPrime/SiaPrime/modules/gateway" 19 "gitlab.com/SiaPrime/SiaPrime/modules/host" 20 "gitlab.com/SiaPrime/SiaPrime/modules/miner" 21 "gitlab.com/SiaPrime/SiaPrime/modules/renter/hostdb" 22 "gitlab.com/SiaPrime/SiaPrime/modules/transactionpool" 23 modWallet "gitlab.com/SiaPrime/SiaPrime/modules/wallet" 24 "gitlab.com/SiaPrime/SiaPrime/types" 25 ) 26 27 // newTestingWallet is a helper function that creates a ready-to-use wallet 28 // and mines some coins into it. 29 func newTestingWallet(testdir string, cs modules.ConsensusSet, tp modules.TransactionPool) (modules.Wallet, error) { 30 w, err := modWallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir)) 31 if err != nil { 32 return nil, err 33 } 34 key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet) 35 encrypted, err := w.Encrypted() 36 if err != nil { 37 return nil, err 38 } 39 if !encrypted { 40 _, err = w.Encrypt(key) 41 if err != nil { 42 return nil, err 43 } 44 } 45 err = w.Unlock(key) 46 if err != nil { 47 return nil, err 48 } 49 // give it some money 50 m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir)) 51 if err != nil { 52 return nil, err 53 } 54 for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ { 55 _, err := m.AddBlock() 56 if err != nil { 57 return nil, err 58 } 59 } 60 return w, nil 61 } 62 63 // newTestingHost is a helper function that creates a ready-to-use host. 64 func newTestingHost(testdir string, cs modules.ConsensusSet, tp modules.TransactionPool) (modules.Host, error) { 65 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 66 if err != nil { 67 return nil, err 68 } 69 w, err := newTestingWallet(testdir, cs, tp) 70 if err != nil { 71 return nil, err 72 } 73 h, err := host.New(cs, g, tp, w, "localhost:0", filepath.Join(testdir, modules.HostDir)) 74 if err != nil { 75 return nil, err 76 } 77 78 // configure host to accept contracts 79 settings := h.InternalSettings() 80 settings.AcceptingContracts = true 81 settings.MaxDuration = types.BlockHeight(4 * types.BlocksPerMonth) 82 err = h.SetInternalSettings(settings) 83 if err != nil { 84 return nil, err 85 } 86 87 // add storage to host 88 storageFolder := filepath.Join(testdir, "storage") 89 err = os.MkdirAll(storageFolder, 0700) 90 if err != nil { 91 return nil, err 92 } 93 err = h.AddStorageFolder(storageFolder, modules.SectorSize*64) 94 if err != nil { 95 return nil, err 96 } 97 98 return h, nil 99 } 100 101 // newTestingContractor is a helper function that creates a ready-to-use 102 // contractor. 103 func newTestingContractor(testdir string, g modules.Gateway, cs modules.ConsensusSet, tp modules.TransactionPool) (*Contractor, error) { 104 w, err := newTestingWallet(testdir, cs, tp) 105 if err != nil { 106 return nil, err 107 } 108 hdb, err := hostdb.New(g, cs, tp, filepath.Join(testdir, "hostdb")) 109 if err != nil { 110 return nil, err 111 } 112 return New(cs, w, tp, hdb, filepath.Join(testdir, "contractor")) 113 } 114 115 // newTestingTrio creates a Host, Contractor, and TestMiner that can be used 116 // for testing host/renter interactions. 117 func newTestingTrio(name string) (modules.Host, *Contractor, modules.TestMiner, error) { 118 testdir := build.TempDir("contractor", name) 119 120 // create miner 121 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 122 if err != nil { 123 return nil, nil, nil, err 124 } 125 cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 126 if err != nil { 127 return nil, nil, nil, err 128 } 129 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 130 if err != nil { 131 return nil, nil, nil, err 132 } 133 w, err := modWallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir)) 134 if err != nil { 135 return nil, nil, nil, err 136 } 137 key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet) 138 encrypted, err := w.Encrypted() 139 if err != nil { 140 return nil, nil, nil, err 141 } 142 if !encrypted { 143 _, err = w.Encrypt(key) 144 if err != nil { 145 return nil, nil, nil, err 146 } 147 } 148 err = w.Unlock(key) 149 if err != nil { 150 return nil, nil, nil, err 151 } 152 m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir)) 153 if err != nil { 154 return nil, nil, nil, err 155 } 156 157 // create host and contractor, using same consensus set and gateway 158 h, err := newTestingHost(filepath.Join(testdir, "Host"), cs, tp) 159 if err != nil { 160 return nil, nil, nil, build.ExtendErr("error creating testing host", err) 161 } 162 c, err := newTestingContractor(filepath.Join(testdir, "Contractor"), g, cs, tp) 163 if err != nil { 164 return nil, nil, nil, err 165 } 166 167 // announce the host 168 err = h.Announce() 169 if err != nil { 170 return nil, nil, nil, build.ExtendErr("error announcing host", err) 171 } 172 173 // mine a block, processing the announcement 174 _, err = m.AddBlock() 175 if err != nil { 176 return nil, nil, nil, err 177 } 178 179 // wait for hostdb to scan host 180 for i := 0; i < 50 && len(c.hdb.ActiveHosts()) == 0; i++ { 181 time.Sleep(time.Millisecond * 100) 182 } 183 if len(c.hdb.ActiveHosts()) == 0 { 184 return nil, nil, nil, errors.New("host did not make it into the contractor hostdb in time") 185 } 186 187 return h, c, m, nil 188 } 189 190 // TestIntegrationFormContract tests that the contractor can form contracts 191 // with the host module. 192 func TestIntegrationFormContract(t *testing.T) { 193 if testing.Short() { 194 t.SkipNow() 195 } 196 t.Parallel() 197 h, c, _, err := newTestingTrio(t.Name()) 198 if err != nil { 199 t.Fatal(err) 200 } 201 defer h.Close() 202 defer c.Close() 203 204 // acquire the contract maintenance lock for the duration of the test. This 205 // prevents theadedContractMaintenance from running. 206 c.maintenanceLock.Lock() 207 defer c.maintenanceLock.Unlock() 208 209 // get the host's entry from the db 210 hostEntry, ok := c.hdb.Host(h.PublicKey()) 211 if !ok { 212 t.Fatal("no entry for host in db") 213 } 214 215 // set an allowance but don't use SetAllowance to avoid automatic contract 216 // formation. 217 c.mu.Lock() 218 c.allowance = modules.DefaultAllowance 219 c.mu.Unlock() 220 221 // form a contract with the host 222 _, _, err = c.managedNewContract(hostEntry, types.SiacoinPrecision.Mul64(50), c.blockHeight+100) 223 if err != nil { 224 t.Fatal(err) 225 } 226 } 227 228 // TestFormContractSmallAllowance tests to make sure that a contract doesn't 229 // form when there are insufficient funds in the allowance 230 func TestFormContractSmallAllowance(t *testing.T) { 231 if testing.Short() { 232 t.SkipNow() 233 } 234 t.Parallel() 235 h, c, _, err := newTestingTrio(t.Name()) 236 if err != nil { 237 t.Fatal(err) 238 } 239 defer h.Close() 240 defer c.Close() 241 242 // get the host's entry from the db 243 hostEntry, ok := c.hdb.Host(h.PublicKey()) 244 if !ok { 245 t.Fatal("no entry for host in db") 246 } 247 248 // set an allowance but don't use SetAllowance to avoid automatic contract 249 // formation. Setting funds to 1SC to mimic bug report found in production. 250 // Using production number of hosts as well 251 c.mu.Lock() 252 c.allowance = modules.DefaultAllowance 253 c.allowance.Funds = types.SiacoinPrecision.Mul64(1) 254 c.allowance.Hosts = uint64(50) 255 initialContractFunds := c.allowance.Funds.Div64(c.allowance.Hosts).Div64(3) 256 c.mu.Unlock() 257 258 // try to form a contract with the host 259 _, _, err = c.managedNewContract(hostEntry, initialContractFunds, c.blockHeight+100) 260 if err == nil { 261 t.Fatal("Expected underflow error for insufficient funds") 262 } 263 } 264 265 // TestIntegrationReviseContract tests that the contractor can revise a 266 // contract previously formed with a host. 267 func TestIntegrationReviseContract(t *testing.T) { 268 if testing.Short() { 269 t.SkipNow() 270 } 271 t.Parallel() 272 // create testing trio 273 h, c, _, err := newTestingTrio(t.Name()) 274 if err != nil { 275 t.Fatal(err) 276 } 277 defer h.Close() 278 defer c.Close() 279 280 // acquire the contract maintenance lock for the duration of the test. This 281 // prevents theadedContractMaintenance from running. 282 c.maintenanceLock.Lock() 283 defer c.maintenanceLock.Unlock() 284 285 // get the host's entry from the db 286 hostEntry, ok := c.hdb.Host(h.PublicKey()) 287 if !ok { 288 t.Fatal("no entry for host in db") 289 } 290 291 // set an allowance but don't use SetAllowance to avoid automatic contract 292 // formation. 293 c.mu.Lock() 294 c.allowance = modules.DefaultAllowance 295 c.mu.Unlock() 296 297 // form a contract with the host 298 _, contract, err := c.managedNewContract(hostEntry, types.SiacoinPrecision.Mul64(50), c.blockHeight+100) 299 if err != nil { 300 t.Fatal(err) 301 } 302 303 // revise the contract 304 editor, err := c.Editor(contract.HostPublicKey, nil) 305 if err != nil { 306 t.Fatal(err) 307 } 308 data := fastrand.Bytes(int(modules.SectorSize)) 309 _, err = editor.Upload(data) 310 if err != nil { 311 t.Fatal(err) 312 } 313 err = editor.Close() 314 if err != nil { 315 t.Fatal(err) 316 } 317 } 318 319 // TestIntegrationUploadDownload tests that the contractor can upload data to 320 // a host and download it intact. 321 func TestIntegrationUploadDownload(t *testing.T) { 322 if testing.Short() { 323 t.SkipNow() 324 } 325 t.Parallel() 326 // create testing trio 327 h, c, _, err := newTestingTrio(t.Name()) 328 if err != nil { 329 t.Fatal(err) 330 } 331 defer h.Close() 332 defer c.Close() 333 334 // get the host's entry from the db 335 hostEntry, ok := c.hdb.Host(h.PublicKey()) 336 if !ok { 337 t.Fatal("no entry for host in db") 338 } 339 340 // set an allowance but don't use SetAllowance to avoid automatic contract 341 // formation. 342 c.mu.Lock() 343 c.allowance = modules.DefaultAllowance 344 c.mu.Unlock() 345 346 // form a contract with the host 347 _, contract, err := c.managedNewContract(hostEntry, types.SiacoinPrecision.Mul64(50), c.blockHeight+100) 348 if err != nil { 349 t.Fatal(err) 350 } 351 352 // revise the contract 353 editor, err := c.Editor(contract.HostPublicKey, nil) 354 if err != nil { 355 t.Fatal(err) 356 } 357 data := fastrand.Bytes(int(modules.SectorSize)) 358 root, err := editor.Upload(data) 359 if err != nil { 360 t.Fatal(err) 361 } 362 err = editor.Close() 363 if err != nil { 364 t.Fatal(err) 365 } 366 367 // download the data 368 downloader, err := c.Downloader(contract.HostPublicKey, nil) 369 if err != nil { 370 t.Fatal(err) 371 } 372 retrieved, err := downloader.Download(root, 0, uint32(modules.SectorSize)) 373 if err != nil { 374 t.Fatal(err) 375 } 376 if !bytes.Equal(data, retrieved) { 377 t.Fatal("downloaded data does not match original") 378 } 379 err = downloader.Close() 380 if err != nil { 381 t.Fatal(err) 382 } 383 } 384 385 // TestIntegrationRenew tests that the contractor can renew a previously- 386 // formed file contract. 387 func TestIntegrationRenew(t *testing.T) { 388 if testing.Short() { 389 t.SkipNow() 390 } 391 t.Parallel() 392 // create testing trio 393 h, c, _, err := newTestingTrio(t.Name()) 394 if err != nil { 395 t.Fatal(err) 396 } 397 defer h.Close() 398 defer c.Close() 399 400 // set an allowance and wait for a contract to be formed. 401 if err := c.SetAllowance(modules.DefaultAllowance); err != nil { 402 t.Fatal(err) 403 } 404 if err := build.Retry(10, time.Second, func() error { 405 if len(c.Contracts()) == 0 { 406 return errors.New("no contracts were formed") 407 } 408 return nil 409 }); err != nil { 410 t.Fatal(err) 411 } 412 // get the contract 413 contract := c.Contracts()[0] 414 415 // revise the contract 416 editor, err := c.Editor(contract.HostPublicKey, nil) 417 if err != nil { 418 t.Fatal(err) 419 } 420 data := fastrand.Bytes(int(modules.SectorSize)) 421 // insert the sector 422 root, err := editor.Upload(data) 423 if err != nil { 424 t.Fatal(err) 425 } 426 err = editor.Close() 427 if err != nil { 428 t.Fatal(err) 429 } 430 431 // renew the contract 432 err = c.managedUpdateContractUtility(contract.ID, modules.ContractUtility{GoodForRenew: true}) 433 if err != nil { 434 t.Fatal(err) 435 } 436 oldContract, ok := c.staticContracts.Acquire(contract.ID) 437 if !ok { 438 t.Fatal("failed to acquire contract") 439 } 440 contract, err = c.managedRenew(oldContract, types.SiacoinPrecision.Mul64(50), c.blockHeight+200) 441 if err != nil { 442 t.Fatal(err) 443 } 444 c.staticContracts.Return(oldContract) 445 446 // check renewed contract 447 if contract.EndHeight != c.blockHeight+200 { 448 t.Fatal(contract.EndHeight) 449 } 450 451 // download the renewed contract 452 downloader, err := c.Downloader(contract.HostPublicKey, nil) 453 if err != nil { 454 t.Fatal(err) 455 } 456 retrieved, err := downloader.Download(root, 0, uint32(modules.SectorSize)) 457 if err != nil { 458 t.Fatal(err) 459 } 460 if !bytes.Equal(data, retrieved) { 461 t.Fatal("downloaded data does not match original") 462 } 463 err = downloader.Close() 464 if err != nil { 465 t.Fatal(err) 466 } 467 468 // renew to a lower height 469 err = c.managedUpdateContractUtility(contract.ID, modules.ContractUtility{GoodForRenew: true}) 470 if err != nil { 471 t.Fatal(err) 472 } 473 oldContract, _ = c.staticContracts.Acquire(contract.ID) 474 contract, err = c.managedRenew(oldContract, types.SiacoinPrecision.Mul64(50), c.blockHeight+100) 475 if err != nil { 476 t.Fatal(err) 477 } 478 c.staticContracts.Return(oldContract) 479 if contract.EndHeight != c.blockHeight+100 { 480 t.Fatal(contract.EndHeight) 481 } 482 483 // revise the contract 484 editor, err = c.Editor(contract.HostPublicKey, nil) 485 if err != nil { 486 t.Fatal(err) 487 } 488 data = fastrand.Bytes(int(modules.SectorSize)) 489 // insert the sector 490 _, err = editor.Upload(data) 491 if err != nil { 492 t.Fatal(err) 493 } 494 err = editor.Close() 495 if err != nil { 496 t.Fatal(err) 497 } 498 } 499 500 // TestIntegrationDownloaderCaching tests that downloaders are properly cached 501 // by the contractor. When two downloaders are requested for the same 502 // contract, only one underlying downloader should be created. 503 func TestIntegrationDownloaderCaching(t *testing.T) { 504 if testing.Short() { 505 t.SkipNow() 506 } 507 t.Parallel() 508 // create testing trio 509 h, c, _, err := newTestingTrio(t.Name()) 510 if err != nil { 511 t.Fatal(err) 512 } 513 defer h.Close() 514 defer c.Close() 515 516 // set an allowance and wait for a contract to be formed. 517 if err := c.SetAllowance(modules.DefaultAllowance); err != nil { 518 t.Fatal(err) 519 } 520 if err := build.Retry(10, time.Second, func() error { 521 if len(c.Contracts()) == 0 { 522 return errors.New("no contracts were formed") 523 } 524 return nil 525 }); err != nil { 526 t.Fatal(err) 527 } 528 // get the contract 529 contract := c.Contracts()[0] 530 531 // create a downloader 532 d1, err := c.Downloader(contract.HostPublicKey, nil) 533 if err != nil { 534 t.Fatal(err) 535 } 536 537 // create another downloader 538 d2, err := c.Downloader(contract.HostPublicKey, nil) 539 if err != nil { 540 t.Fatal(err) 541 } 542 543 // downloaders should match 544 if d1 != d2 { 545 t.Fatal("downloader was not cached") 546 } 547 548 // close one of the downloaders; it should not fully close, since d1 is 549 // still using it 550 d2.Close() 551 552 c.mu.RLock() 553 _, ok := c.downloaders[contract.ID] 554 _, sok := c.sessions[contract.ID] 555 c.mu.RUnlock() 556 if !ok && !sok { 557 t.Fatal("expected downloader to still be present") 558 } 559 560 // create another downloader 561 d3, err := c.Downloader(contract.HostPublicKey, nil) 562 if err != nil { 563 t.Fatal(err) 564 } 565 566 // downloaders should match 567 if d3 != d1 { 568 t.Fatal("closing one client should not fully close the downloader") 569 } 570 571 // close both downloaders 572 d1.Close() 573 d2.Close() 574 575 c.mu.RLock() 576 _, ok = c.downloaders[contract.ID] 577 _, sok = c.sessions[contract.ID] 578 c.mu.RUnlock() 579 if ok || sok { 580 t.Fatal("did not expect downloader to still be present") 581 } 582 583 // create another downloader 584 d4, err := c.Downloader(contract.HostPublicKey, nil) 585 if err != nil { 586 t.Fatal(err) 587 } 588 589 // downloaders should match 590 if d4 == d1 { 591 t.Fatal("downloader should not have been cached after all clients were closed") 592 } 593 d4.Close() 594 } 595 596 // TestIntegrationEditorCaching tests that editors are properly cached 597 // by the contractor. When two editors are requested for the same 598 // contract, only one underlying editor should be created. 599 func TestIntegrationEditorCaching(t *testing.T) { 600 if testing.Short() { 601 t.SkipNow() 602 } 603 t.Parallel() 604 // create testing trio 605 h, c, _, err := newTestingTrio(t.Name()) 606 if err != nil { 607 t.Fatal(err) 608 } 609 defer h.Close() 610 defer c.Close() 611 612 // set an allowance and wait for a contract to be formed. 613 if err := c.SetAllowance(modules.DefaultAllowance); err != nil { 614 t.Fatal(err) 615 } 616 if err := build.Retry(10, time.Second, func() error { 617 if len(c.Contracts()) == 0 { 618 return errors.New("no contracts were formed") 619 } 620 return nil 621 }); err != nil { 622 t.Fatal(err) 623 } 624 // get the contract 625 contract := c.Contracts()[0] 626 627 // create an editor 628 d1, err := c.Editor(contract.HostPublicKey, nil) 629 if err != nil { 630 t.Fatal(err) 631 } 632 633 // create another editor 634 d2, err := c.Editor(contract.HostPublicKey, nil) 635 if err != nil { 636 t.Fatal(err) 637 } 638 639 // editors should match 640 if d1 != d2 { 641 t.Fatal("editor was not cached") 642 } 643 644 // close one of the editors; it should not fully close, since d1 is 645 // still using it 646 d2.Close() 647 648 c.mu.RLock() 649 _, ok := c.editors[contract.ID] 650 _, sok := c.sessions[contract.ID] 651 c.mu.RUnlock() 652 if !ok && !sok { 653 t.Fatal("expected editor to still be present") 654 } 655 656 // create another editor 657 d3, err := c.Editor(contract.HostPublicKey, nil) 658 if err != nil { 659 t.Fatal(err) 660 } 661 662 // editors should match 663 if d3 != d1 { 664 t.Fatal("closing one client should not fully close the editor") 665 } 666 667 // close both editors 668 d1.Close() 669 d2.Close() 670 671 c.mu.RLock() 672 _, ok = c.editors[contract.ID] 673 _, sok = c.sessions[contract.ID] 674 c.mu.RUnlock() 675 if ok || sok { 676 t.Fatal("did not expect editor to still be present") 677 } 678 679 // create another editor 680 d4, err := c.Editor(contract.HostPublicKey, nil) 681 if err != nil { 682 t.Fatal(err) 683 } 684 685 // editors should match 686 if d4 == d1 { 687 t.Fatal("editor should not have been cached after all clients were closed") 688 } 689 d4.Close() 690 } 691 692 // TestContractPresenceLeak tests that a renter can not tell from the response 693 // of the host to RPCs if the host has the contract if the renter doesn't 694 // own this contract. See https://gitlab.com/SiaPrime/SiaPrime/issues/2327. 695 func TestContractPresenceLeak(t *testing.T) { 696 if testing.Short() { 697 t.SkipNow() 698 } 699 t.Parallel() 700 // create testing trio 701 h, c, _, err := newTestingTrio(t.Name()) 702 if err != nil { 703 t.Fatal(err) 704 } 705 defer h.Close() 706 defer c.Close() 707 708 // get the host's entry from the db 709 hostEntry, ok := c.hdb.Host(h.PublicKey()) 710 if !ok { 711 t.Fatal("no entry for host in db") 712 } 713 714 // set an allowance but don't use SetAllowance to avoid automatic contract 715 // formation. 716 c.mu.Lock() 717 c.allowance = modules.DefaultAllowance 718 c.mu.Unlock() 719 720 // form a contract with the host 721 _, contract, err := c.managedNewContract(hostEntry, types.SiacoinPrecision.Mul64(10), c.blockHeight+100) 722 if err != nil { 723 t.Fatal(err) 724 } 725 726 // Connect with bad challenge response. Try correct 727 // and incorrect contract IDs. Compare errors. 728 wrongID := contract.ID 729 wrongID[0] ^= 0x01 730 fcids := []types.FileContractID{contract.ID, wrongID} 731 var errors []error 732 733 for _, fcid := range fcids { 734 var challenge crypto.Hash 735 var signature crypto.Signature 736 conn, err := net.Dial("tcp", string(hostEntry.NetAddress)) 737 if err != nil { 738 t.Fatalf("Couldn't dial tpc connection with host @ %v: %v.", string(hostEntry.NetAddress), err) 739 } 740 if err := encoding.WriteObject(conn, modules.RPCDownload); err != nil { 741 t.Fatalf("Couldn't initiate RPC: %v.", err) 742 } 743 if err := encoding.WriteObject(conn, fcid); err != nil { 744 t.Fatalf("Couldn't send fcid: %v.", err) 745 } 746 if err := encoding.ReadObject(conn, &challenge, 32); err != nil { 747 t.Fatalf("Couldn't read challenge: %v.", err) 748 } 749 if err := encoding.WriteObject(conn, signature); err != nil { 750 t.Fatalf("Couldn't send signature: %v.", err) 751 } 752 err = modules.ReadNegotiationAcceptance(conn) 753 if err == nil { 754 t.Fatal("Expected an error, got success.") 755 } 756 errors = append(errors, err) 757 } 758 if errors[0].Error() != errors[1].Error() { 759 t.Fatalf("Expected to get equal errors, got %q and %q.", errors[0], errors[1]) 760 } 761 }