TT Lab
开始
学习 学习路径 课程

不可撤销的变更

处理了三条,宇宙节庆报告却错了

在 TT Lab 中继续学习

目标

观测太空庆典取消作业的批准、审计和当前行,制作出客户说明和机器判定基于同一依据的报告。把只读采集和保留已有文件的发布连接起来。

为什么重要

笔数相同、哈希通过、报告文字很长,都不是正确对象和当前状态的证据。请先学习 Python dict、list、异常、文件,SQL 事务,以及前面的批准版本、分块恢复课程。预计 130 分钟,所以请在到期前用“+时间”延长。必须在最长 180 分钟内完成,会话结束后文件会消失。需要的代码请另外保存。

环境与通用契约

产出物是 /root/evidence/worker.py。镜像里有 PostgreSQL 16、psycopg 3.2.3、Python 3。无需额外安装、网络和权限,用 postgres 用户写入 /root。

评分器会在本地 labdb 的独有临时 schema 中创建下面的表和虚构数据,并且只清理自己建立的 schema。使用传入的 con 的 search_path 和 DSN,不修改 public 或生产数据库。表和列的名称是下面固定的契约,SQL 数据值通过参数传递。不要把客户、ID、观测时间、schema 写死。

CREATE TABLE orders(id integer PRIMARY KEY,tenant text NOT NULL,
 qty integer NOT NULL CHECK(qty BETWEEN 1 AND 1000),
 state text NOT NULL CHECK(state IN ('pending','paid','cancelled')),
 revision integer NOT NULL CHECK(revision>=0));
CREATE TABLE changes(change_id text PRIMARY KEY,tenant text NOT NULL,targets jsonb NOT NULL);
CREATE TABLE audit(change_id text REFERENCES changes(change_id),id integer NOT NULL,
 previous_revision integer NOT NULL,new_revision integer NOT NULL,qty integer NOT NULL,
 PRIMARY KEY(change_id,id));

change_id 和 tenant 是 str 本身,ASCII 英文、数字、下划线、连字符 1–64 个字符。id 是类型恰好为 int 的 1–2147483647,qty 是类型恰好为 int 的 1–1000,批准 revision 是类型恰好为 int 的 0–2147483646。当前和审计的 revision、previous_revision、new_revision 是类型恰好为 int 的 0–2147483647。bool 不允许当作整数。

targets 是 1–64 个、只有 id、revision、qty 三个键的 dict 本身所组成的 list 本身。audit 是键为 id、previous_revision、new_revision、qty 的 dict 本身组成的列表,current 是键为 id、tenant、qty、state、revision 的 dict 本身组成的列表,各为 0–64 个。每个列表内部禁止重复 ID。current 的 state 是 pending、paid、cancelled 之一,并且禁止批准之外的 ID。audit 中批准之外的 ID 不是格式错误,而是要分析的异常。列表在规范化时生成按 ID 排序的深拷贝,不修改输入。

借来的 con 是 autocommit=True、Read Committed,调用开始时是 IDLE。capture 不关闭它,成功和失败之后恢复原隔离级别、只读设置、锁和语句限制,并回到 IDLE。不要把它包在其他外部事务里。只有 publish 拥有连接。有 fault 时,以明确的字符串为参数调用,不隐藏原始错误。

证据与分类格式

evidence 是只有 format、change_id、tenant、targets、audit、current、snapshot、observed_at 八个键的 dict 本身。format 是不是 bool 的 int 1。snapshot 是 PostgreSQL pg_current_snapshot() 的文本,不超过 4096 个字符,形式是十进制数:十进制数:可选的用逗号分隔的十进制数列表。observed_at 是 UTC ISO 字符串,以 Z 结尾,与 Python datetime.isoformat() 以 UTC 输出的形式相同。例:2026-09-13T01:02:03.123456Z。它是把数据库的 transaction_timestamp() 转换而来,并不表示权威认证时间。

capture 的 targets 用 plan 规范化。audit 里只放该 change_id 的,current 里只放与批准 ID 对应的当前行。即使当前行变成了其他客户的,要看到这个差异,也必须观测该 ID。不要因为原审计损坏就去修改或删除它。不要混入其他变更的审计和批准之外的订单。

analyze 的所有 ID list 都是升序。

客户说明与证据包契约

describe 只根据 normalize 和 analyze 的结果,按下面的顺序返回纯文本。每行末尾是 LF,最后一行之后也有一个 LF。方括号的位置填入实际的值。ID 之间以不带空格的逗号分隔,空列表用表示“无”的韩文词。

