{"record":{"id":"232c4eff6495a27d","repo":"hyperledger/fabric","slug":"error-validating-readset","errorCode":null,"errorMessage":"error validating ReadSet","messagePattern":"error validating ReadSet","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"common/configtx/update.go","lineNumber":136,"sourceCode":"\t\treturn nil, errors.Errorf(\"cannot process nil ConfigUpdateEnvelope\")\n\t}\n\n\tconfigUpdate, err := UnmarshalConfigUpdate(configUpdateEnv.ConfigUpdate)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif configUpdate.ChannelId != vi.channelID {\n\t\treturn nil, errors.Errorf(\"ConfigUpdate for channel '%s' but envelope for channel '%s'\", configUpdate.ChannelId, vi.channelID)\n\t}\n\n\treadSet, err := mapConfig(configUpdate.ReadSet, vi.namespace)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error mapping ReadSet\")\n\t}\n\terr = vi.verifyReadSet(readSet)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error validating ReadSet\")\n\t}\n\n\twriteSet, err := mapConfig(configUpdate.WriteSet, vi.namespace)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error mapping WriteSet\")\n\t}\n\n\tdeltaSet := computeDeltaSet(readSet, writeSet)\n\tsignedData, err := protoutil.ConfigUpdateEnvelopeAsSignedData(configUpdateEnv)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = vi.verifyDeltaSet(deltaSet, signedData); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error validating DeltaSet\")\n\t}\n\n\tfullProposedConfig := vi.computeUpdateResult(deltaSet)","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/hyperledger/fabric/blob/2736b63f8fd5932511d56fe68b7039d15977f7f6/common/configtx/update.go#L118-L154","documentation":"verifyReadSet rejected the update because an entry in the ReadSet does not match the current channel config — the read item's version differs from the current version, or the item does not exist. This is optimistic-concurrency validation: the update was built against a stale base config.","triggerScenarios":"Calling proposeConfigUpdate/Validate with a ReadSet whose key versions don't match the validator's current config sequence, typically because another config update was committed since the base config was fetched.","commonSituations":"Two orgs preparing updates concurrently; using a config dump from hours/days ago; rebuilding a channel (new genesis block) while reusing old update files; joining orgs in sequence where each commit bumps versions.","solutions":["Re-fetch the latest channel config, regenerate the update against it, and resubmit.","Coordinate config updates so only one is in flight at a time (or merge pending changes before regenerating).","If the read item was removed, remove it from the ReadSet or restructure the update.","Check the wrapped error to identify the exact key whose version diverged."],"exampleFix":"// before: base config version 3 (stale)\nreadSet[\"Application/Org2\"].version = 3\n// after: re-fetch current config and rebuild\nlatest := fetchLatestConfigBlock(channelID)\nupdate := computeDiff(latest, desiredConfig)","handlingStrategy":"validation","validationCode":"// Compare the ReadSet versions against the live config before submitting\nfunc readSetCurrent(update *cb.ConfigUpdate, live *cb.Config) error {\n    versions := map[string]uint64{}\n    traverseConfigGroups(live.GetChannelGroup(), func(path string, g *cb.ConfigGroup) error {\n        for k, v := range g.GetValues() { versions[path+\"/\"+k] = v.GetVersion() }\n        return nil\n    })\n    // any read item whose recorded version != live version means a stale update\n    return checkReadVersions(update.GetReadSet(), versions)\n}","typeGuard":null,"tryCatchPattern":"if _, err := validator.ProposeConfigUpdate(env, seq); err != nil {\n    if strings.Contains(err.Error(), \"error validating ReadSet\") {\n        // refetch latest config block, regenerate diff, resubmit\n    }\n    return err\n}","preventionTips":["Always rebuild updates from the latest config block immediately before submission.","Serialize config-update workflows: one in-flight update per channel at a time.","After channel re-creation or genesis changes, discard old update files.","Capture the config sequence number when generating an update and verify it before submit."],"tags":["hyperledger-fabric","configtx","readset","stale-config","version-conflict"],"backgroundTag":"stale-config-version","analyzedSha":"2736b63f8fd5932511d56fe68b7039d15977f7f6","analyzedAt":"2026-09-04T08:52:36.465Z","contentChangedAt":"2026-09-04T08:52:36.465Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}