canopy-network/canopy · error · ErrCommitDB

rollback is not supported for nested transactions

Error message

rollback is not supported for nested transactions

What it means

Store.Rollback is an offline maintenance operation that rewinds the database to a target version. It cannot run while the store is a nested transaction, because a txn view lacks authoritative ownership of the underlying DB and rollback would corrupt the versioned state. The library rejects it eagerly with this error.

Source

Thrown at store/store.go:328

// the journal only needs keys
func (s *Store) recordStateChangeKeys(version uint64) lib.ErrorI {
	s.ss.txn.l.Lock()
	keys := make([][]byte, 0, len(s.ss.txn.ops))
	for _, op := range s.ss.txn.ops {
		keys = append(keys, bytes.Clone(op.key))
	}
	s.ss.txn.l.Unlock()
	return s.Indexer.indexStateChangeKeys(version, keys)
}

// Rollback rewinds the store to a previous version (height).
//
// It removes all versioned entries above targetVersion, rebuilds the latest state
// view from historical state at targetVersion, and resets the latest commit pointer.
// NOTE: Rollback is an offline maintenance operation and must only run while the node is stopped.
func (s *Store) Rollback(targetVersion uint64) lib.ErrorI {
	if s.isTxn {
		return ErrCommitDB(fmt.Errorf("rollback is not supported for nested transactions"))
	}
	if targetVersion == 0 {
		return ErrCommitDB(fmt.Errorf("rollback target height must be >= 1"))
	}
	s.mu.Lock()
	defer s.mu.Unlock()

	currentVersion := s.version
	if targetVersion > currentVersion {
		return ErrCommitDB(fmt.Errorf("rollback target height %d exceeds current height %d", targetVersion, currentVersion))
	}
	if targetVersion == currentVersion {
		return nil
	}

	snapshot := s.db.NewSnapshot()
	defer snapshot.Close()

View on GitHub (pinned to ee8197d91d)

Solutions

  1. Open the root Store (not a transaction) for the rollback operation
  2. Ensure the node is fully stopped before running rollback, as required for this offline operation
  3. Refactor maintenance tooling to accept only the root store instance

Example fix

// before
txnStore, _ := store.NewTxn(parent, ...)
txnStore.Rollback(100) // error
// after
rootStore, _ := store.NewStore(db, ...)
rootStore.Rollback(100)
Defensive patterns

Strategy: validation

Validate before calling

func canRollback(s *store.Store) error {
    if s.IsTxn() { return errors.New("rollback requires the root store, not a transaction") }
    return nil
}

Type guard

func isRollbackTarget(s *store.Store) bool { return !s.IsTxn() }

Try / catch

if err := st.Rollback(h); err != nil {
    if strings.Contains(err.Error(), "nested transactions") {
        return rootStore.Rollback(h)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Rollback(targetVersion) on a Store whose isTxn flag is set (a transaction-scoped store), typically during offline maintenance scripts that accidentally obtained a txn handle instead of the root store.

Common situations: Running a rollback/rewind maintenance command against a store instance opened in transaction mode; code refactors that pass a txn store into the rollback utility.

Related errors


AI-assisted analysis of canopy-network/canopy@ee8197d91d (2026-09-06). Data as JSON: /api/errors/33a2b9c146faa596. Report an issue: GitHub.