canopy-network/canopy · error · ErrCommitDB

root is not supported for nested transactions

Error message

root is not supported for nested transactions

What it means

Store.Root returns the Sparse Merkle Tree root hash of the StateCommitStore. A nested transaction has no independently committed SMT root — its root is only meaningful after flushing to the parent — so calling Root() on a txn store (isTxn) returns this error. Commit() internally calls Root(), so committing a txn store hits the same guard.

Source

Thrown at store/store.go:543

		Indexer: &Indexer{NewTxn(s.Indexer.db, s.Indexer.db, nil, false, true, false, nextVersion), s.config},
		metrics: s.metrics,
		mu:      s.mu,
		isTxn:   true,
	}
}

// DB() returns the underlying PebbleDB instance associated with the Store, providing access
// to the database for direct operations and management.
func (s *Store) DB() *pebble.DB { return s.db }

// IsRootCached() reports whether the SMT root is already cached on this store instance.
func (s *Store) IsRootCached() bool { return s.sc != nil }

// Root() retrieves the root hash of the StateCommitStore, representing the current root of the
// Sparse Merkle Tree. This hash is used for verifying the integrity and consistency of the state.
func (s *Store) Root() (root []byte, err lib.ErrorI) {
	if s.isTxn {
		return nil, ErrCommitDB(fmt.Errorf("root is not supported for nested transactions"))
	}
	// if smt not cached
	if s.sc == nil {
		startTime := time.Now()
		defer s.metrics.UpdateStoreRootTime(startTime)
		nextVersion := s.version + 1
		// set up the state commit store
		s.sc = NewDefaultSMT(NewTxn(s.ss.reader, s.ss.writer, stateCommitIDPrefix, false, false, true, nextVersion))
		// commit the SMT directly using the txn ops
		//
		// NOTE: the SMT node cache MUST NOT be persisted across blocks. `node.copy()` is a
		// no-op alias, so the parallel commit mutates cached `*node` objects in place. Reusing
		// them later can serve stale nodes (e.g. from a speculative, uncommitted `Root()` call),
		// diverging from the on-disk snapshot. A fresh per-block cache still caches within the commit.
		if err = s.sc.CommitParallel(s.ss.txn.ops); err != nil {
			return nil, err
		}
		s.metrics.UpdateStoreRootStats(

View on GitHub (pinned to ee8197d91d)

Solutions

  1. Call Root() on the root/parent Store instead of the transaction-scoped handle
  2. Compute the in-transaction root via the transaction's own SMT/cache APIs (e.g. working-tree view) if available, or flush the txn to the parent first
  3. Restructure so root queries happen after the transaction is committed

Example fix

// before
txn, _ := store.NewTxn(parent, ...)
root, _ := txn.Root() // error
// after
root, _ := parent.Root() // root of committed state
// or flush txn changes to parent, then query parent
Defensive patterns

Strategy: type-guard

Validate before calling

if s.IsTxn() { return errors.New("root is only available on the committed store") }

Type guard

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

Try / catch

root, err := st.Root()
if err != nil && strings.Contains(err.Error(), "nested transactions") {
    return parent.Root() // query the committed state instead
}

Prevention

When it happens

Trigger: Calling Root() directly on a transaction-scoped Store, or calling Commit() on a txn store (which invokes Root()) — e.g. trying to read the state root mid-transaction.

Common situations: Application code reading the state root inside a transaction to anchor it externally; tests inspecting roots via a txn handle; code paths that commit or query root through a transaction-scoped store after a refactor.

Related errors


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