gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/wallet_test.go (about)

     1  package wallet
     2  
     3  import (
     4  	"path/filepath"
     5  	"testing"
     6  	"time"
     7  
     8  	"gitlab.com/SiaPrime/SiaPrime/build"
     9  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    10  	"gitlab.com/SiaPrime/SiaPrime/modules"
    11  	"gitlab.com/SiaPrime/SiaPrime/modules/consensus"
    12  	"gitlab.com/SiaPrime/SiaPrime/modules/gateway"
    13  	"gitlab.com/SiaPrime/SiaPrime/modules/miner"
    14  	"gitlab.com/SiaPrime/SiaPrime/modules/transactionpool"
    15  	"gitlab.com/SiaPrime/SiaPrime/types"
    16  )
    17  
    18  // A Wallet tester contains a ConsensusTester and has a bunch of helpful
    19  // functions for facilitating wallet integration testing.
    20  type walletTester struct {
    21  	cs      modules.ConsensusSet
    22  	gateway modules.Gateway
    23  	tpool   modules.TransactionPool
    24  	miner   modules.TestMiner
    25  	wallet  *Wallet
    26  
    27  	walletMasterKey crypto.CipherKey
    28  
    29  	persistDir string
    30  }
    31  
    32  // createWalletTester takes a testing.T and creates a WalletTester.
    33  func createWalletTester(name string, deps modules.Dependencies) (*walletTester, error) {
    34  	// Create the modules
    35  	testdir := build.TempDir(modules.WalletDir, name)
    36  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	w, err := NewCustomWallet(cs, tp, filepath.Join(testdir, modules.WalletDir), deps)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	masterKey := crypto.GenerateSiaKey(crypto.TypeDefaultWallet)
    53  	_, err = w.Encrypt(masterKey)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	err = w.Unlock(masterKey)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.WalletDir))
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	// Assemble all components into a wallet tester.
    67  	wt := &walletTester{
    68  		cs:      cs,
    69  		gateway: g,
    70  		tpool:   tp,
    71  		miner:   m,
    72  		wallet:  w,
    73  
    74  		walletMasterKey: masterKey,
    75  
    76  		persistDir: testdir,
    77  	}
    78  
    79  	// Mine blocks until there is money in the wallet.
    80  	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
    81  		b, _ := wt.miner.FindBlock()
    82  		err := wt.cs.AcceptBlock(b)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  	}
    87  	return wt, nil
    88  }
    89  
    90  // createBlankWalletTester creates a wallet tester that has not mined any
    91  // blocks or encrypted the wallet.
    92  func createBlankWalletTester(name string) (*walletTester, error) {
    93  	// Create the modules
    94  	testdir := build.TempDir(modules.WalletDir, name)
    95  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	w, err := New(cs, tp, filepath.Join(testdir, modules.WalletDir))
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir))
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	// Assemble all components into a wallet tester.
   117  	wt := &walletTester{
   118  		gateway: g,
   119  		cs:      cs,
   120  		tpool:   tp,
   121  		miner:   m,
   122  		wallet:  w,
   123  
   124  		persistDir: testdir,
   125  	}
   126  	return wt, nil
   127  }
   128  
   129  // closeWt closes all of the modules in the wallet tester.
   130  func (wt *walletTester) closeWt() error {
   131  	errs := []error{
   132  		wt.gateway.Close(),
   133  		wt.cs.Close(),
   134  		wt.tpool.Close(),
   135  		wt.miner.Close(),
   136  		wt.wallet.Close(),
   137  	}
   138  	return build.JoinErrors(errs, "; ")
   139  }
   140  
   141  // TestNilInputs tries starting the wallet using nil inputs.
   142  func TestNilInputs(t *testing.T) {
   143  	if testing.Short() {
   144  		t.SkipNow()
   145  	}
   146  	testdir := build.TempDir(modules.WalletDir, t.Name())
   147  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
   148  	if err != nil {
   149  		t.Fatal(err)
   150  	}
   151  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
   152  	if err != nil {
   153  		t.Fatal(err)
   154  	}
   155  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
   156  	if err != nil {
   157  		t.Fatal(err)
   158  	}
   159  
   160  	wdir := filepath.Join(testdir, modules.WalletDir)
   161  	_, err = New(cs, nil, wdir)
   162  	if err != errNilTpool {
   163  		t.Error(err)
   164  	}
   165  	_, err = New(nil, tp, wdir)
   166  	if err != errNilConsensusSet {
   167  		t.Error(err)
   168  	}
   169  	_, err = New(nil, nil, wdir)
   170  	if err != errNilConsensusSet {
   171  		t.Error(err)
   172  	}
   173  }
   174  
   175  // TestAllAddresses checks that AllAddresses returns all of the wallet's
   176  // addresses in sorted order.
   177  func TestAllAddresses(t *testing.T) {
   178  	if testing.Short() {
   179  		t.Skip()
   180  	}
   181  	wt, err := createBlankWalletTester(t.Name())
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  	defer wt.closeWt()
   186  
   187  	wt.wallet.keys[types.UnlockHash{1}] = spendableKey{}
   188  	wt.wallet.keys[types.UnlockHash{5}] = spendableKey{}
   189  	wt.wallet.keys[types.UnlockHash{0}] = spendableKey{}
   190  	wt.wallet.keys[types.UnlockHash{2}] = spendableKey{}
   191  	wt.wallet.keys[types.UnlockHash{4}] = spendableKey{}
   192  	wt.wallet.keys[types.UnlockHash{3}] = spendableKey{}
   193  	addrs, err := wt.wallet.AllAddresses()
   194  	if err != nil {
   195  		t.Fatal(err)
   196  	}
   197  	for i := range addrs {
   198  		if addrs[i][0] != byte(i) {
   199  			t.Error("address sorting failed:", i, addrs[i][0])
   200  		}
   201  	}
   202  }
   203  
   204  // TestCloseWallet tries to close the wallet.
   205  func TestCloseWallet(t *testing.T) {
   206  	if testing.Short() {
   207  		t.Skip()
   208  	}
   209  	testdir := build.TempDir(modules.WalletDir, t.Name())
   210  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
   211  	if err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
   215  	if err != nil {
   216  		t.Fatal(err)
   217  	}
   218  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
   219  	if err != nil {
   220  		t.Fatal(err)
   221  	}
   222  	wdir := filepath.Join(testdir, modules.WalletDir)
   223  	w, err := New(cs, tp, wdir)
   224  	if err != nil {
   225  		t.Fatal(err)
   226  	}
   227  	if err := w.Close(); err != nil {
   228  		t.Fatal(err)
   229  	}
   230  }
   231  
   232  // TestRescanning verifies that calling Rescanning during a scan operation
   233  // returns true, and false otherwise.
   234  func TestRescanning(t *testing.T) {
   235  	if testing.Short() {
   236  		t.SkipNow()
   237  	}
   238  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   239  	if err != nil {
   240  		t.Fatal(err)
   241  	}
   242  	defer wt.closeWt()
   243  
   244  	// A fresh wallet should not be rescanning.
   245  	rescanning, err := wt.wallet.Rescanning()
   246  	if err != nil {
   247  		t.Fatal(err)
   248  	}
   249  	if rescanning {
   250  		t.Fatal("fresh wallet should not report that a scan is underway")
   251  	}
   252  
   253  	// lock the wallet
   254  	wt.wallet.Lock()
   255  
   256  	// spawn an unlock goroutine
   257  	errChan := make(chan error)
   258  	go func() {
   259  		// acquire the write lock so that Unlock acquires the trymutex, but
   260  		// cannot proceed further
   261  		wt.wallet.mu.Lock()
   262  		errChan <- wt.wallet.Unlock(wt.walletMasterKey)
   263  	}()
   264  
   265  	// wait for goroutine to start, after which Rescanning should return true
   266  	time.Sleep(time.Millisecond * 10)
   267  	rescanning, err = wt.wallet.Rescanning()
   268  	if err != nil {
   269  		t.Fatal(err)
   270  	}
   271  	if !rescanning {
   272  		t.Fatal("wallet should report that a scan is underway")
   273  	}
   274  
   275  	// release the mutex and allow the call to complete
   276  	wt.wallet.mu.Unlock()
   277  	if err := <-errChan; err != nil {
   278  		t.Fatal("unlock failed:", err)
   279  	}
   280  
   281  	// Rescanning should now return false again
   282  	rescanning, err = wt.wallet.Rescanning()
   283  	if err != nil {
   284  		t.Fatal(err)
   285  	}
   286  	if rescanning {
   287  		t.Fatal("wallet should not report that a scan is underway")
   288  	}
   289  }
   290  
   291  // TestFutureAddressGeneration checks if the right amount of future addresses
   292  // is generated after calling NextAddress() or locking + unlocking the wallet.
   293  func TestLookaheadGeneration(t *testing.T) {
   294  	if testing.Short() {
   295  		t.SkipNow()
   296  	}
   297  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   298  	if err != nil {
   299  		t.Fatal(err)
   300  	}
   301  	defer wt.closeWt()
   302  
   303  	// Check if number of future keys is correct
   304  	wt.wallet.mu.RLock()
   305  	progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx)
   306  	wt.wallet.mu.RUnlock()
   307  	if err != nil {
   308  		t.Fatal("Couldn't fetch primary seed from db")
   309  	}
   310  
   311  	actualKeys := uint64(len(wt.wallet.lookahead))
   312  	expectedKeys := maxLookahead(progress)
   313  	if actualKeys != expectedKeys {
   314  		t.Errorf("expected len(lookahead) == %d but was %d", actualKeys, expectedKeys)
   315  	}
   316  
   317  	// Generate some more keys
   318  	for i := 0; i < 100; i++ {
   319  		wt.wallet.NextAddress()
   320  	}
   321  
   322  	// Lock and unlock
   323  	wt.wallet.Lock()
   324  	wt.wallet.Unlock(wt.walletMasterKey)
   325  
   326  	wt.wallet.mu.RLock()
   327  	progress, err = dbGetPrimarySeedProgress(wt.wallet.dbTx)
   328  	wt.wallet.mu.RUnlock()
   329  	if err != nil {
   330  		t.Fatal("Couldn't fetch primary seed from db")
   331  	}
   332  
   333  	actualKeys = uint64(len(wt.wallet.lookahead))
   334  	expectedKeys = maxLookahead(progress)
   335  	if actualKeys != expectedKeys {
   336  		t.Errorf("expected len(lookahead) == %d but was %d", actualKeys, expectedKeys)
   337  	}
   338  
   339  	wt.wallet.mu.RLock()
   340  	defer wt.wallet.mu.RUnlock()
   341  	for i := range wt.wallet.keys {
   342  		_, exists := wt.wallet.lookahead[i]
   343  		if exists {
   344  			t.Fatal("wallet keys contained a key which is also present in lookahead")
   345  		}
   346  	}
   347  }
   348  
   349  // TestAdvanceLookaheadNoRescan tests if a transaction to multiple lookahead addresses
   350  // is handled correctly without forcing a wallet rescan.
   351  func TestAdvanceLookaheadNoRescan(t *testing.T) {
   352  	if testing.Short() {
   353  		t.SkipNow()
   354  	}
   355  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   356  	if err != nil {
   357  		t.Fatal(err)
   358  	}
   359  	defer wt.closeWt()
   360  
   361  	builder, err := wt.wallet.StartTransaction()
   362  	if err != nil {
   363  		t.Fatal(err)
   364  	}
   365  	payout := types.ZeroCurrency
   366  
   367  	// Get the current progress
   368  	wt.wallet.mu.RLock()
   369  	progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx)
   370  	wt.wallet.mu.RUnlock()
   371  	if err != nil {
   372  		t.Fatal("Couldn't fetch primary seed from db")
   373  	}
   374  
   375  	// choose 10 keys in the lookahead and remember them
   376  	var receivingAddresses []types.UnlockHash
   377  	for _, sk := range generateKeys(wt.wallet.primarySeed, progress, 10) {
   378  		sco := types.SiacoinOutput{
   379  			UnlockHash: sk.UnlockConditions.UnlockHash(),
   380  			Value:      types.NewCurrency64(1e3),
   381  		}
   382  
   383  		builder.AddSiacoinOutput(sco)
   384  		payout = payout.Add(sco.Value)
   385  		receivingAddresses = append(receivingAddresses, sk.UnlockConditions.UnlockHash())
   386  	}
   387  
   388  	err = builder.FundSiacoins(payout)
   389  	if err != nil {
   390  		t.Fatal(err)
   391  	}
   392  
   393  	tSet, err := builder.Sign(true)
   394  	if err != nil {
   395  		t.Fatal(err)
   396  	}
   397  
   398  	err = wt.tpool.AcceptTransactionSet(tSet)
   399  	if err != nil {
   400  		t.Fatal(err)
   401  	}
   402  
   403  	_, err = wt.miner.AddBlock()
   404  	if err != nil {
   405  		t.Fatal(err)
   406  	}
   407  
   408  	// Check if the receiving addresses were moved from future keys to keys
   409  	wt.wallet.mu.RLock()
   410  	defer wt.wallet.mu.RUnlock()
   411  	for _, uh := range receivingAddresses {
   412  		_, exists := wt.wallet.lookahead[uh]
   413  		if exists {
   414  			t.Fatal("UnlockHash still exists in wallet lookahead")
   415  		}
   416  
   417  		_, exists = wt.wallet.keys[uh]
   418  		if !exists {
   419  			t.Fatal("UnlockHash not in map of spendable keys")
   420  		}
   421  	}
   422  }
   423  
   424  // TestAdvanceLookaheadNoRescan tests if a transaction to multiple lookahead addresses
   425  // is handled correctly forcing a wallet rescan.
   426  func TestAdvanceLookaheadForceRescan(t *testing.T) {
   427  	if testing.Short() {
   428  		t.SkipNow()
   429  	}
   430  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   431  	if err != nil {
   432  		t.Fatal(err)
   433  	}
   434  	defer wt.closeWt()
   435  
   436  	// Mine blocks without payouts so that the balance stabilizes
   437  	for i := types.BlockHeight(0); i < types.MaturityDelay; i++ {
   438  		wt.addBlockNoPayout()
   439  	}
   440  
   441  	// Get the current progress and balance
   442  	wt.wallet.mu.RLock()
   443  	progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx)
   444  	wt.wallet.mu.RUnlock()
   445  	if err != nil {
   446  		t.Fatal("Couldn't fetch primary seed from db")
   447  	}
   448  	startBal, _, _, err := wt.wallet.ConfirmedBalance()
   449  	if err != nil {
   450  		t.Fatal(err)
   451  	}
   452  
   453  	// Send coins to an address with a high seed index, just outside the
   454  	// lookahead range. It will not be initially detected, but later the
   455  	// rescan should find it.
   456  	highIndex := progress + uint64(len(wt.wallet.lookahead)) + 5
   457  	farAddr := generateSpendableKey(wt.wallet.primarySeed, highIndex).UnlockConditions.UnlockHash()
   458  	farPayout := types.SiacoinPrecision.Mul64(8888)
   459  
   460  	builder, err := wt.wallet.StartTransaction()
   461  	if err != nil {
   462  		t.Fatal(err)
   463  	}
   464  	builder.AddSiacoinOutput(types.SiacoinOutput{
   465  		UnlockHash: farAddr,
   466  		Value:      farPayout,
   467  	})
   468  	err = builder.FundSiacoins(farPayout)
   469  	if err != nil {
   470  		t.Fatal(err)
   471  	}
   472  
   473  	txnSet, err := builder.Sign(true)
   474  	if err != nil {
   475  		t.Fatal(err)
   476  	}
   477  
   478  	err = wt.tpool.AcceptTransactionSet(txnSet)
   479  	if err != nil {
   480  		t.Fatal(err)
   481  	}
   482  	wt.addBlockNoPayout()
   483  	newBal, _, _, err := wt.wallet.ConfirmedBalance()
   484  	if err != nil {
   485  		t.Fatal(err)
   486  	}
   487  	if !startBal.Sub(newBal).Equals(farPayout) {
   488  		t.Fatal("wallet should not recognize coins sent to very high seed index")
   489  	}
   490  
   491  	builder, err = wt.wallet.StartTransaction()
   492  	if err != nil {
   493  		t.Fatal(err)
   494  	}
   495  	var payout types.Currency
   496  
   497  	// choose 10 keys in the lookahead and remember them
   498  	var receivingAddresses []types.UnlockHash
   499  	for uh, index := range wt.wallet.lookahead {
   500  		// Only choose keys that force a rescan
   501  		if index < progress+lookaheadRescanThreshold {
   502  			continue
   503  		}
   504  		sco := types.SiacoinOutput{
   505  			UnlockHash: uh,
   506  			Value:      types.SiacoinPrecision.Mul64(1000),
   507  		}
   508  		builder.AddSiacoinOutput(sco)
   509  		payout = payout.Add(sco.Value)
   510  		receivingAddresses = append(receivingAddresses, uh)
   511  
   512  		if len(receivingAddresses) >= 10 {
   513  			break
   514  		}
   515  	}
   516  
   517  	err = builder.FundSiacoins(payout)
   518  	if err != nil {
   519  		t.Fatal(err)
   520  	}
   521  
   522  	txnSet, err = builder.Sign(true)
   523  	if err != nil {
   524  		t.Fatal(err)
   525  	}
   526  
   527  	err = wt.tpool.AcceptTransactionSet(txnSet)
   528  	if err != nil {
   529  		t.Fatal(err)
   530  	}
   531  	wt.addBlockNoPayout()
   532  
   533  	// Allow the wallet rescan to finish
   534  	time.Sleep(time.Second * 2)
   535  
   536  	// Check that high seed index txn was discovered in the rescan
   537  	rescanBal, _, _, err := wt.wallet.ConfirmedBalance()
   538  	if err != nil {
   539  		t.Fatal(err)
   540  	}
   541  	if !rescanBal.Equals(startBal) {
   542  		t.Fatal("wallet did not discover txn after rescan")
   543  	}
   544  
   545  	// Check if the receiving addresses were moved from future keys to keys
   546  	wt.wallet.mu.RLock()
   547  	defer wt.wallet.mu.RUnlock()
   548  	for _, uh := range receivingAddresses {
   549  		_, exists := wt.wallet.lookahead[uh]
   550  		if exists {
   551  			t.Fatal("UnlockHash still exists in wallet lookahead")
   552  		}
   553  
   554  		_, exists = wt.wallet.keys[uh]
   555  		if !exists {
   556  			t.Fatal("UnlockHash not in map of spendable keys")
   557  		}
   558  	}
   559  }
   560  
   561  // TestDistantWallets tests if two wallets that use the same seed stay
   562  // synchronized.
   563  func TestDistantWallets(t *testing.T) {
   564  	if testing.Short() {
   565  		t.SkipNow()
   566  	}
   567  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   568  	if err != nil {
   569  		t.Fatal(err)
   570  	}
   571  	defer wt.closeWt()
   572  
   573  	// Create another wallet with the same seed.
   574  	w2, err := New(wt.cs, wt.tpool, build.TempDir(modules.WalletDir, t.Name()+"2", modules.WalletDir))
   575  	if err != nil {
   576  		t.Fatal(err)
   577  	}
   578  	err = w2.InitFromSeed(nil, wt.wallet.primarySeed)
   579  	if err != nil {
   580  		t.Fatal(err)
   581  	}
   582  	sk := crypto.NewWalletKey(crypto.HashObject(wt.wallet.primarySeed))
   583  	err = w2.Unlock(sk)
   584  	if err != nil {
   585  		t.Fatal(err)
   586  	}
   587  
   588  	// Use the first wallet.
   589  	for i := uint64(0); i < lookaheadBuffer/2; i++ {
   590  		_, err = wt.wallet.SendSiacoins(types.SiacoinPrecision, types.UnlockHash{})
   591  		if err != nil {
   592  			t.Fatal(err)
   593  		}
   594  		wt.addBlockNoPayout()
   595  	}
   596  
   597  	// The second wallet's balance should update accordingly.
   598  	w1bal, _, _, err := wt.wallet.ConfirmedBalance()
   599  	if err != nil {
   600  		t.Fatal(err)
   601  	}
   602  	w2bal, _, _, err := w2.ConfirmedBalance()
   603  	if err != nil {
   604  		t.Fatal(err)
   605  	}
   606  
   607  	if !w1bal.Equals(w2bal) {
   608  		t.Fatal("balances do not match:", w1bal, w2bal)
   609  	}
   610  
   611  	// Send coins to an address with a very high seed index, outside the
   612  	// lookahead range. w2 should not detect it.
   613  	tbuilder, err := wt.wallet.StartTransaction()
   614  	if err != nil {
   615  		t.Fatal(err)
   616  	}
   617  	farAddr := generateSpendableKey(wt.wallet.primarySeed, lookaheadBuffer*10).UnlockConditions.UnlockHash()
   618  	value := types.SiacoinPrecision.Mul64(1e3)
   619  	tbuilder.AddSiacoinOutput(types.SiacoinOutput{
   620  		UnlockHash: farAddr,
   621  		Value:      value,
   622  	})
   623  	err = tbuilder.FundSiacoins(value)
   624  	if err != nil {
   625  		t.Fatal(err)
   626  	}
   627  	txnSet, err := tbuilder.Sign(true)
   628  	if err != nil {
   629  		t.Fatal(err)
   630  	}
   631  	err = wt.tpool.AcceptTransactionSet(txnSet)
   632  	if err != nil {
   633  		t.Fatal(err)
   634  	}
   635  	wt.addBlockNoPayout()
   636  
   637  	if newBal, _, _, err := w2.ConfirmedBalance(); !newBal.Equals(w2bal.Sub(value)) {
   638  		if err != nil {
   639  			t.Fatal(err)
   640  		}
   641  		t.Fatal("wallet should not recognize coins sent to very high seed index")
   642  	}
   643  }