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

     1  package wallet
     2  
     3  import (
     4  	"reflect"
     5  	"testing"
     6  
     7  	"gitlab.com/NebulousLabs/fastrand"
     8  	"gitlab.com/SiaPrime/SiaPrime/crypto"
     9  	"gitlab.com/SiaPrime/SiaPrime/modules"
    10  	"gitlab.com/SiaPrime/SiaPrime/types"
    11  )
    12  
    13  // TestSignTransaction constructs a valid, signed transaction using the
    14  // wallet's UnspentOutputs and SignTransaction methods.
    15  func TestSignTransaction(t *testing.T) {
    16  	if testing.Short() {
    17  		t.SkipNow()
    18  	}
    19  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
    20  	if err != nil {
    21  		t.Fatal(err)
    22  	}
    23  	defer wt.closeWt()
    24  
    25  	// load siafunds into the wallet
    26  	err = wt.wallet.LoadSiagKeys(wt.walletMasterKey, []string{"../../types/siag0of1of1.siakey"})
    27  	if err != nil {
    28  		t.Error(err)
    29  	}
    30  
    31  	// get a siacoin output and a siafund output
    32  	outputs, err := wt.wallet.UnspentOutputs()
    33  	if err != nil {
    34  		t.Fatal(err)
    35  	}
    36  	var sco, sfo modules.UnspentOutput
    37  	for _, o := range outputs {
    38  		if o.FundType == types.SpecifierSiacoinOutput {
    39  			sco = o
    40  		} else if o.FundType == types.SpecifierSiafundOutput {
    41  			sfo = o
    42  		}
    43  	}
    44  	scuc, err := wt.wallet.UnlockConditions(sco.UnlockHash)
    45  	if err != nil {
    46  		t.Fatal(err)
    47  	}
    48  	sfuc, err := wt.wallet.UnlockConditions(sfo.UnlockHash)
    49  	if err != nil {
    50  		t.Fatal(err)
    51  	}
    52  
    53  	// create a transaction that sends both outputs to the void
    54  	txn := types.Transaction{
    55  		SiafundInputs: []types.SiafundInput{{
    56  			ParentID:         types.SiafundOutputID(sfo.ID),
    57  			UnlockConditions: sfuc,
    58  		}},
    59  		SiafundOutputs: []types.SiafundOutput{{
    60  			Value:      sfo.Value,
    61  			UnlockHash: types.UnlockHash{},
    62  		}},
    63  
    64  		SiacoinInputs: []types.SiacoinInput{{
    65  			ParentID:         types.SiacoinOutputID(sco.ID),
    66  			UnlockConditions: scuc,
    67  		}},
    68  		SiacoinOutputs: []types.SiacoinOutput{{
    69  			Value:      sco.Value,
    70  			UnlockHash: types.UnlockHash{},
    71  		}},
    72  		TransactionSignatures: []types.TransactionSignature{
    73  			{
    74  				ParentID:      crypto.Hash(sco.ID),
    75  				CoveredFields: types.CoveredFields{WholeTransaction: true},
    76  			},
    77  			{
    78  				ParentID:      crypto.Hash(sfo.ID),
    79  				CoveredFields: types.CoveredFields{WholeTransaction: true},
    80  			},
    81  		},
    82  	}
    83  
    84  	// sign the transaction
    85  	err = wt.wallet.SignTransaction(&txn, nil)
    86  	if err != nil {
    87  		t.Fatal(err)
    88  	}
    89  	// txn should now have signatures
    90  	if len(txn.TransactionSignatures[0].Signature) == 0 || len(txn.TransactionSignatures[1].Signature) == 0 {
    91  		t.Fatal("transaction was not signed")
    92  	}
    93  
    94  	// the resulting transaction should be valid; submit it to the tpool and
    95  	// mine a block to confirm it
    96  	height, _ := wt.wallet.Height()
    97  	err = txn.StandaloneValid(height)
    98  	if err != nil {
    99  		t.Fatal(err)
   100  	}
   101  	err = wt.tpool.AcceptTransactionSet([]types.Transaction{txn})
   102  	if err != nil {
   103  		t.Fatal(err)
   104  	}
   105  	err = wt.addBlockNoPayout()
   106  	if err != nil {
   107  		t.Fatal(err)
   108  	}
   109  
   110  	// the wallet should no longer list the outputs as spendable
   111  	outputs, err = wt.wallet.UnspentOutputs()
   112  	if err != nil {
   113  		t.Fatal(err)
   114  	}
   115  	for _, o := range outputs {
   116  		if o.ID == sco.ID || o.ID == sfo.ID {
   117  			t.Fatal("spent output still listed as spendable")
   118  		}
   119  	}
   120  }
   121  
   122  // TestSignTransactionNoWallet tests the SignTransaction function.
   123  func TestSignTransactionNoWallet(t *testing.T) {
   124  	// generate a seed
   125  	var seed modules.Seed
   126  	fastrand.Read(seed[:])
   127  
   128  	// generate a key from that seed. Use a random index < 1000 to test
   129  	// whether SignTransaction can find it.
   130  	sk := generateSpendableKey(seed, fastrand.Uint64n(1000))
   131  
   132  	// create a transaction that sends 1 SC and 1 SF to the void
   133  	txn := types.Transaction{
   134  		SiacoinInputs: []types.SiacoinInput{{
   135  			ParentID:         types.SiacoinOutputID{1}, // doesn't need to actually exist
   136  			UnlockConditions: sk.UnlockConditions,
   137  		}},
   138  		SiacoinOutputs: []types.SiacoinOutput{{
   139  			Value:      types.NewCurrency64(1),
   140  			UnlockHash: types.UnlockHash{},
   141  		}},
   142  		SiafundInputs: []types.SiafundInput{{
   143  			ParentID:         types.SiafundOutputID{2}, // doesn't need to actually exist
   144  			UnlockConditions: sk.UnlockConditions,
   145  		}},
   146  		SiafundOutputs: []types.SiafundOutput{{
   147  			Value:      types.NewCurrency64(1),
   148  			UnlockHash: types.UnlockHash{},
   149  		}},
   150  		TransactionSignatures: []types.TransactionSignature{
   151  			{
   152  				ParentID:      crypto.Hash{1},
   153  				CoveredFields: types.CoveredFields{WholeTransaction: true},
   154  			},
   155  			{
   156  				ParentID:      crypto.Hash{2},
   157  				CoveredFields: types.CoveredFields{WholeTransaction: true},
   158  			},
   159  		},
   160  	}
   161  
   162  	// can't sign without toSign argument
   163  	if err := SignTransaction(&txn, seed, nil, 0); err == nil {
   164  		t.Fatal("expected error when attempting to sign without specifying ParentIDs")
   165  	}
   166  
   167  	// sign the transaction
   168  	toSign := []crypto.Hash{
   169  		txn.TransactionSignatures[0].ParentID,
   170  		txn.TransactionSignatures[1].ParentID,
   171  	}
   172  	if err := SignTransaction(&txn, seed, toSign, 0); err != nil {
   173  		t.Fatal(err)
   174  	}
   175  	// txn should now have a signature
   176  	if len(txn.TransactionSignatures[0].Signature) == 0 {
   177  		t.Fatal("transaction was not signed")
   178  	}
   179  	// the resulting transaction should be valid
   180  	if err := txn.StandaloneValid(0); err != nil {
   181  		t.Fatal(err)
   182  	}
   183  }
   184  
   185  // TestUnspentOutputs tests the UnspentOutputs method of the wallet.
   186  func TestUnspentOutputs(t *testing.T) {
   187  	if testing.Short() {
   188  		t.SkipNow()
   189  	}
   190  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   191  	if err != nil {
   192  		t.Fatal(err)
   193  	}
   194  	defer wt.closeWt()
   195  
   196  	// create a dummy address and send coins to it
   197  	addr := types.UnlockHash{1}
   198  
   199  	_, err = wt.wallet.SendSiacoins(types.SiacoinPrecision.Mul64(77), addr)
   200  	if err != nil {
   201  		t.Fatal(err)
   202  	}
   203  	wt.miner.AddBlock()
   204  
   205  	// define a helper function to check whether addr appears in
   206  	// UnspentOutputs
   207  	addrIsPresent := func() bool {
   208  		outputs, err := wt.wallet.UnspentOutputs()
   209  		if err != nil {
   210  			t.Fatal(err)
   211  		}
   212  		for _, o := range outputs {
   213  			if o.UnlockHash == addr {
   214  				return true
   215  			}
   216  		}
   217  		return false
   218  	}
   219  
   220  	// initially, the output should not show up in UnspentOutputs, because the
   221  	// address is not being tracked yet
   222  	if addrIsPresent() {
   223  		t.Fatal("shouldn't see addr in UnspentOutputs yet")
   224  	}
   225  
   226  	// add the address, but tell the wallet it hasn't been used yet. The
   227  	// wallet won't rescan, so it still won't see any outputs.
   228  	err = wt.wallet.AddWatchAddresses([]types.UnlockHash{addr}, true)
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  	if addrIsPresent() {
   233  		t.Fatal("shouldn't see addr in UnspentOutputs yet")
   234  	}
   235  
   236  	// remove the address, then add it again, this time telling the wallet
   237  	// that it has been used.
   238  	err = wt.wallet.RemoveWatchAddresses([]types.UnlockHash{addr}, true)
   239  	if err != nil {
   240  		t.Fatal(err)
   241  	}
   242  	err = wt.wallet.AddWatchAddresses([]types.UnlockHash{addr}, false)
   243  	if err != nil {
   244  		t.Fatal(err)
   245  	}
   246  
   247  	// output should now show up
   248  	if !addrIsPresent() {
   249  		t.Fatal("addr not present in UnspentOutputs after AddWatchAddresses")
   250  	}
   251  
   252  	// remove the address, but tell the wallet that the address hasn't been
   253  	// used. The wallet won't rescan, so the output should still show up.
   254  	err = wt.wallet.RemoveWatchAddresses([]types.UnlockHash{addr}, true)
   255  	if err != nil {
   256  		t.Fatal(err)
   257  	}
   258  	if !addrIsPresent() {
   259  		t.Fatal("addr should still be present in UnspentOutputs")
   260  	}
   261  
   262  	// add and remove the address again, this time triggering a rescan. The
   263  	// output should no longer appear.
   264  	err = wt.wallet.AddWatchAddresses([]types.UnlockHash{addr}, true)
   265  	if err != nil {
   266  		t.Fatal(err)
   267  	}
   268  	err = wt.wallet.RemoveWatchAddresses([]types.UnlockHash{addr}, false)
   269  	if err != nil {
   270  		t.Fatal(err)
   271  	}
   272  	if addrIsPresent() {
   273  		t.Fatal("shouldn't see addr in UnspentOutputs")
   274  	}
   275  }
   276  
   277  // TestWatchOnly tests the ability of the wallet to track addresses that it
   278  // does not own.
   279  func TestWatchOnly(t *testing.T) {
   280  	if testing.Short() {
   281  		t.SkipNow()
   282  	}
   283  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   284  	if err != nil {
   285  		t.Fatal(err)
   286  	}
   287  	defer wt.closeWt()
   288  
   289  	// create an address manually and send coins to it
   290  	sk := generateSpendableKey(modules.Seed{}, 1234)
   291  	addr := sk.UnlockConditions.UnlockHash()
   292  
   293  	_, err = wt.wallet.SendSiacoins(types.SiacoinPrecision.Mul64(77), addr)
   294  	if err != nil {
   295  		t.Fatal(err)
   296  	}
   297  
   298  	// define a helper function to check whether addr appears in
   299  	// UnspentOutputs
   300  	addrIsPresent := func() bool {
   301  		outputs, err := wt.wallet.UnspentOutputs()
   302  		if err != nil {
   303  			t.Fatal(err)
   304  		}
   305  		for _, o := range outputs {
   306  			if o.UnlockHash == addr {
   307  				return true
   308  			}
   309  		}
   310  		return false
   311  	}
   312  
   313  	// the output should not show up in UnspentOutputs, because the address is
   314  	// not being tracked yet
   315  	if addrIsPresent() {
   316  		t.Fatal("shouldn't see addr in UnspentOutputs")
   317  	}
   318  
   319  	// track the address
   320  	err = wt.wallet.AddWatchAddresses([]types.UnlockHash{addr}, false)
   321  	if err != nil {
   322  		t.Fatal(err)
   323  	}
   324  
   325  	// the address will now show up in WatchAddresses. Even though we haven't
   326  	// mined the block sending the coins yet, the output will show up in
   327  	// UnspentOutputs because it's in the transaction pool.
   328  	addrs, err := wt.wallet.WatchAddresses()
   329  	if err != nil {
   330  		t.Fatal(err)
   331  	} else if len(addrs) != 1 || addrs[0] != addr {
   332  		t.Fatal("expecting addr to be watched, got", addrs)
   333  	}
   334  	if !addrIsPresent() {
   335  		t.Fatal("addr not present in UnspentOutputs after AddWatchAddresses")
   336  	}
   337  
   338  	// mine the block; the output should still be present.
   339  	wt.miner.AddBlock()
   340  	if !addrIsPresent() {
   341  		t.Fatal("addr not present in UnspentOutputs after AddWatchAddresses")
   342  	}
   343  
   344  	// create a transaction that sends an output to the void
   345  	outputs, err := wt.wallet.UnspentOutputs()
   346  	if err != nil {
   347  		t.Fatal(err)
   348  	}
   349  	var output modules.UnspentOutput
   350  	for _, output = range outputs {
   351  		if output.UnlockHash == addr {
   352  			break
   353  		}
   354  	}
   355  	txn := types.Transaction{
   356  		SiacoinInputs: []types.SiacoinInput{{
   357  			ParentID:         types.SiacoinOutputID(output.ID),
   358  			UnlockConditions: sk.UnlockConditions,
   359  		}},
   360  		SiacoinOutputs: []types.SiacoinOutput{{
   361  			Value:      output.Value,
   362  			UnlockHash: types.UnlockHash{},
   363  		}},
   364  		TransactionSignatures: []types.TransactionSignature{{
   365  			ParentID:      crypto.Hash(output.ID),
   366  			CoveredFields: types.CoveredFields{WholeTransaction: true},
   367  		}},
   368  	}
   369  
   370  	// sign the transaction
   371  	sig := crypto.SignHash(txn.SigHash(0, wt.cs.Height()), sk.SecretKeys[0])
   372  	txn.TransactionSignatures[0].Signature = sig[:]
   373  
   374  	// the resulting transaction should be valid; submit it to the tpool and
   375  	// mine a block to confirm it
   376  	height, _ := wt.wallet.Height()
   377  	err = txn.StandaloneValid(height)
   378  	if err != nil {
   379  		t.Fatal(err)
   380  	}
   381  	err = wt.tpool.AcceptTransactionSet([]types.Transaction{txn})
   382  	if err != nil {
   383  		t.Fatal(err)
   384  	}
   385  	err = wt.addBlockNoPayout()
   386  	if err != nil {
   387  		t.Fatal(err)
   388  	}
   389  
   390  	// the wallet should no longer list the resulting output as spendable
   391  	if addrIsPresent() {
   392  		t.Fatal("shouldn't see addr in UnspentOutputs after spending it")
   393  	}
   394  	// stop tracking the address
   395  	err = wt.wallet.RemoveWatchAddresses([]types.UnlockHash{addr}, false)
   396  	if err != nil {
   397  		t.Fatal(err)
   398  	}
   399  	// the address should no longer appear in WatchAddresses
   400  	addrs, err = wt.wallet.WatchAddresses()
   401  	if err != nil {
   402  		t.Fatal(err)
   403  	} else if len(addrs) != 0 {
   404  		t.Fatal("expecting no watched addresses, got", addrs)
   405  	}
   406  }
   407  
   408  // TestUnlockConditions tests the UnlockConditions and AddUnlockConditions
   409  // methods of the wallet.
   410  func TestUnlockConditions(t *testing.T) {
   411  	if testing.Short() {
   412  		t.SkipNow()
   413  	}
   414  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   415  	if err != nil {
   416  		t.Fatal(err)
   417  	}
   418  	defer wt.closeWt()
   419  
   420  	// add some random unlock conditions
   421  	sk := generateSpendableKey(modules.Seed{}, 1234)
   422  	if err := wt.wallet.AddUnlockConditions(sk.UnlockConditions); err != nil {
   423  		t.Fatal(err)
   424  	}
   425  
   426  	// the unlock conditions should now be listed for the address
   427  	uc, err := wt.wallet.UnlockConditions(sk.UnlockConditions.UnlockHash())
   428  	if err != nil {
   429  		t.Fatal(err)
   430  	}
   431  	if !reflect.DeepEqual(uc, sk.UnlockConditions) {
   432  		t.Fatal("unlock conditions do not match")
   433  	}
   434  
   435  	// no unlock conditions should be returned for a random address
   436  	uc, err = wt.wallet.UnlockConditions(types.UnlockHash{1})
   437  	if err == nil {
   438  		t.Fatal("expected error when requested unlock conditions of random address")
   439  	}
   440  }