github.com/NebulousLabs/Sia@v1.3.7/modules/wallet/database.go (about)

     1  package wallet
     2  
     3  import (
     4  	"encoding/binary"
     5  	"fmt"
     6  	"reflect"
     7  	"time"
     8  
     9  	"github.com/NebulousLabs/Sia/encoding"
    10  	"github.com/NebulousLabs/Sia/modules"
    11  	"github.com/NebulousLabs/Sia/types"
    12  	"github.com/NebulousLabs/errors"
    13  	"github.com/NebulousLabs/fastrand"
    14  
    15  	"github.com/coreos/bbolt"
    16  )
    17  
    18  var (
    19  	// bucketProcessedTransactions stores ProcessedTransactions in
    20  	// chronological order. Only transactions relevant to the wallet are
    21  	// stored. The key of this bucket is an autoincrementing integer.
    22  	bucketProcessedTransactions = []byte("bucketProcessedTransactions")
    23  	// bucketProcessedTxnIndex maps a ProcessedTransactions ID to it's
    24  	// autoincremented index in bucketProcessedTransactions
    25  	bucketProcessedTxnIndex = []byte("bucketProcessedTxnKey")
    26  	// bucketAddrTransactions maps an UnlockHash to the
    27  	// ProcessedTransactions that it appears in.
    28  	bucketAddrTransactions = []byte("bucketAddrTransactions")
    29  	// bucketSiacoinOutputs maps a SiacoinOutputID to its SiacoinOutput. Only
    30  	// outputs that the wallet controls are stored. The wallet uses these
    31  	// outputs to fund transactions.
    32  	bucketSiacoinOutputs = []byte("bucketSiacoinOutputs")
    33  	// bucketSiacoinOutputs maps a SiafundOutputID to its SiafundOutput. Only
    34  	// outputs that the wallet controls are stored. The wallet uses these
    35  	// outputs to fund transactions.
    36  	bucketSiafundOutputs = []byte("bucketSiafundOutputs")
    37  	// bucketSpentOutputs maps an OutputID to the height at which it was
    38  	// spent. Only outputs spent by the wallet are stored. The wallet tracks
    39  	// these outputs so that it can reuse them if they are not confirmed on
    40  	// the blockchain.
    41  	bucketSpentOutputs = []byte("bucketSpentOutputs")
    42  	// bucketWallet contains various fields needed by the wallet, such as its
    43  	// UID, EncryptionVerification, and PrimarySeedFile.
    44  	bucketWallet = []byte("bucketWallet")
    45  
    46  	dbBuckets = [][]byte{
    47  		bucketProcessedTransactions,
    48  		bucketProcessedTxnIndex,
    49  		bucketAddrTransactions,
    50  		bucketSiacoinOutputs,
    51  		bucketSiafundOutputs,
    52  		bucketSpentOutputs,
    53  		bucketWallet,
    54  	}
    55  
    56  	errNoKey = errors.New("key does not exist")
    57  
    58  	// these keys are used in bucketWallet
    59  	keyAuxiliarySeedFiles     = []byte("keyAuxiliarySeedFiles")
    60  	keyConsensusChange        = []byte("keyConsensusChange")
    61  	keyConsensusHeight        = []byte("keyConsensusHeight")
    62  	keyEncryptionVerification = []byte("keyEncryptionVerification")
    63  	keyPrimarySeedFile        = []byte("keyPrimarySeedFile")
    64  	keyPrimarySeedProgress    = []byte("keyPrimarySeedProgress")
    65  	keySiafundPool            = []byte("keySiafundPool")
    66  	keySpendableKeyFiles      = []byte("keySpendableKeyFiles")
    67  	keyUID                    = []byte("keyUID")
    68  )
    69  
    70  // threadedDBUpdate commits the active database transaction and starts a new
    71  // transaction.
    72  func (w *Wallet) threadedDBUpdate() {
    73  	if err := w.tg.Add(); err != nil {
    74  		return
    75  	}
    76  	defer w.tg.Done()
    77  
    78  	for {
    79  		select {
    80  		case <-time.After(2 * time.Minute):
    81  		case <-w.tg.StopChan():
    82  			return
    83  		}
    84  		w.mu.Lock()
    85  		err := w.syncDB()
    86  		w.mu.Unlock()
    87  		if err != nil {
    88  			// If the database is having problems, we need to close it to
    89  			// protect it. This will likely cause a panic somewhere when another
    90  			// caller tries to access dbTx but it is nil.
    91  			w.log.Severe("ERROR: syncDB encountered an error. Closing database to protect wallet. wallet may crash:", err)
    92  			w.db.Close()
    93  			return
    94  		}
    95  	}
    96  }
    97  
    98  // syncDB commits the current global transaction and immediately begins a
    99  // new one. It must be called with a write-lock.
   100  func (w *Wallet) syncDB() error {
   101  	// If the rollback flag is set, it means that somewhere in the middle of an
   102  	// atomic update there  was a failure, and that failure needs to be rolled
   103  	// back. An error will be returned.
   104  	if w.dbRollback {
   105  		err := errors.New("database unable to sync - rollback requested")
   106  		return errors.Compose(err, w.dbTx.Rollback())
   107  	}
   108  
   109  	// commit the current tx
   110  	err := w.dbTx.Commit()
   111  	if err != nil {
   112  		w.log.Severe("ERROR: failed to apply database update:", err)
   113  		err = errors.Compose(err, w.dbTx.Rollback())
   114  		return errors.AddContext(err, "unable to commit dbTx in syncDB")
   115  	}
   116  	// begin a new tx
   117  	w.dbTx, err = w.db.Begin(true)
   118  	if err != nil {
   119  		w.log.Severe("ERROR: failed to start database update:", err)
   120  		return errors.AddContext(err, "unable to begin new dbTx in syncDB")
   121  	}
   122  	return nil
   123  }
   124  
   125  // dbReset wipes and reinitializes a wallet database.
   126  func dbReset(tx *bolt.Tx) error {
   127  	for _, bucket := range dbBuckets {
   128  		err := tx.DeleteBucket(bucket)
   129  		if err != nil {
   130  			return err
   131  		}
   132  		_, err = tx.CreateBucket(bucket)
   133  		if err != nil {
   134  			return err
   135  		}
   136  	}
   137  
   138  	// reinitialize the database with default values
   139  	wb := tx.Bucket(bucketWallet)
   140  	wb.Put(keyUID, fastrand.Bytes(len(uniqueID{})))
   141  	wb.Put(keyConsensusHeight, encoding.Marshal(uint64(0)))
   142  	wb.Put(keyAuxiliarySeedFiles, encoding.Marshal([]seedFile{}))
   143  	wb.Put(keySpendableKeyFiles, encoding.Marshal([]spendableKeyFile{}))
   144  	dbPutConsensusHeight(tx, 0)
   145  	dbPutConsensusChangeID(tx, modules.ConsensusChangeBeginning)
   146  	dbPutSiafundPool(tx, types.ZeroCurrency)
   147  
   148  	return nil
   149  }
   150  
   151  // dbPut is a helper function for storing a marshalled key/value pair.
   152  func dbPut(b *bolt.Bucket, key, val interface{}) error {
   153  	return b.Put(encoding.Marshal(key), encoding.Marshal(val))
   154  }
   155  
   156  // dbGet is a helper function for retrieving a marshalled key/value pair. val
   157  // must be a pointer.
   158  func dbGet(b *bolt.Bucket, key, val interface{}) error {
   159  	valBytes := b.Get(encoding.Marshal(key))
   160  	if valBytes == nil {
   161  		return errNoKey
   162  	}
   163  	return encoding.Unmarshal(valBytes, val)
   164  }
   165  
   166  // dbDelete is a helper function for deleting a marshalled key/value pair.
   167  func dbDelete(b *bolt.Bucket, key interface{}) error {
   168  	return b.Delete(encoding.Marshal(key))
   169  }
   170  
   171  // dbForEach is a helper function for iterating over a bucket and calling fn
   172  // on each entry. fn must be a function with two parameters. The key/value
   173  // bytes of each bucket entry will be unmarshalled into the types of fn's
   174  // parameters.
   175  func dbForEach(b *bolt.Bucket, fn interface{}) error {
   176  	// check function type
   177  	fnVal, fnTyp := reflect.ValueOf(fn), reflect.TypeOf(fn)
   178  	if fnTyp.Kind() != reflect.Func || fnTyp.NumIn() != 2 {
   179  		panic("bad fn type: needed func(key, val), got " + fnTyp.String())
   180  	}
   181  
   182  	return b.ForEach(func(keyBytes, valBytes []byte) error {
   183  		key, val := reflect.New(fnTyp.In(0)), reflect.New(fnTyp.In(1))
   184  		if err := encoding.Unmarshal(keyBytes, key.Interface()); err != nil {
   185  			return err
   186  		} else if err := encoding.Unmarshal(valBytes, val.Interface()); err != nil {
   187  			return err
   188  		}
   189  		fnVal.Call([]reflect.Value{key.Elem(), val.Elem()})
   190  		return nil
   191  	})
   192  }
   193  
   194  // Type-safe wrappers around the db helpers
   195  
   196  func dbPutSiacoinOutput(tx *bolt.Tx, id types.SiacoinOutputID, output types.SiacoinOutput) error {
   197  	return dbPut(tx.Bucket(bucketSiacoinOutputs), id, output)
   198  }
   199  func dbGetSiacoinOutput(tx *bolt.Tx, id types.SiacoinOutputID) (output types.SiacoinOutput, err error) {
   200  	err = dbGet(tx.Bucket(bucketSiacoinOutputs), id, &output)
   201  	return
   202  }
   203  func dbDeleteSiacoinOutput(tx *bolt.Tx, id types.SiacoinOutputID) error {
   204  	return dbDelete(tx.Bucket(bucketSiacoinOutputs), id)
   205  }
   206  func dbForEachSiacoinOutput(tx *bolt.Tx, fn func(types.SiacoinOutputID, types.SiacoinOutput)) error {
   207  	return dbForEach(tx.Bucket(bucketSiacoinOutputs), fn)
   208  }
   209  
   210  func dbPutSiafundOutput(tx *bolt.Tx, id types.SiafundOutputID, output types.SiafundOutput) error {
   211  	return dbPut(tx.Bucket(bucketSiafundOutputs), id, output)
   212  }
   213  func dbGetSiafundOutput(tx *bolt.Tx, id types.SiafundOutputID) (output types.SiafundOutput, err error) {
   214  	err = dbGet(tx.Bucket(bucketSiafundOutputs), id, &output)
   215  	return
   216  }
   217  func dbDeleteSiafundOutput(tx *bolt.Tx, id types.SiafundOutputID) error {
   218  	return dbDelete(tx.Bucket(bucketSiafundOutputs), id)
   219  }
   220  func dbForEachSiafundOutput(tx *bolt.Tx, fn func(types.SiafundOutputID, types.SiafundOutput)) error {
   221  	return dbForEach(tx.Bucket(bucketSiafundOutputs), fn)
   222  }
   223  
   224  func dbPutSpentOutput(tx *bolt.Tx, id types.OutputID, height types.BlockHeight) error {
   225  	return dbPut(tx.Bucket(bucketSpentOutputs), id, height)
   226  }
   227  func dbGetSpentOutput(tx *bolt.Tx, id types.OutputID) (height types.BlockHeight, err error) {
   228  	err = dbGet(tx.Bucket(bucketSpentOutputs), id, &height)
   229  	return
   230  }
   231  func dbDeleteSpentOutput(tx *bolt.Tx, id types.OutputID) error {
   232  	return dbDelete(tx.Bucket(bucketSpentOutputs), id)
   233  }
   234  
   235  func dbPutAddrTransactions(tx *bolt.Tx, addr types.UnlockHash, txns []uint64) error {
   236  	return dbPut(tx.Bucket(bucketAddrTransactions), addr, txns)
   237  }
   238  func dbGetAddrTransactions(tx *bolt.Tx, addr types.UnlockHash) (txns []uint64, err error) {
   239  	err = dbGet(tx.Bucket(bucketAddrTransactions), addr, &txns)
   240  	return
   241  }
   242  
   243  // dbAddAddrTransaction appends a single transaction index to the set of
   244  // transactions associated with addr. If the index is already in the set, it is
   245  // not added again.
   246  func dbAddAddrTransaction(tx *bolt.Tx, addr types.UnlockHash, txn uint64) error {
   247  	txns, err := dbGetAddrTransactions(tx, addr)
   248  	if err != nil && err != errNoKey {
   249  		return err
   250  	}
   251  	for _, i := range txns {
   252  		if i == txn {
   253  			return nil
   254  		}
   255  	}
   256  	return dbPutAddrTransactions(tx, addr, append(txns, txn))
   257  }
   258  
   259  // dbAddProcessedTransactionAddrs updates bucketAddrTransactions to associate
   260  // every address in pt with txn, which is assumed to be pt's index in
   261  // bucketProcessedTransactions.
   262  func dbAddProcessedTransactionAddrs(tx *bolt.Tx, pt modules.ProcessedTransaction, txn uint64) error {
   263  	addrs := make(map[types.UnlockHash]struct{})
   264  	for _, input := range pt.Inputs {
   265  		addrs[input.RelatedAddress] = struct{}{}
   266  	}
   267  	for _, output := range pt.Outputs {
   268  		// miner fees don't have an address, so skip them
   269  		if output.FundType == types.SpecifierMinerFee {
   270  			continue
   271  		}
   272  		addrs[output.RelatedAddress] = struct{}{}
   273  	}
   274  	for addr := range addrs {
   275  		if err := dbAddAddrTransaction(tx, addr, txn); err != nil {
   276  			return errors.AddContext(err, fmt.Sprintf("failed to add txn %v to address %v",
   277  				pt.TransactionID, addr))
   278  		}
   279  	}
   280  	return nil
   281  }
   282  
   283  // bucketProcessedTransactions works a little differently: the key is
   284  // meaningless, only used to order the transactions chronologically.
   285  
   286  // decodeProcessedTransaction decodes a marshalled processedTransaction
   287  func decodeProcessedTransaction(ptBytes []byte, pt *modules.ProcessedTransaction) error {
   288  	err := encoding.Unmarshal(ptBytes, pt)
   289  	if err != nil {
   290  		// COMPATv1.2.1: try decoding into old transaction type
   291  		var oldpt v121ProcessedTransaction
   292  		err = encoding.Unmarshal(ptBytes, &oldpt)
   293  		*pt = convertProcessedTransaction(oldpt)
   294  	}
   295  	return err
   296  }
   297  
   298  func dbDeleteTransactionIndex(tx *bolt.Tx, txid types.TransactionID) error {
   299  	return dbDelete(tx.Bucket(bucketProcessedTxnIndex), txid)
   300  }
   301  func dbPutTransactionIndex(tx *bolt.Tx, txid types.TransactionID, key []byte) error {
   302  	return dbPut(tx.Bucket(bucketProcessedTxnIndex), txid, key)
   303  }
   304  
   305  func dbGetTransactionIndex(tx *bolt.Tx, txid types.TransactionID) (key []byte, err error) {
   306  	key = make([]byte, 8)
   307  	err = dbGet(tx.Bucket(bucketProcessedTxnIndex), txid, &key)
   308  	return
   309  }
   310  
   311  // initProcessedTxnIndex initializes the bucketProcessedTxnIndex with the
   312  // elements from bucketProcessedTransactions
   313  func initProcessedTxnIndex(tx *bolt.Tx) error {
   314  	it := dbProcessedTransactionsIterator(tx)
   315  	indexBytes := make([]byte, 8)
   316  	for it.next() {
   317  		index, pt := it.key(), it.value()
   318  		binary.BigEndian.PutUint64(indexBytes, index)
   319  		if err := dbPutTransactionIndex(tx, pt.TransactionID, indexBytes); err != nil {
   320  			return err
   321  		}
   322  	}
   323  	return nil
   324  }
   325  
   326  func dbAppendProcessedTransaction(tx *bolt.Tx, pt modules.ProcessedTransaction) error {
   327  	b := tx.Bucket(bucketProcessedTransactions)
   328  	key, err := b.NextSequence()
   329  	if err != nil {
   330  		return errors.AddContext(err, "failed to get next sequence from bucket")
   331  	}
   332  	// big-endian is used so that the keys are properly sorted
   333  	keyBytes := make([]byte, 8)
   334  	binary.BigEndian.PutUint64(keyBytes, key)
   335  	if err = b.Put(keyBytes, encoding.Marshal(pt)); err != nil {
   336  		return errors.AddContext(err, "failed to store processed txn in database")
   337  	}
   338  
   339  	// add used index to bucketProcessedTxnIndex
   340  	if err = dbPutTransactionIndex(tx, pt.TransactionID, keyBytes); err != nil {
   341  		return errors.AddContext(err, "failed to store txn index in database")
   342  	}
   343  
   344  	// also add this txid to the bucketAddrTransactions
   345  	if err = dbAddProcessedTransactionAddrs(tx, pt, key); err != nil {
   346  		return errors.AddContext(err, "failed to add processed transaction to addresses in database")
   347  	}
   348  	return nil
   349  }
   350  
   351  func dbGetLastProcessedTransaction(tx *bolt.Tx) (pt modules.ProcessedTransaction, err error) {
   352  	seq := tx.Bucket(bucketProcessedTransactions).Sequence()
   353  	keyBytes := make([]byte, 8)
   354  	binary.BigEndian.PutUint64(keyBytes, seq)
   355  	val := tx.Bucket(bucketProcessedTransactions).Get(keyBytes)
   356  	err = decodeProcessedTransaction(val, &pt)
   357  	return
   358  }
   359  
   360  func dbDeleteLastProcessedTransaction(tx *bolt.Tx) error {
   361  	// Get the last processed txn.
   362  	pt, err := dbGetLastProcessedTransaction(tx)
   363  	if err != nil {
   364  		return errors.New("can't delete from empty bucket")
   365  	}
   366  	// Delete its txid from the index bucket.
   367  	if err := dbDeleteTransactionIndex(tx, pt.TransactionID); err != nil {
   368  		return errors.AddContext(err, "couldn't delete txn index")
   369  	}
   370  	// Delete the last processed txn and decrement the sequence.
   371  	b := tx.Bucket(bucketProcessedTransactions)
   372  	seq := b.Sequence()
   373  	keyBytes := make([]byte, 8)
   374  	binary.BigEndian.PutUint64(keyBytes, seq)
   375  	return errors.Compose(b.SetSequence(seq-1), b.Delete(keyBytes))
   376  }
   377  
   378  func dbGetProcessedTransaction(tx *bolt.Tx, index uint64) (pt modules.ProcessedTransaction, err error) {
   379  	// big-endian is used so that the keys are properly sorted
   380  	indexBytes := make([]byte, 8)
   381  	binary.BigEndian.PutUint64(indexBytes, index)
   382  	val := tx.Bucket(bucketProcessedTransactions).Get(indexBytes)
   383  	err = decodeProcessedTransaction(val, &pt)
   384  	return
   385  }
   386  
   387  // A processedTransactionsIter iterates through the ProcessedTransactions bucket.
   388  type processedTransactionsIter struct {
   389  	c   *bolt.Cursor
   390  	seq uint64
   391  	pt  modules.ProcessedTransaction
   392  }
   393  
   394  // next decodes the next ProcessedTransaction, returning false if the end of
   395  // the bucket has been reached.
   396  func (it *processedTransactionsIter) next() bool {
   397  	var seqBytes, ptBytes []byte
   398  	if it.pt.TransactionID == (types.TransactionID{}) {
   399  		// this is the first time next has been called, so cursor is not
   400  		// initialized yet
   401  		seqBytes, ptBytes = it.c.First()
   402  	} else {
   403  		seqBytes, ptBytes = it.c.Next()
   404  	}
   405  	if seqBytes == nil {
   406  		return false
   407  	}
   408  	it.seq = binary.BigEndian.Uint64(seqBytes)
   409  	return decodeProcessedTransaction(ptBytes, &it.pt) == nil
   410  }
   411  
   412  // key returns the key for the most recently decoded ProcessedTransaction.
   413  func (it *processedTransactionsIter) key() uint64 {
   414  	return it.seq
   415  }
   416  
   417  // value returns the most recently decoded ProcessedTransaction.
   418  func (it *processedTransactionsIter) value() modules.ProcessedTransaction {
   419  	return it.pt
   420  }
   421  
   422  // dbProcessedTransactionsIterator creates a new processedTransactionsIter.
   423  func dbProcessedTransactionsIterator(tx *bolt.Tx) *processedTransactionsIter {
   424  	return &processedTransactionsIter{
   425  		c: tx.Bucket(bucketProcessedTransactions).Cursor(),
   426  	}
   427  }
   428  
   429  // dbGetWalletUID returns the UID assigned to the wallet's primary seed.
   430  func dbGetWalletUID(tx *bolt.Tx) (uid uniqueID) {
   431  	copy(uid[:], tx.Bucket(bucketWallet).Get(keyUID))
   432  	return
   433  }
   434  
   435  // dbGetPrimarySeedProgress returns the number of keys generated from the
   436  // primary seed.
   437  func dbGetPrimarySeedProgress(tx *bolt.Tx) (progress uint64, err error) {
   438  	err = encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keyPrimarySeedProgress), &progress)
   439  	return
   440  }
   441  
   442  // dbPutPrimarySeedProgress sets the primary seed progress counter.
   443  func dbPutPrimarySeedProgress(tx *bolt.Tx, progress uint64) error {
   444  	return tx.Bucket(bucketWallet).Put(keyPrimarySeedProgress, encoding.Marshal(progress))
   445  }
   446  
   447  // dbGetConsensusChangeID returns the ID of the last ConsensusChange processed by the wallet.
   448  func dbGetConsensusChangeID(tx *bolt.Tx) (cc modules.ConsensusChangeID) {
   449  	copy(cc[:], tx.Bucket(bucketWallet).Get(keyConsensusChange))
   450  	return
   451  }
   452  
   453  // dbPutConsensusChangeID stores the ID of the last ConsensusChange processed by the wallet.
   454  func dbPutConsensusChangeID(tx *bolt.Tx, cc modules.ConsensusChangeID) error {
   455  	return tx.Bucket(bucketWallet).Put(keyConsensusChange, cc[:])
   456  }
   457  
   458  // dbGetConsensusHeight returns the height that the wallet has scanned to.
   459  func dbGetConsensusHeight(tx *bolt.Tx) (height types.BlockHeight, err error) {
   460  	err = encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keyConsensusHeight), &height)
   461  	return
   462  }
   463  
   464  // dbPutConsensusHeight stores the height that the wallet has scanned to.
   465  func dbPutConsensusHeight(tx *bolt.Tx, height types.BlockHeight) error {
   466  	return tx.Bucket(bucketWallet).Put(keyConsensusHeight, encoding.Marshal(height))
   467  }
   468  
   469  // dbGetSiafundPool returns the value of the siafund pool.
   470  func dbGetSiafundPool(tx *bolt.Tx) (pool types.Currency, err error) {
   471  	err = encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keySiafundPool), &pool)
   472  	return
   473  }
   474  
   475  // dbPutSiafundPool stores the value of the siafund pool.
   476  func dbPutSiafundPool(tx *bolt.Tx, pool types.Currency) error {
   477  	return tx.Bucket(bucketWallet).Put(keySiafundPool, encoding.Marshal(pool))
   478  }
   479  
   480  // COMPATv121: these types were stored in the db in v1.2.2 and earlier.
   481  type (
   482  	v121ProcessedInput struct {
   483  		FundType       types.Specifier
   484  		WalletAddress  bool
   485  		RelatedAddress types.UnlockHash
   486  		Value          types.Currency
   487  	}
   488  
   489  	v121ProcessedOutput struct {
   490  		FundType       types.Specifier
   491  		MaturityHeight types.BlockHeight
   492  		WalletAddress  bool
   493  		RelatedAddress types.UnlockHash
   494  		Value          types.Currency
   495  	}
   496  
   497  	v121ProcessedTransaction struct {
   498  		Transaction           types.Transaction
   499  		TransactionID         types.TransactionID
   500  		ConfirmationHeight    types.BlockHeight
   501  		ConfirmationTimestamp types.Timestamp
   502  		Inputs                []v121ProcessedInput
   503  		Outputs               []v121ProcessedOutput
   504  	}
   505  )
   506  
   507  func convertProcessedTransaction(oldpt v121ProcessedTransaction) (pt modules.ProcessedTransaction) {
   508  	pt.Transaction = oldpt.Transaction
   509  	pt.TransactionID = oldpt.TransactionID
   510  	pt.ConfirmationHeight = oldpt.ConfirmationHeight
   511  	pt.ConfirmationTimestamp = oldpt.ConfirmationTimestamp
   512  	pt.Inputs = make([]modules.ProcessedInput, len(oldpt.Inputs))
   513  	for i, in := range oldpt.Inputs {
   514  		pt.Inputs[i] = modules.ProcessedInput{
   515  			FundType:       in.FundType,
   516  			WalletAddress:  in.WalletAddress,
   517  			RelatedAddress: in.RelatedAddress,
   518  			Value:          in.Value,
   519  		}
   520  	}
   521  	pt.Outputs = make([]modules.ProcessedOutput, len(oldpt.Outputs))
   522  	for i, out := range oldpt.Outputs {
   523  		pt.Outputs[i] = modules.ProcessedOutput{
   524  			FundType:       out.FundType,
   525  			MaturityHeight: out.MaturityHeight,
   526  			WalletAddress:  out.WalletAddress,
   527  			RelatedAddress: out.RelatedAddress,
   528  			Value:          out.Value,
   529  		}
   530  	}
   531  	return
   532  }