hyperledger/fabric · error

no ledger context: %s %s %+v

Error message

no ledger context: %s %s

 %+v

What it means

isValidTxSim looks up the TransactionContext for a (channelID, txid) pair and requires that a TXSimulator is attached. The error means the chaincode issued a ledger read/write message (GetState, PutState, etc.) for a txid with no registered transaction simulator — i.e., the message arrived outside a valid transaction context, or after the context was deleted (e.g., post-commit or in Init without simulator). The formatted message includes channel, txid, and underlying error details.

Source

Thrown at core/chaincode/handler.go:589

}

func (h *Handler) Notify(msg *pb.ChaincodeMessage) {
	tctx := h.TXContexts.Get(msg.ChannelId, msg.Txid)
	if tctx == nil {
		chaincodeLogger.Debugf("notifier Txid:%s, channelID:%s does not exist for handling message %s", msg.Txid, msg.ChannelId, msg.Type)
		return
	}

	chaincodeLogger.Debugf("[%s] notifying Txid:%s, channelID:%s", shorttxid(msg.Txid), msg.Txid, msg.ChannelId)
	tctx.ResponseNotifier <- msg
	tctx.CloseQueryIterators()
}

// is this a txid for which there is a valid txsim
func (h *Handler) isValidTxSim(channelID string, txid string, fmtStr string, args ...any) (*TransactionContext, error) {
	txContext := h.TXContexts.Get(channelID, txid)
	if txContext == nil || txContext.TXSimulator == nil {
		err := errors.Errorf(fmtStr, args...)
		chaincodeLogger.Errorf("no ledger context: %s %s\n\n %+v", channelID, txid, err)
		return nil, err
	}
	return txContext, nil
}

// register Txid to prevent overlapping handle messages from chaincode
func (h *Handler) registerTxid(msg *pb.ChaincodeMessage) bool {
	// Check if this is the unique state request from this chaincode txid
	if h.ActiveTransactions.Add(msg.ChannelId, msg.Txid) {
		return true
	}

	// Log the issue and drop the request
	chaincodeLogger.Errorf("[%s] Another request pending for this CC: %s, Txid: %s, ChannelID: %s. Cannot process.", shorttxid(msg.Txid), h.chaincodeID, msg.Txid, msg.ChannelId)
	return false
}

View on GitHub (pinned to 2736b63f8f)

Solutions

  1. Ensure all ledger reads/writes happen inside the single chaincode Invoke/Init call, before it returns
  2. Do not spawn goroutines that use the tx simulator after the transaction completes; re-invoke instead
  3. Verify the txid/channel passed to HandleTransaction matches an active (not yet deleted) transaction context
  4. Do not call GetState/PutState from Init if a simulator is unavailable; defer state writes to Invoke

Example fix

// before: goroutine uses simulator after tx ends
go func() { stub.PutState("k", v) }()
// after: keep ledger ops synchronous inside Invoke
func (c *CC) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    if err := stub.PutState("k", []byte("v")); err != nil { return shim.Error(err.Error()) }
    return shim.Success(nil)
}
Defensive patterns

Strategy: try-catch

Try / catch

val, err := stub.GetState(key)
if err != nil {
    if strings.Contains(err.Error(), "no ledger context") {
        // tx context gone: retry via a NEW transaction invocation, not with same txid
        return shim.Error("transaction context expired; re-invoke")
    }
    return shim.Error(err.Error())
}

Prevention

When it happens

Trigger: Chaincode calls GetState/PutState/DelState after the transaction has completed or been cleaned up; calling state APIs from a goroutine that outlives the transaction; passing a wrong txid through HandleTransaction/getTxContextForInvoke; calling query APIs during Init when no simulator exists.

Common situations: Long-running chaincode goroutines or timers that touch the ledger after the tx ends; retrying a request with an expired txid; invoking state APIs inside Init; channel/txid mismatches in custom peer-to-chaincode plumbing.

Related errors


AI-assisted analysis of hyperledger/fabric@2736b63f8f (2026-09-04). Data as JSON: /api/errors/bf01ae6c8ceec9d8. Report an issue: GitHub.