Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/evm/single/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ var RunCmd = &cobra.Command{
return err
}

datastore, err := store.NewDefaultKVStore(nodeConfig.RootDir, nodeConfig.DBPath, "evm-single")
datastore, err := store.NewDefaultKVStore(nodeConfig.RootDir, nodeConfig.DBPath, "rollkit")
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions apps/evm/single/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func main() {
rollcmd.VersionCmd,
rollcmd.NetInfoCmd,
rollcmd.StoreUnsafeCleanCmd,
rollcmd.RollbackCmd,
rollcmd.KeysCmd(),
)

Expand Down
10 changes: 5 additions & 5 deletions apps/testapp/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ var RunCmd = &cobra.Command{
}

// Create test implementations
executor, err := kvexecutor.NewKVExecutor(nodeConfig.RootDir, nodeConfig.DBPath)
if err != nil {
return err
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand All @@ -53,11 +49,15 @@ var RunCmd = &cobra.Command{
return err
}

datastore, err := store.NewDefaultKVStore(nodeConfig.RootDir, nodeConfig.DBPath, "testapp")
datastore, err := store.NewDefaultKVStore(nodeConfig.RootDir, nodeConfig.DBPath, "rollkit")
if err != nil {
return err
}

executor, err := kvexecutor.NewKVExecutor(datastore)
if err != nil {
return err
}
singleMetrics, err := single.NopMetrics()
if err != nil {
return err
Expand Down
10 changes: 6 additions & 4 deletions apps/testapp/kv/http_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"strings"
"testing"
"time"

"github.com/ipfs/go-datastore"
)

func TestHandleTx(t *testing.T) {
Expand Down Expand Up @@ -45,7 +47,7 @@ func TestHandleTx(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand Down Expand Up @@ -130,7 +132,7 @@ func TestHandleKV_Get(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand Down Expand Up @@ -170,7 +172,7 @@ func TestHandleKV_Get(t *testing.T) {

func TestHTTPServerStartStop(t *testing.T) {
// Create a test server that listens on a random port
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand Down Expand Up @@ -214,7 +216,7 @@ func TestHTTPServerStartStop(t *testing.T) {

// TestHTTPServerContextCancellation tests that the server shuts down properly when the context is cancelled
func TestHTTPServerContextCancellation(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand Down
36 changes: 31 additions & 5 deletions apps/testapp/kv/kvexecutor.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@ type KVExecutor struct {
}

// NewKVExecutor creates a new instance of KVExecutor with initialized store and mempool channel.
func NewKVExecutor(rootdir, dbpath string) (*KVExecutor, error) {
datastore, err := store.NewDefaultKVStore(rootdir, dbpath, "executor")
if err != nil {
return nil, err
}
func NewKVExecutor(db ds.Batching) (*KVExecutor, error) {
datastore := store.NewPrefixKV(db, "kv_store")
return &KVExecutor{
db: datastore,
txChan: make(chan []byte, txChannelBufferSize),
Expand Down Expand Up @@ -243,6 +240,35 @@ func (k *KVExecutor) SetFinal(ctx context.Context, blockHeight uint64) error {
return k.db.Put(ctx, ds.NewKey("/finalizedHeight"), []byte(fmt.Sprintf("%d", blockHeight)))
}

// Rollback reverts the state to the previous block height.
// For the KV executor, this removes any state changes at the current height.
// Note: This implementation assumes that state changes are tracked by height keys.
func (k *KVExecutor) Rollback(ctx context.Context, currentHeight uint64) ([]byte, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}

// Validate height constraints
if currentHeight <= 1 {
return nil, fmt.Errorf("cannot rollback from height %d: must be > 1", currentHeight)
}

// For a simple KV store, we'll implement a basic rollback by clearing
// any height-specific state and returning to the current state root.
// In a production system, you'd want to track state changes per height.

// For this simple implementation, we'll just compute and return the current state root
// since the KV store doesn't track height-specific state changes.
stateRoot, err := k.computeStateRoot(ctx)
if err != nil {
return nil, fmt.Errorf("failed to compute state root during rollback: %w", err)
}

return stateRoot, nil
}

// InjectTx adds a transaction to the mempool channel.
// Uses a non-blocking send to avoid blocking the caller if the channel is full.
func (k *KVExecutor) InjectTx(tx []byte) {
Expand Down
40 changes: 35 additions & 5 deletions apps/testapp/kv/kvexecutor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"strings"
"testing"
"time"

"github.com/ipfs/go-datastore"
)

func TestInitChain_Idempotency(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand Down Expand Up @@ -42,7 +44,7 @@ func TestInitChain_Idempotency(t *testing.T) {
}

func TestGetTxs(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand Down Expand Up @@ -108,7 +110,7 @@ func TestGetTxs(t *testing.T) {
}

func TestExecuteTxs_Valid(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand Down Expand Up @@ -136,7 +138,7 @@ func TestExecuteTxs_Valid(t *testing.T) {
}

func TestExecuteTxs_Invalid(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand All @@ -154,7 +156,7 @@ func TestExecuteTxs_Invalid(t *testing.T) {
}

func TestSetFinal(t *testing.T) {
exec, err := NewKVExecutor(t.TempDir(), "testdb")
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
Expand All @@ -172,3 +174,31 @@ func TestSetFinal(t *testing.T) {
t.Error("Expected error for blockHeight 0, got nil")
}
}

func TestRollback(t *testing.T) {
exec, err := NewKVExecutor(datastore.NewMapDatastore())
if err != nil {
t.Fatalf("Failed to create KVExecutor: %v", err)
}
ctx := context.Background()

// Test rollback from height 1 (should fail)
_, err = exec.Rollback(ctx, 1)
if err == nil {
t.Error("Expected error when rolling back from height 1")
}
expectedError := "cannot rollback from height 1: must be > 1"
if err.Error() != expectedError {
t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error())
}

// Test successful rollback from height 2
stateRoot, err := exec.Rollback(ctx, 2)
if err != nil {
t.Errorf("Expected no error for rollback from height 2, got: %v", err)
}

if stateRoot == nil {
t.Error("Expected non-nil state root from rollback")
}
}
1 change: 1 addition & 0 deletions apps/testapp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func main() {
rollcmd.VersionCmd,
rollcmd.NetInfoCmd,
rollcmd.StoreUnsafeCleanCmd,
rollcmd.RollbackCmd,
rollcmd.KeysCmd(),
initCmd,
)
Expand Down
93 changes: 82 additions & 11 deletions block/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
BaseHeader: types.BaseHeader{
ChainID: genesis.ChainID,
Height: genesis.InitialHeight,
Time: uint64(genesis.GenesisDAStartTime.UnixNano()),
Time: uint64(genesis.GenesisDAStartTime.UnixNano()), //nolint:gosec // G115: Conversion is safe, time values fit in uint64
},
}

Expand Down Expand Up @@ -254,16 +254,17 @@
DAHeight: 0,
}
return s, nil
} else if err != nil {
}
if err != nil {
logger.Error("error while getting state", "error", err)
return types.State{}, err
} else {
// Perform a sanity-check to stop the user from
// using a higher genesis than the last stored state.
// if they meant to hard-fork, they should have cleared the stored State
if uint64(genesis.InitialHeight) > s.LastBlockHeight { //nolint:unconvert
return types.State{}, fmt.Errorf("genesis.InitialHeight (%d) is greater than last stored state's LastBlockHeight (%d)", genesis.InitialHeight, s.LastBlockHeight)
}
}

Check warning on line 261 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L261

Added line #L261 was not covered by tests

// Perform a sanity-check to stop the user from
// using a higher genesis than the last stored state.
// if they meant to hard-fork, they should have cleared the stored State
if uint64(genesis.InitialHeight) > s.LastBlockHeight { //nolint:unconvert
return types.State{}, fmt.Errorf("genesis.InitialHeight (%d) is greater than last stored state's LastBlockHeight (%d)", genesis.InitialHeight, s.LastBlockHeight)
}

return s, nil
Expand Down Expand Up @@ -770,7 +771,7 @@
return m.execApplyBlock(ctx, m.lastState, header, data)
}

func (m *Manager) Validate(ctx context.Context, header *types.SignedHeader, data *types.Data) error {
func (m *Manager) Validate(_ context.Context, header *types.SignedHeader, data *types.Data) error {
m.lastStateMtx.RLock()
defer m.lastStateMtx.RUnlock()
return m.execValidate(m.lastState, header, data)
Expand Down Expand Up @@ -923,7 +924,13 @@
for _, data := range batchData {
// Encode length as 4-byte big-endian integer
lengthBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(lengthBytes, uint32(len(data)))
dataLen := len(data)
// Note: In practice, data chunks should never exceed uint32 max size
// This check prevents integer overflow but should not occur in normal operation
if dataLen > 0x7FFFFFFF { // Use a reasonable limit to avoid issues
dataLen = 0x7FFFFFFF
}
binary.LittleEndian.PutUint32(lengthBytes, uint32(dataLen)) //nolint:gosec // G115: Conversion is safe after bounds check

// Append length prefix
result = append(result, lengthBytes...)
Expand Down Expand Up @@ -1071,3 +1078,67 @@
valid, err := signedData.Signer.PubKey.Verify(dataBytes, signedData.Signature)
return err == nil && valid
}

// RollbackLastBlock reverts the chain state to the previous block.
// This method allows recovery from unrecoverable errors by rolling back
// the most recent block that has not been finalized.
func (m *Manager) RollbackLastBlock(ctx context.Context) error {

Check warning on line 1085 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L1084-L1085

Added lines #L1084 - L1085 were not covered by tests
m.lastStateMtx.Lock()
defer m.lastStateMtx.Unlock()

currentHeight := m.lastState.LastBlockHeight
if currentHeight <= 1 {
return fmt.Errorf("cannot rollback from height %d: must be > 1", currentHeight)
}

m.logger.Info("Rolling back last block", "currentHeight", currentHeight, "targetHeight", currentHeight-1)

// First, rollback the execution layer
prevStateRoot, err := m.exec.Rollback(ctx, currentHeight)
if err != nil {
return fmt.Errorf("failed to rollback execution layer: %w", err)
}

// Then, rollback the store to the previous height
targetHeight := currentHeight - 1
if err := m.store.RollbackToHeight(ctx, targetHeight); err != nil {
return fmt.Errorf("failed to rollback store: %w", err)
}

// Update the manager's internal state to reflect the rollback
// Get the previous block's state from the store
prevState, err := m.store.GetState(ctx)
if err != nil {
return fmt.Errorf("failed to get state after rollback: %w", err)
}

// Verify that the state root matches what the execution layer returned
if !bytes.Equal(prevState.AppHash, prevStateRoot) {
m.logger.Warn("State root mismatch after rollback",
"storeStateRoot", fmt.Sprintf("%x", prevState.AppHash),
"execStateRoot", fmt.Sprintf("%x", prevStateRoot))
}

// Update the last state to the rolled-back state

Check warning on line 1122 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L1121-L1122

Added lines #L1121 - L1122 were not covered by tests
m.lastState = prevState

// Clear any cached data for the rolled-back block
_, err = m.store.GetHeader(ctx, currentHeight)
if err == nil {
// Note: We can't remove from cache as there's no Remove method in the interface
// This is acceptable as the cache will eventually expire or be overwritten

Check warning on line 1129 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L1126-L1129

Added lines #L1126 - L1129 were not covered by tests
m.logger.Debug("Header exists in cache after rollback, will be overwritten on next access")
}

// Reset DA included height if it was at the rolled-back height
if m.daIncludedHeight.Load() >= currentHeight {
m.daIncludedHeight.Store(targetHeight)
}

m.logger.Info("Successfully rolled back block",
"rolledBackHeight", currentHeight,
"newHeight", targetHeight,

Check warning on line 1140 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L1137-L1140

Added lines #L1137 - L1140 were not covered by tests
"newStateRoot", fmt.Sprintf("%x", prevStateRoot))

return nil
}
4 changes: 4 additions & 0 deletions block/publish_block_p2p_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ func (m mockExecutor) SetFinal(ctx context.Context, blockHeight uint64) error {
return nil
}

func (m mockExecutor) Rollback(ctx context.Context, currentHeight uint64) ([]byte, error) {
return bytesN(32), nil
}

var rnd = rand.New(rand.NewSource(1)) //nolint:gosec // test code only

func bytesN(n int) []byte {
Expand Down
Loading
Loading