Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 276
feat: add block rollback support#2499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
17364067866d1b7cc1c71e85a8ceb9ff07db3cb4a2ef081e6bbca2bab8004b7c87c3a741bb318e84b4c9bb6a1752e8ca9d26c024d6c2cbab75b77068ae3b6318b3cbb139779fede5086eacea0ae3d67c605bedabFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package cmd | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strconv" | ||
| kvexecutor "github.com/evstack/ev-node/apps/testapp/kv" | ||
| rollcmd "github.com/evstack/ev-node/pkg/cmd" | ||
| "github.com/evstack/ev-node/pkg/store" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| var RollbackCmd = &cobra.Command{ | ||
| Use: "rollback <height>", | ||
| Short: "Rollback the testapp node", | ||
| Args: cobra.RangeArgs(0, 1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| nodeConfig, err := rollcmd.ParseConfig(cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
| datastore, err := store.NewDefaultKVStore(nodeConfig.RootDir, nodeConfig.DBPath, "testapp") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| storeWrapper := store.New(datastore) | ||
| executor, err := kvexecutor.NewKVExecutor(nodeConfig.RootDir, nodeConfig.DBPath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| cmd.Println("Starting rollback operation") | ||
| currentHeight, err := storeWrapper.Height(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get current height: %w", err) | ||
| } | ||
| var targetHeight uint64 = currentHeight - 1 | ||
| if len(args) > 0 { | ||
| targetHeight, err = strconv.ParseUint(args[0], 10, 64) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to parse target height: %w", err) | ||
| } | ||
| } | ||
| // rollback ev-node store | ||
| if err := storeWrapper.Rollback(ctx, targetHeight); err != nil { | ||
| return fmt.Errorf("rollback failed: %w", err) | ||
| } | ||
| // rollback execution store | ||
| if err := executor.Rollback(ctx, targetHeight); err != nil { | ||
| return fmt.Errorf("rollback failed: %w", err) | ||
| } | ||
| cmd.Println("Rollback completed successfully") | ||
| return nil | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -16,6 +16,7 @@ import ( | ||||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||||
| genesisInitializedKey = ds.NewKey("/genesis/initialized") | ||||||||||||||||||||||||||||||||
| genesisStateRootKey = ds.NewKey("/genesis/stateroot") | ||||||||||||||||||||||||||||||||
| heightKeyPrefix = ds.NewKey("/height") | ||||||||||||||||||||||||||||||||
| finalizedHeightKey = ds.NewKey("/finalizedHeight") | ||||||||||||||||||||||||||||||||
| // Define a buffer size for the transaction channel | ||||||||||||||||||||||||||||||||
| txChannelBufferSize = 10000 | ||||||||||||||||||||||||||||||||
| @@ -49,18 +50,56 @@ func NewKVExecutor(rootdir, dbpath string) (*KVExecutor, error) { | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // GetStoreValue is a helper for the HTTP interface to retrieve the value for a key from the database. | ||||||||||||||||||||||||||||||||
| // It searches across all block heights to find the latest value for the given key. | ||||||||||||||||||||||||||||||||
| func (k *KVExecutor) GetStoreValue(ctx context.Context, key string) (string, bool) { | ||||||||||||||||||||||||||||||||
| dsKey := ds.NewKey(key) | ||||||||||||||||||||||||||||||||
| valueBytes, err := k.db.Get(ctx, dsKey) | ||||||||||||||||||||||||||||||||
| if errors.Is(err, ds.ErrNotFound) { | ||||||||||||||||||||||||||||||||
| // Query all keys to find height-prefixed versions of this key | ||||||||||||||||||||||||||||||||
| q := query.Query{} | ||||||||||||||||||||||||||||||||
| results, err := k.db.Query(ctx, q) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| fmt.Printf("Error querying DB for key '%s': %v\n", key, err) | ||||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| // Log the error or handle it appropriately | ||||||||||||||||||||||||||||||||
| fmt.Printf("Error getting value from DB: %v\n", err) | ||||||||||||||||||||||||||||||||
| defer results.Close() | ||||||||||||||||||||||||||||||||
| heightPrefix := heightKeyPrefix.String() | ||||||||||||||||||||||||||||||||
| var latestValue string | ||||||||||||||||||||||||||||||||
| var latestHeight uint64 | ||||||||||||||||||||||||||||||||
| found := false | ||||||||||||||||||||||||||||||||
| for result := range results.Next() { | ||||||||||||||||||||||||||||||||
| if result.Error != nil { | ||||||||||||||||||||||||||||||||
| fmt.Printf("Error iterating query results for key '%s': %v\n", key, result.Error) | ||||||||||||||||||||||||||||||||
Check warningCode scanning / CodeQL Log entries created from user input Medium test
This log entry depends on a user-provided value Error loading related location LoadingUh oh!There was an error while loading. Please reload this page.
Show autofix suggestionHide autofix suggestion Copilot AutofixAI 12 months ago To fix the problem, we need to sanitize the user input before logging it. For plain text logs, the recommended approach is to remove any newline (
Suggested changeset
1 apps/testapp/kv/kvexecutor.go
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again. | ||||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| resultKey := result.Key | ||||||||||||||||||||||||||||||||
| // Check if this is a height-prefixed key that matches our target key | ||||||||||||||||||||||||||||||||
| if strings.HasPrefix(resultKey, heightPrefix+"/") { | ||||||||||||||||||||||||||||||||
| // Extract height and actual key: /height/{height}/{actual_key} | ||||||||||||||||||||||||||||||||
| parts := strings.Split(strings.TrimPrefix(resultKey, heightPrefix+"/"), "/") | ||||||||||||||||||||||||||||||||
| if len(parts) >= 2 { | ||||||||||||||||||||||||||||||||
| var keyHeight uint64 | ||||||||||||||||||||||||||||||||
| if _, err := fmt.Sscanf(parts[0], "%d", &keyHeight); err == nil { | ||||||||||||||||||||||||||||||||
| // Reconstruct the actual key by joining all parts after the height | ||||||||||||||||||||||||||||||||
| actualKey := strings.Join(parts[1:], "/") | ||||||||||||||||||||||||||||||||
| if actualKey == key { | ||||||||||||||||||||||||||||||||
| // This key matches - check if it's the latest height | ||||||||||||||||||||||||||||||||
| if !found || keyHeight > latestHeight { | ||||||||||||||||||||||||||||||||
| latestHeight = keyHeight | ||||||||||||||||||||||||||||||||
| latestValue = string(result.Value) | ||||||||||||||||||||||||||||||||
| found = true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if !found { | ||||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return string(valueBytes), true | ||||||||||||||||||||||||||||||||
| return latestValue, true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // computeStateRoot computes a deterministic state root by querying all keys, sorting them, | ||||||||||||||||||||||||||||||||
| @@ -206,11 +245,14 @@ func (k *KVExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight u | ||||||||||||||||||||||||||||||||
| if key == "" { | ||||||||||||||||||||||||||||||||
| return nil, 0, errors.New("empty key in transaction") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| dsKey := ds.NewKey(key) | ||||||||||||||||||||||||||||||||
| dsKey := getTxKey(blockHeight, key) | ||||||||||||||||||||||||||||||||
| // Prevent writing reserved keys via transactions | ||||||||||||||||||||||||||||||||
| if reservedKeys[dsKey] { | ||||||||||||||||||||||||||||||||
| return nil, 0, fmt.Errorf("transaction attempts to modify reserved key: %s", key) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| err = batch.Put(ctx, dsKey, []byte(value)) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| // This error is unlikely for Put unless the context is cancelled. | ||||||||||||||||||||||||||||||||
| @@ -263,3 +305,96 @@ func (k *KVExecutor) InjectTx(tx []byte) { | ||||||||||||||||||||||||||||||||
| // Consider adding metrics here | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // Rollback reverts the state to the previous block height. | ||||||||||||||||||||||||||||||||
| func (k *KVExecutor) Rollback(ctx context.Context, height uint64) error { | ||||||||||||||||||||||||||||||||
| select { | ||||||||||||||||||||||||||||||||
| case <-ctx.Done(): | ||||||||||||||||||||||||||||||||
| return ctx.Err() | ||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // Validate height constraints | ||||||||||||||||||||||||||||||||
| if height == 0 { | ||||||||||||||||||||||||||||||||
| return fmt.Errorf("cannot rollback to height 0: invalid height") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // Create a batch for atomic rollback operation | ||||||||||||||||||||||||||||||||
| batch, err := k.db.Batch(ctx) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to create batch for rollback: %w", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // Query all keys to find those with height > target height | ||||||||||||||||||||||||||||||||
| q := query.Query{} | ||||||||||||||||||||||||||||||||
| results, err := k.db.Query(ctx, q) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to query keys for rollback: %w", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| defer results.Close() | ||||||||||||||||||||||||||||||||
| keysToDelete := make([]ds.Key, 0) | ||||||||||||||||||||||||||||||||
| heightPrefix := heightKeyPrefix.String() | ||||||||||||||||||||||||||||||||
| for result := range results.Next() { | ||||||||||||||||||||||||||||||||
| if result.Error != nil { | ||||||||||||||||||||||||||||||||
| return fmt.Errorf("error iterating query results during rollback: %w", result.Error) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| key := result.Key | ||||||||||||||||||||||||||||||||
| // Check if this is a height-prefixed key | ||||||||||||||||||||||||||||||||
| if strings.HasPrefix(key, heightPrefix+"/") { | ||||||||||||||||||||||||||||||||
| // Extract height from key: /height/{height}/{actual_key} (see getTxKey) | ||||||||||||||||||||||||||||||||
| parts := strings.Split(strings.TrimPrefix(key, heightPrefix+"/"), "/") | ||||||||||||||||||||||||||||||||
| if len(parts) > 0 { | ||||||||||||||||||||||||||||||||
| var keyHeight uint64 | ||||||||||||||||||||||||||||||||
| if _, err := fmt.Sscanf(parts[0], "%d", &keyHeight); err == nil { | ||||||||||||||||||||||||||||||||
| // If this key's height is greater than target, mark for deletion | ||||||||||||||||||||||||||||||||
| if keyHeight > height { | ||||||||||||||||||||||||||||||||
| keysToDelete = append(keysToDelete, ds.NewKey(key)) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // Delete all keys with height > target height | ||||||||||||||||||||||||||||||||
| for _, key := range keysToDelete { | ||||||||||||||||||||||||||||||||
| select { | ||||||||||||||||||||||||||||||||
| case <-ctx.Done(): | ||||||||||||||||||||||||||||||||
| return ctx.Err() | ||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| err = batch.Delete(ctx, key) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to stage delete operation for key '%s' during rollback: %w", key.String(), err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // Update finalized height if necessary - it should not exceed rollback height | ||||||||||||||||||||||||||||||||
| finalizedHeightKey := ds.NewKey("/finalizedHeight") | ||||||||||||||||||||||||||||||||
| if finalizedHeightBytes, err := k.db.Get(ctx, finalizedHeightKey); err == nil { | ||||||||||||||||||||||||||||||||
| var finalizedHeight uint64 | ||||||||||||||||||||||||||||||||
| if _, err := fmt.Sscanf(string(finalizedHeightBytes), "%d", &finalizedHeight); err == nil { | ||||||||||||||||||||||||||||||||
| if finalizedHeight > height { | ||||||||||||||||||||||||||||||||
| err = batch.Put(ctx, finalizedHeightKey, fmt.Appendf([]byte{}, "%d", height)) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to update finalized height during rollback: %w", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // Commit the batch atomically | ||||||||||||||||||||||||||||||||
| err = batch.Commit(ctx) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to commit rollback batch: %w", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| func getTxKey(height uint64, txKey string) ds.Key { | ||||||||||||||||||||||||||||||||
| return heightKeyPrefix.Child(ds.NewKey(fmt.Sprintf("%d/%s", height, txKey))) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Check warning
Code scanning / CodeQL
Log entries created from user input Medium test
Copilot Autofix
AI 12 months ago
To fix the problem, we need to sanitize the user-provided
keybefore logging it. Since the logs are plain text, the recommended approach is to remove any newline (\n) and carriage return (\r) characters from thekeybefore including it in the log message. This can be done usingstrings.ReplaceAll. The fix should be applied directly at the point where the log message is constructed, i.e., inapps/testapp/kv/kvexecutor.goat line 59. We need to ensure that thestringspackage is imported (it already is), so no new imports are needed.