Skip to content

Commit

Permalink
multi: store unecrypted scripts in address mgr.
Browse files Browse the repository at this point in the history
This removes the need to encrypt  a script and its hash before storage in the address manager.
  • Loading branch information
dnldd committed Jul 9, 2019
1 parent 18d887d commit be6405a
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 100 deletions.
84 changes: 15 additions & 69 deletions wallet/udb/addressdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/binary"
"time"

"github.com/decred/dcrd/dcrutil"
"github.com/decred/dcrwallet/errors"
"github.com/decred/dcrwallet/wallet/v2/walletdb"
)
Expand Down Expand Up @@ -108,8 +109,8 @@ type dbImportedAddressRow struct {
// address in the database.
type dbScriptAddressRow struct {
dbAddressRow
encryptedHash []byte
encryptedScript []byte
hash []byte
script []byte
}

// Key names for various database fields.
Expand Down Expand Up @@ -964,58 +965,6 @@ func serializeImportedAddress(encryptedPubKey, encryptedPrivKey []byte) []byte {
return rawData
}

// deserializeScriptAddress deserializes the raw data from the passed address
// row as a script address.
func deserializeScriptAddress(row *dbAddressRow) (*dbScriptAddressRow, error) {
// The serialized script address raw data format is:
// <encscripthashlen><encscripthash><encscriptlen><encscript>
//
// 4 bytes encrypted script hash len + encrypted script hash + 4 bytes
// encrypted script len + encrypted script

// Given the above, the length of the entry must be at a minimum
// the constant value sizes.
if len(row.rawData) < 8 {
return nil, errors.E(errors.IO, errors.Errorf("bad script address len %d", len(row.rawData)))
}

retRow := dbScriptAddressRow{
dbAddressRow: *row,
}

hashLen := binary.LittleEndian.Uint32(row.rawData[0:4])
retRow.encryptedHash = make([]byte, hashLen)
copy(retRow.encryptedHash, row.rawData[4:4+hashLen])
offset := 4 + hashLen
scriptLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4])
offset += 4
retRow.encryptedScript = make([]byte, scriptLen)
copy(retRow.encryptedScript, row.rawData[offset:offset+scriptLen])

return &retRow, nil
}

// serializeScriptAddress returns the serialization of the raw data field for
// a script address.
func serializeScriptAddress(encryptedHash, encryptedScript []byte) []byte {
// The serialized script address raw data format is:
// <encscripthashlen><encscripthash><encscriptlen><encscript>
//
// 4 bytes encrypted script hash len + encrypted script hash + 4 bytes
// encrypted script len + encrypted script

hashLen := uint32(len(encryptedHash))
scriptLen := uint32(len(encryptedScript))
rawData := make([]byte, 8+hashLen+scriptLen)
binary.LittleEndian.PutUint32(rawData[0:4], hashLen)
copy(rawData[4:4+hashLen], encryptedHash)
offset := 4 + hashLen
binary.LittleEndian.PutUint32(rawData[offset:offset+4], scriptLen)
offset += 4
copy(rawData[offset:offset+scriptLen], encryptedScript)
return rawData
}

// fetchAddressByHash loads address information for the provided address hash
// from the database. The returned value is one of the address rows for the
// specific address type. The caller should use type assertions to ascertain
Expand All @@ -1040,7 +989,11 @@ func fetchAddressByHash(ns walletdb.ReadBucket, addrHash []byte) (interface{}, e
case adtImport:
return deserializeImportedAddress(row)
case adtScript:
return deserializeScriptAddress(row)
return &dbScriptAddressRow{
dbAddressRow: *row,
hash: dcrutil.Hash160(row.rawData),
script: row.rawData,
}, nil
}

