{"record":{"id":"c2ed5870a54c43b9","repo":"canopy-network/canopy","slug":"root-is-not-supported-for-nested-transactions","errorCode":null,"errorMessage":"root is not supported for nested transactions","messagePattern":"root is not supported for nested transactions","errorType":"error_code","errorClass":"ErrCommitDB","httpStatus":null,"severity":"error","filePath":"store/store.go","lineNumber":543,"sourceCode":"\t\tIndexer: &Indexer{NewTxn(s.Indexer.db, s.Indexer.db, nil, false, true, false, nextVersion), s.config},\n\t\tmetrics: s.metrics,\n\t\tmu:      s.mu,\n\t\tisTxn:   true,\n\t}\n}\n\n// DB() returns the underlying PebbleDB instance associated with the Store, providing access\n// to the database for direct operations and management.\nfunc (s *Store) DB() *pebble.DB { return s.db }\n\n// IsRootCached() reports whether the SMT root is already cached on this store instance.\nfunc (s *Store) IsRootCached() bool { return s.sc != nil }\n\n// Root() retrieves the root hash of the StateCommitStore, representing the current root of the\n// Sparse Merkle Tree. This hash is used for verifying the integrity and consistency of the state.\nfunc (s *Store) Root() (root []byte, err lib.ErrorI) {\n\tif s.isTxn {\n\t\treturn nil, ErrCommitDB(fmt.Errorf(\"root is not supported for nested transactions\"))\n\t}\n\t// if smt not cached\n\tif s.sc == nil {\n\t\tstartTime := time.Now()\n\t\tdefer s.metrics.UpdateStoreRootTime(startTime)\n\t\tnextVersion := s.version + 1\n\t\t// set up the state commit store\n\t\ts.sc = NewDefaultSMT(NewTxn(s.ss.reader, s.ss.writer, stateCommitIDPrefix, false, false, true, nextVersion))\n\t\t// commit the SMT directly using the txn ops\n\t\t//\n\t\t// NOTE: the SMT node cache MUST NOT be persisted across blocks. `node.copy()` is a\n\t\t// no-op alias, so the parallel commit mutates cached `*node` objects in place. Reusing\n\t\t// them later can serve stale nodes (e.g. from a speculative, uncommitted `Root()` call),\n\t\t// diverging from the on-disk snapshot. A fresh per-block cache still caches within the commit.\n\t\tif err = s.sc.CommitParallel(s.ss.txn.ops); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ts.metrics.UpdateStoreRootStats(","sourceCodeStart":525,"sourceCodeEnd":561,"githubUrl":"https://github.com/canopy-network/canopy/blob/ee8197d91dd410f6592cb650a94c925ee6dc8bad/store/store.go#L525-L561","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Call Root() on the root/parent Store instead of the transaction-scoped handle","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","Restructure so root queries happen after the transaction is committed"],"exampleFix":"// before\ntxn, _ := store.NewTxn(parent, ...)\nroot, _ := txn.Root() // error\n// after\nroot, _ := parent.Root() // root of committed state\n// or flush txn changes to parent, then query parent","handlingStrategy":"type-guard","validationCode":"if s.IsTxn() { return errors.New(\"root is only available on the committed store\") }","typeGuard":"func canReadRoot(s *store.Store) bool { return !s.IsTxn() }","tryCatchPattern":"root, err := st.Root()\nif err != nil && strings.Contains(err.Error(), \"nested transactions\") {\n    return parent.Root() // query the committed state instead\n}","preventionTips":["Expose root queries only through the root store abstraction","For in-transaction root needs, use the txn's working SMT/cache APIs, not Root()","Add code-review checks that Root()/Commit() call sites hold the root store"],"tags":["store","transaction","root","smt"],"backgroundTag":"operation-not-supported","analyzedSha":"ee8197d91dd410f6592cb650a94c925ee6dc8bad","analyzedAt":"2026-09-06T09:30:15.973Z","contentChangedAt":"2026-09-06T09:30:15.973Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}