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 }