diff --git a/cmd/util/cmd/execution-state-extract/cmd.go b/cmd/util/cmd/execution-state-extract/cmd.go index 7c10e8dbdcb..55728b428a8 100644 --- a/cmd/util/cmd/execution-state-extract/cmd.go +++ b/cmd/util/cmd/execution-state-extract/cmd.go @@ -15,15 +15,17 @@ import ( ) var ( - flagExecutionStateDir string - flagOutputDir string - flagBlockHash string - flagStateCommitment string - flagDatadir string - flagChain string - flagNoMigration bool - flagNoReport bool - flagNWorker int + flagExecutionStateDir string + flagOutputDir string + flagBlockHash string + flagStateCommitment string + flagDatadir string + flagChain string + flagNWorker int + flagNoMigration bool + flagNoReport bool + flagValidateMigration bool + flagLogVerboseValidationError bool ) var Cmd = &cobra.Command{ @@ -59,6 +61,13 @@ func init() { "don't report the state") Cmd.Flags().IntVar(&flagNWorker, "n-migrate-worker", 10, "number of workers to migrate payload concurrently") + + Cmd.Flags().BoolVar(&flagValidateMigration, "validate", false, + "validate migrated Cadence values (atree migration)") + + Cmd.Flags().BoolVar(&flagLogVerboseValidationError, "log-verbose-validation-error", false, + "log entire Cadence values on validation error (atree migration)") + } func run(*cobra.Command, []string) { @@ -131,16 +140,21 @@ func run(*cobra.Command, []string) { log.Warn().Msgf("--no-report flag is deprecated") } - if flagNoMigration { - log.Warn().Msgf("--no-migration flag is deprecated") + if flagValidateMigration { + log.Warn().Msgf("atree migration validation flag is enabled and will increase duration of migration") + } + + if flagLogVerboseValidationError { + log.Warn().Msgf("atree migration has verbose validation error logging enabled which may increase size of log") } err := extractExecutionState( + log.Logger, flagExecutionStateDir, stateCommitment, flagOutputDir, - log.Logger, flagNWorker, + !flagNoMigration, ) if err != nil { diff --git a/cmd/util/cmd/execution-state-extract/execution_state_extract.go b/cmd/util/cmd/execution-state-extract/execution_state_extract.go index 1436f0ccc94..90bcd70533d 100644 --- a/cmd/util/cmd/execution-state-extract/execution_state_extract.go +++ b/cmd/util/cmd/execution-state-extract/execution_state_extract.go @@ -9,6 +9,7 @@ import ( "github.com/rs/zerolog" "go.uber.org/atomic" + migrators "github.com/onflow/flow-go/cmd/util/ledger/migrations" "github.com/onflow/flow-go/cmd/util/ledger/reporters" "github.com/onflow/flow-go/ledger" "github.com/onflow/flow-go/ledger/common/hash" @@ -27,11 +28,12 @@ func getStateCommitment(commits storage.Commits, blockHash flow.Identifier) (flo } func extractExecutionState( + log zerolog.Logger, dir string, targetHash flow.StateCommitment, outputDir string, - log zerolog.Logger, nWorker int, // number of concurrent worker to migation payloads + runMigrations bool, ) error { log.Info().Msg("init WAL") @@ -83,6 +85,30 @@ func extractExecutionState( }() var migrations []ledger.Migration + + if runMigrations { + rwf := reporters.NewReportFileWriterFactory(dir, log) + + migrations = []ledger.Migration{ + migrators.CreateAccountBasedMigration( + log, + nWorker, + []migrators.AccountBasedMigration{ + migrators.NewAtreeRegisterMigrator( + rwf, + flagValidateMigration, + flagLogVerboseValidationError, + ), + + &migrators.DeduplicateContractNamesMigration{}, + + // This will fix storage used discrepancies caused by the + // DeduplicateContractNamesMigration. + &migrators.AccountUsageMigrator{}, + }), + } + } + newState := ledger.State(targetHash) // migrate the trie if there are migrations diff --git a/cmd/util/cmd/execution-state-extract/execution_state_extract_test.go b/cmd/util/cmd/execution-state-extract/execution_state_extract_test.go index 018c5474c66..2f91ea7d603 100644 --- a/cmd/util/cmd/execution-state-extract/execution_state_extract_test.go +++ b/cmd/util/cmd/execution-state-extract/execution_state_extract_test.go @@ -60,11 +60,12 @@ func TestExtractExecutionState(t *testing.T) { t.Run("empty WAL doesn't find anything", func(t *testing.T) { withDirs(t, func(datadir, execdir, outdir string) { err := extractExecutionState( + zerolog.Nop(), execdir, unittest.StateCommitmentFixture(), outdir, - zerolog.Nop(), 10, + false, ) require.Error(t, err) }) diff --git a/cmd/util/ledger/migrations/atree_register_migration.go b/cmd/util/ledger/migrations/atree_register_migration.go new file mode 100644 index 00000000000..e4d0acf7cc8 --- /dev/null +++ b/cmd/util/ledger/migrations/atree_register_migration.go @@ -0,0 +1,443 @@ +package migrations + +import ( + "context" + "errors" + "fmt" + "io" + runtime2 "runtime" + "time" + + "github.com/rs/zerolog" + + "github.com/onflow/atree" + "github.com/onflow/cadence/runtime" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/interpreter" + "github.com/onflow/cadence/runtime/stdlib" + + "github.com/onflow/flow-go/cmd/util/ledger/reporters" + "github.com/onflow/flow-go/cmd/util/ledger/util" + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/convert" + "github.com/onflow/flow-go/model/flow" + util2 "github.com/onflow/flow-go/module/util" +) + +// AtreeRegisterMigrator is a migrator that converts the storage of an account from the +// old atree format to the new atree format. +// Account "storage used" should be correctly updated after the migration. +type AtreeRegisterMigrator struct { + log zerolog.Logger + + sampler zerolog.Sampler + rw reporters.ReportWriter + rwf reporters.ReportWriterFactory + + nWorkers int + + validateMigratedValues bool + logVerboseValidationError bool +} + +var _ AccountBasedMigration = (*AtreeRegisterMigrator)(nil) +var _ io.Closer = (*AtreeRegisterMigrator)(nil) + +func NewAtreeRegisterMigrator( + rwf reporters.ReportWriterFactory, + validateMigratedValues bool, + logVerboseValidationError bool, +) *AtreeRegisterMigrator { + + sampler := util2.NewTimedSampler(30 * time.Second) + + migrator := &AtreeRegisterMigrator{ + sampler: sampler, + rwf: rwf, + rw: rwf.ReportWriter("atree-register-migrator"), + validateMigratedValues: validateMigratedValues, + logVerboseValidationError: logVerboseValidationError, + } + + return migrator +} + +func (m *AtreeRegisterMigrator) Close() error { + // close the report writer so it flushes to file + m.rw.Close() + + return nil +} + +func (m *AtreeRegisterMigrator) InitMigration( + log zerolog.Logger, + _ []*ledger.Payload, + nWorkers int, +) error { + m.log = log.With().Str("migration", "atree-register-migration").Logger() + m.nWorkers = nWorkers + + return nil +} + +func (m *AtreeRegisterMigrator) MigrateAccount( + _ context.Context, + address common.Address, + oldPayloads []*ledger.Payload, +) ([]*ledger.Payload, error) { + // create all the runtime components we need for the migration + mr, err := newMigratorRuntime(address, oldPayloads) + if err != nil { + return nil, fmt.Errorf("failed to create migrator runtime: %w", err) + } + + // keep track of all storage maps that were accessed + // if they are empty they won't be changed, but we still need to copy them over + storageMapIds := make(map[string]struct{}) + + // Do the storage conversion + changes, err := m.migrateAccountStorage(mr, storageMapIds) + if err != nil { + if errors.Is(err, skippableAccountError) { + return oldPayloads, nil + } + return nil, fmt.Errorf("failed to convert storage for address %s: %w", address.Hex(), err) + } + + originalLen := len(oldPayloads) + + newPayloads, err := m.validateChangesAndCreateNewRegisters(mr, changes, storageMapIds) + if err != nil { + if errors.Is(err, skippableAccountError) { + return oldPayloads, nil + } + return nil, err + } + + if m.validateMigratedValues { + err = validateCadenceValues(address, oldPayloads, newPayloads, m.log, m.logVerboseValidationError) + if err != nil { + return nil, err + } + } + + newLen := len(newPayloads) + + if newLen > originalLen { + // this is possible, its not something to be worried about. + m.rw.Write(migrationProblem{ + Address: address.Hex(), + Key: "", + Size: len(mr.Snapshot.Payloads), + Kind: "more_registers_after_migration", + Msg: fmt.Sprintf("original: %d, new: %d", originalLen, newLen), + }) + } + + return newPayloads, nil +} + +func (m *AtreeRegisterMigrator) migrateAccountStorage( + mr *migratorRuntime, + storageMapIds map[string]struct{}, +) (map[flow.RegisterID]flow.RegisterValue, error) { + + // iterate through all domains and migrate them + for _, domain := range domains { + err := m.convertStorageDomain(mr, storageMapIds, domain) + if err != nil { + return nil, fmt.Errorf("failed to convert storage domain %s : %w", domain, err) + } + } + + err := mr.Storage.Commit(mr.Interpreter, false) + if err != nil { + return nil, fmt.Errorf("failed to commit storage: %w", err) + } + + // finalize the transaction + result, err := mr.TransactionState.FinalizeMainTransaction() + if err != nil { + return nil, fmt.Errorf("failed to finalize main transaction: %w", err) + } + + return result.WriteSet, nil +} + +func (m *AtreeRegisterMigrator) convertStorageDomain( + mr *migratorRuntime, + storageMapIds map[string]struct{}, + domain string, +) error { + + storageMap := mr.Storage.GetStorageMap(mr.Address, domain, false) + if storageMap == nil { + // no storage for this domain + return nil + } + storageMapIds[string(atree.SlabIndexToLedgerKey(storageMap.StorageID().Index))] = struct{}{} + + iterator := storageMap.Iterator(util.NopMemoryGauge{}) + keys := make([]interpreter.StringStorageMapKey, 0, storageMap.Count()) + // to be safe avoid modifying the map while iterating + for { + key := iterator.NextKey() + if key == nil { + break + } + + stringKey, ok := key.(interpreter.StringAtreeValue) + if !ok { + return fmt.Errorf("invalid key type %T, expected interpreter.StringAtreeValue", key) + } + + keys = append(keys, interpreter.StringStorageMapKey(stringKey)) + } + + for _, key := range keys { + err := func() error { + var value interpreter.Value + + err := capturePanic(func() { + value = storageMap.ReadValue(util.NopMemoryGauge{}, key) + }) + if err != nil { + return fmt.Errorf("failed to read value for key %s: %w", key, err) + } + + value, err = m.cloneValue(mr, value) + + if err != nil { + return fmt.Errorf("failed to clone value for key %s: %w", key, err) + } + + err = capturePanic(func() { + // set value will first purge the old value + storageMap.SetValue(mr.Interpreter, key, value) + }) + + if err != nil { + return fmt.Errorf("failed to set value for key %s: %w", key, err) + } + + return nil + }() + if err != nil { + + m.rw.Write(migrationProblem{ + Address: mr.Address.Hex(), + Size: len(mr.Snapshot.Payloads), + Key: string(key), + Kind: "migration_failure", + Msg: err.Error(), + }) + return skippableAccountError + } + } + + return nil +} + +func (m *AtreeRegisterMigrator) validateChangesAndCreateNewRegisters( + mr *migratorRuntime, + changes map[flow.RegisterID]flow.RegisterValue, + storageMapIds map[string]struct{}, +) ([]*ledger.Payload, error) { + originalPayloadsSnapshot := mr.Snapshot + originalPayloads := originalPayloadsSnapshot.Payloads + newPayloads := make([]*ledger.Payload, 0, len(originalPayloads)) + + // store state payload so that it can be updated + var statePayload *ledger.Payload + progressLog := func(int) {} + + for id, value := range changes { + progressLog(1) + // delete all values that were changed from the original payloads so that we can + // check what remains + delete(originalPayloads, id) + + if len(value) == 0 { + // value was deleted + continue + } + + ownerAddress, err := common.BytesToAddress([]byte(id.Owner)) + if err != nil { + return nil, fmt.Errorf("failed to convert owner address: %w", err) + } + + if ownerAddress != mr.Address { + // something was changed that does not belong to this account. Log it. + m.log.Error(). + Str("key", id.String()). + Str("owner_address", ownerAddress.Hex()). + Str("account", mr.Address.Hex()). + Msg("key is part of the change set, but is for a different account") + } + + key := convert.RegisterIDToLedgerKey(id) + + if statePayload == nil && isAccountKey(key) { + statePayload = ledger.NewPayload(key, value) + // we will append this later + continue + } + + newPayloads = append(newPayloads, ledger.NewPayload(key, value)) + } + + removedSize := uint64(0) + + // add all values that were not changed + if len(originalPayloads) > 0 { + + for id, value := range originalPayloads { + progressLog(1) + + if len(value.Value()) == 0 { + // this is strange, but we don't want to add empty values. Log it. + m.log.Warn().Msgf("empty value for key %s", id) + continue + } + + key := convert.RegisterIDToLedgerKey(id) + if statePayload == nil && isAccountKey(key) { + statePayload = value + // we will append this later + continue + } + + if id.IsInternalState() { + // this is expected. Move it to the new payloads + newPayloads = append(newPayloads, value) + continue + } + + if _, isADomainKey := domainsLookupMap[id.Key]; isADomainKey { + // this is expected. Move it to the new payloads + newPayloads = append(newPayloads, value) + continue + } + + if _, ok := storageMapIds[id.Key]; ok { + // This is needed because storage map can be empty. + // Empty storage map only exists in old payloads because there isn't any element to migrate. + newPayloads = append(newPayloads, value) + continue + } + + m.rw.Write(migrationProblem{ + Address: mr.Address.Hex(), + Key: id.String(), + Size: len(mr.Snapshot.Payloads), + Kind: "not_migrated", + Msg: fmt.Sprintf("%x", value.Value()), + }) + + size, err := payloadSize(key, value) + if err != nil { + return nil, fmt.Errorf("failed to get payload size: %w", err) + } + + removedSize += size + + // this is ok + // return nil, skippableAccountError + } + } + + if statePayload == nil { + return nil, fmt.Errorf("state payload was not found") + } + + // since some registers were removed, we need to update the storage used + if removedSize > 0 { + status, err := environment.AccountStatusFromBytes(statePayload.Value()) + if err != nil { + return nil, fmt.Errorf("could not parse account status: %w", err) + } + + status.SetStorageUsed(status.StorageUsed() - removedSize) + + newPayload, err := newPayloadWithValue(statePayload, status.ToBytes()) + if err != nil { + return nil, fmt.Errorf("cannot create new payload with value: %w", err) + } + + statePayload = newPayload + } + + newPayloads = append(newPayloads, statePayload) + + return newPayloads, nil +} + +func (m *AtreeRegisterMigrator) cloneValue( + mr *migratorRuntime, + value interpreter.Value, +) (interpreter.Value, error) { + + err := capturePanic(func() { + // force the value to be read entirely + value = value.Clone(mr.Interpreter) + }) + if err != nil { + return nil, err + } + return value, nil +} + +// capturePanic captures panics and converts them to errors +// this is needed for some cadence functions that panic on error +func capturePanic(f func()) (err error) { + defer func() { + if r := recover(); r != nil { + var stack [100000]byte + n := runtime2.Stack(stack[:], false) + fmt.Printf("%s", stack[:n]) + + switch x := r.(type) { + case runtime.Error: + err = fmt.Errorf("runtime error @%s: %w", x.Location, x) + case error: + err = x + default: + err = fmt.Errorf("panic: %v", r) + } + } + }() + f() + + return +} + +// convert all domains +var domains = []string{ + common.PathDomainStorage.Identifier(), + common.PathDomainPrivate.Identifier(), + common.PathDomainPublic.Identifier(), + runtime.StorageDomainContract, + stdlib.InboxStorageDomain, +} + +var domainsLookupMap = map[string]struct{}{ + common.PathDomainStorage.Identifier(): {}, + common.PathDomainPrivate.Identifier(): {}, + common.PathDomainPublic.Identifier(): {}, + runtime.StorageDomainContract: {}, + stdlib.InboxStorageDomain: {}, +} + +// migrationProblem is a struct for reporting errors +type migrationProblem struct { + Address string + // Size is the account size in register count + Size int + Key string + Kind string + Msg string +} + +var skippableAccountError = errors.New("account can be skipped") diff --git a/cmd/util/ledger/migrations/atree_register_migration_test.go b/cmd/util/ledger/migrations/atree_register_migration_test.go new file mode 100644 index 00000000000..da6d9f7fdfb --- /dev/null +++ b/cmd/util/ledger/migrations/atree_register_migration_test.go @@ -0,0 +1,157 @@ +package migrations_test + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/cmd/util/ledger/migrations" + "github.com/onflow/flow-go/cmd/util/ledger/reporters" + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/convert" + "github.com/onflow/flow-go/ledger/common/pathfinder" + "github.com/onflow/flow-go/ledger/complete" + "github.com/onflow/flow-go/ledger/complete/wal" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/metrics" +) + +func TestAtreeRegisterMigration(t *testing.T) { + log := zerolog.New(zerolog.NewTestWriter(t)) + dir := t.TempDir() + + validation := migrations.NewCadenceDataValidationMigrations( + reporters.NewReportFileWriterFactory(dir, log), + 2, + ) + + // Localnet v0.31 was used to produce an execution state that can be used for the tests. + t.Run( + "test v0.31 state", + testWithExistingState( + + log, + "test-data/bootstrapped_v0.31", + migrations.CreateAccountBasedMigration(log, 2, + []migrations.AccountBasedMigration{ + validation.PreMigration(), + migrations.NewAtreeRegisterMigrator(reporters.NewReportFileWriterFactory(dir, log), true, false), + validation.PostMigration(), + }, + ), + func(t *testing.T, oldPayloads []*ledger.Payload, newPayloads []*ledger.Payload) { + + oldPayloadsMap := make(map[flow.RegisterID]*ledger.Payload, len(oldPayloads)) + + for _, payload := range oldPayloads { + key, err := payload.Key() + require.NoError(t, err) + id, err := convert.LedgerKeyToRegisterID(key) + require.NoError(t, err) + oldPayloadsMap[id] = payload + } + + newPayloadsMap := make(map[flow.RegisterID]*ledger.Payload, len(newPayloads)) + + for _, payload := range newPayloads { + key, err := payload.Key() + require.NoError(t, err) + id, err := convert.LedgerKeyToRegisterID(key) + require.NoError(t, err) + newPayloadsMap[id] = payload + } + + for key, payload := range newPayloadsMap { + value := newPayloadsMap[key].Value() + + // TODO: currently the migration does not change the payload values because + // the atree version is not changed. This should be changed in the future. + require.Equal(t, payload.Value(), value) + } + + // commented out helper code to dump the payloads to csv files for manual inspection + //dumpPayloads := func(n string, payloads []ledger.Payload) { + // f, err := os.Create(n) + // require.NoError(t, err) + // + // defer f.Close() + // + // for _, payload := range payloads { + // key, err := payload.Key() + // require.NoError(t, err) + // _, err = f.WriteString(fmt.Sprintf("%x,%s\n", key.String(), payload.Value())) + // require.NoError(t, err) + // } + //} + //dumpPayloads("old.csv", oldPayloads) + //dumpPayloads("new.csv", newPayloads) + + }, + ), + ) + +} + +func testWithExistingState( + log zerolog.Logger, + inputDir string, + migration ledger.Migration, + f func( + t *testing.T, + oldPayloads []*ledger.Payload, + newPayloads []*ledger.Payload, + ), +) func(t *testing.T) { + return func(t *testing.T) { + diskWal, err := wal.NewDiskWAL( + log, + nil, + metrics.NewNoopCollector(), + inputDir, + complete.DefaultCacheSize, + pathfinder.PathByteSize, + wal.SegmentSize, + ) + require.NoError(t, err) + + led, err := complete.NewLedger( + diskWal, + complete.DefaultCacheSize, + &metrics.NoopCollector{}, + log, + complete.DefaultPathFinderVersion) + require.NoError(t, err) + + var oldPayloads []*ledger.Payload + + // we sandwitch the migration between two identity migrations + // so we can capture the Payloads before and after the migration + var mig = []ledger.Migration{ + func(payloads []*ledger.Payload) ([]*ledger.Payload, error) { + oldPayloads = make([]*ledger.Payload, len(payloads)) + copy(oldPayloads, payloads) + return payloads, nil + }, + + migration, + + func(newPayloads []*ledger.Payload) ([]*ledger.Payload, error) { + + f(t, oldPayloads, newPayloads) + + return newPayloads, nil + }, + } + + newState, err := led.MostRecentTouchedState() + require.NoError(t, err) + + _, err = led.MigrateAt( + newState, + mig, + complete.DefaultPathFinderVersion, + ) + require.NoError(t, err) + } +} diff --git a/cmd/util/ledger/migrations/cadence_data_validation.go b/cmd/util/ledger/migrations/cadence_data_validation.go new file mode 100644 index 00000000000..baaa75a0e6b --- /dev/null +++ b/cmd/util/ledger/migrations/cadence_data_validation.go @@ -0,0 +1,329 @@ +package migrations + +import ( + "bytes" + "context" + "fmt" + "sort" + "sync" + + "github.com/rs/zerolog" + + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/interpreter" + + "github.com/onflow/flow-go/cmd/util/ledger/reporters" + "github.com/onflow/flow-go/cmd/util/ledger/util" + "github.com/onflow/flow-go/crypto/hash" + "github.com/onflow/flow-go/ledger" +) + +// CadenceDataValidationMigrations are pre and post steps to a migration that compares +// cadence data on each account before and after the migration to ensure that the +// migration did not change the cadence data. +type CadenceDataValidationMigrations struct { + // reporter writer factory for creating reports of problematic accounts + rwf reporters.ReportWriterFactory + + accountHashesMu sync.RWMutex + accountHashes map[common.Address][]byte + + nWorkers int +} + +func NewCadenceDataValidationMigrations( + rwf reporters.ReportWriterFactory, + nWorkers int, +) *CadenceDataValidationMigrations { + return &CadenceDataValidationMigrations{ + rwf: rwf, + accountHashes: make(map[common.Address][]byte, 40_000_000), + nWorkers: nWorkers, + } +} + +func (m *CadenceDataValidationMigrations) PreMigration() AccountBasedMigration { + return &preMigration{ + v: m, + } +} + +func (m *CadenceDataValidationMigrations) PostMigration() AccountBasedMigration { + return &postMigration{ + rwf: m.rwf, + v: m, + } +} + +func (m *CadenceDataValidationMigrations) setAccountHash(key common.Address, value []byte) { + m.accountHashesMu.Lock() + defer m.accountHashesMu.Unlock() + + m.accountHashes[key] = value +} + +func (m *CadenceDataValidationMigrations) getAccountHash(key common.Address) ([]byte, bool) { + m.accountHashesMu.RLock() + defer m.accountHashesMu.RUnlock() + + value, ok := m.accountHashes[key] + return value, ok +} + +func (m *CadenceDataValidationMigrations) deleteAccountHash(address common.Address) { + m.accountHashesMu.Lock() + defer m.accountHashesMu.Unlock() + + delete(m.accountHashes, address) +} + +type preMigration struct { + log zerolog.Logger + + // reference to parent for common data + v *CadenceDataValidationMigrations +} + +func (m *preMigration) Close() error { + return nil +} + +var _ AccountBasedMigration = (*preMigration)(nil) + +func (m *preMigration) InitMigration( + log zerolog.Logger, + _ []*ledger.Payload, + _ int, +) error { + m.log = log.With().Str("component", "CadenceDataValidationPreMigration").Logger() + + return nil +} + +func (m *preMigration) MigrateAccount( + _ context.Context, + address common.Address, + payloads []*ledger.Payload, +) ([]*ledger.Payload, error) { + + accountHash, err := m.v.hashAccountCadenceValues(address, payloads) + if err != nil { + m.log.Error(). + Err(err). + Hex("address", address[:]). + Msg("failed to hash cadence values") + + // on error still continue with the migration + return payloads, nil + } + + m.v.setAccountHash(address, accountHash) + + return payloads, nil +} + +type postMigration struct { + log zerolog.Logger + + rwf reporters.ReportWriterFactory + rw reporters.ReportWriter + + v *CadenceDataValidationMigrations +} + +var _ AccountBasedMigration = &postMigration{} + +func (m *postMigration) Close() error { + for address := range m.v.accountHashes { + m.log.Error(). + Hex("address", address[:]). + Msg("cadence values missing after migration") + + m.rw.Write( + cadenceDataValidationReportEntry{ + + Address: address.Hex(), + Problem: "cadence values missing after migration", + }, + ) + } + return nil +} + +func (m *postMigration) InitMigration( + log zerolog.Logger, + _ []*ledger.Payload, + _ int, +) error { + m.log = log.With().Str("component", "CadenceDataValidationPostMigration").Logger() + m.rw = m.rwf.ReportWriter("cadence_data_validation") + + return nil +} + +func (m *postMigration) MigrateAccount( + ctx context.Context, + address common.Address, + payloads []*ledger.Payload, +) ([]*ledger.Payload, error) { + newHash, err := m.v.hashAccountCadenceValues(address, payloads) + if err != nil { + m.log.Info(). + Err(err). + Hex("address", address[:]). + Msg("failed to hash cadence values") + return payloads, nil + } + + accountHash, ok := m.v.getAccountHash(address) + + if !ok { + m.log.Error(). + Hex("address", address[:]). + Msg("cadence values missing before migration") + + m.rw.Write( + cadenceDataValidationReportEntry{ + + Address: address.Hex(), + Problem: "cadence values missing before migration", + }, + ) + } + if !bytes.Equal(accountHash, newHash) { + m.log.Error(). + Hex("address", address[:]). + Msg("cadence values mismatch") + + m.rw.Write( + cadenceDataValidationReportEntry{ + + Address: address.Hex(), + Problem: "cadence values mismatch", + }, + ) + } + + // remove the address from the map so we can check if there are any + // missing addresses + m.v.deleteAccountHash(address) + + return payloads, nil +} + +func (m *CadenceDataValidationMigrations) hashAccountCadenceValues( + address common.Address, + payloads []*ledger.Payload, +) ([]byte, error) { + hasher := newHasher() + mr, err := newMigratorRuntime(address, payloads) + if err != nil { + return nil, err + } + + for _, domain := range domains { + domainHash, err := m.hashDomainCadenceValues(mr, domain) + if err != nil { + return nil, fmt.Errorf("failed to hash storage domain %s : %w", domain, err) + } + _, err = hasher.Write(domainHash) + if err != nil { + return nil, fmt.Errorf("failed to write hash: %w", err) + } + } + + return hasher.SumHash(), nil +} + +func (m *CadenceDataValidationMigrations) hashDomainCadenceValues( + mr *migratorRuntime, + domain string, +) ([]byte, error) { + hasher := newHasher() + var storageMap *interpreter.StorageMap + err := capturePanic(func() { + storageMap = mr.Storage.GetStorageMap(mr.Address, domain, false) + }) + if err != nil { + return nil, fmt.Errorf("failed to get storage map: %w", err) + } + if storageMap == nil { + // no storage for this domain + return nil, nil + } + + hashes := make(sortableHashes, 0, storageMap.Count()) + + iterator := storageMap.Iterator(util.NopMemoryGauge{}) + for { + key, value := iterator.Next() + if key == nil { + break + } + + h, err := m.recursiveString(value, hasher) + if err != nil { + return nil, fmt.Errorf("failed to convert value to string: %w", err) + } + + hasher.Reset() + + hashes = append(hashes, h) + } + + return hashes.SortAndHash(hasher) +} + +func (m *CadenceDataValidationMigrations) recursiveString( + value interpreter.Value, + hasher hash.Hasher, +) ([]byte, error) { + + var s string + err := capturePanic( + func() { + s = value.RecursiveString(interpreter.SeenReferences{}) + }) + if err != nil { + return nil, fmt.Errorf("failed to convert value to string: %w", err) + } + + h := hasher.ComputeHash([]byte(s)) + return h, nil +} + +func newHasher() hash.Hasher { + return hash.NewSHA3_256() +} + +type sortableHashes [][]byte + +func (s sortableHashes) Len() int { + return len(s) +} + +func (s sortableHashes) Less(i, j int) bool { + return bytes.Compare(s[i], s[j]) < 0 +} + +func (s sortableHashes) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s sortableHashes) SortAndHash(hasher hash.Hasher) ([]byte, error) { + defer hasher.Reset() + sort.Sort(s) + for _, h := range s { + _, err := hasher.Write(h) + if err != nil { + return nil, fmt.Errorf("failed to write hash: %w", err) + } + + } + return hasher.SumHash(), nil +} + +type cadenceDataValidationReportEntry struct { + Address string `json:"address"` + Problem string `json:"problem"` +} diff --git a/cmd/util/ledger/migrations/cadence_value_validation.go b/cmd/util/ledger/migrations/cadence_value_validation.go new file mode 100644 index 00000000000..ff45b2e2c97 --- /dev/null +++ b/cmd/util/ledger/migrations/cadence_value_validation.go @@ -0,0 +1,599 @@ +package migrations + +import ( + "fmt" + "strings" + "time" + + "github.com/onflow/atree" + "github.com/onflow/cadence" + "github.com/onflow/cadence/runtime" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/interpreter" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + + "github.com/onflow/flow-go/cmd/util/ledger/util" + "github.com/onflow/flow-go/ledger" +) + +var nopMemoryGauge = util.NopMemoryGauge{} + +// TODO: optimize memory by reusing payloads snapshot created for migration +func validateCadenceValues( + address common.Address, + oldPayloads []*ledger.Payload, + newPayloads []*ledger.Payload, + log zerolog.Logger, + verboseLogging bool, +) error { + // Create all the runtime components we need for comparing Cadence values. + oldRuntime, err := newReadonlyStorageRuntime(oldPayloads) + if err != nil { + return fmt.Errorf("failed to create validator runtime with old payloads: %w", err) + } + + newRuntime, err := newReadonlyStorageRuntime(newPayloads) + if err != nil { + return fmt.Errorf("failed to create validator runtime with new payloads: %w", err) + } + + // Iterate through all domains and compare cadence values. + for _, domain := range domains { + err := validateStorageDomain(address, oldRuntime, newRuntime, domain, log, verboseLogging) + if err != nil { + return err + } + } + + return nil +} + +func validateStorageDomain( + address common.Address, + oldRuntime *readonlyStorageRuntime, + newRuntime *readonlyStorageRuntime, + domain string, + log zerolog.Logger, + verboseLogging bool, +) error { + + oldStorageMap := oldRuntime.Storage.GetStorageMap(address, domain, false) + + newStorageMap := newRuntime.Storage.GetStorageMap(address, domain, false) + + if oldStorageMap == nil && newStorageMap == nil { + // No storage for this domain. + return nil + } + + if oldStorageMap == nil && newStorageMap != nil { + return fmt.Errorf("old storage map is nil, new storage map isn't nil") + } + + if oldStorageMap != nil && newStorageMap == nil { + return fmt.Errorf("old storage map isn't nil, new storage map is nil") + } + + if oldStorageMap.Count() != newStorageMap.Count() { + return fmt.Errorf("old storage map count %d, new storage map count %d", oldStorageMap.Count(), newStorageMap.Count()) + } + + oldIterator := oldStorageMap.Iterator(nopMemoryGauge) + for { + key, oldValue := oldIterator.Next() + if key == nil { + break + } + + stringKey, ok := key.(interpreter.StringAtreeValue) + if !ok { + return fmt.Errorf("invalid key type %T, expected interpreter.StringAtreeValue", key) + } + + newValue := newStorageMap.ReadValue(nopMemoryGauge, interpreter.StringStorageMapKey(stringKey)) + + err := cadenceValueEqual(oldRuntime.Interpreter, oldValue, newRuntime.Interpreter, newValue) + if err != nil { + if verboseLogging { + log.Info(). + Str("address", address.Hex()). + Str("domain", domain). + Str("key", string(stringKey)). + Str("trace", err.Error()). + Str("old value", oldValue.String()). + Str("new value", newValue.String()). + Msgf("failed to validate value") + } + + return fmt.Errorf("failed to validate value for address %s, domain %s, key %s: %s", address.Hex(), domain, key, err.Error()) + } + } + + return nil +} + +type validationError struct { + trace []string + errorMsg string + traceReversed bool +} + +func newValidationErrorf(format string, a ...any) *validationError { + return &validationError{ + errorMsg: fmt.Sprintf(format, a...), + } +} + +func (e *validationError) addTrace(trace string) { + e.trace = append(e.trace, trace) +} + +func (e *validationError) Error() string { + if len(e.trace) == 0 { + return fmt.Sprintf("failed to validate: %s", e.errorMsg) + } + // Reverse trace + if !e.traceReversed { + for i, j := 0, len(e.trace)-1; i < j; i, j = i+1, j-1 { + e.trace[i], e.trace[j] = e.trace[j], e.trace[i] + } + e.traceReversed = true + } + trace := strings.Join(e.trace, ".") + return fmt.Sprintf("failed to validate %s: %s", trace, e.errorMsg) +} + +func cadenceValueEqual( + vInterpreter *interpreter.Interpreter, + v interpreter.Value, + otherInterpreter *interpreter.Interpreter, + other interpreter.Value, +) *validationError { + switch v := v.(type) { + case *interpreter.ArrayValue: + return cadenceArrayValueEqual(vInterpreter, v, otherInterpreter, other) + + case *interpreter.CompositeValue: + return cadenceCompositeValueEqual(vInterpreter, v, otherInterpreter, other) + + case *interpreter.DictionaryValue: + return cadenceDictionaryValueEqual(vInterpreter, v, otherInterpreter, other) + + case *interpreter.SomeValue: + return cadenceSomeValueEqual(vInterpreter, v, otherInterpreter, other) + + default: + oldValue, ok := v.(interpreter.EquatableValue) + if !ok { + return newValidationErrorf( + "value doesn't implement interpreter.EquatableValue: %T", + oldValue, + ) + } + if !oldValue.Equal(nil, interpreter.EmptyLocationRange, other) { + return newValidationErrorf( + "values differ: %v (%T) != %v (%T)", + oldValue, + oldValue, + other, + other, + ) + } + } + + return nil +} + +func cadenceSomeValueEqual( + vInterpreter *interpreter.Interpreter, + v *interpreter.SomeValue, + otherInterpreter *interpreter.Interpreter, + other interpreter.Value, +) *validationError { + otherSome, ok := other.(*interpreter.SomeValue) + if !ok { + return newValidationErrorf("types differ: %T != %T", v, other) + } + + innerValue := v.InnerValue(vInterpreter, interpreter.EmptyLocationRange) + + otherInnerValue := otherSome.InnerValue(otherInterpreter, interpreter.EmptyLocationRange) + + return cadenceValueEqual(vInterpreter, innerValue, otherInterpreter, otherInnerValue) +} + +func cadenceArrayValueEqual( + vInterpreter *interpreter.Interpreter, + v *interpreter.ArrayValue, + otherInterpreter *interpreter.Interpreter, + other interpreter.Value, +) *validationError { + otherArray, ok := other.(*interpreter.ArrayValue) + if !ok { + return newValidationErrorf("types differ: %T != %T", v, other) + } + + count := v.Count() + if count != otherArray.Count() { + return newValidationErrorf("array counts differ: %d != %d", count, otherArray.Count()) + } + + if v.Type == nil { + if otherArray.Type != nil { + return newValidationErrorf("array types differ: nil != %s", otherArray.Type) + } + } else { // v.Type != nil + if otherArray.Type == nil { + return newValidationErrorf("array types differ: %s != nil", v.Type) + } else if !v.Type.Equal(otherArray.Type) { + return newValidationErrorf("array types differ: %s != %s", v.Type, otherArray.Type) + } + } + + for i := 0; i < count; i++ { + element := v.Get(vInterpreter, interpreter.EmptyLocationRange, i) + otherElement := otherArray.Get(otherInterpreter, interpreter.EmptyLocationRange, i) + + err := cadenceValueEqual(vInterpreter, element, otherInterpreter, otherElement) + if err != nil { + err.addTrace(fmt.Sprintf("(%s[%d])", v.Type, i)) + return err + } + } + + return nil +} + +func cadenceCompositeValueEqual( + vInterpreter *interpreter.Interpreter, + v *interpreter.CompositeValue, + otherInterpreter *interpreter.Interpreter, + other interpreter.Value, +) *validationError { + otherComposite, ok := other.(*interpreter.CompositeValue) + if !ok { + return newValidationErrorf("types differ: %T != %T", v, other) + } + + if !v.StaticType(vInterpreter).Equal(otherComposite.StaticType(otherInterpreter)) { + return newValidationErrorf( + "composite types differ: %s != %s", + v.StaticType(vInterpreter), + otherComposite.StaticType(otherInterpreter), + ) + } + + if v.Kind != otherComposite.Kind { + return newValidationErrorf( + "composite kinds differ: %d != %d", + v.Kind, + otherComposite.Kind, + ) + } + + var err *validationError + vFieldNames := make([]string, 0, 10) // v's field names + v.ForEachField(nopMemoryGauge, func(fieldName string, fieldValue interpreter.Value) bool { + otherFieldValue := otherComposite.GetField(otherInterpreter, interpreter.EmptyLocationRange, fieldName) + + err = cadenceValueEqual(vInterpreter, fieldValue, otherInterpreter, otherFieldValue) + if err != nil { + err.addTrace(fmt.Sprintf("(%s.%s)", v.TypeID(), fieldName)) + return false + } + + vFieldNames = append(vFieldNames, fieldName) + return true + }) + + // TODO: Use CompositeValue.FieldCount() from Cadence after it is merged and available. + otherFieldNames := make([]string, 0, len(vFieldNames)) // otherComposite's field names + otherComposite.ForEachField(nopMemoryGauge, func(fieldName string, _ interpreter.Value) bool { + otherFieldNames = append(otherFieldNames, fieldName) + return true + }) + + if len(vFieldNames) != len(otherFieldNames) { + return newValidationErrorf( + "composite %s fields differ: %v != %v", + v.TypeID(), + vFieldNames, + otherFieldNames, + ) + } + + return err +} + +func cadenceDictionaryValueEqual( + vInterpreter *interpreter.Interpreter, + v *interpreter.DictionaryValue, + otherInterpreter *interpreter.Interpreter, + other interpreter.Value, +) *validationError { + otherDictionary, ok := other.(*interpreter.DictionaryValue) + if !ok { + return newValidationErrorf("types differ: %T != %T", v, other) + } + + if v.Count() != otherDictionary.Count() { + return newValidationErrorf("dict counts differ: %d != %d", v.Count(), otherDictionary.Count()) + } + + if !v.Type.Equal(otherDictionary.Type) { + return newValidationErrorf("dict types differ: %s != %s", v.Type, otherDictionary.Type) + } + + oldIterator := v.Iterator() + for { + key := oldIterator.NextKey(nopMemoryGauge) + if key == nil { + break + } + + oldValue, oldValueExist := v.Get(vInterpreter, interpreter.EmptyLocationRange, key) + if !oldValueExist { + err := newValidationErrorf("old value doesn't exist with key %v (%T)", key, key) + err.addTrace(fmt.Sprintf("(%s[%s])", v.Type, key)) + return err + } + newValue, newValueExist := otherDictionary.Get(otherInterpreter, interpreter.EmptyLocationRange, key) + if !newValueExist { + err := newValidationErrorf("new value doesn't exist with key %v (%T)", key, key) + err.addTrace(fmt.Sprintf("(%s[%s])", otherDictionary.Type, key)) + return err + } + err := cadenceValueEqual(vInterpreter, oldValue, otherInterpreter, newValue) + if err != nil { + err.addTrace(fmt.Sprintf("(%s[%s])", otherDictionary.Type, key)) + return err + } + } + + return nil +} + +type readonlyStorageRuntime struct { + Interpreter *interpreter.Interpreter + Storage *runtime.Storage +} + +func newReadonlyStorageRuntime(payloads []*ledger.Payload) ( + *readonlyStorageRuntime, + error, +) { + snapshot, err := util.NewPayloadSnapshot(payloads) + if err != nil { + return nil, fmt.Errorf("failed to create payload snapshot: %w", err) + } + + readonlyLedger := util.NewPayloadsReadonlyLedger(snapshot) + + storage := runtime.NewStorage(readonlyLedger, nopMemoryGauge) + + env := runtime.NewBaseInterpreterEnvironment(runtime.Config{ + AccountLinkingEnabled: true, + // Attachments are enabled everywhere except for Mainnet + AttachmentsEnabled: true, + // Capability Controllers are enabled everywhere except for Mainnet + CapabilityControllersEnabled: true, + }) + + env.Configure( + &NoopRuntimeInterface{}, + runtime.NewCodesAndPrograms(), + storage, + nil, + ) + + inter, err := interpreter.NewInterpreter(nil, nil, env.InterpreterConfig) + if err != nil { + return nil, err + } + + return &readonlyStorageRuntime{ + Interpreter: inter, + Storage: storage, + }, nil +} + +// NoopRuntimeInterface is a runtime interface that can be used in migrations. +type NoopRuntimeInterface struct { +} + +func (NoopRuntimeInterface) ResolveLocation(_ []runtime.Identifier, _ runtime.Location) ([]runtime.ResolvedLocation, error) { + panic("unexpected ResolveLocation call") +} + +func (NoopRuntimeInterface) GetCode(_ runtime.Location) ([]byte, error) { + panic("unexpected GetCode call") +} + +func (NoopRuntimeInterface) GetAccountContractCode(_ common.AddressLocation) ([]byte, error) { + panic("unexpected GetAccountContractCode call") +} + +func (NoopRuntimeInterface) GetOrLoadProgram(_ runtime.Location, _ func() (*interpreter.Program, error)) (*interpreter.Program, error) { + panic("unexpected GetOrLoadProgram call") +} + +func (NoopRuntimeInterface) MeterMemory(_ common.MemoryUsage) error { + return nil +} + +func (NoopRuntimeInterface) MeterComputation(_ common.ComputationKind, _ uint) error { + return nil +} + +func (NoopRuntimeInterface) GetValue(_, _ []byte) (value []byte, err error) { + panic("unexpected GetValue call") +} + +func (NoopRuntimeInterface) SetValue(_, _, _ []byte) (err error) { + panic("unexpected SetValue call") +} + +func (NoopRuntimeInterface) CreateAccount(_ runtime.Address) (address runtime.Address, err error) { + panic("unexpected CreateAccount call") +} + +func (NoopRuntimeInterface) AddEncodedAccountKey(_ runtime.Address, _ []byte) error { + panic("unexpected AddEncodedAccountKey call") +} + +func (NoopRuntimeInterface) RevokeEncodedAccountKey(_ runtime.Address, _ int) (publicKey []byte, err error) { + panic("unexpected RevokeEncodedAccountKey call") +} + +func (NoopRuntimeInterface) AddAccountKey(_ runtime.Address, _ *runtime.PublicKey, _ runtime.HashAlgorithm, _ int) (*runtime.AccountKey, error) { + panic("unexpected AddAccountKey call") +} + +func (NoopRuntimeInterface) GetAccountKey(_ runtime.Address, _ int) (*runtime.AccountKey, error) { + panic("unexpected GetAccountKey call") +} + +func (NoopRuntimeInterface) RevokeAccountKey(_ runtime.Address, _ int) (*runtime.AccountKey, error) { + panic("unexpected RevokeAccountKey call") +} + +func (NoopRuntimeInterface) UpdateAccountContractCode(_ common.AddressLocation, _ []byte) (err error) { + panic("unexpected UpdateAccountContractCode call") +} + +func (NoopRuntimeInterface) RemoveAccountContractCode(common.AddressLocation) (err error) { + panic("unexpected RemoveAccountContractCode call") +} + +func (NoopRuntimeInterface) GetSigningAccounts() ([]runtime.Address, error) { + panic("unexpected GetSigningAccounts call") +} + +func (NoopRuntimeInterface) ProgramLog(_ string) error { + panic("unexpected ProgramLog call") +} + +func (NoopRuntimeInterface) EmitEvent(_ cadence.Event) error { + panic("unexpected EmitEvent call") +} + +func (NoopRuntimeInterface) ValueExists(_, _ []byte) (exists bool, err error) { + panic("unexpected ValueExists call") +} + +func (NoopRuntimeInterface) GenerateUUID() (uint64, error) { + panic("unexpected GenerateUUID call") +} + +func (NoopRuntimeInterface) GetComputationLimit() uint64 { + panic("unexpected GetComputationLimit call") +} + +func (NoopRuntimeInterface) SetComputationUsed(_ uint64) error { + panic("unexpected SetComputationUsed call") +} + +func (NoopRuntimeInterface) DecodeArgument(_ []byte, _ cadence.Type) (cadence.Value, error) { + panic("unexpected DecodeArgument call") +} + +func (NoopRuntimeInterface) GetCurrentBlockHeight() (uint64, error) { + panic("unexpected GetCurrentBlockHeight call") +} + +func (NoopRuntimeInterface) GetBlockAtHeight(_ uint64) (block runtime.Block, exists bool, err error) { + panic("unexpected GetBlockAtHeight call") +} + +func (NoopRuntimeInterface) ReadRandom([]byte) error { + panic("unexpected ReadRandom call") +} + +func (NoopRuntimeInterface) VerifySignature(_ []byte, _ string, _ []byte, _ []byte, _ runtime.SignatureAlgorithm, _ runtime.HashAlgorithm) (bool, error) { + panic("unexpected VerifySignature call") +} + +func (NoopRuntimeInterface) Hash(_ []byte, _ string, _ runtime.HashAlgorithm) ([]byte, error) { + panic("unexpected Hash call") +} + +func (NoopRuntimeInterface) GetAccountBalance(_ common.Address) (value uint64, err error) { + panic("unexpected GetAccountBalance call") +} + +func (NoopRuntimeInterface) GetAccountAvailableBalance(_ common.Address) (value uint64, err error) { + panic("unexpected GetAccountAvailableBalance call") +} + +func (NoopRuntimeInterface) GetStorageUsed(_ runtime.Address) (value uint64, err error) { + panic("unexpected GetStorageUsed call") +} + +func (NoopRuntimeInterface) GetStorageCapacity(_ runtime.Address) (value uint64, err error) { + panic("unexpected GetStorageCapacity call") +} + +func (NoopRuntimeInterface) ImplementationDebugLog(_ string) error { + panic("unexpected ImplementationDebugLog call") +} + +func (NoopRuntimeInterface) ValidatePublicKey(_ *runtime.PublicKey) error { + panic("unexpected ValidatePublicKey call") +} + +func (NoopRuntimeInterface) GetAccountContractNames(_ runtime.Address) ([]string, error) { + panic("unexpected GetAccountContractNames call") +} + +func (NoopRuntimeInterface) AllocateStorageIndex(_ []byte) (atree.StorageIndex, error) { + panic("unexpected AllocateStorageIndex call") +} + +func (NoopRuntimeInterface) ComputationUsed() (uint64, error) { + panic("unexpected ComputationUsed call") +} + +func (NoopRuntimeInterface) MemoryUsed() (uint64, error) { + panic("unexpected MemoryUsed call") +} + +func (NoopRuntimeInterface) InteractionUsed() (uint64, error) { + panic("unexpected InteractionUsed call") +} + +func (NoopRuntimeInterface) SetInterpreterSharedState(_ *interpreter.SharedState) { + panic("unexpected SetInterpreterSharedState call") +} + +func (NoopRuntimeInterface) GetInterpreterSharedState() *interpreter.SharedState { + panic("unexpected GetInterpreterSharedState call") +} + +func (NoopRuntimeInterface) AccountKeysCount(_ runtime.Address) (uint64, error) { + panic("unexpected AccountKeysCount call") +} + +func (NoopRuntimeInterface) BLSVerifyPOP(_ *runtime.PublicKey, _ []byte) (bool, error) { + panic("unexpected BLSVerifyPOP call") +} + +func (NoopRuntimeInterface) BLSAggregateSignatures(_ [][]byte) ([]byte, error) { + panic("unexpected BLSAggregateSignatures call") +} + +func (NoopRuntimeInterface) BLSAggregatePublicKeys(_ []*runtime.PublicKey) (*runtime.PublicKey, error) { + panic("unexpected BLSAggregatePublicKeys call") +} + +func (NoopRuntimeInterface) ResourceOwnerChanged(_ *interpreter.Interpreter, _ *interpreter.CompositeValue, _ common.Address, _ common.Address) { + panic("unexpected ResourceOwnerChanged call") +} + +func (NoopRuntimeInterface) GenerateAccountID(_ common.Address) (uint64, error) { + panic("unexpected GenerateAccountID call") +} + +func (NoopRuntimeInterface) RecordTrace(_ string, _ runtime.Location, _ time.Duration, _ []attribute.KeyValue) { + panic("unexpected RecordTrace call") +} diff --git a/cmd/util/ledger/migrations/cadence_value_validation_test.go b/cmd/util/ledger/migrations/cadence_value_validation_test.go new file mode 100644 index 00000000000..ab52742a5fd --- /dev/null +++ b/cmd/util/ledger/migrations/cadence_value_validation_test.go @@ -0,0 +1,268 @@ +package migrations + +import ( + "bytes" + "fmt" + "strconv" + "testing" + + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/interpreter" + + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/convert" + "github.com/onflow/flow-go/model/flow" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestValidateCadenceValues(t *testing.T) { + address, err := common.HexToAddress("0x1") + require.NoError(t, err) + + domain := common.PathDomainStorage.Identifier() + + t.Run("no mismatch", func(t *testing.T) { + log := zerolog.New(zerolog.NewTestWriter(t)) + + err := validateCadenceValues( + address, + createTestPayloads(t, address, domain), + createTestPayloads(t, address, domain), + log, + false, + ) + require.NoError(t, err) + }) + + t.Run("has mismatch", func(t *testing.T) { + var w bytes.Buffer + log := zerolog.New(&w) + + createPayloads := func(nestedArrayValue interpreter.UInt64Value) []*ledger.Payload { + + // Create account status payload + accountStatus := environment.NewAccountStatus() + accountStatusPayload := ledger.NewPayload( + convert.RegisterIDToLedgerKey( + flow.AccountStatusRegisterID(flow.ConvertAddress(address)), + ), + accountStatus.ToBytes(), + ) + + mr, err := newMigratorRuntime(address, []*ledger.Payload{accountStatusPayload}) + require.NoError(t, err) + + // Create new storage map + storageMap := mr.Storage.GetStorageMap(mr.Address, domain, true) + + // Add Cadence ArrayValue with nested CadenceArray + nestedArray := interpreter.NewArrayValue( + mr.Interpreter, + interpreter.EmptyLocationRange, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeUInt64, + }, + address, + interpreter.NewUnmeteredUInt64Value(0), + nestedArrayValue, + ) + + storageMap.WriteValue( + mr.Interpreter, + interpreter.StringStorageMapKey(strconv.FormatUint(storageMap.Count(), 10)), + interpreter.NewArrayValue( + mr.Interpreter, + interpreter.EmptyLocationRange, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeAnyStruct, + }, + address, + nestedArray, + ), + ) + + err = mr.Storage.Commit(mr.Interpreter, false) + require.NoError(t, err) + + // finalize the transaction + result, err := mr.TransactionState.FinalizeMainTransaction() + require.NoError(t, err) + + payloads := make([]*ledger.Payload, 0, len(result.WriteSet)) + for id, value := range result.WriteSet { + key := convert.RegisterIDToLedgerKey(id) + payloads = append(payloads, ledger.NewPayload(key, value)) + } + + return payloads + } + + oldPayloads := createPayloads(interpreter.NewUnmeteredUInt64Value(1)) + newPayloads := createPayloads(interpreter.NewUnmeteredUInt64Value(2)) + wantErrorMsg := "failed to validate value for address 0000000000000001, domain storage, key 0: failed to validate ([AnyStruct][0]).([UInt64][1]): values differ: 1 (interpreter.UInt64Value) != 2 (interpreter.UInt64Value)" + wantVerboseMsg := "{\"level\":\"info\",\"address\":\"0000000000000001\",\"domain\":\"storage\",\"key\":\"0\",\"trace\":\"failed to validate ([AnyStruct][0]).([UInt64][1]): values differ: 1 (interpreter.UInt64Value) != 2 (interpreter.UInt64Value)\",\"old value\":\"[[0, 1]]\",\"new value\":\"[[0, 2]]\",\"message\":\"failed to validate value\"}\n" + + // Disable verbose logging + err := validateCadenceValues( + address, + oldPayloads, + newPayloads, + log, + false, + ) + require.ErrorContains(t, err, wantErrorMsg) + require.Equal(t, 0, w.Len()) + + // Enable verbose logging + err = validateCadenceValues( + address, + oldPayloads, + newPayloads, + log, + true, + ) + require.ErrorContains(t, err, wantErrorMsg) + require.Equal(t, wantVerboseMsg, w.String()) + }) +} + +func createTestPayloads(t *testing.T, address common.Address, domain string) []*ledger.Payload { + + // Create account status payload + accountStatus := environment.NewAccountStatus() + accountStatusPayload := ledger.NewPayload( + convert.RegisterIDToLedgerKey( + flow.AccountStatusRegisterID(flow.ConvertAddress(address)), + ), + accountStatus.ToBytes(), + ) + + mr, err := newMigratorRuntime(address, []*ledger.Payload{accountStatusPayload}) + require.NoError(t, err) + + // Create new storage map + storageMap := mr.Storage.GetStorageMap(mr.Address, domain, true) + + // Add Cadence UInt64Value + storageMap.WriteValue( + mr.Interpreter, + interpreter.StringStorageMapKey(strconv.FormatUint(storageMap.Count(), 10)), + interpreter.NewUnmeteredUInt64Value(1), + ) + + // Add Cadence SomeValue + storageMap.WriteValue( + mr.Interpreter, + interpreter.StringStorageMapKey(strconv.FormatUint(storageMap.Count(), 10)), + interpreter.NewUnmeteredSomeValueNonCopying(interpreter.NewUnmeteredStringValue("InnerValueString")), + ) + + // Add Cadence ArrayValue + const arrayCount = 10 + i := uint64(0) + storageMap.WriteValue( + mr.Interpreter, + interpreter.StringStorageMapKey(strconv.FormatUint(storageMap.Count(), 10)), + interpreter.NewArrayValueWithIterator( + mr.Interpreter, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeAnyStruct, + }, + address, + 0, + func() interpreter.Value { + if i == arrayCount { + return nil + } + v := interpreter.NewUnmeteredUInt64Value(i) + i++ + return v + }, + ), + ) + + // Add Cadence DictionaryValue + const dictCount = 10 + dictValues := make([]interpreter.Value, 0, dictCount*2) + for i := 0; i < dictCount; i++ { + k := interpreter.NewUnmeteredUInt64Value(uint64(i)) + v := interpreter.NewUnmeteredStringValue(fmt.Sprintf("value %d", i)) + dictValues = append(dictValues, k, v) + } + + storageMap.WriteValue( + mr.Interpreter, + interpreter.StringStorageMapKey(strconv.FormatUint(storageMap.Count(), 10)), + interpreter.NewDictionaryValueWithAddress( + mr.Interpreter, + interpreter.EmptyLocationRange, + interpreter.DictionaryStaticType{ + KeyType: interpreter.PrimitiveStaticTypeUInt64, + ValueType: interpreter.PrimitiveStaticTypeString, + }, + address, + dictValues..., + ), + ) + + // Add Cadence CompositeValue + storageMap.WriteValue( + mr.Interpreter, + interpreter.StringStorageMapKey(strconv.FormatUint(storageMap.Count(), 10)), + interpreter.NewCompositeValue( + mr.Interpreter, + interpreter.EmptyLocationRange, + common.StringLocation("test"), + "Test", + common.CompositeKindStructure, + []interpreter.CompositeField{ + {Name: "field1", Value: interpreter.NewUnmeteredStringValue("value1")}, + {Name: "field2", Value: interpreter.NewUnmeteredStringValue("value2")}, + }, + address, + ), + ) + + // Add Cadence DictionaryValue with nested CadenceArray + nestedArrayValue := interpreter.NewArrayValue( + mr.Interpreter, + interpreter.EmptyLocationRange, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeUInt64, + }, + address, + interpreter.NewUnmeteredUInt64Value(0), + ) + + storageMap.WriteValue( + mr.Interpreter, + interpreter.StringStorageMapKey(strconv.FormatUint(storageMap.Count(), 10)), + interpreter.NewArrayValue( + mr.Interpreter, + interpreter.EmptyLocationRange, + interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeAnyStruct, + }, + address, + nestedArrayValue, + ), + ) + + err = mr.Storage.Commit(mr.Interpreter, false) + require.NoError(t, err) + + // finalize the transaction + result, err := mr.TransactionState.FinalizeMainTransaction() + require.NoError(t, err) + + payloads := make([]*ledger.Payload, 0, len(result.WriteSet)) + for id, value := range result.WriteSet { + key := convert.RegisterIDToLedgerKey(id) + payloads = append(payloads, ledger.NewPayload(key, value)) + } + + return payloads +} diff --git a/cmd/util/ledger/migrations/migrator_runtime.go b/cmd/util/ledger/migrations/migrator_runtime.go new file mode 100644 index 00000000000..7157e705e7a --- /dev/null +++ b/cmd/util/ledger/migrations/migrator_runtime.go @@ -0,0 +1,84 @@ +package migrations + +import ( + "fmt" + + "github.com/onflow/cadence/runtime" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/interpreter" + + "github.com/onflow/flow-go/cmd/util/ledger/util" + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/storage/state" + "github.com/onflow/flow-go/ledger" +) + +// migratorRuntime is a runtime that can be used to run a migration on a single account +func newMigratorRuntime( + address common.Address, + payloads []*ledger.Payload, +) ( + *migratorRuntime, + error, +) { + snapshot, err := util.NewPayloadSnapshot(payloads) + if err != nil { + return nil, fmt.Errorf("failed to create payload snapshot: %w", err) + } + transactionState := state.NewTransactionState(snapshot, state.DefaultParameters()) + accounts := environment.NewAccounts(transactionState) + + accountsAtreeLedger := util.NewAccountsAtreeLedger(accounts) + storage := runtime.NewStorage(accountsAtreeLedger, util.NopMemoryGauge{}) + + ri := &util.MigrationRuntimeInterface{ + Accounts: accounts, + } + + env := runtime.NewBaseInterpreterEnvironment(runtime.Config{ + AccountLinkingEnabled: true, + // Attachments are enabled everywhere except for Mainnet + AttachmentsEnabled: true, + // Capability Controllers are enabled everywhere except for Mainnet + CapabilityControllersEnabled: true, + }) + + env.Configure( + ri, + runtime.NewCodesAndPrograms(), + storage, + runtime.NewCoverageReport(), + ) + + inter, err := interpreter.NewInterpreter( + nil, + nil, + env.InterpreterConfig) + if err != nil { + return nil, err + } + + return &migratorRuntime{ + Address: address, + Payloads: payloads, + Snapshot: snapshot, + TransactionState: transactionState, + Interpreter: inter, + Storage: storage, + Accounts: accountsAtreeLedger, + }, nil +} + +type migratorRuntime struct { + Snapshot *util.PayloadSnapshot + TransactionState state.NestedTransactionPreparer + Interpreter *interpreter.Interpreter + Storage *runtime.Storage + Payloads []*ledger.Payload + Address common.Address + Accounts *util.AccountsAtreeLedger +} + +func (mr *migratorRuntime) GetReadOnlyStorage() *runtime.Storage { + return runtime.NewStorage(util.NewPayloadsReadonlyLedger(mr.Snapshot), util.NopMemoryGauge{}) +} diff --git a/cmd/util/ledger/migrations/test-data/bootstrapped_v0.31/.gitignore b/cmd/util/ledger/migrations/test-data/bootstrapped_v0.31/.gitignore new file mode 100644 index 00000000000..0342671c3bc --- /dev/null +++ b/cmd/util/ledger/migrations/test-data/bootstrapped_v0.31/.gitignore @@ -0,0 +1,4 @@ +* + +!.gitignore +!00000000 diff --git a/cmd/util/ledger/migrations/test-data/bootstrapped_v0.31/00000000 b/cmd/util/ledger/migrations/test-data/bootstrapped_v0.31/00000000 new file mode 100644 index 00000000000..48a6440b89f Binary files /dev/null and b/cmd/util/ledger/migrations/test-data/bootstrapped_v0.31/00000000 differ