{"record":{"id":"8a746998cab53161","repo":"JuliusBrussee/caveman","slug":"proposalrun-seq-d-row-hash-mismatch-tampered","errorCode":null,"errorMessage":"proposalrun: seq %d row_hash mismatch (tampered)","messagePattern":"proposalrun: seq (.+?) row_hash mismatch \\(tampered\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/proposalrun/proposalrun.go","lineNumber":364,"sourceCode":"\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":346,"sourceCodeEnd":370,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/proposalrun/proposalrun.go#L346-L370","documentation":"VerifyChain recomputes each row's hash with RowHash and compares it to the stored RowHash. A mismatch means one or more of the hashed fields (prev_hash, seq, action, detail, cost_usd, created_at) was modified after the row was written — i.e. the audit record was tampered with or corrupted, or the hash was computed with a different canonicalization than the verifier uses.","triggerScenarios":"Calling VerifyChain(runs) where RowHash(recomputed) != stored r.RowHash for some seq: any field edited in the DB, created_at rewritten with different precision/timezone, cost_usd stored with lossy rounding, or a mixed library version hashing differently.","commonSituations":"Manual DB edits to detail or cost, migrations changing timestamptz precision or float storage, timezone handling that shifts created_at, application bug writing cost with rounding before hashing, actual tampering being investigated.","solutions":["Diff the reported row's fields against the original source/event to find which field changed; restore it from backup or event log.","Check for precision changes: created_at must be UTC microsecond-truncated RFC3339Nano and cost_usd fixed-6 as RowHash expects.","Verify all writers use the same library version of RowHash (canonicalization changes break old rows).","If tampering is suspected, preserve the row for forensics and escalate; do not 'fix' the hash to match the edited data.","Lock the table against direct updates (revoke UPDATE, triggers) so fields can't change post-write."],"exampleFix":"// before\ncreatedAt := time.Now() // sub-microsecond, local zone stored → hash mismatch on verify\n// after\ncreatedAt := time.Now().UTC().Truncate(time.Microsecond) // matches RowHash's formatTime","handlingStrategy":"try-catch","validationCode":"ok, _ := func() (bool, error) {\n\tgot, err := proposalrun.RowHash(r.PrevHash, r.Seq, r.Action, r.Detail, r.CostUSD, r.CreatedAt)\n\treturn err == nil && got == r.RowHash, err\n}()","typeGuard":null,"tryCatchPattern":"if err := proposalrun.VerifyChain(runs); err != nil {\n\tif strings.Contains(err.Error(), \"row_hash mismatch\") {\n\t\t// preserve row for forensics; do not overwrite hash\n\t}\n\treturn err\n}","preventionTips":["Store created_at UTC microsecond-truncated and cost fixed-6 exactly as RowHash formats them","Pin the RowHash library version; re-hash explicitly if canonicalization ever changes","Revoke UPDATE/DELETE on the runs table to keep written rows immutable"],"tags":["audit-chain","tampering","hash-mismatch","integrity"],"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"}