gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/seed.go (about) 1 package wallet 2 3 import ( 4 "runtime" 5 "sync" 6 7 bolt "github.com/coreos/bbolt" 8 "gitlab.com/NebulousLabs/errors" 9 "gitlab.com/NebulousLabs/fastrand" 10 "gitlab.com/SiaPrime/SiaPrime/crypto" 11 "gitlab.com/SiaPrime/SiaPrime/encoding" 12 "gitlab.com/SiaPrime/SiaPrime/modules" 13 "gitlab.com/SiaPrime/SiaPrime/types" 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.CipherKey, 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.CipherKey, 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.CipherKey, 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 height, err2 := dbGetConsensusHeight(w.dbTx) 362 w.mu.Unlock() 363 if err != nil { 364 return types.Currency{}, types.Currency{}, err 365 } 366 if err2 != nil { 367 return types.Currency{}, types.Currency{}, err2 368 } 369 370 // scan blockchain for outputs, filtering out 'dust' (outputs that cost 371 // more in fees than they are worth) 372 s := newSeedScanner(seed, w.log) 373 _, maxFee := w.tpool.FeeEstimation() 374 const outputSize = 350 // approx. size in bytes of an output and accompanying signature 375 const maxOutputs = 50 // approx. number of outputs that a transaction can handle 376 s.dustThreshold = maxFee.Mul64(outputSize) 377 if err = s.scan(w.cs, w.tg.StopChan()); err != nil { 378 return 379 } 380 381 if len(s.siacoinOutputs) == 0 && len(s.siafundOutputs) == 0 { 382 // if we aren't sweeping any coins or funds, then just return an 383 // error; no reason to proceed 384 return types.Currency{}, types.Currency{}, errors.New("nothing to sweep") 385 } 386 387 // Flatten map to slice 388 var siacoinOutputs, siafundOutputs []scannedOutput 389 for _, sco := range s.siacoinOutputs { 390 siacoinOutputs = append(siacoinOutputs, sco) 391 } 392 for _, sfo := range s.siafundOutputs { 393 siafundOutputs = append(siafundOutputs, sfo) 394 } 395 396 for len(siacoinOutputs) > 0 || len(siafundOutputs) > 0 { 397 // process up to maxOutputs siacoinOutputs 398 txnSiacoinOutputs := make([]scannedOutput, maxOutputs) 399 n := copy(txnSiacoinOutputs, siacoinOutputs) 400 txnSiacoinOutputs = txnSiacoinOutputs[:n] 401 siacoinOutputs = siacoinOutputs[n:] 402 403 // process up to (maxOutputs-n) siafundOutputs 404 txnSiafundOutputs := make([]scannedOutput, maxOutputs-n) 405 n = copy(txnSiafundOutputs, siafundOutputs) 406 txnSiafundOutputs = txnSiafundOutputs[:n] 407 siafundOutputs = siafundOutputs[n:] 408 409 var txnCoins, txnFunds types.Currency 410 411 // construct a transaction that spends the outputs 412 tb, err := w.StartTransaction() 413 if err != nil { 414 return types.ZeroCurrency, types.ZeroCurrency, err 415 } 416 defer func() { 417 if err != nil { 418 tb.Drop() 419 } 420 }() 421 var sweptCoins, sweptFunds types.Currency // total values of swept outputs 422 for _, output := range txnSiacoinOutputs { 423 // construct a siacoin input that spends the output 424 sk := generateSpendableKey(seed, output.seedIndex) 425 tb.AddSiacoinInput(types.SiacoinInput{ 426 ParentID: types.SiacoinOutputID(output.id), 427 UnlockConditions: sk.UnlockConditions, 428 }) 429 // add a signature for the input 430 sweptCoins = sweptCoins.Add(output.value) 431 } 432 for _, output := range txnSiafundOutputs { 433 // construct a siafund input that spends the output 434 sk := generateSpendableKey(seed, output.seedIndex) 435 tb.AddSiafundInput(types.SiafundInput{ 436 ParentID: types.SiafundOutputID(output.id), 437 UnlockConditions: sk.UnlockConditions, 438 }) 439 // add a signature for the input 440 sweptFunds = sweptFunds.Add(output.value) 441 } 442 443 // estimate the transaction size and fee. NOTE: this equation doesn't 444 // account for other fields in the transaction, but since we are 445 // multiplying by maxFee, lowballing is ok 446 estTxnSize := (len(txnSiacoinOutputs) + len(txnSiafundOutputs)) * outputSize 447 estFee := maxFee.Mul64(uint64(estTxnSize)) 448 tb.AddMinerFee(estFee) 449 450 // calculate total siacoin payout 451 if sweptCoins.Cmp(estFee) > 0 { 452 txnCoins = sweptCoins.Sub(estFee) 453 } 454 txnFunds = sweptFunds 455 456 switch { 457 case txnCoins.IsZero() && txnFunds.IsZero(): 458 // if we aren't sweeping any coins or funds, then just return an 459 // error; no reason to proceed 460 return types.Currency{}, types.Currency{}, errors.New("transaction fee exceeds value of swept outputs") 461 462 case !txnCoins.IsZero() && txnFunds.IsZero(): 463 // if we're sweeping coins but not funds, add a siacoin output for 464 // them 465 tb.AddSiacoinOutput(types.SiacoinOutput{ 466 Value: txnCoins, 467 UnlockHash: uc.UnlockHash(), 468 }) 469 470 case txnCoins.IsZero() && !txnFunds.IsZero(): 471 // if we're sweeping funds but not coins, add a siafund output for 472 // them. This is tricky because we still need to pay for the 473 // transaction fee, but we can't simply subtract the fee from the 474 // output value like we can with swept coins. Instead, we need to fund 475 // the fee using the existing wallet balance. 476 tb.AddSiafundOutput(types.SiafundOutput{ 477 Value: txnFunds, 478 UnlockHash: uc.UnlockHash(), 479 }) 480 err = tb.FundSiacoins(estFee) 481 if err != nil { 482 return types.Currency{}, types.Currency{}, errors.New("couldn't pay transaction fee on swept funds: " + err.Error()) 483 } 484 485 case !txnCoins.IsZero() && !txnFunds.IsZero(): 486 // if we're sweeping both coins and funds, add a siacoin output and a 487 // siafund output 488 tb.AddSiacoinOutput(types.SiacoinOutput{ 489 Value: txnCoins, 490 UnlockHash: uc.UnlockHash(), 491 }) 492 tb.AddSiafundOutput(types.SiafundOutput{ 493 Value: txnFunds, 494 UnlockHash: uc.UnlockHash(), 495 }) 496 } 497 498 // add signatures for all coins and funds (manually, since tb doesn't have 499 // access to the signing keys) 500 txn, parents := tb.View() 501 for _, output := range txnSiacoinOutputs { 502 sk := generateSpendableKey(seed, output.seedIndex) 503 addSignatures(&txn, types.FullCoveredFields, sk.UnlockConditions, crypto.Hash(output.id), sk, height) 504 } 505 for _, sfo := range txnSiafundOutputs { 506 sk := generateSpendableKey(seed, sfo.seedIndex) 507 addSignatures(&txn, types.FullCoveredFields, sk.UnlockConditions, crypto.Hash(sfo.id), sk, height) 508 } 509 // Usually, all the inputs will come from swept outputs. However, there is 510 // an edge case in which inputs will be added from the wallet. To cover 511 // this case, we iterate through the SiacoinInputs and add a signature for 512 // any input that belongs to the wallet. 513 w.mu.RLock() 514 for _, input := range txn.SiacoinInputs { 515 if key, ok := w.keys[input.UnlockConditions.UnlockHash()]; ok { 516 addSignatures(&txn, types.FullCoveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key, height) 517 } 518 } 519 w.mu.RUnlock() 520 521 // Append transaction to txnSet 522 txnSet := append(parents, txn) 523 524 // submit the transactions 525 err = w.tpool.AcceptTransactionSet(txnSet) 526 if err != nil { 527 return types.ZeroCurrency, types.ZeroCurrency, err 528 } 529 530 w.log.Println("Creating a transaction set to sweep a seed, IDs:") 531 for _, txn := range txnSet { 532 w.log.Println("\t", txn.ID()) 533 } 534 535 coins = coins.Add(txnCoins) 536 funds = funds.Add(txnFunds) 537 } 538 return 539 }