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

     1  package wallet
     2  
     3  // TODO: Theoretically, the transaction builder in this wallet supports
     4  // multisig, but there are no automated tests to verify that.
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"sort"
    10  	"sync"
    11  
    12  	bolt "github.com/coreos/bbolt"
    13  	"gitlab.com/NebulousLabs/errors"
    14  	"gitlab.com/NebulousLabs/threadgroup"
    15  
    16  	"gitlab.com/SiaPrime/SiaPrime/build"
    17  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    18  	"gitlab.com/SiaPrime/SiaPrime/encoding"
    19  	"gitlab.com/SiaPrime/SiaPrime/modules"
    20  	"gitlab.com/SiaPrime/SiaPrime/persist"
    21  	siasync "gitlab.com/SiaPrime/SiaPrime/sync"
    22  	"gitlab.com/SiaPrime/SiaPrime/types"
    23  )
    24  
    25  const (
    26  	// RespendTimeout records the number of blocks that the wallet will wait
    27  	// before spending an output that has been spent in the past. If the
    28  	// transaction spending the output has not made it to the transaction pool
    29  	// after the limit, the assumption is that it never will.
    30  	RespendTimeout = 40
    31  )
    32  
    33  var (
    34  	errNilConsensusSet = errors.New("wallet cannot initialize with a nil consensus set")
    35  	errNilTpool        = errors.New("wallet cannot initialize with a nil transaction pool")
    36  )
    37  
    38  // spendableKey is a set of secret keys plus the corresponding unlock
    39  // conditions.  The public key can be derived from the secret key and then
    40  // matched to the corresponding public keys in the unlock conditions. All
    41  // addresses that are to be used in 'FundSiacoins' or 'FundSiafunds' in the
    42  // transaction builder must conform to this form of spendable key.
    43  type spendableKey struct {
    44  	UnlockConditions types.UnlockConditions
    45  	SecretKeys       []crypto.SecretKey
    46  }
    47  
    48  // Wallet is an object that tracks balances, creates keys and addresses,
    49  // manages building and sending transactions.
    50  type Wallet struct {
    51  	// encrypted indicates whether the wallet has been encrypted (i.e.
    52  	// initialized). unlocked indicates whether the wallet is currently
    53  	// storing secret keys in memory. subscribed indicates whether the wallet
    54  	// has subscribed to the consensus set yet - the wallet is unable to
    55  	// subscribe to the consensus set until it has been unlocked for the first
    56  	// time. The primary seed is used to generate new addresses for the
    57  	// wallet.
    58  	encrypted   bool
    59  	unlocked    bool
    60  	subscribed  bool
    61  	primarySeed modules.Seed
    62  
    63  	// The wallet's dependencies.
    64  	cs    modules.ConsensusSet
    65  	tpool modules.TransactionPool
    66  	deps  modules.Dependencies
    67  
    68  	// The following set of fields are responsible for tracking the confirmed
    69  	// outputs, and for being able to spend them. The seeds are used to derive
    70  	// the keys that are tracked on the blockchain. All keys are pregenerated
    71  	// from the seeds, when checking new outputs or spending outputs, the seeds
    72  	// are not referenced at all. The seeds are only stored so that the user
    73  	// may access them.
    74  	seeds        []modules.Seed
    75  	keys         map[types.UnlockHash]spendableKey
    76  	lookahead    map[types.UnlockHash]uint64
    77  	watchedAddrs map[types.UnlockHash]struct{}
    78  
    79  	// unconfirmedProcessedTransactions tracks unconfirmed transactions.
    80  	//
    81  	// TODO: Replace this field with a linked list. Currently when a new
    82  	// transaction set diff is provided, the entire array needs to be
    83  	// reallocated. Since this can happen tens of times per second, and the
    84  	// array can have tens of thousands of elements, it's a performance issue.
    85  	unconfirmedSets                  map[modules.TransactionSetID][]types.TransactionID
    86  	unconfirmedProcessedTransactions []modules.ProcessedTransaction
    87  
    88  	// The wallet's database tracks its seeds, keys, outputs, and
    89  	// transactions. A global db transaction is maintained in memory to avoid
    90  	// excessive disk writes. Any operations involving dbTx must hold an
    91  	// exclusive lock.
    92  	//
    93  	// If dbRollback is set, then when the database syncs it will perform a
    94  	// rollback instead of a commit. For safety reasons, the db will close and
    95  	// the wallet will close if a rollback is performed.
    96  	db         *persist.BoltDatabase
    97  	dbRollback bool
    98  	dbTx       *bolt.Tx
    99  
   100  	persistDir string
   101  	log        *persist.Logger
   102  	mu         sync.RWMutex
   103  
   104  	// A separate TryMutex is used to protect against concurrent unlocking or
   105  	// initialization.
   106  	scanLock siasync.TryMutex
   107  
   108  	// The wallet's ThreadGroup tells tracked functions to shut down and
   109  	// blocks until they have all exited before returning from Close.
   110  	tg threadgroup.ThreadGroup
   111  
   112  	// defragDisabled determines if the wallet is set to defrag outputs once it
   113  	// reaches a certain threshold
   114  	defragDisabled bool
   115  }
   116  
   117  // Height return the internal processed consensus height of the wallet
   118  func (w *Wallet) Height() (types.BlockHeight, error) {
   119  	if err := w.tg.Add(); err != nil {
   120  		return types.BlockHeight(0), modules.ErrWalletShutdown
   121  	}
   122  	defer w.tg.Done()
   123  
   124  	w.mu.Lock()
   125  	defer w.mu.Unlock()
   126  	w.syncDB()
   127  
   128  	var height uint64
   129  	err := w.db.View(func(tx *bolt.Tx) error {
   130  		return encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keyConsensusHeight), &height)
   131  	})
   132  	if err != nil {
   133  		return types.BlockHeight(0), err
   134  	}
   135  	return types.BlockHeight(height), nil
   136  }
   137  
   138  // LastAddresses returns the last n addresses starting at the last seedProgress
   139  // for which an address was generated. If n is greater than the current
   140  // progress, fewer than n keys will be returned. That means all addresses can
   141  // be retrieved in reverse order by simply supplying math.MaxUint64 for n.
   142  func (w *Wallet) LastAddresses(n uint64) ([]types.UnlockHash, error) {
   143  	if err := w.tg.Add(); err != nil {
   144  		return nil, modules.ErrWalletShutdown
   145  	}
   146  	defer w.tg.Done()
   147  
   148  	w.mu.Lock()
   149  	defer w.mu.Unlock()
   150  
   151  	// Get the current seed progress from disk.
   152  	var seedProgress uint64
   153  	err := w.db.View(func(tx *bolt.Tx) (err error) {
   154  		seedProgress, err = dbGetPrimarySeedProgress(tx)
   155  		return
   156  	})
   157  	if err != nil {
   158  		return []types.UnlockHash{}, err
   159  	}
   160  	// At most seedProgess addresses can be requested.
   161  	if n > seedProgress {
   162  		n = seedProgress
   163  	}
   164  	start := seedProgress - n
   165  	// Generate the keys.
   166  	keys := generateKeys(w.primarySeed, start, n)
   167  	uhs := make([]types.UnlockHash, 0, len(keys))
   168  	for i := len(keys) - 1; i >= 0; i-- {
   169  		uhs = append(uhs, keys[i].UnlockConditions.UnlockHash())
   170  	}
   171  	return uhs, nil
   172  }
   173  
   174  // New creates a new wallet, loading any known addresses from the input file
   175  // name and then using the file to save in the future. Keys and addresses are
   176  // not loaded into the wallet during the call to 'new', but rather during the
   177  // call to 'Unlock'.
   178  func New(cs modules.ConsensusSet, tpool modules.TransactionPool, persistDir string) (*Wallet, error) {
   179  	return NewCustomWallet(cs, tpool, persistDir, modules.ProdDependencies)
   180  }
   181  
   182  // NewCustomWallet creates a new wallet using custom dependencies.
   183  func NewCustomWallet(cs modules.ConsensusSet, tpool modules.TransactionPool, persistDir string, deps modules.Dependencies) (*Wallet, error) {
   184  	// Check for nil dependencies.
   185  	if cs == nil {
   186  		return nil, errNilConsensusSet
   187  	}
   188  	if tpool == nil {
   189  		return nil, errNilTpool
   190  	}
   191  
   192  	// Initialize the data structure.
   193  	w := &Wallet{
   194  		cs:    cs,
   195  		tpool: tpool,
   196  
   197  		keys:         make(map[types.UnlockHash]spendableKey),
   198  		lookahead:    make(map[types.UnlockHash]uint64),
   199  		watchedAddrs: make(map[types.UnlockHash]struct{}),
   200  
   201  		unconfirmedSets: make(map[modules.TransactionSetID][]types.TransactionID),
   202  
   203  		persistDir: persistDir,
   204  
   205  		deps: deps,
   206  	}
   207  	err := w.initPersist()
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	return w, nil
   212  }
   213  
   214  // Close terminates all ongoing processes involving the wallet, enabling
   215  // garbage collection.
   216  func (w *Wallet) Close() error {
   217  	if err := w.tg.Stop(); err != nil {
   218  		return err
   219  	}
   220  	var errs []error
   221  	// Lock the wallet outside of mu.Lock because Lock uses its own mu.Lock.
   222  	// Once the wallet is locked it cannot be unlocked except using the
   223  	// unexported unlock method (w.Unlock returns an error if the wallet's
   224  	// ThreadGroup is stopped).
   225  	if w.managedUnlocked() {
   226  		if err := w.managedLock(); err != nil {
   227  			errs = append(errs, err)
   228  		}
   229  	}
   230  
   231  	w.cs.Unsubscribe(w)
   232  	w.tpool.Unsubscribe(w)
   233  
   234  	if err := w.log.Close(); err != nil {
   235  		errs = append(errs, fmt.Errorf("log.Close failed: %v", err))
   236  	}
   237  	return build.JoinErrors(errs, "; ")
   238  }
   239  
   240  // AllAddresses returns all addresses that the wallet is able to spend from,
   241  // including unseeded addresses. Addresses are returned sorted in byte-order.
   242  func (w *Wallet) AllAddresses() ([]types.UnlockHash, error) {
   243  	if err := w.tg.Add(); err != nil {
   244  		return []types.UnlockHash{}, modules.ErrWalletShutdown
   245  	}
   246  	defer w.tg.Done()
   247  
   248  	w.mu.RLock()
   249  	defer w.mu.RUnlock()
   250  
   251  	addrs := make([]types.UnlockHash, 0, len(w.keys))
   252  	for addr := range w.keys {
   253  		addrs = append(addrs, addr)
   254  	}
   255  	sort.Slice(addrs, func(i, j int) bool {
   256  		return bytes.Compare(addrs[i][:], addrs[j][:]) < 0
   257  	})
   258  	return addrs, nil
   259  }
   260  
   261  // Rescanning reports whether the wallet is currently rescanning the
   262  // blockchain.
   263  func (w *Wallet) Rescanning() (bool, error) {
   264  	if err := w.tg.Add(); err != nil {
   265  		return false, modules.ErrWalletShutdown
   266  	}
   267  	defer w.tg.Done()
   268  
   269  	rescanning := !w.scanLock.TryLock()
   270  	if !rescanning {
   271  		w.scanLock.Unlock()
   272  	}
   273  	return rescanning, nil
   274  }
   275  
   276  // Settings returns the wallet's current settings
   277  func (w *Wallet) Settings() (modules.WalletSettings, error) {
   278  	if err := w.tg.Add(); err != nil {
   279  		return modules.WalletSettings{}, modules.ErrWalletShutdown
   280  	}
   281  	defer w.tg.Done()
   282  	return modules.WalletSettings{
   283  		NoDefrag: w.defragDisabled,
   284  	}, nil
   285  }
   286  
   287  // SetSettings will update the settings for the wallet.
   288  func (w *Wallet) SetSettings(s modules.WalletSettings) error {
   289  	if err := w.tg.Add(); err != nil {
   290  		return modules.ErrWalletShutdown
   291  	}
   292  	defer w.tg.Done()
   293  
   294  	w.mu.Lock()
   295  	w.defragDisabled = s.NoDefrag
   296  	w.mu.Unlock()
   297  	return nil
   298  }