{"record":{"id":"d27ea1165c729211","repo":"hyperledger/fabric","slug":"the-blocktolive-in-the-following-existing-collecti-d27ea1","errorCode":null,"errorMessage":"the BlockToLive in the following existing collections must not be modified: %v","messagePattern":"the BlockToLive in the following existing collections must not be modified: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/handlers/validation/builtin/v13/lscc_validation_logic.go","lineNumber":175,"sourceCode":"\t// modified for the existing collections.\n\tfor _, oldCollectionConfig := range oldCollectionConfigs {\n\n\t\toldCollection := oldCollectionConfig.GetStaticCollectionConfig()\n\t\t// It cannot be nil\n\t\tif oldCollection == nil {\n\t\t\treturn policyErr(fmt.Errorf(\"unknown collection configuration type\"))\n\t\t}\n\n\t\toldCollectionName := oldCollection.GetName()\n\t\tnewCollection := newCollectionsMap[oldCollectionName]\n\t\t// BlockToLive cannot be changed\n\t\tif newCollection.GetBlockToLive() != oldCollection.GetBlockToLive() {\n\t\t\tmodifiedCollectionsBTL = append(modifiedCollectionsBTL, oldCollectionName)\n\t\t}\n\t}\n\n\tif len(modifiedCollectionsBTL) > 0 {\n\t\treturn policyErr(fmt.Errorf(\"the BlockToLive in the following existing collections must not be modified: %v\",\n\t\t\tmodifiedCollectionsBTL))\n\t}\n\n\treturn nil\n}\n\nfunc validateNewCollectionConfigsAgainstOld(newCollectionConfigs []*pb.CollectionConfig, oldCollectionConfigs []*pb.CollectionConfig,\n) error {\n\tnewCollectionsMap := make(map[string]*pb.StaticCollectionConfig, len(newCollectionConfigs))\n\n\tfor _, newCollectionConfig := range newCollectionConfigs {\n\t\tnewCollection := newCollectionConfig.GetStaticCollectionConfig()\n\t\t// Collection object itself is stored as value so that we can\n\t\t// check whether the block to live is changed -- FAB-7810\n\t\tnewCollectionsMap[newCollection.GetName()] = newCollection\n\t}\n\n\tif err := checkForMissingCollections(newCollectionsMap, oldCollectionConfigs); err != nil {","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/hyperledger/fabric/blob/2736b63f8fd5932511d56fe68b7039d15977f7f6/core/handlers/validation/builtin/v13/lscc_validation_logic.go#L157-L193","documentation":"BlockToLive (the number of blocks after which private data is purged) is immutable once a collection is defined. checkForModifiedCollectionsBTL compares old and new BlockToLive values for every existing collection and rejects the upgrade transaction if any differ, listing the offending collections.","triggerScenarios":"Submitting a chaincode upgrade where a collection keeps its name but its blockToLive value differs from the previously committed definition, e.g. changing blockToLive from 0 to 1000.","commonSituations":"Editing an exported collections JSON to 'tune' retention and reusing it in an upgrade; changing blockToLive to try to purge existing private data faster; tooling that regenerates the config with different defaults (e.g. omitting a previously set blockToLive).","solutions":["Restore the original blockToLive value for every existing collection in the new config","Keep the old collections file as the base for upgrades and only add new collections with new blockToLive values","Remember retention is fixed at creation; use new collections or off-chain purge strategies if retention must change"],"exampleFix":"// before (upgrade changing existing collection)\n[{\"name\": \"coll1\", \"blockToLive\": 1000}]\n// after (keep original; new collection may set its own)\n[{\"name\": \"coll1\", \"blockToLive\": 0}]","handlingStrategy":"validation","validationCode":"function validateBtlUnchanged(oldConfigs, newConfigs) {\n  const newMap = new Map(newConfigs.map(c => [c.name, c]));\n  const changed = oldConfigs\n    .filter(c => newMap.has(c.name) && newMap.get(c.name).blockToLive !== c.blockToLive)\n    .map(c => c.name);\n  if (changed.length) throw new Error(`blockToLive must not change for: ${changed.join(', ')}`);\n}","typeGuard":"function btlIsImmutable(oldConfigs, newConfigs) {\n  const newMap = new Map(newConfigs.map(c => [c.name, c]));\n  return oldConfigs.every(c => !newMap.has(c.name) || newMap.get(c.name).blockToLive === c.blockToLive);\n}","tryCatchPattern":"try {\n  await contract.submitTransaction('UpgradeChaincode', ...args);\n} catch (err) {\n  if (String(err).includes('BlockToLive')) {\n    // restore original blockToLive values and resubmit\n  }\n  throw err;\n}","preventionTips":["Keep the original collections file as the base for all upgrades and only append new collections","Treat blockToLive as immutable at collection design time; choose retention values carefully","Diff old vs new configs (name + blockToLive) in CI before every upgrade"],"tags":["hyperledger-fabric","private-data","block-to-live","collection-upgrade"],"backgroundTag":"collection-config-incompatible","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"}