diff --git a/api/wallet_test.go b/api/wallet_test.go index 1e104e3b4c..3413bcc762 100644 --- a/api/wallet_test.go +++ b/api/wallet_test.go @@ -833,3 +833,122 @@ func TestWalletReset(t *testing.T) { t.Error("wallet is not unlocked") } } + +func TestWalletSiafunds(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + t.Parallel() + + walletPassword := "testpass" + key := crypto.TwofishKey(crypto.HashObject(walletPassword)) + testdir := build.TempDir("api", t.Name()) + st, err := assembleServerTester(key, testdir) + if err != nil { + t.Fatal(err) + } + defer st.server.Close() + + // mine some money + for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ { + _, err := st.miner.AddBlock() + if err != nil { + t.Fatal(err) + } + } + + // record transactions + var wtg WalletTransactionsGET + err = st.getAPI("/wallet/transactions?startheight=0&endheight=100", &wtg) + if err != nil { + t.Fatal(err) + } + numTxns := len(wtg.ConfirmedTransactions) + + // load siafunds into the wallet + siagPath, _ := filepath.Abs("../types/siag0of1of1.siakey") + loadSiagValues := url.Values{} + loadSiagValues.Set("keyfiles", siagPath) + loadSiagValues.Set("encryptionpassword", walletPassword) + err = st.stdPostAPI("/wallet/siagkey", loadSiagValues) + if err != nil { + t.Fatal(err) + } + + err = st.getAPI("/wallet/transactions?startheight=0&endheight=100", &wtg) + if err != nil { + t.Fatal(err) + } + if len(wtg.ConfirmedTransactions) != numTxns+1 { + t.Errorf("expected %v transactions, got %v", numTxns+1, len(wtg.ConfirmedTransactions)) + } + + // check balance + var wg WalletGET + err = st.getAPI("/wallet", &wg) + if err != nil { + t.Fatal(err) + } + if wg.SiafundBalance.Cmp64(2000) != 0 { + t.Fatalf("bad siafund balance: expected %v, got %v", 2000, wg.SiafundBalance) + } + + // spend the siafunds into the wallet seed + var wag WalletAddressGET + err = st.getAPI("/wallet/address", &wag) + if err != nil { + t.Fatal(err) + } + sendSiafundsValues := url.Values{} + sendSiafundsValues.Set("amount", "2000") + sendSiafundsValues.Set("destination", wag.Address.String()) + err = st.stdPostAPI("/wallet/siafunds", sendSiafundsValues) + if err != nil { + t.Fatal(err) + } + + // Announce the host and form an allowance with it. This will result in a + // siafund claim. + err = st.announceHost() + if err != nil { + t.Fatal(err) + } + err = st.setHostStorage() + if err != nil { + t.Fatal(err) + } + err = st.acceptContracts() + if err != nil { + t.Fatal(err) + } + // mine a block so that the announcement makes it into the blockchain + _, err = st.miner.AddBlock() + if err != nil { + t.Fatal(err) + } + + // form allowance + allowanceValues := url.Values{} + testFunds := "10000000000000000000000000000" // 10k SC + testPeriod := "20" + allowanceValues.Set("funds", testFunds) + allowanceValues.Set("period", testPeriod) + err = st.stdPostAPI("/renter", allowanceValues) + if err != nil { + t.Fatal(err) + } + + // mine a block so that the file contract makes it into the blockchain + _, err = st.miner.AddBlock() + if err != nil { + t.Fatal(err) + } + // wallet should now have a claim balance + err = st.getAPI("/wallet", &wg) + if err != nil { + t.Fatal(err) + } + if wg.SiacoinClaimBalance.IsZero() { + t.Fatal("expected non-zero claim balance") + } +} diff --git a/modules/wallet/database.go b/modules/wallet/database.go index 332fe76009..3bdd1bacc7 100644 --- a/modules/wallet/database.go +++ b/modules/wallet/database.go @@ -67,6 +67,7 @@ var ( keyConsensusHeight = []byte("keyConsensusHeight") keySpendableKeyFiles = []byte("keySpendableKeyFiles") keyAuxiliarySeedFiles = []byte("keyAuxiliarySeedFiles") + keySiafundPool = []byte("keySiafundPool") errNoKey = errors.New("key does not exist") ) @@ -107,6 +108,32 @@ func (w *Wallet) syncDB() { } } +// dbReset wipes and reinitializes a wallet database. +func dbReset(tx *bolt.Tx) error { + for _, bucket := range dbBuckets { + err := tx.DeleteBucket(bucket) + if err != nil { + return err + } + _, err = tx.CreateBucket(bucket) + if err != nil { + return err + } + } + + // reinitialize the database with default values + wb := tx.Bucket(bucketWallet) + wb.Put(keyUID, fastrand.Bytes(len(uniqueID{}))) + wb.Put(keyConsensusHeight, encoding.Marshal(uint64(0))) + wb.Put(keyAuxiliarySeedFiles, encoding.Marshal([]seedFile{})) + wb.Put(keySpendableKeyFiles, encoding.Marshal([]spendableKeyFile{})) + dbPutConsensusHeight(tx, 0) + dbPutConsensusChangeID(tx, modules.ConsensusChangeBeginning) + dbPutSiafundPool(tx, types.ZeroCurrency) + + return nil +} + // dbPut is a helper function for storing a marshalled key/value pair. func dbPut(b *bolt.Bucket, key, val interface{}) error { return b.Put(encoding.Marshal(key), encoding.Marshal(val)) @@ -207,33 +234,6 @@ func dbDeleteSpentOutput(tx *bolt.Tx, id types.OutputID) error { return dbDelete(tx.Bucket(bucketSpentOutputs), id) } -// dbReset wipes and reinitializes a wallet database. -func dbReset(tx *bolt.Tx) error { - for _, bucket := range dbBuckets { - err := tx.DeleteBucket(bucket) - if err != nil { - return err - } - _, err = tx.CreateBucket(bucket) - if err != nil { - return err - } - } - - // reinitialize the database with default values - wb := tx.Bucket(bucketWallet) - uid := make([]byte, len(uniqueID{})) - fastrand.Read(uid[:]) - wb.Put(keyUID, uid) - wb.Put(keyConsensusHeight, encoding.Marshal(uint64(0))) - wb.Put(keyAuxiliarySeedFiles, encoding.Marshal([]seedFile{})) - wb.Put(keySpendableKeyFiles, encoding.Marshal([]spendableKeyFile{})) - dbPutConsensusHeight(tx, 0) - dbPutConsensusChangeID(tx, modules.ConsensusChangeBeginning) - - return nil -} - // bucketProcessedTransactions works a little differently: the key is // meaningless, only used to order the transactions chronologically. @@ -306,3 +306,14 @@ func dbGetConsensusHeight(tx *bolt.Tx) (height types.BlockHeight, err error) { func dbPutConsensusHeight(tx *bolt.Tx, height types.BlockHeight) error { return tx.Bucket(bucketWallet).Put(keyConsensusHeight, encoding.Marshal(height)) } + +// dbGetSiafundPool returns the value of the siafund pool. +func dbGetSiafundPool(tx *bolt.Tx) (pool types.Currency, err error) { + err = encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keySiafundPool), &pool) + return +} + +// dbPutSiafundPool stores the value of the siafund pool. +func dbPutSiafundPool(tx *bolt.Tx, pool types.Currency) error { + return tx.Bucket(bucketWallet).Put(keySiafundPool, encoding.Marshal(pool)) +} diff --git a/modules/wallet/encrypt.go b/modules/wallet/encrypt.go index c9b4ddf507..40ef6506a2 100644 --- a/modules/wallet/encrypt.go +++ b/modules/wallet/encrypt.go @@ -336,7 +336,6 @@ func (w *Wallet) Reset() error { w.keys = make(map[types.UnlockHash]spendableKey) w.seeds = []modules.Seed{} w.unconfirmedProcessedTransactions = []modules.ProcessedTransaction{} - w.siafundPool = types.Currency{} w.unlocked = false w.encrypted = false w.subscribed = false diff --git a/modules/wallet/money.go b/modules/wallet/money.go index 82449f1063..80dd619fe7 100644 --- a/modules/wallet/money.go +++ b/modules/wallet/money.go @@ -2,6 +2,7 @@ package wallet import ( "github.com/NebulousLabs/Sia/build" + "github.com/NebulousLabs/Sia/modules" "github.com/NebulousLabs/Sia/types" ) @@ -15,9 +16,10 @@ type sortedOutputs struct { // ConfirmedBalance returns the balance of the wallet according to all of the // confirmed transactions. func (w *Wallet) ConfirmedBalance() (siacoinBalance types.Currency, siafundBalance types.Currency, siafundClaimBalance types.Currency) { - // ensure durability of reported balance w.mu.Lock() defer w.mu.Unlock() + + // ensure durability of reported balance w.syncDB() dbForEachSiacoinOutput(w.dbTx, func(_ types.SiacoinOutputID, sco types.SiacoinOutput) { @@ -25,9 +27,20 @@ func (w *Wallet) ConfirmedBalance() (siacoinBalance types.Currency, siafundBalan siacoinBalance = siacoinBalance.Add(sco.Value) } }) + + siafundPool, err := dbGetSiafundPool(w.dbTx) + if err != nil { + return + } dbForEachSiafundOutput(w.dbTx, func(_ types.SiafundOutputID, sfo types.SiafundOutput) { siafundBalance = siafundBalance.Add(sfo.Value) - siafundClaimBalance = siafundClaimBalance.Add(w.siafundPool.Sub(sfo.ClaimStart).Mul(sfo.Value).Div(types.SiafundCount)) + if sfo.ClaimStart.Cmp(siafundPool) > 0 { + // Skip claims larger than the siafund pool. This should only + // occur if the siafund pool has not been initialized yet. + w.log.Debugf("skipping claim with start value %v because siafund pool is only %v", sfo.ClaimStart, siafundPool) + return + } + siafundClaimBalance = siafundClaimBalance.Add(siafundPool.Sub(sfo.ClaimStart).Mul(sfo.Value).Div(types.SiafundCount)) }) return } @@ -61,6 +74,9 @@ func (w *Wallet) SendSiacoins(amount types.Currency, dest types.UnlockHash) ([]t return nil, err } defer w.tg.Done() + if !w.unlocked { + return nil, modules.ErrLockedWallet + } _, tpoolFee := w.tpool.FeeEstimation() tpoolFee = tpoolFee.Mul64(750) // Estimated transaction size in bytes @@ -94,6 +110,9 @@ func (w *Wallet) SendSiafunds(amount types.Currency, dest types.UnlockHash) ([]t return nil, err } defer w.tg.Done() + if !w.unlocked { + return nil, modules.ErrLockedWallet + } _, tpoolFee := w.tpool.FeeEstimation() tpoolFee = tpoolFee.Mul64(750) // Estimated transaction size in bytes diff --git a/modules/wallet/persist.go b/modules/wallet/persist.go index 748299ad96..7cd929a936 100644 --- a/modules/wallet/persist.go +++ b/modules/wallet/persist.go @@ -10,6 +10,7 @@ import ( "github.com/NebulousLabs/Sia/encoding" "github.com/NebulousLabs/Sia/modules" "github.com/NebulousLabs/Sia/persist" + "github.com/NebulousLabs/Sia/types" "github.com/NebulousLabs/fastrand" "github.com/NebulousLabs/bolt" @@ -66,6 +67,9 @@ func (w *Wallet) openDB(filename string) (err error) { if wb.Get(keySpendableKeyFiles) == nil { wb.Put(keySpendableKeyFiles, encoding.Marshal([]spendableKeyFile{})) } + if wb.Get(keySiafundPool) == nil { + wb.Put(keySiafundPool, encoding.Marshal(types.ZeroCurrency)) + } // check whether wallet is encrypted w.encrypted = tx.Bucket(bucketWallet).Get(keyEncryptionVerification) != nil diff --git a/modules/wallet/seed.go b/modules/wallet/seed.go index db5a670627..ae05bb7600 100644 --- a/modules/wallet/seed.go +++ b/modules/wallet/seed.go @@ -241,6 +241,16 @@ func (w *Wallet) LoadSeed(masterKey crypto.TwofishKey, seed modules.Seed) error w.integrateSeed(seed, seedProgress) w.seeds = append(w.seeds, seed) + // delete the set of processed transactions; they will be recreated + // when we rescan + if err = w.dbTx.DeleteBucket(bucketProcessedTransactions); err != nil { + return err + } + if _, err = w.dbTx.CreateBucket(bucketProcessedTransactions); err != nil { + return err + } + w.unconfirmedProcessedTransactions = nil + // reset the consensus change ID and height in preparation for rescan err = dbPutConsensusChangeID(w.dbTx, modules.ConsensusChangeBeginning) if err != nil { diff --git a/modules/wallet/unseeded.go b/modules/wallet/unseeded.go index 0f68c274b0..a7bfa5d220 100644 --- a/modules/wallet/unseeded.go +++ b/modules/wallet/unseeded.go @@ -159,6 +159,7 @@ func (w *Wallet) loadSiagKeys(masterKey crypto.TwofishKey, keyfiles []string) er if err != nil { return err } + w.integrateSpendableKey(masterKey, sk) return nil } @@ -168,9 +169,47 @@ func (w *Wallet) LoadSiagKeys(masterKey crypto.TwofishKey, keyfiles []string) er return err } defer w.tg.Done() - w.mu.Lock() - defer w.mu.Unlock() - return w.loadSiagKeys(masterKey, keyfiles) + + // load the keys and reset the consensus change ID and height in preparation for rescan + err := func() error { + w.mu.Lock() + defer w.mu.Unlock() + err := w.loadSiagKeys(masterKey, keyfiles) + if err != nil { + return err + } + + if err = w.dbTx.DeleteBucket(bucketProcessedTransactions); err != nil { + return err + } + if _, err = w.dbTx.CreateBucket(bucketProcessedTransactions); err != nil { + return err + } + w.unconfirmedProcessedTransactions = nil + err = dbPutConsensusChangeID(w.dbTx, modules.ConsensusChangeBeginning) + if err != nil { + return err + } + return dbPutConsensusHeight(w.dbTx, 0) + }() + if err != nil { + return err + } + + // rescan the blockchain + w.cs.Unsubscribe(w) + w.tpool.Unsubscribe(w) + + done := make(chan struct{}) + go w.rescanMessage(done) + defer close(done) + + err = w.cs.ConsensusSetSubscribe(w, modules.ConsensusChangeBeginning) + if err != nil { + return err + } + w.tpool.TransactionPoolSubscribe(w) + return nil } // Load033xWallet loads a v0.3.3.x wallet as an unseeded key, such that the @@ -180,30 +219,66 @@ func (w *Wallet) Load033xWallet(masterKey crypto.TwofishKey, filepath033x string return err } defer w.tg.Done() - w.mu.Lock() - defer w.mu.Unlock() - var savedKeys []savedKey033x - err := encoding.ReadFile(filepath033x, &savedKeys) - if err != nil { - return err - } - var seedsLoaded int - for _, savedKey := range savedKeys { - spendKey := spendableKey{ - UnlockConditions: savedKey.UnlockConditions, - SecretKeys: []crypto.SecretKey{savedKey.SecretKey}, + // load the keys and reset the consensus change ID and height in preparation for rescan + err := func() error { + w.mu.Lock() + defer w.mu.Unlock() + + var savedKeys []savedKey033x + err := encoding.ReadFile(filepath033x, &savedKeys) + if err != nil { + return err } - err = w.loadSpendableKey(masterKey, spendKey) - if err != nil && err != errDuplicateSpendableKey { + var seedsLoaded int + for _, savedKey := range savedKeys { + spendKey := spendableKey{ + UnlockConditions: savedKey.UnlockConditions, + SecretKeys: []crypto.SecretKey{savedKey.SecretKey}, + } + err = w.loadSpendableKey(masterKey, spendKey) + if err != nil && err != errDuplicateSpendableKey { + return err + } + if err == nil { + seedsLoaded++ + } + w.integrateSpendableKey(masterKey, spendKey) + } + if seedsLoaded == 0 { + return errAllDuplicates + } + + if err = w.dbTx.DeleteBucket(bucketProcessedTransactions); err != nil { return err } - if err == nil { - seedsLoaded++ + if _, err = w.dbTx.CreateBucket(bucketProcessedTransactions); err != nil { + return err } + w.unconfirmedProcessedTransactions = nil + err = dbPutConsensusChangeID(w.dbTx, modules.ConsensusChangeBeginning) + if err != nil { + return err + } + return dbPutConsensusHeight(w.dbTx, 0) + }() + if err != nil { + return err } - if seedsLoaded == 0 { - return errAllDuplicates + + // rescan the blockchain + w.cs.Unsubscribe(w) + w.tpool.Unsubscribe(w) + + done := make(chan struct{}) + go w.rescanMessage(done) + defer close(done) + + err = w.cs.ConsensusSetSubscribe(w, modules.ConsensusChangeBeginning) + if err != nil { + return err } + w.tpool.TransactionPoolSubscribe(w) + return nil } diff --git a/modules/wallet/update.go b/modules/wallet/update.go index 52cc52745a..047bbbcc9f 100644 --- a/modules/wallet/update.go +++ b/modules/wallet/update.go @@ -87,10 +87,14 @@ func (w *Wallet) updateConfirmedSet(tx *bolt.Tx, cc modules.ConsensusChange) err } } for _, diff := range cc.SiafundPoolDiffs { + var err error if diff.Direction == modules.DiffApply { - w.siafundPool = diff.Adjusted + err = dbPutSiafundPool(tx, diff.Adjusted) } else { - w.siafundPool = diff.Previous + err = dbPutSiafundPool(tx, diff.Previous) + } + if err != nil { + w.log.Severe("Could not update siafund pool:", err) } } return nil @@ -250,7 +254,11 @@ func (w *Wallet) applyHistory(tx *bolt.Tx, applied []types.Block) error { if err != nil { return fmt.Errorf("could not get historic claim start: %v", err) } - claimValue := w.siafundPool.Sub(startVal).Mul(sfiValue) + siafundPool, err := dbGetSiafundPool(w.dbTx) + if err != nil { + return fmt.Errorf("could not get siafund pool: %v", err) + } + claimValue := siafundPool.Sub(startVal).Mul(sfiValue) pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{ FundType: types.SpecifierClaimOutput, MaturityHeight: consensusHeight + types.MaturityDelay, diff --git a/modules/wallet/wallet.go b/modules/wallet/wallet.go index 040670bbaa..66eca219de 100644 --- a/modules/wallet/wallet.go +++ b/modules/wallet/wallet.go @@ -58,13 +58,9 @@ type Wallet struct { subscribed bool primarySeed modules.Seed - // The wallet's dependencies. siafundPool is tracked separately from the - // consensus set to minimize the number of queries that the wallet needs - // to make to the consensus set; queries to the consensus set are very - // slow. - cs modules.ConsensusSet - tpool modules.TransactionPool - siafundPool types.Currency + // The wallet's dependencies. + cs modules.ConsensusSet + tpool modules.TransactionPool // The following set of fields are responsible for tracking the confirmed // outputs, and for being able to spend them. The seeds are used to derive