Files
tendermint/scripts/keymigrate/migrate.go
M. J. Fromberger f3858e52de scmigrate: ensure target key is correctly renamed (#8276)
Prior to v0.35, the keys for seen-commit records included the applicable
height.  In v0.35 and beyond, we only keep the record for the latest height,
and its key does not include the height.

Update the seen-commit migration to ensure that the record we retain after
migration is correctly renamed to omit the height from its key.

Update the test cases to check for this condition after migrating.
2022-04-07 15:09:09 -07:00

358 lines
8.3 KiB
Go

// Package keymigrate translates all legacy formatted keys to their
// new components.
//
// The key migration operation as implemented provides a potential
// model for database migration operations. Crucially, the migration
// as implemented does not depend on any tendermint code.
package keymigrate
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"math/rand"
"runtime"
"strconv"
"github.com/creachadair/taskgroup"
"github.com/google/orderedcode"
dbm "github.com/tendermint/tm-db"
)
type (
keyID []byte
migrateFunc func(keyID) (keyID, error)
)
func getAllLegacyKeys(db dbm.DB) ([]keyID, error) {
var out []keyID
iter, err := db.Iterator(nil, nil)
if err != nil {
return nil, err
}
for ; iter.Valid(); iter.Next() {
k := iter.Key()
// make sure it's a key with a legacy format, and skip
// all other keys, to make it safe to resume the migration.
if !keyIsLegacy(k) {
continue
}
// Make an explicit copy, since not all tm-db backends do.
out = append(out, []byte(string(k)))
}
if err = iter.Error(); err != nil {
return nil, err
}
if err = iter.Close(); err != nil {
return nil, err
}
return out, nil
}
func keyIsLegacy(key keyID) bool {
for _, prefix := range []keyID{
// core "store"
keyID("consensusParamsKey:"),
keyID("abciResponsesKey:"),
keyID("validatorsKey:"),
keyID("stateKey"),
keyID("H:"),
keyID("P:"),
keyID("C:"),
keyID("SC:"),
keyID("BH:"),
// light
keyID("size"),
keyID("lb/"),
// evidence
keyID([]byte{0x00}),
keyID([]byte{0x01}),
// tx index
keyID("tx.height/"),
keyID("tx.hash/"),
} {
if bytes.HasPrefix(key, prefix) {
return true
}
}
// this means it's a tx index...
if bytes.Count(key, []byte("/")) >= 3 {
return true
}
return keyIsHash(key)
}
func keyIsHash(key keyID) bool {
return len(key) == 32 && !bytes.Contains(key, []byte("/"))
}
func migrateKey(key keyID) (keyID, error) {
switch {
case bytes.HasPrefix(key, keyID("H:")):
val, err := strconv.Atoi(string(key[2:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(0), int64(val))
case bytes.HasPrefix(key, keyID("P:")):
parts := bytes.Split(key[2:], []byte(":"))
if len(parts) != 2 {
return nil, fmt.Errorf("block parts key has %d rather than 2 components",
len(parts))
}
valOne, err := strconv.Atoi(string(parts[0]))
if err != nil {
return nil, err
}
valTwo, err := strconv.Atoi(string(parts[1]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(1), int64(valOne), int64(valTwo))
case bytes.HasPrefix(key, keyID("C:")):
val, err := strconv.Atoi(string(key[2:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(2), int64(val))
case bytes.HasPrefix(key, keyID("SC:")):
val, err := strconv.Atoi(string(key[3:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(3), int64(val))
case bytes.HasPrefix(key, keyID("BH:")):
val, err := strconv.Atoi(string(key[3:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(4), int64(val))
case bytes.HasPrefix(key, keyID("validatorsKey:")):
val, err := strconv.Atoi(string(key[14:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(5), int64(val))
case bytes.HasPrefix(key, keyID("consensusParamsKey:")):
val, err := strconv.Atoi(string(key[19:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(6), int64(val))
case bytes.HasPrefix(key, keyID("abciResponsesKey:")):
val, err := strconv.Atoi(string(key[17:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(7), int64(val))
case bytes.HasPrefix(key, keyID("stateKey")):
return orderedcode.Append(nil, int64(8))
case bytes.HasPrefix(key, []byte{0x00}): // committed evidence
return convertEvidence(key, 9)
case bytes.HasPrefix(key, []byte{0x01}): // pending evidence
return convertEvidence(key, 10)
case bytes.HasPrefix(key, keyID("lb/")):
if len(key) < 24 {
return nil, fmt.Errorf("light block evidence %q in invalid format", string(key))
}
val, err := strconv.Atoi(string(key[len(key)-20:]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, int64(11), int64(val))
case bytes.HasPrefix(key, keyID("size")):
return orderedcode.Append(nil, int64(12))
case bytes.HasPrefix(key, keyID("tx.height")):
parts := bytes.Split(key, []byte("/"))
if len(parts) != 4 {
return nil, fmt.Errorf("key has %d parts rather than 4", len(parts))
}
parts = parts[1:] // drop prefix
elems := make([]interface{}, 0, len(parts)+1)
elems = append(elems, "tx.height")
for idx, pt := range parts {
val, err := strconv.Atoi(string(pt))
if err != nil {
return nil, err
}
if idx == 0 {
elems = append(elems, fmt.Sprintf("%d", val))
} else {
elems = append(elems, int64(val))
}
}
return orderedcode.Append(nil, elems...)
case bytes.Count(key, []byte("/")) >= 3: // tx indexer
parts := bytes.Split(key, []byte("/"))
elems := make([]interface{}, 0, 4)
if len(parts) == 4 {
elems = append(elems, string(parts[0]), string(parts[1]))
val, err := strconv.Atoi(string(parts[2]))
if err != nil {
return nil, err
}
elems = append(elems, int64(val))
val2, err := strconv.Atoi(string(parts[3]))
if err != nil {
return nil, err
}
elems = append(elems, int64(val2))
} else {
elems = append(elems, string(parts[0]))
parts = parts[1:]
val, err := strconv.Atoi(string(parts[len(parts)-1]))
if err != nil {
return nil, err
}
val2, err := strconv.Atoi(string(parts[len(parts)-2]))
if err != nil {
return nil, err
}
appKey := bytes.Join(parts[:len(parts)-3], []byte("/"))
elems = append(elems, string(appKey), int64(val), int64(val2))
}
return orderedcode.Append(nil, elems...)
case keyIsHash(key):
return orderedcode.Append(nil, "tx.hash", string(key))
default:
return nil, fmt.Errorf("key %q is in the wrong format", string(key))
}
}
func convertEvidence(key keyID, newPrefix int64) ([]byte, error) {
parts := bytes.Split(key[1:], []byte("/"))
if len(parts) != 2 {
return nil, fmt.Errorf("evidence key is malformed with %d parts not 2",
len(parts))
}
hb, err := hex.DecodeString(string(parts[0]))
if err != nil {
return nil, err
}
evidenceHash, err := hex.DecodeString(string(parts[1]))
if err != nil {
return nil, err
}
return orderedcode.Append(nil, newPrefix, binary.BigEndian.Uint64(hb), string(evidenceHash))
}
func replaceKey(db dbm.DB, key keyID, gooseFn migrateFunc) error {
exists, err := db.Has(key)
if err != nil {
return err
}
if !exists {
return nil
}
newKey, err := gooseFn(key)
if err != nil {
return err
}
val, err := db.Get(key)
if err != nil {
return err
}
batch := db.NewBatch()
if err = batch.Set(newKey, val); err != nil {
return err
}
if err = batch.Delete(key); err != nil {
return err
}
// 10% of the time, force a write to disk, but mostly don't,
// because it's faster.
if rand.Intn(100)%10 == 0 { // nolint:gosec
if err = batch.WriteSync(); err != nil {
return err
}
} else {
if err = batch.Write(); err != nil {
return err
}
}
if err = batch.Close(); err != nil {
return err
}
return nil
}
// Migrate converts all legacy key formats to new key formats. The
// operation is idempotent, so it's safe to resume a failed
// operation. The operation is somewhat parallelized, relying on the
// concurrency safety of the underlying databases.
//
// Migrate has "continue on error" semantics and will iterate through
// all legacy keys attempt to migrate them, and will collect all
// errors and will return only at the end of the operation.
//
// The context allows for a safe termination of the operation
// (e.g connected to a singal handler,) to abort the operation
// in-between migration operations.
func Migrate(ctx context.Context, db dbm.DB) error {
keys, err := getAllLegacyKeys(db)
if err != nil {
return err
}
var errs []string
g, start := taskgroup.New(func(err error) error {
errs = append(errs, err.Error())
return err
}).Limit(runtime.NumCPU())
for _, key := range keys {
key := key
start(func() error {
if err := ctx.Err(); err != nil {
return err
}
return replaceKey(db, key, migrateKey)
})
}
if g.Wait() != nil {
return fmt.Errorf("encountered errors during migration: %q", errs)
}
return nil
}