github.com/ZuluSpl0it/Sia@v1.3.7/modules/wallet/seed.go (about) 1 package wallet 2 3 import ( 4 "runtime" 5 "sync" 6 7 "github.com/NebulousLabs/Sia/crypto" 8 "github.com/NebulousLabs/Sia/encoding" 9 "github.com/NebulousLabs/Sia/modules" 10 "github.com/NebulousLabs/Sia/types" 11 "github.com/NebulousLabs/errors" 12 "github.com/NebulousLabs/fastrand" 13 "github.com/coreos/bbolt" 14 ) 15 16 var ( 17 errKnownSeed = errors.New("seed is already known") 18 ) 19 20 type ( 21 // uniqueID is a unique id randomly generated and put at the front of every 22 // persistence object. It is used to make sure that a different encryption 23 // key can be used for every persistence object. 24 uniqueID [crypto.EntropySize]byte 25 26 // seedFile stores an encrypted wallet seed on disk. 27 seedFile struct { 28 UID uniqueID 29 EncryptionVerification crypto.Ciphertext 30 Seed crypto.Ciphertext 31 } 32 ) 33 34 // generateSpendableKey creates the keys and unlock conditions for seed at a 35 // given index. 36 func generateSpendableKey(seed modules.Seed, index uint64) spendableKey { 37 sk, pk := crypto.GenerateKeyPairDeterministic(crypto.HashAll(seed, index)) 38 return spendableKey{ 39 UnlockConditions: types.UnlockConditions{ 40 PublicKeys: []types.SiaPublicKey{types.Ed25519PublicKey(pk)}, 41 SignaturesRequired: 1, 42 }, 43 SecretKeys: []crypto.SecretKey{sk}, 44 } 45 } 46 47 // generateKeys generates n keys from seed, starting from index start. 48 func generateKeys(seed modules.Seed, start, n uint64) []spendableKey { 49 // generate in parallel, one goroutine per core. 50 keys := make([]spendableKey, n) 51 var wg sync.WaitGroup 52 wg.Add(runtime.NumCPU()) 53 for cpu := 0; cpu < runtime.NumCPU(); cpu++ { 54 go func(offset uint64) { 55 defer wg.Done() 56 for i := offset; i < n; i += uint64(runtime.NumCPU()) { 57 // NOTE: don't bother trying to optimize generateSpendableKey; 58 // profiling shows that ed25519 key generation consumes far 59 // more CPU time than encoding or hashing. 60 keys[i] = generateSpendableKey(seed, start+i) 61 } 62 }(uint64(cpu)) 63 } 64 wg.Wait() 65 return keys 66 } 67 68 // createSeedFile creates and encrypts a seedFile. 69 func createSeedFile(masterKey crypto.TwofishKey, seed modules.Seed) seedFile { 70 var sf seedFile 71 fastrand.Read(sf.UID[:]) 72 sek := uidEncryptionKey(masterKey, sf.UID) 73 sf.EncryptionVerification = sek.EncryptBytes(verificationPlaintext) 74 sf.Seed = sek.EncryptBytes(seed[:]) 75 return sf 76 } 77 78 // decryptSeedFile decrypts a seed file using the encryption key. 79 func decryptSeedFile(masterKey crypto.TwofishKey, sf seedFile) (seed modules.Seed, err error) { 80 // Verify that the provided master key is the correct key. 81 decryptionKey := uidEncryptionKey(masterKey, sf.UID) 82 err = verifyEncryption(decryptionKey, sf.EncryptionVerification) 83 if err != nil { 84 return modules.Seed{}, err 85 } 86 87 // Decrypt and return the seed. 88 plainSeed, err := decryptionKey.DecryptBytes(sf.Seed) 89 if err != nil { 90 return modules.Seed{}, err 91 } 92 copy(seed[:], plainSeed) 93 return seed, nil 94 } 95 96 // regenerateLookahead creates future keys up to a maximum of maxKeys keys 97 func (w *Wallet) regenerateLookahead(start uint64) { 98 // Check how many keys need to be generated 99 maxKeys := maxLookahead(start) 100 existingKeys := uint64(len(w.lookahead)) 101 102 for i, k := range generateKeys(w.primarySeed, start+existingKeys, maxKeys-existingKeys) { 103 w.lookahead[k.UnlockConditions.UnlockHash()] = start + existingKeys + uint64(i) 104 } 105 } 106 107 // integrateSeed generates n spendableKeys from the seed and loads them into 108 // the wallet. 109 func (w *Wallet) integrateSeed(seed modules.Seed, n uint64) { 110 for _, sk := range generateKeys(seed, 0, n) { 111 w.keys[sk.UnlockConditions.UnlockHash()] = sk 112 } 113 } 114 115 // nextPrimarySeedAddress fetches the next n addresses from the primary seed. 116 func (w *Wallet) nextPrimarySeedAddresses(tx *bolt.Tx, n uint64) ([]types.UnlockConditions, error) { 117 // Check that the wallet has been unlocked. 118 if !w.unlocked { 119 return []types.UnlockConditions{}, modules.ErrLockedWallet 120 } 121 122 // Fetch and increment the seed progress. 123 progress, err := dbGetPrimarySeedProgress(tx) 124 if err != nil { 125 return []types.UnlockConditions{}, err 126 } 127 if err = dbPutPrimarySeedProgress(tx, progress+n); err != nil { 128 return []types.UnlockConditions{}, err 129 } 130 // Integrate the next keys into the wallet, and return the unlock 131 // conditions. Also remove new keys from the future keys and update them 132 // according to new progress 133 spendableKeys := generateKeys(w.primarySeed, progress, n) 134 ucs := make([]types.UnlockConditions, 0, len(spendableKeys)) 135 for _, spendableKey := range spendableKeys { 136 w.keys[spendableKey.UnlockConditions.UnlockHash()] = spendableKey 137 delete(w.lookahead, spendableKey.UnlockConditions.UnlockHash()) 138 ucs = append(ucs, spendableKey.UnlockConditions) 139 } 140 w.regenerateLookahead(progress + n) 141 142 return ucs, nil 143 } 144 145 // nextPrimarySeedAddress fetches the next address from the primary seed. 146 func (w *Wallet) nextPrimarySeedAddress(tx *bolt.Tx) (types.UnlockConditions, error) { 147 ucs, err := w.nextPrimarySeedAddresses(tx, 1) 148 if err != nil { 149 return types.UnlockConditions{}, err 150 } 151 return ucs[0], nil 152 } 153 154 // AllSeeds returns a list of all seeds known to and used by the wallet. 155 func (w *Wallet) AllSeeds() ([]modules.Seed, error) { 156 w.mu.Lock() 157 defer w.mu.Unlock() 158 if !w.unlocked { 159 return nil, modules.ErrLockedWallet 160 } 161 return append([]modules.Seed{w.primarySeed}, w.seeds...), nil 162 } 163 164 // PrimarySeed returns the decrypted primary seed of the wallet, as well as 165 // the number of addresses that the seed can be safely used to generate. 166 func (w *Wallet) PrimarySeed() (modules.Seed, uint64, error) { 167 w.mu.Lock() 168 defer w.mu.Unlock() 169 if !w.unlocked { 170 return modules.Seed{}, 0, modules.ErrLockedWallet 171 } 172 progress, err := dbGetPrimarySeedProgress(w.dbTx) 173 if err != nil { 174 return modules.Seed{}, 0, err 175 } 176 177 // addresses remaining is maxScanKeys-progress; generating more keys than 178 // that risks not being able to recover them when using SweepSeed or 179 // InitFromSeed. 180 remaining := maxScanKeys - progress 181 if progress > maxScanKeys { 182 remaining = 0 183 } 184 return w.primarySeed, remaining, nil 185 } 186 187 // NextAddresses returns n unlock hashes that are ready to receive siacoins or 188 // siafunds. The addresses are generated using the primary address seed. 189 // 190 // Warning: If this function is used to generate large numbers of addresses, 191 // those addresses should be used. Otherwise the lookahead might not be able to 192 // keep up and multiple wallets with the same seed might desync. 193 func (w *Wallet) NextAddresses(n uint64) ([]types.UnlockConditions, error) { 194 if err := w.tg.Add(); err != nil { 195 return []types.UnlockConditions{}, err 196 } 197 defer w.tg.Done() 198 199 // TODO: going to the db is slow; consider creating 100 addresses at a 200 // time. 201 w.mu.Lock() 202 ucs, err := w.nextPrimarySeedAddresses(w.dbTx, n) 203 err = errors.Compose(err, w.syncDB()) 204 w.mu.Unlock() 205 if err != nil { 206 return []types.UnlockConditions{}, err 207 } 208 209 return ucs, err 210 } 211 212 // NextAddress returns an unlock hash that is ready to receive siacoins or 213 // siafunds. The address is generated using the primary address seed. 214 func (w *Wallet) NextAddress() (types.UnlockConditions, error) { 215 ucs, err := w.NextAddresses(1) 216 if err != nil { 217 return types.UnlockConditions{}, err 218 } 219 return ucs[0], nil 220 } 221 222 // LoadSeed will track all of the addresses generated by the input seed, 223 // reclaiming any funds that were lost due to a deleted file or lost encryption 224 // key. An error will be returned if the seed has already been integrated with 225 // the wallet. 226 func (w *Wallet) LoadSeed(masterKey crypto.TwofishKey, seed modules.Seed) error { 227 if err := w.tg.Add(); err != nil { 228 return err 229 } 230 defer w.tg.Done() 231 232 if !w.cs.Synced() { 233 return errors.New("cannot load seed until blockchain is synced") 234 } 235 236 if !w.scanLock.TryLock() { 237 return errScanInProgress 238 } 239 defer w.scanLock.Unlock() 240 241 // Because the recovery seed does not have a UID, duplication must be 242 // prevented by comparing with the list of decrypted seeds. This can only 243 // occur while the wallet is unlocked. 244 w.mu.RLock() 245 if !w.unlocked { 246 w.mu.RUnlock() 247 return modules.ErrLockedWallet 248 } 249 for _, wSeed := range append([]modules.Seed{w.primarySeed}, w.seeds...) { 250 if seed == wSeed { 251 w.mu.RUnlock() 252 return errKnownSeed 253 } 254 } 255 w.mu.RUnlock() 256 257 // scan blockchain to determine how many keys to generate for the seed 258 s := newSeedScanner(seed, w.log) 259 if err := s.scan(w.cs, w.tg.StopChan()); err != nil { 260 return err 261 } 262 // Add 4% as a buffer because the seed may have addresses in the wild 263 // that have not appeared in the blockchain yet. 264 seedProgress := s.largestIndexSeen + 500 265 seedProgress += seedProgress / 25 266 w.log.Printf("INFO: found key index %v in blockchain. Setting auxiliary seed progress to %v", s.largestIndexSeen, seedProgress) 267 268 err := func() error { 269 w.mu.Lock() 270 defer w.mu.Unlock() 271 272 err := checkMasterKey(w.dbTx, masterKey) 273 if err != nil { 274 return err 275 } 276 277 // create a seedFile for the seed 278 sf := createSeedFile(masterKey, seed) 279 280 // add the seedFile 281 var current []seedFile 282 err = encoding.Unmarshal(w.dbTx.Bucket(bucketWallet).Get(keyAuxiliarySeedFiles), ¤t) 283 if err != nil { 284 return err 285 } 286 err = w.dbTx.Bucket(bucketWallet).Put(keyAuxiliarySeedFiles, encoding.Marshal(append(current, sf))) 287 if err != nil { 288 return err 289 } 290 291 // load the seed's keys 292 w.integrateSeed(seed, seedProgress) 293 w.seeds = append(w.seeds, seed) 294 295 // delete the set of processed transactions; they will be recreated 296 // when we rescan 297 if err = w.dbTx.DeleteBucket(bucketProcessedTransactions); err != nil { 298 return err 299 } 300 if _, err = w.dbTx.CreateBucket(bucketProcessedTransactions); err != nil { 301 return err 302 } 303 w.unconfirmedProcessedTransactions = nil 304 305 // reset the consensus change ID and height in preparation for rescan 306 err = dbPutConsensusChangeID(w.dbTx, modules.ConsensusChangeBeginning) 307 if err != nil { 308 return err 309 } 310 return dbPutConsensusHeight(w.dbTx, 0) 311 }() 312 if err != nil { 313 return err 314 } 315 316 // rescan the blockchain 317 w.cs.Unsubscribe(w) 318 w.tpool.Unsubscribe(w) 319 320 done := make(chan struct{}) 321 go w.rescanMessage(done) 322 defer close(done) 323 324 err = w.cs.ConsensusSetSubscribe(w, modules.ConsensusChangeBeginning, w.tg.StopChan()) 325 if err != nil { 326 return err 327 } 328 w.tpool.TransactionPoolSubscribe(w) 329 return nil 330 } 331 332 // SweepSeed scans the blockchain for outputs generated from seed and creates 333 // a transaction that transfers them to the wallet. Note that this incurs a 334 // transaction fee. It returns the total value of the outputs, minus the fee. 335 // If only siafunds were found, the fee is deducted from the wallet. 336 func (w *Wallet) SweepSeed(seed modules.Seed) (coins, funds types.Currency, err error) { 337 if err = w.tg.Add(); err != nil { 338 return 339 } 340 defer w.tg.Done() 341 342 if !w.scanLock.TryLock() { 343 return types.Currency{}, types.Currency{}, errScanInProgress 344 } 345 defer w.scanLock.Unlock() 346 347 w.mu.RLock() 348 match := seed == w.primarySeed 349 w.mu.RUnlock() 350 if match { 351 return types.Currency{}, types.Currency{}, errors.New("cannot sweep primary seed") 352 } 353 354 if !w.cs.Synced() { 355 return types.Currency{}, types.Currency{}, errors.New("cannot sweep until blockchain is synced") 356 } 357 358 // get an address to spend into 359 w.mu.Lock() 360 uc, err := w.nextPrimarySeedAddress(w.dbTx) 361 w.mu.Unlock() 362 if err != nil { 363 return 364 } 365 366 // scan blockchain for outputs, filtering out 'dust' (outputs that cost 367 // more in fees than they are worth) 368 s := newSeedScanner(seed, w.log) 369 _, maxFee := w.tpool.FeeEstimation() 370 const outputSize = 350 // approx. size in bytes of an output and accompanying signature 371 const maxOutputs = 50 // approx. number of outputs that a transaction can handle 372 s.dustThreshold = maxFee.Mul64(outputSize) 373 if err = s.scan(w.cs, w.tg.StopChan()); err != nil { 374 return 375 } 376 377 if len(s.siacoinOutputs) == 0 && len(s.siafundOutputs) == 0 { 378 // if we aren't sweeping any coins or funds, then just return an 379 // error; no reason to proceed 380 return types.Currency{}, types.Currency{}, errors.New("nothing to sweep") 381 } 382 383 // Flatten map to slice 384 var siacoinOutputs, siafundOutputs []scannedOutput 385 for _, sco := range s.siacoinOutputs { 386 siacoinOutputs = append(siacoinOutputs, sco) 387 } 388 for _, sfo := range s.siafundOutputs { 389 siafundOutputs = append(siafundOutputs, sfo) 390 } 391 392 for len(siacoinOutputs) > 0 || len(siafundOutputs) > 0 { 393 // process up to maxOutputs siacoinOutputs 394 txnSiacoinOutputs := make([]scannedOutput, maxOutputs) 395 n := copy(txnSiacoinOutputs, siacoinOutputs) 396 txnSiacoinOutputs = txnSiacoinOutputs[:n] 397 siacoinOutputs = siacoinOutputs[n:] 398 399 // process up to (maxOutputs-n) siafundOutputs 400 txnSiafundOutputs := make([]scannedOutput, maxOutputs-n) 401 n = copy(txnSiafundOutputs, siafundOutputs) 402 txnSiafundOutputs = txnSiafundOutputs[:n] 403 siafundOutputs = siafundOutputs[n:] 404 405 var txnCoins, txnFunds types.Currency 406 407 // construct a transaction that spends the outputs 408 tb, err := w.StartTransaction() 409 if err != nil { 410 return types.ZeroCurrency, types.ZeroCurrency, err 411 } 412 defer func() { 413 if err != nil { 414 tb.Drop() 415 } 416 }() 417 var sweptCoins, sweptFunds types.Currency // total values of swept outputs 418 for _, output := range txnSiacoinOutputs { 419 // construct a siacoin input that spends the output 420 sk := generateSpendableKey(seed, output.seedIndex) 421 tb.AddSiacoinInput(types.SiacoinInput{ 422 ParentID: types.SiacoinOutputID(output.id), 423 UnlockConditions: sk.UnlockConditions, 424 }) 425 // add a signature for the input 426 sweptCoins = sweptCoins.Add(output.value) 427 } 428 for _, output := range txnSiafundOutputs { 429 // construct a siafund input that spends the output 430 sk := generateSpendableKey(seed, output.seedIndex) 431 tb.AddSiafundInput(types.SiafundInput{ 432 ParentID: types.SiafundOutputID(output.id), 433 UnlockConditions: sk.UnlockConditions, 434 }) 435 // add a signature for the input 436 sweptFunds = sweptFunds.Add(output.value) 437 } 438 439 // estimate the transaction size and fee. NOTE: this equation doesn't 440 // account for other fields in the transaction, but since we are 441 // multiplying by maxFee, lowballing is ok 442 estTxnSize := (len(txnSiacoinOutputs) + len(txnSiafundOutputs)) * outputSize 443 estFee := maxFee.Mul64(uint64(estTxnSize)) 444 tb.AddMinerFee(estFee) 445 446 // calculate total siacoin payout 447 if sweptCoins.Cmp(estFee) > 0 { 448 txnCoins = sweptCoins.Sub(estFee) 449 } 450 txnFunds = sweptFunds 451 452 switch { 453 case txnCoins.IsZero() && txnFunds.IsZero(): 454 // if we aren't sweeping any coins or funds, then just return an 455 // error; no reason to proceed 456 return types.Currency{}, types.Currency{}, errors.New("transaction fee exceeds value of swept outputs") 457 458 case !txnCoins.IsZero() && txnFunds.IsZero(): 459 // if we're sweeping coins but not funds, add a siacoin output for 460 // them 461 tb.AddSiacoinOutput(types.SiacoinOutput{ 462 Value: txnCoins, 463 UnlockHash: uc.UnlockHash(), 464 }) 465 466 case txnCoins.IsZero() && !txnFunds.IsZero(): 467 // if we're sweeping funds but not coins, add a siafund output for 468 // them. This is tricky because we still need to pay for the 469 // transaction fee, but we can't simply subtract the fee from the 470 // output value like we can with swept coins. Instead, we need to fund 471 // the fee using the existing wallet balance. 472 tb.AddSiafundOutput(types.SiafundOutput{ 473 Value: txnFunds, 474 UnlockHash: uc.UnlockHash(), 475 }) 476 err = tb.FundSiacoins(estFee) 477 if err != nil { 478 return types.Currency{}, types.Currency{}, errors.New("couldn't pay transaction fee on swept funds: " + err.Error()) 479 } 480 481 case !txnCoins.IsZero() && !txnFunds.IsZero(): 482 // if we're sweeping both coins and funds, add a siacoin output and a 483 // siafund output 484 tb.AddSiacoinOutput(types.SiacoinOutput{ 485 Value: txnCoins, 486 UnlockHash: uc.UnlockHash(), 487 }) 488 tb.AddSiafundOutput(types.SiafundOutput{ 489 Value: txnFunds, 490 UnlockHash: uc.UnlockHash(), 491 }) 492 } 493 494 // add signatures for all coins and funds (manually, since tb doesn't have 495 // access to the signing keys) 496 txn, parents := tb.View() 497 for _, output := range txnSiacoinOutputs { 498 sk := generateSpendableKey(seed, output.seedIndex) 499 addSignatures(&txn, types.FullCoveredFields, sk.UnlockConditions, crypto.Hash(output.id), sk) 500 } 501 for _, sfo := range txnSiafundOutputs { 502 sk := generateSpendableKey(seed, sfo.seedIndex) 503 addSignatures(&txn, types.FullCoveredFields, sk.UnlockConditions, crypto.Hash(sfo.id), sk) 504 } 505 // Usually, all the inputs will come from swept outputs. However, there is 506 // an edge case in which inputs will be added from the wallet. To cover 507 // this case, we iterate through the SiacoinInputs and add a signature for 508 // any input that belongs to the wallet. 509 w.mu.RLock() 510 for _, input := range txn.SiacoinInputs { 511 if key, ok := w.keys[input.UnlockConditions.UnlockHash()]; ok { 512 addSignatures(&txn, types.FullCoveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key) 513 } 514 } 515 w.mu.RUnlock() 516 517 // Append transaction to txnSet 518 txnSet := append(parents, txn) 519 520 // submit the transactions 521 err = w.tpool.AcceptTransactionSet(txnSet) 522 if err != nil { 523 return types.ZeroCurrency, types.ZeroCurrency, err 524 } 525 526 w.log.Println("Creating a transaction set to sweep a seed, IDs:") 527 for _, txn := range txnSet { 528 w.log.Println("\t", txn.ID()) 529 } 530 531 coins = coins.Add(txnCoins) 532 funds = funds.Add(txnFunds) 533 } 534 return 535 }