d0938d817f
This commit implements a new secure, scalable, hierarchical deterministic wallet address manager package. The following is an overview of features: - BIP0032 hierarchical deterministic keys - BIP0043/BIP0044 multi-account hierarchy - Strong focus on security: - Fully encrypted database including public information such as addresses as well as private information such as private keys and scripts needed to redeem pay-to-script-hash transactions - Hardened against memory scraping through the use of actively clearing private material from memory when locked - Different crypto keys used for public, private, and script data - Ability for different passphrases for public and private data - Scrypt-based key derivation - NaCl-based secretbox cryptography (XSalsa20 and Poly1305) - Multi-tier scalable key design to allow instant password changes regardless of the number of addresses stored - Import WIF keys - Import pay-to-script-hash scripts for things such as multi-signature transactions - Ability to export a watching-only version which does not contain any private key material - Programmatically detectable errors, including encapsulation of errors from packages it relies on - Address synchronization capabilities This commit only provides the implementation package. It does not include integration into to the existing wallet code base or conversion of existing addresses. That functionality will be provided by future commits.
241 lines
7.6 KiB
Go
241 lines
7.6 KiB
Go
/*
|
|
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
package waddrmgr
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/conformal/btcwire"
|
|
)
|
|
|
|
const (
|
|
// maxRecentHashes is the maximum number of hashes to keep in history
|
|
// for the purposes of rollbacks.
|
|
maxRecentHashes = 20
|
|
)
|
|
|
|
// BlockStamp defines a block (by height and a unique hash) and is
|
|
// used to mark a point in the blockchain that an address manager element is
|
|
// synced to.
|
|
type BlockStamp struct {
|
|
Height int32
|
|
Hash btcwire.ShaHash
|
|
}
|
|
|
|
// syncState houses the sync state of the manager. It consists of the recently
|
|
// seen blocks as height, as well as the start and current sync block stamps.
|
|
type syncState struct {
|
|
// startBlock is the first block that can be safely used to start a
|
|
// rescan. It is either the block the manager was created with, or
|
|
// the earliest block provided with imported addresses or scripts.
|
|
startBlock BlockStamp
|
|
|
|
// syncedTo is the current block the addresses in the manager are known
|
|
// to be synced against.
|
|
syncedTo BlockStamp
|
|
|
|
// recentHeight is the most recently seen sync height.
|
|
recentHeight int32
|
|
|
|
// recentHashes is a list of the last several seen block hashes.
|
|
recentHashes []btcwire.ShaHash
|
|
}
|
|
|
|
// iter returns a BlockIterator that can be used to iterate over the recently
|
|
// seen blocks in the sync state.
|
|
func (s *syncState) iter(mtx *sync.RWMutex) *BlockIterator {
|
|
if s.recentHeight == -1 || len(s.recentHashes) == 0 {
|
|
return nil
|
|
}
|
|
return &BlockIterator{
|
|
mtx: mtx,
|
|
height: s.recentHeight,
|
|
index: len(s.recentHashes) - 1,
|
|
syncInfo: s,
|
|
}
|
|
}
|
|
|
|
// newSyncState returns a new sync state with the provided parameters.
|
|
func newSyncState(startBlock, syncedTo *BlockStamp, recentHeight int32,
|
|
recentHashes []btcwire.ShaHash) *syncState {
|
|
|
|
return &syncState{
|
|
startBlock: *startBlock,
|
|
syncedTo: *syncedTo,
|
|
recentHeight: recentHeight,
|
|
recentHashes: recentHashes,
|
|
}
|
|
}
|
|
|
|
// BlockIterator allows for the forwards and backwards iteration of recently
|
|
// seen blocks.
|
|
type BlockIterator struct {
|
|
mtx *sync.RWMutex
|
|
height int32
|
|
index int
|
|
syncInfo *syncState
|
|
}
|
|
|
|
// Next returns the next recently seen block or false if there is not one.
|
|
func (it *BlockIterator) Next() bool {
|
|
it.mtx.RLock()
|
|
defer it.mtx.RUnlock()
|
|
|
|
if it.index+1 >= len(it.syncInfo.recentHashes) {
|
|
return false
|
|
}
|
|
it.index++
|
|
return true
|
|
}
|
|
|
|
// Prev returns the previous recently seen block or false if there is not one.
|
|
func (it *BlockIterator) Prev() bool {
|
|
it.mtx.RLock()
|
|
defer it.mtx.RUnlock()
|
|
|
|
if it.index-1 < 0 {
|
|
return false
|
|
}
|
|
it.index--
|
|
return true
|
|
}
|
|
|
|
// BlockStamp returns the block stamp associated with the recently seen block
|
|
// the iterator is currently pointing to.
|
|
func (it *BlockIterator) BlockStamp() BlockStamp {
|
|
it.mtx.RLock()
|
|
defer it.mtx.RUnlock()
|
|
|
|
return BlockStamp{
|
|
Height: it.syncInfo.recentHeight -
|
|
int32(len(it.syncInfo.recentHashes)-1-it.index),
|
|
Hash: it.syncInfo.recentHashes[it.index],
|
|
}
|
|
}
|
|
|
|
// NewIterateRecentBlocks returns an iterator for recently-seen blocks.
|
|
// The iterator starts at the most recently-added block, and Prev should
|
|
// be used to access earlier blocks.
|
|
//
|
|
// NOTE: Ideally this should not really be a part of the address manager as it
|
|
// is intended for syncing purposes. It is being exposed here for now to go
|
|
// with the other syncing code. Ultimately, all syncing code should probably
|
|
// go into its own package and share the data store.
|
|
func (m *Manager) NewIterateRecentBlocks() *BlockIterator {
|
|
m.mtx.RLock()
|
|
defer m.mtx.RUnlock()
|
|
|
|
return m.syncState.iter(&m.mtx)
|
|
}
|
|
|
|
// SetSyncedTo marks the address manager to be in sync with the recently-seen
|
|
// block described by the blockstamp. When the provided blockstamp is nil,
|
|
// the oldest blockstamp of the block the manager was created at and of all
|
|
// imported addresses will be used. This effectively allows the manager to be
|
|
// marked as unsynced back to the oldest known point any of the addresses have
|
|
// appeared in the block chain.
|
|
func (m *Manager) SetSyncedTo(bs *BlockStamp) error {
|
|
m.mtx.Lock()
|
|
defer m.mtx.Unlock()
|
|
|
|
// Update the recent history.
|
|
//
|
|
// NOTE: The values in the memory sync state aren't directly modified
|
|
// here in case the forthcoming db update fails. The memory sync state
|
|
// is updated with these values as needed after the db updates.
|
|
recentHeight := m.syncState.recentHeight
|
|
recentHashes := m.syncState.recentHashes
|
|
if bs == nil {
|
|
// Use the stored start blockstamp and reset recent hashes and
|
|
// height when the provided blockstamp is nil.
|
|
bs = &m.syncState.startBlock
|
|
recentHeight = m.syncState.startBlock.Height
|
|
recentHashes = nil
|
|
|
|
} else if bs.Height < recentHeight {
|
|
// When the new block stamp height is prior to the most recently
|
|
// seen height, a rollback is being performed. Thus, when the
|
|
// previous block stamp is already saved, remove anything after
|
|
// it. Otherwise, the rollback must be too far in history, so
|
|
// clear the recent hashes and set the recent height to the
|
|
// current block stamp height.
|
|
numHashes := len(recentHashes)
|
|
idx := numHashes - 1 - int(recentHeight-bs.Height)
|
|
if idx >= 0 && idx < numHashes && recentHashes[idx] == bs.Hash {
|
|
// subslice out the removed hashes.
|
|
recentHeight = bs.Height
|
|
recentHashes = recentHashes[:idx]
|
|
} else {
|
|
recentHeight = bs.Height
|
|
recentHashes = nil
|
|
}
|
|
|
|
} else if bs.Height != recentHeight+1 {
|
|
// At this point the new block stamp height is after the most
|
|
// recently seen block stamp, so it should be the next height in
|
|
// sequence. When this is not the case, the recent history is
|
|
// no longer valid, so clear the recent hashes and set the
|
|
// recent height to the current block stamp height.
|
|
recentHeight = bs.Height
|
|
recentHashes = nil
|
|
} else {
|
|
// The only case left is when the new block stamp height is the
|
|
// next height in sequence after the most recently seen block
|
|
// stamp, so update it accordingly.
|
|
recentHeight = bs.Height
|
|
}
|
|
|
|
// Enforce maximum number of recent hashes.
|
|
if len(recentHashes) == maxRecentHashes {
|
|
// Shift everything down one position and add the new hash in
|
|
// the last position.
|
|
copy(recentHashes, recentHashes[1:])
|
|
recentHashes[maxRecentHashes-1] = bs.Hash
|
|
} else {
|
|
recentHashes = append(recentHashes, bs.Hash)
|
|
}
|
|
|
|
// Update the database.
|
|
err := m.db.Update(func(tx *managerTx) error {
|
|
err := tx.PutSyncedTo(bs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.PutRecentBlocks(recentHeight, recentHashes)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update memory now that the database is updated.
|
|
m.syncState.syncedTo = *bs
|
|
m.syncState.recentHashes = recentHashes
|
|
m.syncState.recentHeight = recentHeight
|
|
return nil
|
|
}
|
|
|
|
// SyncedTo returns details about the block height and hash that the address
|
|
// manager is synced through at the very least. The intention is that callers
|
|
// can use this information for intelligently initiating rescans to sync back to
|
|
// the best chain from the last known good block.
|
|
func (m *Manager) SyncedTo() BlockStamp {
|
|
m.mtx.Lock()
|
|
defer m.mtx.Unlock()
|
|
|
|
return m.syncState.syncedTo
|
|
}
|