return nil, errors.E(errors.IO, errors.Errorf("unknown address type %d", row.addrType))
Expand Down Expand Up @@ -1110,18 +1063,17 @@ func putImportedAddress(ns walletdb.ReadWriteBucket, addressID []byte, account u

// putScriptAddress stores the provided script address information to the
// database.
func putScriptAddress(ns walletdb.ReadWriteBucket, addressID []byte, account uint32,
status syncStatus, encryptedHash, encryptedScript []byte) error {
func putScriptAddress(ns walletdb.ReadWriteBucket, account uint32,
status syncStatus, hash, script []byte) error {

rawData := serializeScriptAddress(encryptedHash, encryptedScript)
addrRow := dbAddressRow{
addrType: adtScript,
account: account,
addTime: uint64(time.Now().Unix()),
syncStatus: status,
rawData: rawData,
rawData: script,
}
return putAddress(ns, addressID, &addrRow)
return putAddress(ns, hash, &addrRow)
}

// existsAddress returns whether or not the address id exists in the database.
Expand Down Expand Up @@ -1325,16 +1277,10 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket, dbVersion uint32) error {
}

for k, row := range importedScriptAddrSet {
srow, err := deserializeScriptAddress(row)
if err != nil {
return err
}
// Reserialize the script address without the script and store it.
row.rawData = nil

// Reserialize the script address without the script
// and store it.
row.rawData = serializeScriptAddress(srow.encryptedHash,
nil)
err = bucket.Put([]byte(k), serializeAddressRow(row))
err := bucket.Put([]byte(k), serializeAddressRow(row))
if err != nil {
return errors.E(errors.IO, err)
}
Expand Down
33 changes: 3 additions & 30 deletions wallet/udb/addressmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -854,13 +854,7 @@ func (m *Manager) importedAddressRowToManaged(row *dbImportedAddressRow) (Manage
// scriptAddressRowToManaged returns a new managed address based on script
// address data loaded from the database.
func (m *Manager) scriptAddressRowToManaged(row *dbScriptAddressRow) (ManagedAddress, error) {
// Use the crypto public key to decrypt the imported script hash.
scriptHash, err := m.cryptoKeyPub.Decrypt(row.encryptedHash)
if err != nil {
return nil, errors.E(errors.Crypto, errors.Errorf("decrypt imported P2SH address: %v", err))
}

return newScriptAddress(m, row.account, scriptHash)
return newScriptAddress(m, row.account, row.hash)
}

// rowInterfaceToManaged returns a new managed address based on the given
Expand Down Expand Up @@ -1255,27 +1249,9 @@ func (m *Manager) ImportScript(ns walletdb.ReadWriteBucket, script []byte) (Mana
return nil, errors.E(errors.Locked)
}

// Encrypt the script hash using the crypto public key so it is
// accessible when the address manager is locked or watching-only.
encryptedHash, err := m.cryptoKeyPub.Encrypt(scriptHash)
if err != nil {
return nil, errors.E(errors.Crypto, errors.Errorf("encrypt script hash: %v", err))
}

// Encrypt the script for storage in database using the crypto script
// key when not a watching-only address manager.
var encryptedScript []byte
if !m.watchingOnly {
encryptedScript, err = m.cryptoKeyScript.Encrypt(script)
if err != nil {
return nil, errors.E(errors.Crypto, errors.Errorf("encrypt script: %v", err))
}
}

// Save the new imported address to the db and update start block (if
// needed) in a single transaction.
err = putScriptAddress(ns, scriptHash, ImportedAddrAccount,
ssNone, encryptedHash, encryptedScript)
err := putScriptAddress(ns, ImportedAddrAccount, ssNone, scriptHash, script)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1999,10 +1975,7 @@ func (m *Manager) RedeemScript(ns walletdb.ReadBucket, addr dcrutil.Address) (sc
}
switch a := addrInterface.(type) {
case *dbScriptAddressRow:
script, err = m.cryptoKeyScript.Decrypt(a.encryptedScript)
if err != nil {
return nil, nil, errors.E(errors.Crypto, errors.Errorf("decrypt imported script: %v", err))
}
script = a.script

case *dbChainAddressRow, *dbImportedAddressRow:
return nil, nil, errors.E(errors.Invalid, "redeem script lookup requires P2SH address")
Expand Down
86 changes: 85 additions & 1 deletion wallet/udb/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package udb

import (
"crypto/sha256"
"encoding/binary"
"fmt"

"github.com/decred/dcrd/blockchain/stake"
"github.com/decred/dcrd/chaincfg"
Expand Down Expand Up @@ -109,10 +111,15 @@ const (
// accounting of total locked funds.
ticketCommitmentsVersion = 12

// scriptStorageVersion is the thirteenth version of the database. It
// updates the serialization semantics of the address row data and
// removes the need encrypt the script and its hash before storage.
scriptStorageVersion = 13

// DBVersion is the latest version of the database that is understood by the
// program. Databases with recorded versions higher than this will fail to
// open (meaning any upgrades prevent reverting to older software).
DBVersion = ticketCommitmentsVersion
DBVersion = scriptStorageVersion
)

// upgrades maps between old database versions and the upgrade function to
Expand All @@ -130,6 +137,7 @@ var upgrades = [...]func(walletdb.ReadWriteTx, []byte, *chaincfg.Params) error{
cfVersion - 1: cfUpgrade,
lastProcessedTxsBlockVersion - 1: lastProcessedTxsBlockUpgrade,
ticketCommitmentsVersion - 1: ticketCommitmentsUpgrade,
scriptStorageVersion - 1: scriptStorageUpgrade,
}

func lastUsedAddressIndexUpgrade(tx walletdb.ReadWriteTx, publicPassphrase []byte, params *chaincfg.Params) error {
Expand Down Expand Up @@ -994,6 +1002,82 @@ func ticketCommitmentsUpgrade(tx walletdb.ReadWriteTx, publicPassphrase []byte,
return unifiedDBMetadata{}.putVersion(metadataBucket, newVersion)
}

func scriptStorageUpgrade(tx walletdb.ReadWriteTx, publicPassphrase []byte, params *chaincfg.Params) error {
const oldVersion = 12
const newVersion = 13

metadataBucket := tx.ReadWriteBucket(unifiedDBMetadata{}.rootBucketKey())
addrmgrBucket := tx.ReadWriteBucket(waddrmgrBucketKey)

// Assert that this function is only called on version 12 databases.
dbVersion, err := unifiedDBMetadata{}.getVersion(metadataBucket)
if err != nil {
return err
}
if dbVersion != oldVersion {
return errors.E(errors.Invalid, "scriptStorageUpgrade inappropriately called")
}

amgr, err := loadManager(addrmgrBucket, publicPassphrase, params)
if err != nil {
return err
}

addrbkt := addrmgrBucket.NestedReadBucket(addrBucketName)
toUpdate := make(map[string]*dbScriptAddressRow, 0)
cursor := addrbkt.ReadCursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
row, err := deserializeAddressRow(v)
if err != nil {
return err
}

switch row.addrType {
case adtScript:
// deserialize the address row.
hashLen := binary.LittleEndian.Uint32(row.rawData[0:4])
encryptedHash := make([]byte, hashLen)
copy(encryptedHash, row.rawData[4:4+hashLen])
offset := 4 + hashLen
scriptLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4])
offset += 4
encryptedScript := make([]byte, scriptLen)
copy(encryptedScript, row.rawData[offset:offset+scriptLen])

// decrypt the script and hash.
scriptHash, err := amgr.cryptoKeyPub.Decrypt(encryptedHash)
if err != nil {
return fmt.Errorf("unable to decrypt stored script hash %v", err)
}

script, err := amgr.cryptoKeyScript.Decrypt(encryptedScript)
if err != nil {
return fmt.Errorf("unable to decrypt stored script %v", err)
}

// reconstruct the address row.
row := &dbScriptAddressRow{
dbAddressRow: *row,
hash: scriptHash,
script: script,
}

toUpdate[string(k)] = row
}
}

// persist reconstructed address rows.
for _, row := range toUpdate {
err := putScriptAddress(addrmgrBucket, row.account, row.syncStatus, row.hash, row.script)
if err != nil {
return err
}
}

// Write the new database version.
return unifiedDBMetadata{}.putVersion(metadataBucket, newVersion)
}

// Upgrade checks whether the any upgrades are necessary before the database is
// ready for application usage. If any are, they are performed.
func Upgrade(db walletdb.DB, publicPassphrase []byte, params *chaincfg.Params) error {
Expand Down
52 changes: 52 additions & 0 deletions wallet/udb/upgrades_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package udb
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
Expand Down Expand Up @@ -37,6 +38,7 @@ var dbUpgradeTests = [...]struct {
// No upgrade test for V9, it is a fix for V8 and the previous test still applies
// TODO: V10 upgrade test
{verifyV12Upgrade, "v11.db.gz"},
{verifyV13Upgrade, "v12.db.gz"},
}

var pubPass = []byte("public")
Expand Down Expand Up @@ -531,3 +533,53 @@ func verifyV12Upgrade(t *testing.T, db walletdb.DB) {
t.Error(err)
}
}

func verifyV13Upgrade(t *testing.T, db walletdb.DB) {
_, txStore, _, err := Open(db, &chaincfg.TestNet3Params, pubPass)
if err != nil {
t.Fatalf("Open after Upgrade failed: %v", err)
}

err = walletdb.View(db, func(tx walletdb.ReadTx) error {
amgrns := tx.ReadBucket(waddrmgrBucketKey)
txmgrns := tx.ReadBucket(wtxmgrBucketKey)
addrbkt := amgrns.NestedReadBucket(addrBucketName)
err := addrbkt.ForEach(func(k []byte, v []byte) error {
row, err := deserializeAddressRow(v)
if err != nil {
return fmt.Errorf("unable to deserialize address row: %v", err)
}

switch row.addrType {
case adtScript:
script := row.rawData
scriptHash := dcrutil.Hash160(script)
derivedKey := sha256.Sum256(scriptHash)

if !bytes.Equal(k, derivedKey[:]) {
return fmt.Errorf("expected matching keys, "+
"derived key %s is not equal to %s",
string(derivedKey[:]), string(k))
}

scr, err := txStore.GetTxScript(txmgrns, scriptHash)
if err != nil {
return fmt.Errorf("unable to fetch tx script: %v", err)
}

if !bytes.Equal(k, derivedKey[:]) {
return fmt.Errorf("expected matching scripts, address "+
"row script %s is not equal to tx store script %s",
script, scr)
}
}

return nil
})

return err
})
if err != nil {
t.Error(err)
}
}

0 comments on commit be6405a

Please sign in to comment.