github.com/piotrnar/gocoin@v0.0.0-20240512203912-faa0448c5e96/lib/utxo/unspent_db.go (about)

     1  package utxo
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/binary"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/piotrnar/gocoin/lib/btc"
    16  	"github.com/piotrnar/gocoin/lib/others/sys"
    17  	"github.com/piotrnar/gocoin/lib/script"
    18  )
    19  
    20  var (
    21  	UTXO_WRITING_TIME_TARGET        = 5 * time.Minute // Take it easy with flushing UTXO.db onto disk
    22  	UTXO_SKIP_SAVE_BLOCKS    uint32 = 0
    23  	UTXO_PURGE_UNSPENDABLE   bool   = false
    24  )
    25  
    26  var Memory_Malloc = func(le int) []byte {
    27  	return make([]byte, le)
    28  }
    29  
    30  var Memory_Free = func([]byte) {
    31  }
    32  
    33  type FunctionWalkUnspent func(*UtxoRec)
    34  
    35  type CallbackFunctions struct {
    36  	// If NotifyTx is set, it will be called each time a new unspent
    37  	// output is being added or removed. When being removed, btc.TxOut is nil.
    38  	NotifyTxAdd func(*UtxoRec)
    39  	NotifyTxDel func(*UtxoRec, []bool)
    40  }
    41  
    42  // BlockChanges is used to pass block's changes to UnspentDB.
    43  type BlockChanges struct {
    44  	Height          uint32
    45  	LastKnownHeight uint32 // put here zero to disable this feature
    46  	AddList         []*UtxoRec
    47  	DeledTxs        map[[32]byte][]bool
    48  	UndoData        map[[32]byte]*UtxoRec
    49  }
    50  
    51  type UnspentDB struct {
    52  	HashMap  [256](map[UtxoKeyType][]byte)
    53  	MapMutex [256]sync.RWMutex // used to access HashMap
    54  
    55  	LastBlockHash      []byte
    56  	LastBlockHeight    uint32
    57  	ComprssedUTXO      bool
    58  	dir_utxo, dir_undo string
    59  	volatimemode       bool
    60  	UnwindBufLen       uint32
    61  	DirtyDB            sys.SyncBool
    62  	sync.Mutex
    63  
    64  	abortwritingnow   chan bool
    65  	WritingInProgress sys.SyncBool
    66  	writingDone       sync.WaitGroup
    67  	lastFileClosed    sync.WaitGroup
    68  
    69  	CurrentHeightOnDisk uint32
    70  	hurryup             chan bool
    71  	DoNotWriteUndoFiles bool
    72  	CB                  CallbackFunctions
    73  
    74  	undo_dir_created bool
    75  }
    76  
    77  type NewUnspentOpts struct {
    78  	Dir             string
    79  	Rescan          bool
    80  	VolatimeMode    bool
    81  	CB              CallbackFunctions
    82  	AbortNow        *bool
    83  	CompressRecords bool
    84  	RecordsPrealloc uint
    85  }
    86  
    87  func NewUnspentDb(opts *NewUnspentOpts) (db *UnspentDB) {
    88  	//var maxbl_fn string
    89  	db = new(UnspentDB)
    90  	db.dir_utxo = opts.Dir
    91  	db.dir_undo = db.dir_utxo + "undo" + string(os.PathSeparator)
    92  	db.volatimemode = opts.VolatimeMode
    93  	db.UnwindBufLen = 2560
    94  	db.CB = opts.CB
    95  	db.abortwritingnow = make(chan bool, 1)
    96  	db.hurryup = make(chan bool, 1)
    97  
    98  	os.Remove(db.dir_undo + "tmp") // Remove unfinished undo file
    99  	if files, er := filepath.Glob(db.dir_utxo + "*.db.tmp"); er == nil {
   100  		for _, f := range files {
   101  			os.Remove(f) // Remove unfinished *.db.tmp files
   102  		}
   103  	}
   104  
   105  	db.ComprssedUTXO = opts.CompressRecords
   106  	if opts.Rescan {
   107  		for i := range db.HashMap {
   108  			db.HashMap[i] = make(map[UtxoKeyType][]byte, opts.RecordsPrealloc/256)
   109  		}
   110  		return
   111  	}
   112  
   113  	// Load data from disk
   114  	var k UtxoKeyType
   115  	var cnt_dwn, cnt_dwn_from, perc int
   116  	var le uint64
   117  	var u64, tot_recs uint64
   118  	var info string
   119  	var rd *bufio.Reader
   120  	var of *os.File
   121  
   122  	fname := "UTXO.db"
   123  
   124  redo:
   125  	of, er := os.Open(db.dir_utxo + fname)
   126  	if er != nil {
   127  		goto fatal_error
   128  	}
   129  
   130  	rd = bufio.NewReaderSize(of, 0x40000) // read ahed buffer size
   131  
   132  	er = binary.Read(rd, binary.LittleEndian, &u64)
   133  	if er != nil {
   134  		goto fatal_error
   135  	}
   136  	db.LastBlockHeight = uint32(u64)
   137  
   138  	// If the highest bit of the block number is set, the UTXO records are compressed
   139  	db.ComprssedUTXO = (u64 & 0x8000000000000000) != 0
   140  
   141  	db.LastBlockHash = make([]byte, 32)
   142  	_, er = rd.Read(db.LastBlockHash)
   143  	if er != nil {
   144  		goto fatal_error
   145  	}
   146  	er = binary.Read(rd, binary.LittleEndian, &u64)
   147  	if er != nil {
   148  		goto fatal_error
   149  	}
   150  
   151  	//fmt.Println("Last block height", db.LastBlockHeight, "   Number of records", u64)
   152  	cnt_dwn_from = int(u64 / 100)
   153  	perc = 0
   154  
   155  	for i := range db.HashMap {
   156  		db.HashMap[i] = make(map[UtxoKeyType][]byte, int(u64)/256)
   157  	}
   158  	if db.ComprssedUTXO {
   159  		info = fmt.Sprint("\rLoading ", u64, " compressed txs from ", fname, " - ")
   160  	} else {
   161  		info = fmt.Sprint("\rLoading ", u64, " plain txs from ", fname, " - ")
   162  	}
   163  
   164  	for tot_recs = 0; tot_recs < u64; tot_recs++ {
   165  		if opts.AbortNow != nil && *opts.AbortNow {
   166  			break
   167  		}
   168  		le, er = btc.ReadVLen(rd)
   169  		if er != nil {
   170  			goto fatal_error
   171  		}
   172  
   173  		_, er = io.ReadFull(rd, k[:])
   174  		if er != nil {
   175  			goto fatal_error
   176  		}
   177  
   178  		b := Memory_Malloc(int(le) - UtxoIdxLen)
   179  		_, er = io.ReadFull(rd, b)
   180  		if er != nil {
   181  			goto fatal_error
   182  		}
   183  
   184  		// we don't lock RWMutex here as this code is only used during init phase, when no other routines are running
   185  		db.HashMap[k[0]][k] = b
   186  
   187  		if cnt_dwn == 0 {
   188  			fmt.Print(info, perc, "% complete ... ")
   189  			perc++
   190  			cnt_dwn = cnt_dwn_from
   191  		} else {
   192  			cnt_dwn--
   193  		}
   194  	}
   195  	of.Close()
   196  
   197  	fmt.Print("\r                                                                 \r")
   198  
   199  	atomic.StoreUint32(&db.CurrentHeightOnDisk, db.LastBlockHeight)
   200  	if db.ComprssedUTXO {
   201  		FullUtxoRec = FullUtxoRecC
   202  		NewUtxoRecStatic = NewUtxoRecStaticC
   203  		NewUtxoRec = NewUtxoRecC
   204  		OneUtxoRec = OneUtxoRecC
   205  		Serialize = SerializeC
   206  	}
   207  
   208  	return
   209  
   210  fatal_error:
   211  	if of != nil {
   212  		of.Close()
   213  	}
   214  
   215  	println(er.Error())
   216  	if fname != "UTXO.old" {
   217  		fname = "UTXO.old"
   218  		goto redo
   219  	}
   220  	db.LastBlockHeight = 0
   221  	db.LastBlockHash = nil
   222  	for i := range db.HashMap {
   223  		db.HashMap[i] = make(map[UtxoKeyType][]byte, opts.RecordsPrealloc/256)
   224  	}
   225  
   226  	return
   227  }
   228  
   229  func (db *UnspentDB) save() {
   230  	//var cnt_dwn, cnt_dwn_from, perc int
   231  	var abort, hurryup, check_time bool
   232  	var total_records, current_record, data_progress, time_progress int64
   233  
   234  	const save_buffer_min = 0x10000 // write in chunks of ~64KB
   235  	const save_buffer_cnt = 100
   236  
   237  	os.Rename(db.dir_utxo+"UTXO.db", db.dir_utxo+"UTXO.old")
   238  	data_channel := make(chan []byte, save_buffer_cnt)
   239  	exit_channel := make(chan bool, 1)
   240  
   241  	start_time := time.Now()
   242  
   243  	for _i := range db.HashMap {
   244  		total_records += int64(len(db.HashMap[_i]))
   245  	}
   246  
   247  	buf := bytes.NewBuffer(make([]byte, 0, save_buffer_min+0x1000)) // add 4K extra for the last record (it will still be able to grow over it)
   248  	u64 := uint64(db.LastBlockHeight)
   249  	if db.ComprssedUTXO {
   250  		u64 |= 0x8000000000000000
   251  	}
   252  	binary.Write(buf, binary.LittleEndian, u64)
   253  	buf.Write(db.LastBlockHash)
   254  	binary.Write(buf, binary.LittleEndian, uint64(total_records))
   255  
   256  	// The data is written in a separate process
   257  	// so we can abort without waiting for disk.
   258  	db.lastFileClosed.Add(1)
   259  	go func(fname string) {
   260  		of_, er := os.Create(fname)
   261  		if er != nil {
   262  			println("Create file:", er.Error())
   263  			return
   264  		}
   265  
   266  		of := bufio.NewWriter(of_)
   267  
   268  		var dat []byte
   269  		var abort, exit bool
   270  
   271  		for !exit || len(data_channel) > 0 {
   272  			select {
   273  
   274  			case dat = <-data_channel:
   275  				if len(exit_channel) > 0 {
   276  					if abort = <-exit_channel; abort {
   277  						goto exit
   278  					} else {
   279  						exit = true
   280  					}
   281  				}
   282  				of.Write(dat)
   283  
   284  			case abort = <-exit_channel:
   285  				if abort {
   286  					goto exit
   287  				} else {
   288  					exit = true
   289  				}
   290  			}
   291  		}
   292  	exit:
   293  		if abort {
   294  			of_.Close() // abort
   295  			os.Remove(fname)
   296  		} else {
   297  			of.Flush()
   298  			of_.Close()
   299  			os.Rename(fname, db.dir_utxo+"UTXO.db")
   300  		}
   301  		db.lastFileClosed.Done()
   302  	}(db.dir_utxo + btc.NewUint256(db.LastBlockHash).String() + ".db.tmp")
   303  
   304  	if UTXO_WRITING_TIME_TARGET == 0 {
   305  		hurryup = true
   306  	}
   307  	for _i := range db.HashMap {
   308  		db.MapMutex[_i].RLock()
   309  		defer db.MapMutex[_i].RUnlock()
   310  		for k, v := range db.HashMap[_i] {
   311  			if check_time {
   312  				check_time = false
   313  				data_progress = int64(current_record<<20) / int64(total_records)
   314  				time_progress = int64(time.Now().Sub(start_time)<<20) / int64(UTXO_WRITING_TIME_TARGET)
   315  				if data_progress > time_progress {
   316  					select {
   317  					case <-db.abortwritingnow:
   318  						abort = true
   319  						goto finito
   320  					case <-db.hurryup:
   321  						hurryup = true
   322  					case <-time.After((time.Duration(data_progress-time_progress) * UTXO_WRITING_TIME_TARGET) >> 20):
   323  					}
   324  				}
   325  			}
   326  
   327  			for len(data_channel) >= cap(data_channel) {
   328  				select {
   329  				case <-db.abortwritingnow:
   330  					abort = true
   331  					goto finito
   332  				case <-db.hurryup:
   333  					hurryup = true
   334  				case <-time.After(time.Millisecond):
   335  				}
   336  			}
   337  
   338  			btc.WriteVlen(buf, uint64(UtxoIdxLen+len(v)))
   339  			buf.Write(k[:])
   340  			buf.Write(v)
   341  			if buf.Len() >= save_buffer_min {
   342  				data_channel <- buf.Bytes()
   343  				if !hurryup {
   344  					check_time = true
   345  				}
   346  				buf = bytes.NewBuffer(make([]byte, 0, save_buffer_min+0x1000)) // add 4K extra for the last record
   347  			}
   348  
   349  			current_record++
   350  		}
   351  	}
   352  finito:
   353  
   354  	if !abort && buf.Len() > 0 {
   355  		data_channel <- buf.Bytes()
   356  	}
   357  	exit_channel <- abort
   358  
   359  	if !abort {
   360  		db.DirtyDB.Clr()
   361  		//println("utxo written OK in", time.Now().Sub(start_time).String(), timewaits)
   362  		atomic.StoreUint32(&db.CurrentHeightOnDisk, db.LastBlockHeight)
   363  	}
   364  	db.WritingInProgress.Clr()
   365  	db.writingDone.Done()
   366  }
   367  
   368  // CommitBlockTxs commits the given add/del transactions to UTXO and Unwind DBs.
   369  func (db *UnspentDB) CommitBlockTxs(changes *BlockChanges, blhash []byte) (e error) {
   370  	var wg sync.WaitGroup
   371  
   372  	undo_fn := fmt.Sprint(db.dir_undo, changes.Height)
   373  
   374  	db.Mutex.Lock()
   375  	defer db.Mutex.Unlock()
   376  	db.abortWriting()
   377  
   378  	if changes.UndoData != nil {
   379  		wg.Add(1)
   380  		go func() {
   381  			var tmp [0x100000]byte // static record for Serialize to serialize to
   382  			bu := new(bytes.Buffer)
   383  			bu.Write(blhash)
   384  			if changes.UndoData != nil {
   385  				for _, xx := range changes.UndoData {
   386  					bin := Serialize(xx, true, tmp[:])
   387  					btc.WriteVlen(bu, uint64(len(bin)))
   388  					bu.Write(bin)
   389  				}
   390  			}
   391  			if !db.undo_dir_created { // (try to) create undo folder before writing the first file
   392  				os.MkdirAll(db.dir_undo, 0770)
   393  				db.undo_dir_created = true
   394  			}
   395  			os.WriteFile(db.dir_undo+"tmp", bu.Bytes(), 0666)
   396  			os.Rename(db.dir_undo+"tmp", undo_fn)
   397  			wg.Done()
   398  		}()
   399  	}
   400  
   401  	db.commit(changes)
   402  
   403  	if db.LastBlockHash == nil {
   404  		db.LastBlockHash = make([]byte, 32)
   405  	}
   406  	copy(db.LastBlockHash, blhash)
   407  	db.LastBlockHeight = changes.Height
   408  
   409  	if changes.Height > db.UnwindBufLen {
   410  		os.Remove(fmt.Sprint(db.dir_undo, changes.Height-db.UnwindBufLen))
   411  	}
   412  
   413  	db.DirtyDB.Set()
   414  	wg.Wait()
   415  	return
   416  }
   417  
   418  func (db *UnspentDB) UndoBlockTxs(bl *btc.Block, newhash []byte) {
   419  	db.Mutex.Lock()
   420  	defer db.Mutex.Unlock()
   421  	db.abortWriting()
   422  
   423  	for _, tx := range bl.Txs {
   424  		lst := make([]bool, len(tx.TxOut))
   425  		for i := range lst {
   426  			lst[i] = true
   427  		}
   428  		db.del(tx.Hash.Hash[:], lst)
   429  	}
   430  
   431  	fn := fmt.Sprint(db.dir_undo, db.LastBlockHeight)
   432  	var addback []*UtxoRec
   433  
   434  	if _, er := os.Stat(fn); er != nil {
   435  		fn += ".tmp"
   436  	}
   437  
   438  	dat, er := os.ReadFile(fn)
   439  	if er != nil {
   440  		panic(er.Error())
   441  	}
   442  
   443  	off := 32 // ship the block hash
   444  	for off < len(dat) {
   445  		le, n := btc.VLen(dat[off:])
   446  		off += n
   447  		qr := FullUtxoRec(dat[off : off+le])
   448  		off += le
   449  		addback = append(addback, qr)
   450  	}
   451  
   452  	for _, rec := range addback {
   453  		if db.CB.NotifyTxAdd != nil {
   454  			db.CB.NotifyTxAdd(rec)
   455  		}
   456  
   457  		var ind UtxoKeyType
   458  		copy(ind[:], rec.TxID[:])
   459  		db.MapMutex[ind[0]].RLock()
   460  		v := db.HashMap[ind[0]][ind]
   461  		db.MapMutex[ind[0]].RUnlock()
   462  		if v != nil {
   463  			oldrec := NewUtxoRec(ind, v)
   464  			for a := range rec.Outs {
   465  				if rec.Outs[a] == nil {
   466  					rec.Outs[a] = oldrec.Outs[a]
   467  				}
   468  			}
   469  		}
   470  		db.MapMutex[ind[0]].Lock()
   471  		db.HashMap[ind[0]][ind] = Serialize(rec, false, nil)
   472  		db.MapMutex[ind[0]].Unlock()
   473  	}
   474  
   475  	os.Remove(fn)
   476  	db.LastBlockHeight--
   477  	copy(db.LastBlockHash, newhash)
   478  	db.DirtyDB.Set()
   479  }
   480  
   481  // Idle should be called when the main thread is idle.
   482  func (db *UnspentDB) Idle() bool {
   483  	if db.volatimemode {
   484  		return false
   485  	}
   486  
   487  	db.Mutex.Lock()
   488  	defer db.Mutex.Unlock()
   489  
   490  	if db.DirtyDB.Get() && db.LastBlockHeight-atomic.LoadUint32(&db.CurrentHeightOnDisk) > UTXO_SKIP_SAVE_BLOCKS {
   491  		return db.Save()
   492  	}
   493  
   494  	return false
   495  }
   496  
   497  func (db *UnspentDB) Save() bool {
   498  	if db.WritingInProgress.Get() {
   499  		return false
   500  	}
   501  	db.WritingInProgress.Set()
   502  	db.writingDone.Add(1)
   503  	go db.save() // this one will call db.writingDone.Done()
   504  	return true
   505  }
   506  
   507  func (db *UnspentDB) HurryUp() {
   508  	select {
   509  	case db.hurryup <- true:
   510  	default:
   511  	}
   512  }
   513  
   514  // Close flushes the data and closes all the files.
   515  func (db *UnspentDB) Close() {
   516  	db.volatimemode = false
   517  	if db.DirtyDB.Get() {
   518  		db.HurryUp()
   519  		db.Save()
   520  	}
   521  	db.writingDone.Wait()
   522  	db.lastFileClosed.Wait()
   523  }
   524  
   525  // UnspentGet gets the given unspent output.
   526  func (db *UnspentDB) UnspentGet(po *btc.TxPrevOut) (res *btc.TxOut) {
   527  	var ind UtxoKeyType
   528  	var v []byte
   529  	copy(ind[:], po.Hash[:])
   530  
   531  	db.MapMutex[ind[0]].RLock()
   532  	v = db.HashMap[ind[0]][ind]
   533  	db.MapMutex[ind[0]].RUnlock()
   534  	if v != nil {
   535  		res = OneUtxoRec(ind, v, po.Vout)
   536  	}
   537  
   538  	return
   539  }
   540  
   541  // TxPresent returns true if gived TXID is in UTXO.
   542  func (db *UnspentDB) TxPresent(id *btc.Uint256) (res bool) {
   543  	var ind UtxoKeyType
   544  	copy(ind[:], id.Hash[:])
   545  	db.MapMutex[ind[0]].RLock()
   546  	_, res = db.HashMap[ind[0]][ind]
   547  	db.MapMutex[ind[0]].RUnlock()
   548  	return
   549  }
   550  
   551  func (db *UnspentDB) del(hash []byte, outs []bool) {
   552  	var ind UtxoKeyType
   553  	copy(ind[:], hash)
   554  	db.MapMutex[ind[0]].RLock()
   555  	v := db.HashMap[ind[0]][ind]
   556  	db.MapMutex[ind[0]].RUnlock()
   557  	if v == nil {
   558  		return // no such txid in UTXO (just ignorde delete request)
   559  	}
   560  	rec := NewUtxoRec(ind, v)
   561  	if db.CB.NotifyTxDel != nil {
   562  		db.CB.NotifyTxDel(rec, outs)
   563  	}
   564  	var anyout bool
   565  	for i, rm := range outs {
   566  		if rm || UTXO_PURGE_UNSPENDABLE && rec.Outs[i] != nil && script.IsUnspendable(rec.Outs[i].PKScr) {
   567  			rec.Outs[i] = nil
   568  		} else if !anyout && rec.Outs[i] != nil {
   569  			anyout = true
   570  		}
   571  	}
   572  	db.MapMutex[ind[0]].Lock()
   573  	if anyout {
   574  		db.HashMap[ind[0]][ind] = Serialize(rec, false, nil)
   575  	} else {
   576  		delete(db.HashMap[ind[0]], ind)
   577  	}
   578  	db.MapMutex[ind[0]].Unlock()
   579  	Memory_Free(v)
   580  }
   581  
   582  func (db *UnspentDB) commit(changes *BlockChanges) {
   583  	var wg sync.WaitGroup
   584  	// Now aplly the unspent changes
   585  	for _, rec := range changes.AddList {
   586  		var ind UtxoKeyType
   587  		copy(ind[:], rec.TxID[:])
   588  		if db.CB.NotifyTxAdd != nil {
   589  			db.CB.NotifyTxAdd(rec)
   590  		}
   591  		var add_this_tx bool
   592  		if UTXO_PURGE_UNSPENDABLE {
   593  			for idx, r := range rec.Outs {
   594  				if r != nil {
   595  					if script.IsUnspendable(r.PKScr) {
   596  						rec.Outs[idx] = nil
   597  					} else {
   598  						add_this_tx = true
   599  					}
   600  				}
   601  			}
   602  		} else {
   603  			add_this_tx = true
   604  		}
   605  		if add_this_tx {
   606  			wg.Add(1)
   607  			go func(ind UtxoKeyType, rec *UtxoRec) {
   608  				v := Serialize(rec, false, nil)
   609  				db.MapMutex[ind[0]].Lock()
   610  				db.HashMap[ind[0]][ind] = v
   611  				db.MapMutex[ind[0]].Unlock()
   612  				wg.Done()
   613  			}(ind, rec)
   614  		}
   615  	}
   616  	for k, v := range changes.DeledTxs {
   617  		wg.Add(1)
   618  		go func(k [32]byte, v []bool) {
   619  			db.del(k[:], v)
   620  			wg.Done()
   621  		}(k, v)
   622  	}
   623  	wg.Wait()
   624  }
   625  
   626  func (db *UnspentDB) AbortWriting() {
   627  	db.Mutex.Lock()
   628  	db.abortWriting()
   629  	db.Mutex.Unlock()
   630  }
   631  
   632  func (db *UnspentDB) abortWriting() {
   633  	if db.WritingInProgress.Get() {
   634  		db.abortwritingnow <- true
   635  		db.writingDone.Wait()
   636  		select {
   637  		case <-db.abortwritingnow:
   638  		default:
   639  		}
   640  	}
   641  }
   642  
   643  func (db *UnspentDB) UTXOStats() (s string) {
   644  	var outcnt, sum, sumcb uint64
   645  	var filesize, unspendable, unspendable_recs, unspendable_bytes uint64
   646  
   647  	filesize = 8 + 32 + 8 // UTXO.db: block_no + block_hash + rec_cnt
   648  
   649  	var lele int
   650  
   651  	for _i := range db.HashMap {
   652  		db.MapMutex[_i].RLock()
   653  		lele += len(db.HashMap[_i])
   654  		for k, v := range db.HashMap[_i] {
   655  			reclen := uint64(len(v) + UtxoIdxLen)
   656  			filesize += uint64(btc.VLenSize(reclen))
   657  			filesize += reclen
   658  			rec := NewUtxoRecStatic(k, v)
   659  			var spendable_found bool
   660  			for _, r := range rec.Outs {
   661  				if r != nil {
   662  					outcnt++
   663  					sum += r.Value
   664  					if rec.Coinbase {
   665  						sumcb += r.Value
   666  					}
   667  					if script.IsUnspendable(r.PKScr) {
   668  						unspendable++
   669  						unspendable_bytes += uint64(8 + len(r.PKScr))
   670  					} else {
   671  						spendable_found = true
   672  					}
   673  				}
   674  			}
   675  			if !spendable_found {
   676  				unspendable_recs++
   677  			}
   678  		}
   679  		db.MapMutex[_i].RUnlock()
   680  	}
   681  
   682  	s = fmt.Sprintf("UNSPENT: %.8f BTC in %d outs from %d txs. %.8f BTC in coinbase.\n",
   683  		float64(sum)/1e8, outcnt, lele, float64(sumcb)/1e8)
   684  	s += fmt.Sprintf(" MaxTxOutCnt: %d  DirtyDB: %t  Writing: %t  Abort: %t  Compressed: %t\n",
   685  		len(rec_outs), db.DirtyDB.Get(), db.WritingInProgress.Get(), len(db.abortwritingnow) > 0,
   686  		db.ComprssedUTXO)
   687  	s += fmt.Sprintf(" Last Block : %s @ %d\n", btc.NewUint256(db.LastBlockHash).String(),
   688  		db.LastBlockHeight)
   689  	s += fmt.Sprintf(" Unspendable Outputs: %d (%dKB)  txs:%d    UTXO.db file size: %d\n",
   690  		unspendable, unspendable_bytes>>10, unspendable_recs, filesize)
   691  
   692  	return
   693  }
   694  
   695  // GetStats returns DB statistics.
   696  func (db *UnspentDB) GetStats() (s string) {
   697  	var hml int
   698  	for i := range db.HashMap {
   699  		db.MapMutex[i].RLock()
   700  		hml += len(db.HashMap[i])
   701  		db.MapMutex[i].RUnlock()
   702  	}
   703  
   704  	s = fmt.Sprintf("UNSPENT: %d txs.  MaxCnt:%d  Dirt:%t  Writ:%t  Abort:%t  Compr:%t\n",
   705  		hml, len(rec_outs), db.DirtyDB.Get(), db.WritingInProgress.Get(),
   706  		len(db.abortwritingnow) > 0, db.ComprssedUTXO)
   707  	s += fmt.Sprintf(" Last Block : %s @ %d\n", btc.NewUint256(db.LastBlockHash).String(),
   708  		db.LastBlockHeight)
   709  	return
   710  }
   711  
   712  func (db *UnspentDB) PurgeUnspendable(all bool) {
   713  	var unspendable_txs, unspendable_recs uint64
   714  	db.Mutex.Lock()
   715  	db.abortWriting()
   716  
   717  	for _i := range db.HashMap {
   718  		db.MapMutex[_i].Lock()
   719  		for k, v := range db.HashMap[_i] {
   720  			rec := NewUtxoRecStatic(k, v)
   721  			var spendable_found bool
   722  			var record_removed uint64
   723  			for idx, r := range rec.Outs {
   724  				if r != nil {
   725  					if script.IsUnspendable(r.PKScr) {
   726  						if all {
   727  							rec.Outs[idx] = nil
   728  							record_removed++
   729  						}
   730  					} else {
   731  						spendable_found = true
   732  					}
   733  				}
   734  			}
   735  			if !spendable_found {
   736  				Memory_Free(v)
   737  				delete(db.HashMap[k[0]], k)
   738  				unspendable_txs++
   739  			} else if record_removed > 0 {
   740  				db.HashMap[k[0]][k] = Serialize(rec, false, nil)
   741  				Memory_Free(v)
   742  				unspendable_recs += record_removed
   743  			}
   744  		}
   745  		db.MapMutex[_i].Unlock()
   746  	}
   747  
   748  	db.Mutex.Unlock()
   749  
   750  	fmt.Println("Purged", unspendable_txs, "transactions and", unspendable_recs, "extra records")
   751  }