변경: [change_id] / 고객: [tenant]
관측: [observed_at] / 스냅샷: [snapshot]
판정: [hold=보류, incomplete=미완료, complete=관측 시점 완료]
승인 ID: [approved]
확정 ID: [committed]
미확정 ID: [remaining]
현재 일치 ID: [matching]
후속 차이 ID: [drifted]
누락 ID: [missing]
감사 불일치 ID: [invalid_audit]
범위: 일관된 과거 관측이며 현재 상태·출처 인증을 보장하지 않습니다.

包有 payload、sha256 两个键。payload 由 format=1、scope=historical-snapshot-not-current-state-or-authenticity、规范化的 evidence、分析 report、纯文本 summary 五个键组成。sha256 是对 payload 的 canonical 字节取的小写 64 位十六进制数。校验器会从 evidence 重新计算 payload,不只比较哈希,还比较材料、分类、说明和范围的全部。格式版本的 True 不认作 1。用不同的观测时间一致地重新生成的包也可能通过,这就是这种方式在真伪认证上的局限。

文件输出契约

输出 path 是 str 本身的绝对路径,是用户拥有的已有目录里的普通文件位置。允许文件不存在,但符号链接、目录、相对路径是 ValueError。不自动创建父目录。评分会把自己建立的临时输出文件夹作为参数传入,所以输出路径也不要写死。学员自己测试时,请使用像 /root/evidence/report.json 这样自己的文件夹。

把完整的字节写入与目标同一文件夹的唯一临时文件,经过 flush、fsync、关闭之后再替换。普通异常时清理临时文件,保留已有的或新的完整版本。它不是连强制终止、断电、其他进程替换父路径这类攻击都能应对的文件系统安全边界。只在可信的、自己拥有的文件夹里使用。

步骤

  1. 固定批准列表的含义——实现继承 Exception 的 Conflict 和 plan(change_id,tenant,targets)。校验通用输入契约,并返回只有 change_id、tenant、targets 的新 dict。targets 是按 ID 升序的深拷贝,不修改输入。错误的输入是 ValueError。
  2. 采集时不混入不同时点——capture(con,change_id,fault=None) 校验 ID 后,在自己的事务中应用 REPEATABLE READ READ ONLY 以及锁 500ms、语句 2000ms 的上限。依次采集 changes 中对应的批准、该 change_id 的审计、与批准 ID 对应的当前 orders。每次查询之后调用 after-plan、after-audit、after-current 钩子。放入同一次观测的 pg_current_snapshot() 记法和 transaction_timestamp() 的 UTC 字符串,返回下面的 evidence 格式。没有批准,或者保存的批准格式损坏,是 Conflict。
  3. 检查证据的格式和重复——normalize(evidence) 校验下面的键、类型、范围、观测记法,并返回把 targets、audit、current 按 ID 排序的深拷贝。重复 ID、批准之外的 current、未知状态是 ValueError。批准之外的 audit 不删除,而是保留下来以便之后分析。
  4. 把过去的确认和当前的差异分开——analyze(evidence) 在 normalize 之后,按下面的分类契约,返回 approved、committed、remaining、matching、drifted、missing、invalid_audit 的升序 ID list 以及 decision。不修改输入。不要只数审计笔数,也不要自动恢复当前值。
  5. 让客户说明和机器结果基于同一依据——canonical(value) 返回 UTF-8、ensure_ascii=False、sort_keys=True、separators=(逗号,冒号)、allow_nan=False 的 JSON bytes。describe(evidence) 用下面的纯文本格式说明分析结果。bundle(evidence) 把规范化的 evidence、analyze 的 report、describe 的 summary 放入 payload,并把 payload 规范字节的小写 SHA-256 放入 sha256。
  6. 连重新计算过哈希的虚假结论也要拒绝——verify_bundle(value) 校验包的结构、format、scope、哈希记法,并确认 payload 的哈希。从 evidence 重新生成 bundle,与已有的包比对整个规范字节,一致则返回分析 report 的新 dict。字节或结论、说明不一致是 Conflict,错误的格式是 ValueError。decode(raw) 把类型恰好为 bytes 的 1–1048576 个字节按 UTF-8 JSON 解析,返回 verify_bundle 的结果,并以 ValueError 拒绝重复键、非有限常量、被截断的 JSON、错误的 UTF-8。
  7. 文件替换失败时不要抹掉以前的结果——export(path,evidence,fault=None) 校验输出路径,先把规范的包做成不超过 1MiB 的 bytes。写入与目标同一目录里单独的临时文件,flush、fsync 之后关闭,并调用 after-write。用 os.replace 替换目标之后调用 after-replace,并返回包的 sha256。替换之前出错,保留已有文件;替换之后出错,保留新的完整版本。无论正常还是普通异常,都只清理自己的临时文件,并传递原始错误。
  8. 把数据库观测到报告发布连成一体——publish(dsn,change_id,path,fault=None) 在建立连接之前,校验 ID 和输出路径。用 psycopg.connect(dsn,autocommit=True,connect_timeout=2) 打开自己拥有的连接来做 capture,无论成功还是失败都关闭。采集成功之后,在数据库连接已关闭的状态下调用 export,返回 sha256。原样传递采集和发布的钩子和错误,不自动重试,也不修改业务数据库。

