Summary
token/services/network/fabricx/pp/versionkeeper.go models the public-parameters version as an in-process counter, and the first UpdateVersion() call is treated as initialization (no increment). Verified present on main as of 2026-08-18.
Every token transaction the endorser translates attaches a versioned read of the setup hash key (token/services/network/fabricx/endorsement/rwset.go, AddReadAt(MarshalVersion(ppVersion))). So the effective value is keeper = V_now − V_at_process_start: correct only for processes started when the chain's setup key was still at version 0.
Effect
Once the on-chain public parameters have been updated at least once (setup key row version ≥ 1), any endorser that (re)starts afterwards is permanently broken: its first lookup-poll observation counts as "initialization", the counter stays behind by exactly the number of updates it did not witness, and every transaction it endorses is rejected with ABORTED_MVCC_CONFLICT. There is no operational recovery — further PP updates keep the delta constant.
Reproduction (live, Fabric-X 4-party network)
- Deploy PP v_next on-chain (
tokengen update output, setup key row 0→1). Running endorsers observe the change live and keep working (counter = 1, matches).
- Restart one endorser. Its next endorsed transaction lands on-chain with status
ABORTED_MVCC_CONFLICT, while an endorser that was not restarted commits the identical transfer fine.
- No sequence of restarts/redeploys recovers the restarted endorser.
Suggested fix
Initialize VersionKeeper from the actual on-chain row version of the setup key (available via the query service) instead of counting observations from process start.
Related: the PP deployment transaction in tms/deployer.go hardcodes NsVersion: 0 (filed separately) — same family of "version assumed to be 0 forever" issues that only surface once a chain has history.
Summary
token/services/network/fabricx/pp/versionkeeper.gomodels the public-parameters version as an in-process counter, and the firstUpdateVersion()call is treated as initialization (no increment). Verified present onmainas of 2026-08-18.Every token transaction the endorser translates attaches a versioned read of the setup hash key (
token/services/network/fabricx/endorsement/rwset.go,AddReadAt(MarshalVersion(ppVersion))). So the effective value iskeeper = V_now − V_at_process_start: correct only for processes started when the chain's setup key was still at version 0.Effect
Once the on-chain public parameters have been updated at least once (setup key row version ≥ 1), any endorser that (re)starts afterwards is permanently broken: its first lookup-poll observation counts as "initialization", the counter stays behind by exactly the number of updates it did not witness, and every transaction it endorses is rejected with
ABORTED_MVCC_CONFLICT. There is no operational recovery — further PP updates keep the delta constant.Reproduction (live, Fabric-X 4-party network)
tokengen updateoutput, setup key row 0→1). Running endorsers observe the change live and keep working (counter = 1, matches).ABORTED_MVCC_CONFLICT, while an endorser that was not restarted commits the identical transfer fine.Suggested fix
Initialize
VersionKeeperfrom the actual on-chain row version of the setup key (available via the query service) instead of counting observations from process start.Related: the PP deployment transaction in
tms/deployer.gohardcodesNsVersion: 0(filed separately) — same family of "version assumed to be 0 forever" issues that only surface once a chain has history.