Detect reorgs after btcd disconnect/reconnect.

This change saves (at most) the last 20 block hashes to disk.  Upon
btcd connect, in the handshake, btcwallet checks whether btcd's best
chain still contains these blocks, starting from the most recently
added block and continuing until the earliest saved.  If any blocks
are missing, Tx history and UTXOs from any blocks no longer in the
chain are removed, and a rescan is started from after the best block
still in the main chain.

If all previous block hashes are exhausted (either due to a large
reorg, or because not enough blocks have been seen), a full rescan is
triggered (full meaning from the earliest block that matters to this
wallet) since the last synced up to point is no longer available.

The previous 20 seen block hashes are saved to the wallet file, which
required bumping the file version.  Older wallets written with lesser
versions will use the previous reading function, making this change
backwards compatible.
This commit is contained in:
Josh Rickmar 2013-12-06 15:37:07 -05:00
parent e48d115900
commit c97e0d5fc6
4 changed files with 577 additions and 80 deletions

View file

@ -346,7 +346,7 @@ func (a *Account) RescanActiveAddresses() {
// point for block rescanning. Grab the block stamp here.
bs := a.SyncedWith()
log.Debugf("Rescanning account '%v' for new transactions since block height %v hash %v",
log.Debugf("Rescanning account '%v' for new transactions after block height %v hash %v",
a.name, bs.Height, bs.Hash)
// If we're synced with block x, must scan the blocks x+1 to best block.
@ -633,6 +633,9 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo
}
// Add to TxStore.
//
// TODO(jrick): check for duplicates. This could occur if we're
// adding txs for an out of sync btcd on its IBD.
t := &tx.RecvTx{
TxID: *txID,
TimeReceived: time.Now().Unix(),

View file

@ -65,10 +65,10 @@ func (store *AccountStore) Account(name string) (*Account, error) {
}
// Rollback rolls back each Account saved in the store.
//
// TODO(jrick): This must also roll back the UTXO and TX stores, and notify
// all wallets of new account balances.
func (store *AccountStore) Rollback(height int32, hash *btcwire.ShaHash) {
log.Debugf("Rolling back tx history since block height %v hash %v",
height, hash)
store.Lock()
defer store.Unlock()
@ -173,10 +173,6 @@ func (store *AccountStore) CalculateBalance(account string,
// CreateEncryptedWallet creates a new account with a wallet file
// encrypted with passphrase.
//
// TODO(jrick): different passphrases on different accounts in the
// same wallet is a bad idea. Switch this to use one passphrase for all
// account wallet files.
func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase []byte) error {
store.Lock()
defer store.Unlock()

View file

@ -622,9 +622,6 @@ func NtfnBlockConnected(n btcws.Notification) {
// the new block notification. New balance notifications for txs
// in blocks are therefore sent here after all tx notifications
// have arrived.
//
// TODO(jrick): send frontend tx notifications once that's
// implemented.
accountstore.BlockNotify(bs)
@ -635,8 +632,6 @@ func NtfnBlockConnected(n btcws.Notification) {
// NtfnBlockDisconnected handles btcd notifications resulting from
// blocks disconnected from the main chain in the event of a chain
// switch and notifies frontends of the new blockchain height.
//
// TODO(jrick): Rollback Utxo and Tx data
func NtfnBlockDisconnected(n btcws.Notification) {
bdn, ok := n.(*btcws.BlockDisconnectedNtfn)
if !ok {
@ -706,8 +701,6 @@ func (s *server) Start() {
log.Trace("Starting RPC server")
// TODO(jrick): We need some sort of authentication before websocket
// connections are allowed, and perhaps TLS on the server as well.
serveMux := http.NewServeMux()
httpServer := &http.Server{Handler: serveMux}
serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@ -857,9 +850,14 @@ func resendUnminedTxs() {
// settings between the two processes (such as running on different
// Bitcoin networks). If the sanity checks pass, all wallets are set to
// be tracked against chain notifications from this btcd connection.
//
// TODO(jrick): Track and Rescan commands should be replaced with a
// single TrackSince function (or similar) which requests address
// notifications and performs the rescan since some block height.
func BtcdHandshake(ws *websocket.Conn) error {
n := <-NewJSONID
cmd := btcws.NewGetCurrentNetCmd(fmt.Sprintf("btcwallet(%v)", n))
var cmd btcjson.Cmd
cmd = btcws.NewGetCurrentNetCmd(fmt.Sprintf("btcwallet(%v)", n))
mcmd, err := cmd.MarshalJSON()
if err != nil {
return fmt.Errorf("cannot complete btcd handshake: %v", err)
@ -889,24 +887,96 @@ func BtcdHandshake(ws *websocket.Conn) error {
return errors.New("btcd and btcwallet running on different Bitcoin networks")
}
// TODO(jrick): Check that there was not any reorgs done
// since last connection. If so, rollback and rescan to
// catch up.
accountstore.RescanActiveAddresses()
// Begin tracking wallets against this btcd instance.
accountstore.Track()
// (Re)send any unmined transactions to btcd in case of a btcd restart.
resendUnminedTxs()
// Get current blockchain height and best block hash.
if bs, err := GetCurBlock(); err == nil {
NotifyNewBlockChainHeight(frontendNotificationMaster, bs.Height)
NotifyBalances(frontendNotificationMaster)
// Get default account. Only the default account is used to
// track recently-seen blocks.
a, err := accountstore.Account("")
if err != nil {
return fmt.Errorf("cannot get default account: %v", err)
}
// Get current best block. If this is before than the oldest
// saved block hash, assume that this btcd instance is not yet
// synced up to a previous btcd that was last used with this
// wallet.
bs, err := GetCurBlock()
if err != nil {
return fmt.Errorf("cannot get best block: %v", err)
}
NotifyNewBlockChainHeight(frontendNotificationMaster, bs.Height)
NotifyBalances(frontendNotificationMaster)
// TODO(jrick): if height is less than the earliest-saved block
// height, should probably wait for btcd to catch up.
// Check that there was not any reorgs done since last connection.
// If so, rollback and rescan to catch up.
it := a.Wallet.NewIterateRecentBlocks()
for cont := it != nil; cont; cont = it.Prev() {
bs := it.BlockStamp()
log.Debugf("Checking for previous saved block with height %v hash %v",
bs.Height, bs.Hash)
n = <-NewJSONID
// NewGetBlockCmd can't fail, so don't check error.
// TODO(jrick): probably want to remove the error return value.
cmd, _ = btcjson.NewGetBlockCmd(fmt.Sprintf("btcwallet(%v)", n),
bs.Hash.String())
mcmd, _ = cmd.MarshalJSON()
blockMissing := make(chan bool)
replyHandlers.Lock()
replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool {
blockMissing <- err != nil && err.Code == btcjson.ErrBlockNotFound.Code
// No additional replies expected, remove handler.
return true
}
replyHandlers.Unlock()
btcdMsgs <- mcmd
if <-blockMissing {
continue
}
log.Debug("Found matching block.")
// If we had to go back to any previous blocks (it.Next
// returns true), then rollback the next and all child blocks.
// This rollback is done here instead of in the blockMissing
// check above for each removed block because Rollback will
// try to write new tx and utxo files on each rollback.
if it.Next() {
bs := it.BlockStamp()
accountstore.Rollback(bs.Height, &bs.Hash)
}
// Set default account to be marked in sync with the current
// blockstamp. This invalidates the iterator.
a.Wallet.SetSyncedWith(bs)
// Begin tracking wallets against this btcd instance.
accountstore.Track()
accountstore.RescanActiveAddresses()
// (Re)send any unmined transactions to btcd in case of a btcd restart.
resendUnminedTxs()
// Get current blockchain height and best block hash.
return nil
}
log.Warnf("None of the previous saved blocks in btcd chain. Must perform full rescan.")
// Iterator was invalid (wallet has never been synced) or there was a
// huge chain fork + reorg (more than 20 blocks). Since we don't know
// what block (if any) this wallet is synced to, roll back everything
// and start a new rescan since the earliest block wallet must know
// about.
a.fullRescan = true
accountstore.Track()
accountstore.RescanActiveAddresses()
resendUnminedTxs()
return nil
}

View file

@ -249,6 +249,140 @@ func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) {
return pad(32, b), nil
}
type version struct {
major byte
minor byte
bugfix byte
autoincrement byte
}
// Enforce that version satisifies the io.ReaderFrom and
// io.WriterTo interfaces.
var _ io.ReaderFrom = &version{}
var _ io.WriterTo = &version{}
// ReaderFromVersion is an io.ReaderFrom and io.WriterTo that
// can specify any particular wallet file format for reading
// depending on the wallet file version.
type ReaderFromVersion interface {
ReadFromVersion(version, io.Reader) (int64, error)
io.WriterTo
}
func (v version) String() string {
str := fmt.Sprintf("%d.%d", v.major, v.minor)
if v.bugfix != 0x00 || v.autoincrement != 0x00 {
str += fmt.Sprintf(".%d", v.bugfix)
}
if v.autoincrement != 0x00 {
str += fmt.Sprintf(".%d", v.autoincrement)
}
return str
}
func (v version) Uint32() uint32 {
return uint32(v.major)<<6 | uint32(v.minor)<<4 | uint32(v.bugfix)<<2 | uint32(v.autoincrement)
}
func (v *version) ReadFrom(r io.Reader) (int64, error) {
// Read 4 bytes for the version.
versBytes := make([]byte, 4)
n, err := r.Read(versBytes)
if err != nil {
return int64(n), err
}
v.major = versBytes[0]
v.minor = versBytes[1]
v.bugfix = versBytes[2]
v.autoincrement = versBytes[3]
return int64(n), nil
}
func (v *version) WriteTo(w io.Writer) (int64, error) {
// Write 4 bytes for the version.
versBytes := []byte{
v.major,
v.minor,
v.bugfix,
v.autoincrement,
}
n, err := w.Write(versBytes)
return int64(n), err
}
// LT returns whether v is an earlier version than v2.
func (v version) LT(v2 version) bool {
switch {
case v.major < v2.major:
return true
case v.minor < v2.minor:
return true
case v.bugfix < v2.bugfix:
return true
case v.autoincrement < v2.autoincrement:
return true
default:
return false
}
}
// EQ returns whether v2 is an equal version to v.
func (v version) EQ(v2 version) bool {
switch {
case v.major != v2.major:
return false
case v.minor != v2.minor:
return false
case v.bugfix != v2.bugfix:
return false
case v.autoincrement != v2.autoincrement:
return false
default:
return true
}
}
// GT returns whether v is a later version than v2.
func (v version) GT(v2 version) bool {
switch {
case v.major > v2.major:
return true
case v.minor > v2.minor:
return true
case v.bugfix > v2.bugfix:
return true
case v.autoincrement > v2.autoincrement:
return true
default:
return false
}
}
// Various versions.
var (
// VersArmory is the latest version used by Armory.
VersArmory = version{1, 35, 0, 0}
// Vers20LastBlocks is the version where wallet files now hold
// the 20 most recently seen block hashes.
Vers20LastBlocks = version{1, 36, 0, 0}
// VersCurrent is the current wallet file version.
VersCurrent = Vers20LastBlocks
)
type varEntries []io.WriterTo
func (v *varEntries) WriteTo(w io.Writer) (n int64, err error) {
@ -327,11 +461,10 @@ type addressHashKey string
type transactionHashKey string
type comment []byte
// Wallet represents an btcd/Armory wallet in memory. It
// implements the io.ReaderFrom and io.WriterTo interfaces to read
// from and write to any type of byte streams, including files.
// Wallet represents an btcwallet wallet in memory. It implements
// the io.ReaderFrom and io.WriterTo interfaces to read from and
// write to any type of byte streams, including files.
type Wallet struct {
version uint32
net btcwire.BitcoinNet
flags walletFlags
uniqID [6]byte
@ -344,8 +477,7 @@ type Wallet struct {
// These are non-standard and fit in the extra 1024 bytes between the
// root address and the appended entries.
syncedBlockHeight int32
syncedBlockHash btcwire.ShaHash
recent recentBlocks
addrMap map[addressHashKey]*btcAddress
addrCommentMap map[addressHashKey]comment
@ -361,17 +493,6 @@ type Wallet struct {
lastChainIdx int64
}
// UnusedWalletBytes specifies the number of actually unused bytes
// between the root address and the appended entries in a serialized
// wallet. Armory's wallet file format provides 1024 unused bytes
// in this space. btcwallet requires saving a few additional details
// with the wallet file, so the binary sizes of those are subtracted
// from 1024. Currently, these are:
//
// - last synced block height (int32, 4 bytes)
// - last synced block hash (btcwire.ShaHash, btcwire.HashSize bytes)
const UnusedWalletBytes = 1024 - 4 - btcwire.HashSize
// NewWallet creates and initializes a new Wallet. name's and
// desc's binary representation must not exceed 32 and 256 bytes,
// respectively. All address private keys are encrypted with passphrase.
@ -419,7 +540,6 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet,
// Create and fill wallet.
w := &Wallet{
version: 0, // TODO(jrick): implement versioning
// TODO(jrick): not sure we will need uniqID, but would be good for
// compat with armory.
net: net,
@ -427,17 +547,21 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet,
useEncryption: true,
watchingOnly: false,
},
createDate: time.Now().Unix(),
highestUsed: rootKeyChainIdx,
kdfParams: *kdfp,
keyGenerator: *root,
syncedBlockHeight: createdAt.Height,
syncedBlockHash: createdAt.Hash,
addrMap: make(map[addressHashKey]*btcAddress),
addrCommentMap: make(map[addressHashKey]comment),
txCommentMap: make(map[transactionHashKey]comment),
chainIdxMap: make(map[int64]addressHashKey),
lastChainIdx: rootKeyChainIdx,
createDate: time.Now().Unix(),
highestUsed: rootKeyChainIdx,
kdfParams: *kdfp,
keyGenerator: *root,
recent: recentBlocks{
lastHeight: createdAt.Height,
hashes: []*btcwire.ShaHash{
&createdAt.Hash,
},
},
addrMap: make(map[addressHashKey]*btcAddress),
addrCommentMap: make(map[addressHashKey]comment),
txCommentMap: make(map[transactionHashKey]comment),
chainIdxMap: make(map[int64]addressHashKey),
lastChainIdx: rootKeyChainIdx,
}
copy(w.name[:], []byte(name))
copy(w.desc[:], []byte(desc))
@ -478,6 +602,7 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
w.txCommentMap = make(map[transactionHashKey]comment)
var id [8]byte
var vers version
var appendedEntries varEntries
// Iterate through each entry needing to be read. If data
@ -485,7 +610,7 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
// data is a pointer to a fixed sized value.
datas := []interface{}{
&id,
&w.version,
&vers,
&w.net,
&w.flags,
&w.uniqID,
@ -496,17 +621,20 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
&w.kdfParams,
make([]byte, 256),
&w.keyGenerator,
&w.syncedBlockHeight,
&w.syncedBlockHash,
make([]byte, UnusedWalletBytes),
newUnusedSpace(1024, &w.recent),
&appendedEntries,
}
for _, data := range datas {
var err error
if rf, ok := data.(io.ReaderFrom); ok {
read, err = rf.ReadFrom(r)
} else {
read, err = binaryRead(r, binary.LittleEndian, data)
switch d := data.(type) {
case ReaderFromVersion:
read, err = d.ReadFromVersion(vers, r)
case io.ReaderFrom:
read, err = d.ReadFrom(r)
default:
read, err = binaryRead(r, binary.LittleEndian, d)
}
n += read
if err != nil {
@ -597,7 +725,7 @@ func (w *Wallet) WriteTo(wtr io.Writer) (n int64, err error) {
// data is a pointer to a fixed size value.
datas := []interface{}{
&fileID,
&w.version,
&VersCurrent,
&w.net,
&w.flags,
&w.uniqID,
@ -608,9 +736,7 @@ func (w *Wallet) WriteTo(wtr io.Writer) (n int64, err error) {
&w.kdfParams,
make([]byte, 256),
&w.keyGenerator,
&w.syncedBlockHeight,
&w.syncedBlockHash,
make([]byte, UnusedWalletBytes),
newUnusedSpace(1024, &w.recent),
&appendedEntries,
}
var written int64
@ -689,7 +815,6 @@ func (w *Wallet) IsLocked() (locked bool) {
}
// Version returns a wallet's version as a string and int.
// TODO(jrick)
func (w *Wallet) Version() (string, int) {
return "", 0
}
@ -882,17 +1007,62 @@ func (w *Wallet) Net() btcwire.BitcoinNet {
// SetSyncedWith marks the wallet to be in sync with the block
// described by height and hash.
func (w *Wallet) SetSyncedWith(bs *BlockStamp) {
w.syncedBlockHeight = bs.Height
copy(w.syncedBlockHash[:], bs.Hash[:])
// Check if we're trying to rollback the last seen history.
// If so, and this bs is already saved, remove anything
// after and return. Otherwire, remove previous hashes.
if bs.Height < w.recent.lastHeight {
maybeIdx := len(w.recent.hashes) - 1 - int(w.recent.lastHeight-bs.Height)
if maybeIdx >= 0 && maybeIdx < len(w.recent.hashes) &&
*w.recent.hashes[maybeIdx] == bs.Hash {
w.recent.lastHeight = bs.Height
// subslice out the removed hashes.
w.recent.hashes = w.recent.hashes[:maybeIdx]
return
}
w.recent.hashes = nil
}
if bs.Height != w.recent.lastHeight+1 {
w.recent.hashes = nil
}
w.recent.lastHeight = bs.Height
blockSha := new(btcwire.ShaHash)
copy(blockSha[:], bs.Hash[:])
if len(w.recent.hashes) == 20 {
// Make room for the most recent hash.
copy(w.recent.hashes, w.recent.hashes[1:])
// Set new block in the last position.
w.recent.hashes[19] = blockSha
} else {
w.recent.hashes = append(w.recent.hashes, blockSha)
}
}
// SyncedWith returns the height and hash of the block the wallet is
// currently marked to be in sync with.
func (w *Wallet) SyncedWith() *BlockStamp {
return &BlockStamp{
Height: w.syncedBlockHeight,
Hash: w.syncedBlockHash,
nHashes := len(w.recent.hashes)
if nHashes == 0 || w.recent.lastHeight == -1 {
return &BlockStamp{
Height: -1,
}
}
lastSha := w.recent.hashes[nHashes-1]
return &BlockStamp{
Height: w.recent.lastHeight,
Hash: *lastSha,
}
}
// 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.
func (w *Wallet) NewIterateRecentBlocks() RecentBlockIterator {
return w.recent.NewIterator()
}
// EarliestBlockHeight returns the height of the blockchain for when any
@ -1113,6 +1283,264 @@ func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) {
return binaryWrite(w, binary.LittleEndian, b)
}
// recentBlocks holds at most the last 20 seen block hashes as well as
// the block height of the most recently seen block.
type recentBlocks struct {
hashes []*btcwire.ShaHash
lastHeight int32
}
type blockIterator struct {
height int32
index int
rb *recentBlocks
}
func (rb *recentBlocks) ReadFromVersion(v version, r io.Reader) (int64, error) {
if !v.LT(Vers20LastBlocks) {
// Use current version.
return rb.ReadFrom(r)
}
// Old file versions only saved the most recently seen
// block height and hash, not the last 20.
var read int64
var syncedBlockHash btcwire.ShaHash
// Read height.
heightBytes := make([]byte, 4) // 4 bytes for a int32
n, err := r.Read(heightBytes)
if err != nil {
return read + int64(n), err
}
read += int64(n)
rb.lastHeight = int32(binary.LittleEndian.Uint32(heightBytes))
// If height is -1, the last synced block is unknown, so don't try
// to read a block hash.
if rb.lastHeight == -1 {
rb.hashes = nil
return read, nil
}
// Read block hash.
n, err = r.Read(syncedBlockHash[:])
if err != nil {
return read + int64(n), err
}
read += int64(n)
rb.hashes = []*btcwire.ShaHash{
&syncedBlockHash,
}
return read, nil
}
func (rb *recentBlocks) ReadFrom(r io.Reader) (int64, error) {
var read int64
// Read number of saved blocks. This should not exceed 20.
nBlockBytes := make([]byte, 4) // 4 bytes for a uint32
n, err := r.Read(nBlockBytes)
if err != nil {
return read + int64(n), err
}
read += int64(n)
nBlocks := binary.LittleEndian.Uint32(nBlockBytes)
if nBlocks > 20 {
return read, errors.New("number of last seen blocks exceeds maximum of 20")
}
// If number of blocks is 0, our work here is done.
if nBlocks == 0 {
rb.lastHeight = -1
rb.hashes = nil
return read, nil
}
// Read most recently seen block height.
heightBytes := make([]byte, 4) // 4 bytes for a int32
n, err = r.Read(heightBytes)
if err != nil {
return read + int64(n), err
}
read += int64(n)
height := int32(binary.LittleEndian.Uint32(heightBytes))
// height should not be -1 (or any other negative number)
// since at this point we should be reading in at least one
// known block.
if height < 0 {
return read, errors.New("expected a block but specified height is negative")
}
// Set last seen height.
rb.lastHeight = height
// Read nBlocks block hashes. Hashes are expected to be in
// order of oldest to newest, but there's no way to check
// that here.
rb.hashes = make([]*btcwire.ShaHash, 0, nBlocks)
for i := uint32(0); i < nBlocks; i++ {
blockSha := new(btcwire.ShaHash)
n, err := r.Read(blockSha[:])
if err != nil {
return read + int64(n), err
}
read += int64(n)
rb.hashes = append(rb.hashes, blockSha)
}
return read, nil
}
func (rb *recentBlocks) WriteTo(w io.Writer) (int64, error) {
var written int64
// Write number of saved blocks. This should not exceed 20.
nBlocks := uint32(len(rb.hashes))
if nBlocks > 20 {
return written, errors.New("number of last seen blocks exceeds maximum of 20")
}
if nBlocks != 0 && rb.lastHeight < 0 {
return written, errors.New("number of block hashes is positive, but height is negative")
}
if nBlocks == 0 && rb.lastHeight != -1 {
return written, errors.New("no block hashes available, but height is not -1")
}
nBlockBytes := make([]byte, 4) // 4 bytes for a uint32
binary.LittleEndian.PutUint32(nBlockBytes, nBlocks)
n, err := w.Write(nBlockBytes)
if err != nil {
return written + int64(n), err
}
written += int64(n)
// If number of blocks is 0, our work here is done.
if nBlocks == 0 {
return written, nil
}
// Write most recently seen block height.
heightBytes := make([]byte, 4) // 4 bytes for a int32
binary.LittleEndian.PutUint32(heightBytes, uint32(rb.lastHeight))
n, err = w.Write(heightBytes)
if err != nil {
return written + int64(n), err
}
written += int64(n)
// Write block hashes.
for _, hash := range rb.hashes {
n, err := w.Write(hash[:])
if err != nil {
return written + int64(n), err
}
written += int64(n)
}
return written, nil
}
// RecentBlockIterator is a type to iterate through recent-seen
// blocks.
type RecentBlockIterator interface {
Next() bool
Prev() bool
BlockStamp() *BlockStamp
}
func (rb *recentBlocks) NewIterator() RecentBlockIterator {
if rb.lastHeight == -1 {
return nil
}
return &blockIterator{
height: rb.lastHeight,
index: len(rb.hashes) - 1,
rb: rb,
}
}
func (it *blockIterator) Next() bool {
if it.index+1 >= len(it.rb.hashes) {
return false
}
it.index += 1
return true
}
func (it *blockIterator) Prev() bool {
if it.index-1 < 0 {
return false
}
it.index -= 1
return true
}
func (it *blockIterator) BlockStamp() *BlockStamp {
return &BlockStamp{
Height: it.rb.lastHeight - int32(len(it.rb.hashes)-1-it.index),
Hash: *it.rb.hashes[it.index],
}
}
// unusedSpace is a wrapper type to read or write one or more types
// that btcwallet fits into an unused space left by Armory's wallet file
// format.
type unusedSpace struct {
nBytes int // number of unused bytes that armory left.
rfvs []ReaderFromVersion
}
func newUnusedSpace(nBytes int, rfvs ...ReaderFromVersion) *unusedSpace {
return &unusedSpace{
nBytes: nBytes,
rfvs: rfvs,
}
}
func (u *unusedSpace) ReadFromVersion(v version, r io.Reader) (int64, error) {
var read int64
for _, rfv := range u.rfvs {
n, err := rfv.ReadFromVersion(v, r)
if err != nil {
return read + n, err
}
read += n
if read > int64(u.nBytes) {
return read, errors.New("read too much from armory's unused space")
}
}
// Read rest of actually unused bytes.
unused := make([]byte, u.nBytes-int(read))
n, err := r.Read(unused)
return read + int64(n), err
}
func (u *unusedSpace) WriteTo(w io.Writer) (int64, error) {
var written int64
for _, wt := range u.rfvs {
n, err := wt.WriteTo(w)
if err != nil {
return written + n, err
}
written += n
if written > int64(u.nBytes) {
return written, errors.New("wrote too much to armory's unused space")
}
}
// Write rest of actually unused bytes.
unused := make([]byte, u.nBytes-int(written))
n, err := w.Write(unused)
return written + int64(n), err
}
type btcAddress struct {
pubKeyHash [ripemd160.Size]byte
flags addrFlags