参考

固定批准列表的含义

实现继承 Exception 的 Conflict 和 plan(change_id,tenant,targets)。校验通用输入契约,并返回只有 change_id、tenant、targets 的新 dict。targets 是按 ID 升序的深拷贝,不修改输入。错误的输入是 ValueError。

即使笔数相同,也未必是同一个 ID 集合。不要把 bool 当作 int 接受。

采集时不混入不同时点

capture(con,change_id,fault=None) 校验 ID 后,在自己的事务中应用 REPEATABLE READ READ ONLY 以及锁 500ms、语句 2000ms 的上限。依次采集 changes 中对应的批准、该 change_id 的审计、与批准 ID 对应的当前 orders。每次查询之后调用 after-plan、after-audit、after-current 钩子。放入同一次观测的 pg_current_snapshot() 记法和 transaction_timestamp() 的 UTC 字符串,返回下面的 evidence 格式。没有批准,或者保存的批准格式损坏,是 Conflict。

另一个连接会在钩子之间真实提交。如果只用 BEGIN 把多条 SELECT 包起来,在默认隔离级别下时点会混在一起。

检查证据的格式和重复

normalize(evidence) 校验下面的键、类型、范围、观测记法,并返回把 targets、audit、current 按 ID 排序的深拷贝。重复 ID、批准之外的 current、未知状态是 ValueError。批准之外的 audit 不删除,而是保留下来以便之后分析。

请区分格式错误和业务上的不一致。批准之外的审计不是要隐藏的材料,而是要报告的材料。

把过去的确认和当前的差异分开

analyze(evidence) 在 normalize 之后,按下面的分类契约,返回 approved、committed、remaining、matching、drifted、missing、invalid_audit 的升序 ID list 以及 decision。不修改输入。不要只数审计笔数,也不要自动恢复当前值。

审计是过去确认的依据,当前行是查看后续差异的材料。即使状态相同,客户、数量、revision 也可能不同。

让客户说明和机器结果基于同一依据

canonical(value) 返回 UTF-8、ensure_ascii=False、sort_keys=True、separators=(逗号,冒号)、allow_nan=False 的 JSON bytes。describe(evidence) 用下面的纯文本格式说明分析结果。bundle(evidence) 把规范化的 evidence、analyze 的 report、describe 的 summary 放入 payload,并把 payload 规范字节的小写 SHA-256 放入 sha256。

翻译、空白、键顺序一变,哈希也会变。不要把本次格式的规范化规则,说成是其他全部 JSON 标准的保证。

连重新计算过哈希的虚假结论也要拒绝

verify_bundle(value) 校验包的结构、format、scope、哈希记法,并确认 payload 的哈希。从 evidence 重新生成 bundle,与已有的包比对整个规范字节,一致则返回分析 report 的新 dict。字节或结论、说明不一致是 Conflict,错误的格式是 ValueError。decode(raw) 把类型恰好为 bytes 的 1–1048576 个字节按 UTF-8 JSON 解析,返回 verify_bundle 的结果,并以 ValueError 拒绝重复键、非有限常量、被截断的 JSON、错误的 UTF-8。

被篡改的结论的哈希,任何人也都能重新计算。即使内部核对通过,也不等于出处认证或对当前数据库的确认。

文件替换失败时不要抹掉以前的结果

export(path,evidence,fault=None) 校验输出路径,先把规范的包做成不超过 1MiB 的 bytes。写入与目标同一目录里单独的临时文件,flush、fsync 之后关闭,并调用 after-write。用 os.replace 替换目标之后调用 after-replace,并返回包的 sha256。替换之前出错,保留已有文件;替换之后出错,保留新的完整版本。无论正常还是普通异常,都只清理自己的临时文件,并传递原始错误。

如果先用写入模式打开目标,以前的报告就会被截断。请在同一目录里做成完整版本,再一次性替换。

把数据库观测到报告发布连成一体

publish(dsn,change_id,path,fault=None) 在建立连接之前,校验 ID 和输出路径。用 psycopg.connect(dsn,autocommit=True,connect_timeout=2) 打开自己拥有的连接来做 capture,无论成功还是失败都关闭。采集成功之后,在数据库连接已关闭的状态下调用 export,返回 sha256。原样传递采集和发布的钩子和错误,不自动重试,也不修改业务数据库。

等待文件操作期间,不要一直占着快照。即使保存的报告与新观测的结论不同,也不要勉强把其中任何一个调成一致。