{"record":{"id":"4e1df9686206b521","repo":"JuliusBrussee/caveman","slug":"proposalrun-seq-d-prev-hash-does-not-link-to-the","errorCode":null,"errorMessage":"proposalrun: seq %d prev_hash does not link to the prior row","messagePattern":"proposalrun: seq (.+?) prev_hash does not link to the prior row","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/proposalrun/proposalrun.go","lineNumber":357,"sourceCode":"//   - each row's row_hash recomputes from its own stored fields (no tampering).\n//\n// It returns nil for an empty slice (a proposal with no runs yet is consistent).\nfunc VerifyChain(runs []Run) error {\n\tif len(runs) == 0 {\n\t\treturn nil\n\t}\n\tsorted := make([]Run, len(runs))\n\tcopy(sorted, runs)\n\tsort.Slice(sorted, func(i, j int) bool { return sorted[i].Seq < sorted[j].Seq })\n\n\tprevHash := \"\"\n\tfor i, r := range sorted {\n\t\twantSeq := int64(i + 1)\n\t\tif r.Seq != wantSeq {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d out of order (expected %d)\", r.Seq, wantSeq)\n\t\t}\n\t\tif r.PrevHash != prevHash {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d prev_hash does not link to the prior row\", r.Seq)\n\t\t}\n\t\tgot, err := RowHash(r.PrevHash, r.Seq, r.Action, r.Detail, r.CostUSD, r.CreatedAt)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d recompute: %w\", r.Seq, err)\n\t\t}\n\t\tif got != r.RowHash {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d row_hash mismatch (tampered)\", r.Seq)\n\t\t}\n\t\tprevHash = r.RowHash\n\t}\n\treturn nil\n}\n","sourceCodeStart":339,"sourceCodeEnd":370,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/proposalrun/proposalrun.go#L339-L370","documentation":"VerifyChain checks that each row's PrevHash equals the previous row's RowHash (empty for seq 1). This error means the hash link between two adjacent rows is broken: the row's prev_hash was edited, rows were reordered or transplanted from another chain, or a row was inserted without updating descendants' prev_hash values.","triggerScenarios":"Calling VerifyChain(runs) where sorted[i].PrevHash != sorted[i-1].RowHash — e.g. prev_hash manually updated, a row copied from another proposal, or a row appended without chaining to the prior hash.","commonSituations":"Manual SQL updates to repair 'wrong-looking' hashes, importing rows from a different run set, bugs in custom writers that bypass the Append helper, forked chains after a restore.","solutions":["Locate the first broken link (the reported seq) and recompute the chain from that point using the library's Append/RowHash functions.","Restore affected rows from a backup taken before the manual edit.","Rebuild the whole proposal's chain from the original events if the source of truth is available.","Prevent direct UPDATE/DELETE on the table (revoke privileges, triggers) so links can only change via the append API."],"exampleFix":"// before\ndb.Exec(\"UPDATE proposal_runs SET prev_hash = $1 WHERE seq = $2\", someHash, seq)\n// after\nrun := buildNextRun(prevRow, action, detail, cost) // Append computes prev_hash from prevRow.RowHash","handlingStrategy":"try-catch","validationCode":"func linksIntact(runs []proposalrun.Run) bool {\n\tsorted := append([]proposalrun.Run(nil), runs...)\n\tsort.Slice(sorted, func(i, j int) bool { return sorted[i].Seq < sorted[j].Seq })\n\tprev := \"\"\n\tfor _, r := range sorted {\n\t\tif r.PrevHash != prev { return false }\n\t\tprev = r.RowHash\n\t}\n\treturn true\n}","typeGuard":null,"tryCatchPattern":"if err := proposalrun.VerifyChain(runs); err != nil {\n\tif strings.Contains(err.Error(), \"prev_hash does not link\") {\n\t\t// rebuild chain from that seq via the append API\n\t}\n\treturn err\n}","preventionTips":["Only write rows through the library's Append helper so prev_hash is chained automatically","Revoke direct UPDATE on the runs table","Snapshot/backup the chain regularly for recovery"],"tags":["audit-chain","hash-chain","integrity","tampering"],"backgroundTag":"checksum-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}