kairos-chain 3.45.0 → 3.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/synoptis/bin/rpr_verify.rb +171 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/rpr-1.md +141 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/reproduction.rb +316 -0
- data/templates/skillsets/synoptis/test/test_rpr_reproduction.rb +238 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 372dd921a298d45c97b957ae724a253bb22da73b5bb211025fe894eab5de4ff7
|
|
4
|
+
data.tar.gz: f15e37a2caeef2d4d08b92f49608aa52307b1a93124a1742d3f40c10ae145bf8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9fc60d9acee8dcb8b7a2bfb43021d3c8b129cb94471fe8488092683ade4755665faa72860fb8c1bed77d254e977226c3d8cd85bec4dfe95eea2d0dc12352190
|
|
7
|
+
data.tar.gz: e5769c4b23ce1daef9704423f41e097d8969f5b86442ab1592beab4e3dc7623a3a79e30aba51d7ca2d5621e55fd635db740b5489761995bc2b9b33de8eb0b80e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.46.0] - 2026-07-22
|
|
8
|
+
|
|
9
|
+
### Synoptis — reproduction endorsement rpr-1 (AUD-L3 slice 1)
|
|
10
|
+
|
|
11
|
+
Implements AUD-L3 design v0.4 (RPR-1..5, review-converged) slice 1 in the
|
|
12
|
+
synoptis SkillSet. All additive: rpr-1 builds on map-1/khab-1 exactly as
|
|
13
|
+
map-1 built on khab-1; no pre-existing file changed.
|
|
14
|
+
|
|
15
|
+
- `conventions/rpr-1.md`: content-addressed reproduction-endorsement
|
|
16
|
+
convention — re-execution target (inputs/environment/pipeline/output
|
|
17
|
+
digests; the first three form the committed computation identification,
|
|
18
|
+
output excluded), target-bound result-free tolerance declaration
|
|
19
|
+
(bit-identity at rpr-1), reproduction endorsement carrying the adjudicated
|
|
20
|
+
verdict (reproduced / not-reproduced) and its adjudication mode
|
|
21
|
+
(hand / procedure), declaration-set assessment (order-independent, sibling
|
|
22
|
+
targets pooled by computation identification, residue disclosed), map-1 §3
|
|
23
|
+
retraction reused unchanged.
|
|
24
|
+
- `reproduction.rb`: build/parse/digest for target, tolerance, endorsement
|
|
25
|
+
(closed schemas, canonical-serialization equality, refuse-not-coerce);
|
|
26
|
+
endorsement signing/verification via the unchanged map-1 §1.1 attestation
|
|
27
|
+
signature (ChainCredential); foreignness conformance check (RPR-4);
|
|
28
|
+
declaration-set assessment reporting invoked commitment (earliest
|
|
29
|
+
anterior), posterior commitments, digest-tiebroken rank, distinct-digest
|
|
30
|
+
multiplicity, and unresolved-equivalence residue.
|
|
31
|
+
- `bin/rpr_verify.rb`: offline auditor verifier (target / tolerance /
|
|
32
|
+
endorsement / assess / convention). Strict intake (at most one trailing
|
|
33
|
+
LF via delete_suffix — CRLF refused), canonical base-10 numerals only,
|
|
34
|
+
exit-code discipline (0 verified/report, 1 rejected, 2 unresolvable),
|
|
35
|
+
RPR-1/RPR-4 condition disclosure in output.
|
|
36
|
+
- `test/test_rpr_reproduction.rb`: 37 design-constraint assertions
|
|
37
|
+
(RPR-1..5), including presentation-order independence of the assessment
|
|
38
|
+
and rank, CRLF/padding refusal, and map-1 non-interference (75/75
|
|
39
|
+
regression green).
|
|
40
|
+
|
|
41
|
+
Implementation review: 3 rounds, R3 5/5 APPROVE, zero remaining P0/P1
|
|
42
|
+
(executable-probe personas; dispositions in docs/drafts/).
|
|
43
|
+
|
|
7
44
|
## [3.45.0] - 2026-07-21
|
|
8
45
|
|
|
9
46
|
### Synoptis — mutual anchoring protocol MAP-1..4 (map-1, AUD-L2)
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# rpr_verify — offline auditor verifier for rpr-1 reproduction-endorsement
|
|
5
|
+
# artifacts (RPR-1..5, aud_l3_reproducibility_design v0.4).
|
|
6
|
+
#
|
|
7
|
+
# Verifies with EXACTLY the disclosed trust base: the artifacts named on the
|
|
8
|
+
# command line plus the shipped rpr-1 and map-1 convention definitions. No
|
|
9
|
+
# registry, no network, no operator cooperation. (Authentic chain views are
|
|
10
|
+
# the caller's inputs; their integrity is the khab-1/map-1 surfaces' business.)
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# rpr_verify.rb target <target.json>
|
|
14
|
+
# rpr_verify.rb tolerance <tolerance.json> [target.json]
|
|
15
|
+
# with target.json: additionally checks the binding matches that target.
|
|
16
|
+
# rpr_verify.rb endorsement <endorsement.json> <endorser_credential.json> <signature-hex> [operator_credential.json]
|
|
17
|
+
# with operator credential: additionally checks foreignness (RPR-4).
|
|
18
|
+
# rpr_verify.rb assess <targets.json> <declarations.json> <invoked_tolerance_sha256> <endorsement_position>
|
|
19
|
+
# targets.json = JSON array of target record STRINGS
|
|
20
|
+
# declarations.json = JSON array of {"tolerance": "<record string>", "position": <int>}
|
|
21
|
+
# rpr_verify.rb convention
|
|
22
|
+
# prints the shipped rpr-1 convention digest.
|
|
23
|
+
#
|
|
24
|
+
# Exit status: 0 = VERIFIED/REPORT, 1 = REJECTED, 2 = usage / unresolvable.
|
|
25
|
+
|
|
26
|
+
require 'json'
|
|
27
|
+
require 'digest'
|
|
28
|
+
require_relative '../lib/synoptis/anchoring/entry'
|
|
29
|
+
require_relative '../lib/synoptis/anchoring/chain_credential'
|
|
30
|
+
require_relative '../lib/synoptis/anchoring/reproduction'
|
|
31
|
+
|
|
32
|
+
CRED = Synoptis::Anchoring::ChainCredential
|
|
33
|
+
REPRO = Synoptis::Anchoring::Reproduction
|
|
34
|
+
|
|
35
|
+
def die(msg)
|
|
36
|
+
warn "rpr_verify: #{msg}"
|
|
37
|
+
exit 2
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reject(msg)
|
|
41
|
+
puts "REJECTED: #{msg}"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Read a record artifact: at most one trailing newline is tolerated (file
|
|
46
|
+
# convenience); any other surrounding bytes stay in the string and fail the
|
|
47
|
+
# canonical check downstream (refuse-not-coerce — a padded artifact is not
|
|
48
|
+
# the canonical record).
|
|
49
|
+
def read_file(path)
|
|
50
|
+
# delete_suffix, not chomp: chomp("\n") also swallows "\r\n" and a bare
|
|
51
|
+
# trailing "\r", which would coerce CRLF artifacts into canonical form.
|
|
52
|
+
File.read(path).delete_suffix("\n")
|
|
53
|
+
rescue SystemCallError => e
|
|
54
|
+
die "cannot read #{path}: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def load_json(path)
|
|
58
|
+
JSON.parse(File.read(path))
|
|
59
|
+
rescue JSON::ParserError, SystemCallError => e
|
|
60
|
+
die "cannot read #{path}: #{e.message}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def load_json_hash(path, label)
|
|
64
|
+
parsed = load_json(path)
|
|
65
|
+
die "#{path}: #{label} must be a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
|
|
66
|
+
parsed
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Canonical base-10 numerals only: leading zeros are refused, not
|
|
70
|
+
# reinterpreted (a non-canonical numeral is a malformed input, exit 2).
|
|
71
|
+
def strict_int(value, label)
|
|
72
|
+
die "#{label} must be a canonical base-10 integer, got #{value.inspect}" unless value.is_a?(String) && value.match?(/\A(0|[1-9]\d*)\z/)
|
|
73
|
+
Integer(value, 10)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
HEX64 = /\A[a-f0-9]{64}\z/
|
|
77
|
+
|
|
78
|
+
mode = ARGV.shift
|
|
79
|
+
|
|
80
|
+
case mode
|
|
81
|
+
when 'target'
|
|
82
|
+
die 'usage: rpr_verify.rb target <target.json>' unless ARGV.size == 1
|
|
83
|
+
record = read_file(ARGV[0])
|
|
84
|
+
begin
|
|
85
|
+
REPRO.parse_target!(record)
|
|
86
|
+
rescue REPRO::ReproductionError => e
|
|
87
|
+
reject e.message
|
|
88
|
+
end
|
|
89
|
+
puts "VERIFIED: target digest #{REPRO.target_digest(record)}"
|
|
90
|
+
puts " computation identification: #{REPRO.computation_id(record).split('|').map { |h| h[0, 12] }.join('… | ')}… (output excluded, rpr-1 §1)"
|
|
91
|
+
puts 'NOTE: binding fixes referents, never validates them (RPR-2, MPR-4).'
|
|
92
|
+
|
|
93
|
+
when 'tolerance'
|
|
94
|
+
die 'usage: rpr_verify.rb tolerance <tolerance.json> [target.json]' unless (1..2).cover?(ARGV.size)
|
|
95
|
+
record = read_file(ARGV[0])
|
|
96
|
+
tol = begin
|
|
97
|
+
REPRO.parse_tolerance!(record)
|
|
98
|
+
rescue REPRO::ReproductionError => e
|
|
99
|
+
reject e.message
|
|
100
|
+
end
|
|
101
|
+
if ARGV[1]
|
|
102
|
+
target = read_file(ARGV[1])
|
|
103
|
+
begin
|
|
104
|
+
digest = REPRO.target_digest(target)
|
|
105
|
+
rescue REPRO::ReproductionError => e
|
|
106
|
+
reject e.message
|
|
107
|
+
end
|
|
108
|
+
reject "tolerance binds target #{tol['target_sha256'][0, 12]}…, not the supplied target #{digest[0, 12]}…" unless tol['target_sha256'] == digest
|
|
109
|
+
end
|
|
110
|
+
puts "VERIFIED: tolerance digest #{Digest::SHA256.hexdigest(record)} (kind #{tol['kind']}, target #{tol['target_sha256'][0, 12]}…)"
|
|
111
|
+
puts 'NOTE: anteriority to any endorsement is decided by committed order (MPR-8), not by this tool.'
|
|
112
|
+
|
|
113
|
+
when 'endorsement'
|
|
114
|
+
die 'usage: rpr_verify.rb endorsement <endorsement.json> <endorser_credential.json> <signature-hex> [operator_credential.json]' unless (3..4).cover?(ARGV.size)
|
|
115
|
+
record = read_file(ARGV[0])
|
|
116
|
+
cred = load_json_hash(ARGV[1], 'endorser credential')
|
|
117
|
+
ok = begin
|
|
118
|
+
REPRO.verify_endorsement(record, cred, ARGV[2])
|
|
119
|
+
rescue REPRO::ReproductionError, CRED::CredentialError => e
|
|
120
|
+
reject e.message
|
|
121
|
+
end
|
|
122
|
+
reject 'endorsement signature does not verify under the endorser credential' unless ok
|
|
123
|
+
e = REPRO.parse_endorsement!(record)
|
|
124
|
+
if ARGV[3]
|
|
125
|
+
operator = load_json_hash(ARGV[3], 'operator credential')
|
|
126
|
+
begin
|
|
127
|
+
reject 'endorser credential equals operator credential — same-party, not a conforming rpr-1 endorsement (RPR-4)' unless REPRO.foreign?(cred, operator)
|
|
128
|
+
rescue CRED::CredentialError => err
|
|
129
|
+
# An unresolvable operator credential is a caller-input problem, not a
|
|
130
|
+
# verdict about the endorsement (exit-code discipline: 2, not 1).
|
|
131
|
+
die "operator credential unresolvable: #{err.message}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
puts "VERIFIED: #{e['verdict']} endorsement of target #{e['target_sha256'][0, 12]}… under tolerance #{e['tolerance_sha256'][0, 12]}…"
|
|
135
|
+
puts " adjudication mode: #{e['adjudication_mode']}#{e['procedure_sha256'] ? " (procedure #{e['procedure_sha256'][0, 12]}…)" : ''}"
|
|
136
|
+
puts 'CONDITIONS (travel with every stronger reading, rpr-1 §3):'
|
|
137
|
+
puts ' - the verdict asserts reproduction or its failure, never correctness (RPR-1, MPR-6)'
|
|
138
|
+
puts ' - evidence of a committed claim, never proof a re-execution happened (RPR-4)'
|
|
139
|
+
puts ' - distinctness is not independence; a colluding pair can fabricate (RPR-4)'
|
|
140
|
+
puts ' - the named mode is a declaration, not a proof (map-1 §3 non-self-certifying)'
|
|
141
|
+
if ARGV[3]
|
|
142
|
+
puts ' - foreignness checked against the SUPPLIED operator credential only'
|
|
143
|
+
else
|
|
144
|
+
puts ' - foreignness (RPR-4 conformance condition) NOT assessed — no operator credential supplied'
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
when 'assess'
|
|
148
|
+
die 'usage: rpr_verify.rb assess <targets.json> <declarations.json> <invoked_tolerance_sha256> <endorsement_position>' unless ARGV.size == 4
|
|
149
|
+
targets = load_json(ARGV[0])
|
|
150
|
+
die "#{ARGV[0]}: expected a JSON array of target record strings" unless targets.is_a?(Array) && targets.all? { |t| t.is_a?(String) }
|
|
151
|
+
declarations = load_json(ARGV[1])
|
|
152
|
+
die "#{ARGV[1]}: expected a JSON array of {tolerance, position} objects" unless declarations.is_a?(Array)
|
|
153
|
+
invoked = ARGV[2]
|
|
154
|
+
die 'invoked_tolerance_sha256 must be 64-char lowercase hex' unless invoked.match?(HEX64)
|
|
155
|
+
position = strict_int(ARGV[3], 'endorsement_position')
|
|
156
|
+
begin
|
|
157
|
+
report = REPRO.assess_declarations(targets: targets, declarations: declarations,
|
|
158
|
+
invoked_tolerance_sha256: invoked, endorsement_position: position)
|
|
159
|
+
rescue REPRO::ReproductionError => e
|
|
160
|
+
reject e.message
|
|
161
|
+
end
|
|
162
|
+
puts JSON.pretty_generate(report)
|
|
163
|
+
puts 'NOTE: the assessment reports, the reader prices (RPR-3: exposure, not prevention).'
|
|
164
|
+
puts 'NOTE: a produced report always exits 0; the conformance bit is invoked_conforming in the report.'
|
|
165
|
+
|
|
166
|
+
when 'convention'
|
|
167
|
+
puts "rpr-1 convention sha256: #{REPRO.convention_sha256}"
|
|
168
|
+
|
|
169
|
+
else
|
|
170
|
+
die 'usage: rpr_verify.rb target|tolerance|endorsement|assess|convention ... (see header)'
|
|
171
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# rpr-1 — KairosChain Reproduction Endorsement Convention, version 1
|
|
2
|
+
|
|
3
|
+
Status: committed verification convention (RPR-1..5 of
|
|
4
|
+
aud_l3_reproducibility_design v0.4). The SHA-256 over this file's raw bytes is
|
|
5
|
+
the convention integrity digest; every artifact that names `rpr-1` commits it
|
|
6
|
+
as its convention reference. A verifier holding a definition whose digest does
|
|
7
|
+
not match the committed one MUST treat the artifact's convention as
|
|
8
|
+
unresolvable and refuse to verify under it. Any change to this convention is a
|
|
9
|
+
new identifier (`rpr-2`, ...); this file is never edited in place.
|
|
10
|
+
rpr-1 builds on map-1 (credentials, attestation signatures, attestation types,
|
|
11
|
+
retraction) and khab-1 (committed identity, committed order) and modifies
|
|
12
|
+
neither: a map-1 artifact is valid with or without any rpr-1 artifact present.
|
|
13
|
+
|
|
14
|
+
All record literals below are canonical JSON (keys sorted recursively,
|
|
15
|
+
`JSON.generate` compact form). A record is valid only in its canonical
|
|
16
|
+
serialization with exactly the fields shown — a non-canonical or extra-field
|
|
17
|
+
record is not an rpr-1 artifact and is ignored as noise (map-1 §4 precedent).
|
|
18
|
+
|
|
19
|
+
## 1. Re-execution target (RPR-2)
|
|
20
|
+
|
|
21
|
+
{"environment_sha256":"<64-hex>","format":"rpr-1/target",
|
|
22
|
+
"input_sha256":"<64-hex>","output_sha256":"<64-hex>",
|
|
23
|
+
"pipeline_sha256":"<64-hex>"}
|
|
24
|
+
|
|
25
|
+
The target commits both halves of the verdict's referent: what is re-run
|
|
26
|
+
(inputs, environment, pipeline version) and what the re-run output is judged
|
|
27
|
+
against (the committed output). The first three digests — `input_sha256`,
|
|
28
|
+
`environment_sha256`, `pipeline_sha256` — constitute the target's **committed
|
|
29
|
+
computation identification** (design §3(b)): the identification of what is
|
|
30
|
+
re-run, which two targets can share while naming different committed outputs.
|
|
31
|
+
`output_sha256` completes the target but does not participate in that
|
|
32
|
+
identification. The target digest is the SHA-256 of the target's canonical
|
|
33
|
+
JSON. What the digests are digests OF (file formats, environment pinning,
|
|
34
|
+
pipeline identity) is the referenced material's business; rpr-1 commits
|
|
35
|
+
references, it does not validate referents (MPR-4 asymmetry, RPR-2 limit).
|
|
36
|
+
|
|
37
|
+
## 2. Tolerance declaration (RPR-3)
|
|
38
|
+
|
|
39
|
+
{"format":"rpr-1/tolerance","kind":"bit-identity","target_sha256":"<64-hex>"}
|
|
40
|
+
|
|
41
|
+
A tolerance declaration is bound to a named target and is result-free by
|
|
42
|
+
construction: the schema is closed and `kind` is the only expressive field, so
|
|
43
|
+
no outcome-referencing content can be expressed. At rpr-1 the only kind is
|
|
44
|
+
`bit-identity` (re-run output matches the committed output byte for byte);
|
|
45
|
+
richer tolerance grammars (numerical bounds, domain equivalence) are a new
|
|
46
|
+
convention — this narrowness is disclosed, not hidden. The declaration is
|
|
47
|
+
committed as an internal-chain record ahead of the endorsements it governs;
|
|
48
|
+
committed position provides decidable anteriority (MPR-8). One declaration has
|
|
49
|
+
exactly one digest (canonical serialization).
|
|
50
|
+
|
|
51
|
+
### 2.1 Declaration-set assessment (RPR-3)
|
|
52
|
+
|
|
53
|
+
Conformance of an endorsement's tolerance is assessed against the record, not
|
|
54
|
+
the endorser's selection. Given target records, tolerance declarations with
|
|
55
|
+
their committed positions, and the endorsement's committed position, the
|
|
56
|
+
assessment reports:
|
|
57
|
+
|
|
58
|
+
- every anterior declaration bound to the endorsement's target;
|
|
59
|
+
- every anterior declaration bound to any sibling target sharing the same
|
|
60
|
+
committed computation identification (§1) — a menu spread across
|
|
61
|
+
near-duplicate targets is the same menu and is read as one set (at rpr-1,
|
|
62
|
+
sharing = equality of the three computation-identification digests);
|
|
63
|
+
- the multiplicity of that pooled set and the invoked declaration's place in
|
|
64
|
+
it;
|
|
65
|
+
- the unresolved residue: declarations whose `target_sha256` resolves to no
|
|
66
|
+
supplied target record (sharing undecidable there), disclosed rather than
|
|
67
|
+
silently dropped.
|
|
68
|
+
|
|
69
|
+
The assessment reports; the reader prices. A verdict judged under one of many
|
|
70
|
+
anterior declarations is conforming and wears the multiplicity openly; a
|
|
71
|
+
tolerance chosen after the result is non-conforming (its posteriority is
|
|
72
|
+
decidable from committed order). The assessment is a function of the committed
|
|
73
|
+
record alone, never of presentation order: a declaration committed at several
|
|
74
|
+
positions is one declaration (one digest) with several commitments; the
|
|
75
|
+
invoked declaration conforms iff any of its commitments is anterior,
|
|
76
|
+
represented by the earliest, with posterior commitments disclosed alongside;
|
|
77
|
+
multiplicity counts distinct declarations, and the invoked declaration's
|
|
78
|
+
place is reported as its rank in the pooled anterior set ordered by earliest
|
|
79
|
+
anterior position, with equal positions ordered by declaration digest — so
|
|
80
|
+
every reported quantity, ranks included, is decidable from the committed
|
|
81
|
+
record alone.
|
|
82
|
+
|
|
83
|
+
## 3. Reproduction endorsement (RPR-1, RPR-4)
|
|
84
|
+
|
|
85
|
+
hand-adjudicated:
|
|
86
|
+
|
|
87
|
+
{"adjudication_mode":"hand","format":"rpr-1/endorsement",
|
|
88
|
+
"target_sha256":"<64-hex>","tolerance_sha256":"<64-hex>",
|
|
89
|
+
"verdict":"reproduced"}
|
|
90
|
+
|
|
91
|
+
procedure-adjudicated:
|
|
92
|
+
|
|
93
|
+
{"adjudication_mode":"procedure","format":"rpr-1/endorsement",
|
|
94
|
+
"procedure_sha256":"<64-hex>","target_sha256":"<64-hex>",
|
|
95
|
+
"tolerance_sha256":"<64-hex>","verdict":"not-reproduced"}
|
|
96
|
+
|
|
97
|
+
`verdict` is `reproduced` or `not-reproduced`: the affirmative and the
|
|
98
|
+
negative verdict are the same kind of record under the same bindings, so the
|
|
99
|
+
record has a conforming place for failure as well as success (RPR-1).
|
|
100
|
+
`adjudication_mode` names the gate behind the verdict (RPR-4): `hand`, or
|
|
101
|
+
`procedure` with `procedure_sha256` naming the committed adjudication
|
|
102
|
+
procedure whose anterior adoption is the judgment act. `procedure_sha256` is
|
|
103
|
+
present exactly when the mode is `procedure` (closed schema per mode). The
|
|
104
|
+
named mode, like the attestation type itself, is a declaration and not a
|
|
105
|
+
proof (map-1 §3 non-self-certifying).
|
|
106
|
+
|
|
107
|
+
The endorsement is signed under the endorser's map-1 credential using the
|
|
108
|
+
map-1 §1.1 attestation signature, with the payload being the endorsement
|
|
109
|
+
record string. Verification needs only the credential, the record, and the
|
|
110
|
+
signature. The endorsement is carried with `attestation_type` =
|
|
111
|
+
`quality-endorsement` (map-1 §3, unchanged).
|
|
112
|
+
|
|
113
|
+
Foreignness (RPR-4) is a conformance condition: an endorsement whose issuer
|
|
114
|
+
credential digest equals the operator credential digest for the target's
|
|
115
|
+
chain is not a conforming rpr-1 endorsement, whatever else it may be as an
|
|
116
|
+
ordinary same-party claim. Distinctness is not independence: a colluding pair
|
|
117
|
+
can fabricate an endorsement, affirmative or negative, without any run having
|
|
118
|
+
occurred — the endorsement is evidence of a committed claim, never proof that
|
|
119
|
+
a re-execution happened (RPR-4 disclosed limit).
|
|
120
|
+
|
|
121
|
+
What a verified endorsement asserts is exactly: this endorser claimed, at a
|
|
122
|
+
committed moment, that the target was re-run and its output did or did not
|
|
123
|
+
match the target's committed output within the named tolerance — and nothing
|
|
124
|
+
else. No claim of correctness, validity, fitness, or significance (RPR-1,
|
|
125
|
+
MPR-6); absence of any endorsement proves nothing (MPR-9 non-production).
|
|
126
|
+
|
|
127
|
+
## 4. Retraction (RPR-5)
|
|
128
|
+
|
|
129
|
+
A mistaken endorsement is withdrawn by the map-1 §3 `retraction` unchanged:
|
|
130
|
+
same issuer, unambiguous target, appended. A retracted endorsement still
|
|
131
|
+
stands as a committed record that the verdict was once claimed and later
|
|
132
|
+
taken back; nothing is unsaid by deletion and no anchored commitment's
|
|
133
|
+
coverage is subtracted (AHM-4).
|
|
134
|
+
|
|
135
|
+
## 5. Verification procedure and trust base
|
|
136
|
+
|
|
137
|
+
All rpr-1 verification needs only: the artifacts named above, this
|
|
138
|
+
definition, the map-1 definition (for credentials, signatures, types, and
|
|
139
|
+
retraction), and authentic views of the logs/chains the artifacts reference.
|
|
140
|
+
No registry, no network, no operator cooperation for verification —
|
|
141
|
+
production of artifacts remains the endorser's act (MPR-4 asymmetry).
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'entry'
|
|
6
|
+
require_relative 'chain_credential'
|
|
7
|
+
|
|
8
|
+
module Synoptis
|
|
9
|
+
module Anchoring
|
|
10
|
+
# Reproduction endorsement artifacts (aud_l3_reproducibility_design v0.4,
|
|
11
|
+
# RPR-1..5) under the rpr-1 convention: re-execution target, tolerance
|
|
12
|
+
# declaration, reproduction endorsement, and the declaration-set
|
|
13
|
+
# assessment. rpr-1 builds on map-1 (credentials, §1.1 attestation
|
|
14
|
+
# signatures, types, retraction) and modifies nothing there: signing and
|
|
15
|
+
# verification are ChainCredential calls, retraction is the map-1 §3 act
|
|
16
|
+
# unchanged. Refuse-not-coerce for authored artifacts; the assessment is
|
|
17
|
+
# diagnostic and reports rather than enforces (RPR-3: exposure, not
|
|
18
|
+
# prevention).
|
|
19
|
+
module Reproduction
|
|
20
|
+
CONVENTION_ID = 'rpr-1'
|
|
21
|
+
CONVENTION_PATH = File.expand_path('conventions/rpr-1.md', __dir__)
|
|
22
|
+
TARGET_FORMAT = 'rpr-1/target'
|
|
23
|
+
TOLERANCE_FORMAT = 'rpr-1/tolerance'
|
|
24
|
+
ENDORSEMENT_FORMAT = 'rpr-1/endorsement'
|
|
25
|
+
TOLERANCE_KINDS = %w[bit-identity].freeze
|
|
26
|
+
VERDICTS = %w[reproduced not-reproduced].freeze
|
|
27
|
+
MODES = %w[hand procedure].freeze
|
|
28
|
+
HEX_DIGEST = /\A[a-f0-9]{64}\z/
|
|
29
|
+
|
|
30
|
+
# Exactly these fields, sorted (closed schemas; extension = rpr-2).
|
|
31
|
+
TARGET_FIELDS = %w[environment_sha256 format input_sha256 output_sha256 pipeline_sha256].freeze
|
|
32
|
+
# The first three digests in field-name-independent terms: what is
|
|
33
|
+
# re-run. output_sha256 completes the target but is OUTSIDE the
|
|
34
|
+
# committed computation identification (design §3(b)).
|
|
35
|
+
COMPUTATION_ID_FIELDS = %w[environment_sha256 input_sha256 pipeline_sha256].freeze
|
|
36
|
+
TOLERANCE_FIELDS = %w[format kind target_sha256].freeze
|
|
37
|
+
ENDORSEMENT_FIELDS_HAND = %w[adjudication_mode format target_sha256 tolerance_sha256 verdict].freeze
|
|
38
|
+
ENDORSEMENT_FIELDS_PROCEDURE = %w[adjudication_mode format procedure_sha256 target_sha256 tolerance_sha256 verdict].freeze
|
|
39
|
+
|
|
40
|
+
class ReproductionError < StandardError; end
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# SHA-256 of the shipped rpr-1 convention definition's raw bytes.
|
|
45
|
+
def convention_sha256
|
|
46
|
+
@convention_sha256 ||= begin
|
|
47
|
+
Digest::SHA256.hexdigest(File.binread(CONVENTION_PATH))
|
|
48
|
+
rescue SystemCallError => e
|
|
49
|
+
raise ReproductionError, "rpr-1 convention definition unreadable at #{CONVENTION_PATH}: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# -- re-execution target (rpr-1 §1, RPR-2) --
|
|
54
|
+
|
|
55
|
+
def build_target(input_sha256:, environment_sha256:, pipeline_sha256:, output_sha256:)
|
|
56
|
+
record = {
|
|
57
|
+
'environment_sha256' => environment_sha256,
|
|
58
|
+
'format' => TARGET_FORMAT,
|
|
59
|
+
'input_sha256' => input_sha256,
|
|
60
|
+
'output_sha256' => output_sha256,
|
|
61
|
+
'pipeline_sha256' => pipeline_sha256
|
|
62
|
+
}
|
|
63
|
+
%w[input_sha256 environment_sha256 pipeline_sha256 output_sha256].each do |k|
|
|
64
|
+
unless record[k].is_a?(String) && record[k].match?(HEX_DIGEST)
|
|
65
|
+
raise ReproductionError, "target.#{k} must be 64-char lowercase hex, got #{record[k].inspect}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
Entry.canonical_json(record)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_target!(target_string)
|
|
72
|
+
parse_record!(target_string, TARGET_FORMAT, TARGET_FIELDS, 'target') do |t|
|
|
73
|
+
(TARGET_FIELDS - %w[format]).each do |k|
|
|
74
|
+
raise ReproductionError, "target.#{k} must be 64-char lowercase hex" unless t[k].is_a?(String) && t[k].match?(HEX_DIGEST)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def target_digest(target_string)
|
|
80
|
+
parse_target!(target_string)
|
|
81
|
+
Digest::SHA256.hexdigest(target_string.to_s)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The committed computation identification (design §3(b)): the first
|
|
85
|
+
# three digests. Two targets SHARE a computation iff these are equal;
|
|
86
|
+
# output_sha256 never participates.
|
|
87
|
+
def computation_id(target_string)
|
|
88
|
+
t = parse_target!(target_string)
|
|
89
|
+
COMPUTATION_ID_FIELDS.map { |k| t[k] }.join('|')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# -- tolerance declaration (rpr-1 §2, RPR-3) --
|
|
93
|
+
|
|
94
|
+
def build_tolerance(target_sha256:, kind: 'bit-identity')
|
|
95
|
+
raise ReproductionError, "tolerance.kind must be one of #{TOLERANCE_KINDS.join(', ')}, got #{kind.inspect}" unless TOLERANCE_KINDS.include?(kind.to_s)
|
|
96
|
+
unless target_sha256.is_a?(String) && target_sha256.match?(HEX_DIGEST)
|
|
97
|
+
raise ReproductionError, 'tolerance.target_sha256 must be 64-char lowercase hex'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
Entry.canonical_json('format' => TOLERANCE_FORMAT, 'kind' => kind.to_s, 'target_sha256' => target_sha256)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_tolerance!(tolerance_string)
|
|
104
|
+
parse_record!(tolerance_string, TOLERANCE_FORMAT, TOLERANCE_FIELDS, 'tolerance') do |t|
|
|
105
|
+
raise ReproductionError, "tolerance.kind must be one of #{TOLERANCE_KINDS.join(', ')}" unless TOLERANCE_KINDS.include?(t['kind'])
|
|
106
|
+
unless t['target_sha256'].is_a?(String) && t['target_sha256'].match?(HEX_DIGEST)
|
|
107
|
+
raise ReproductionError, 'tolerance.target_sha256 must be 64-char lowercase hex'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def tolerance_digest(tolerance_string)
|
|
113
|
+
parse_tolerance!(tolerance_string)
|
|
114
|
+
Digest::SHA256.hexdigest(tolerance_string.to_s)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# -- reproduction endorsement (rpr-1 §3, RPR-1/RPR-4) --
|
|
118
|
+
|
|
119
|
+
def build_endorsement(target_sha256:, tolerance_sha256:, verdict:, adjudication_mode:, procedure_sha256: nil)
|
|
120
|
+
mode = adjudication_mode.to_s
|
|
121
|
+
raise ReproductionError, "verdict must be one of #{VERDICTS.join(', ')}, got #{verdict.inspect}" unless VERDICTS.include?(verdict.to_s)
|
|
122
|
+
raise ReproductionError, "adjudication_mode must be one of #{MODES.join(', ')}, got #{mode.inspect}" unless MODES.include?(mode)
|
|
123
|
+
|
|
124
|
+
record = {
|
|
125
|
+
'adjudication_mode' => mode,
|
|
126
|
+
'format' => ENDORSEMENT_FORMAT,
|
|
127
|
+
'target_sha256' => target_sha256,
|
|
128
|
+
'tolerance_sha256' => tolerance_sha256,
|
|
129
|
+
'verdict' => verdict.to_s
|
|
130
|
+
}
|
|
131
|
+
if mode == 'procedure'
|
|
132
|
+
unless procedure_sha256.is_a?(String) && procedure_sha256.match?(HEX_DIGEST)
|
|
133
|
+
raise ReproductionError, 'procedure mode requires procedure_sha256 as 64-char lowercase hex (RPR-4: the adopted procedure is named)'
|
|
134
|
+
end
|
|
135
|
+
record['procedure_sha256'] = procedure_sha256
|
|
136
|
+
elsif !procedure_sha256.nil?
|
|
137
|
+
raise ReproductionError, 'hand mode must not carry procedure_sha256 (closed schema per mode)'
|
|
138
|
+
end
|
|
139
|
+
%w[target_sha256 tolerance_sha256].each do |k|
|
|
140
|
+
raise ReproductionError, "endorsement.#{k} must be 64-char lowercase hex" unless record[k].is_a?(String) && record[k].match?(HEX_DIGEST)
|
|
141
|
+
end
|
|
142
|
+
Entry.canonical_json(record)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def parse_endorsement!(endorsement_string)
|
|
146
|
+
parsed = json_object!(endorsement_string, 'endorsement')
|
|
147
|
+
e = parsed.transform_keys(&:to_s)
|
|
148
|
+
raise ReproductionError, "unknown format #{e['format'].inspect} (#{ENDORSEMENT_FORMAT} only)" unless e['format'] == ENDORSEMENT_FORMAT
|
|
149
|
+
raise ReproductionError, "adjudication_mode must be one of #{MODES.join(', ')}" unless MODES.include?(e['adjudication_mode'])
|
|
150
|
+
|
|
151
|
+
expected = e['adjudication_mode'] == 'procedure' ? ENDORSEMENT_FIELDS_PROCEDURE : ENDORSEMENT_FIELDS_HAND
|
|
152
|
+
keys = e.keys.sort
|
|
153
|
+
raise ReproductionError, "endorsement fields must be exactly #{expected.join(', ')}, got #{keys.join(', ')}" unless keys == expected
|
|
154
|
+
raise ReproductionError, "verdict must be one of #{VERDICTS.join(', ')}" unless VERDICTS.include?(e['verdict'])
|
|
155
|
+
(expected - %w[format verdict adjudication_mode]).each do |k|
|
|
156
|
+
raise ReproductionError, "endorsement.#{k} must be 64-char lowercase hex" unless e[k].is_a?(String) && e[k].match?(HEX_DIGEST)
|
|
157
|
+
end
|
|
158
|
+
unless Entry.canonical_json(e) == endorsement_string.to_s
|
|
159
|
+
raise ReproductionError, 'endorsement is not in canonical serialization (one record, one digest)'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
e
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Sign an endorsement under the endorser's map-1 credential (map-1 §1.1
|
|
166
|
+
# attestation signature; the payload is the endorsement record string).
|
|
167
|
+
def sign_endorsement(endorsement_string, credential, key)
|
|
168
|
+
parse_endorsement!(endorsement_string)
|
|
169
|
+
ChainCredential.sign_attestation(credential, key, endorsement_string.to_s)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Verify signature + structure. Returns true/false for the signature;
|
|
173
|
+
# malformed endorsement raises (a verdict about an unresolvable record
|
|
174
|
+
# would be noise dressed as judgment).
|
|
175
|
+
def verify_endorsement(endorsement_string, credential, signature_hex)
|
|
176
|
+
parse_endorsement!(endorsement_string)
|
|
177
|
+
ChainCredential.verify_attestation(credential, endorsement_string.to_s, signature_hex)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Foreignness conformance (RPR-4): a conforming rpr-1 endorsement's
|
|
181
|
+
# issuer credential is NOT the operator credential of the chain the
|
|
182
|
+
# target belongs to. Digest equality is the decidable surface at rpr-1;
|
|
183
|
+
# distinctness is not independence (disclosed in the convention).
|
|
184
|
+
def foreign?(endorser_credential, operator_credential)
|
|
185
|
+
ChainCredential.credential_digest(endorser_credential) !=
|
|
186
|
+
ChainCredential.credential_digest(operator_credential)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# -- declaration-set assessment (rpr-1 §2.1, RPR-3) --
|
|
190
|
+
#
|
|
191
|
+
# +targets+ Array of target record STRINGS (the resolvable universe).
|
|
192
|
+
# +declarations+ Array of {'tolerance' => record string, 'position' => Integer}.
|
|
193
|
+
# +invoked_tolerance_sha256+ digest the endorsement names.
|
|
194
|
+
# +endorsement_position+ committed position of the endorsement.
|
|
195
|
+
#
|
|
196
|
+
# Reports the pooled anterior declaration set for the endorsement's
|
|
197
|
+
# target AND its computation-identification siblings, multiplicity, the
|
|
198
|
+
# invoked declaration's standing, and the unresolved residue. Diagnostic:
|
|
199
|
+
# malformed declarations land in the residue, never raise.
|
|
200
|
+
def assess_declarations(targets:, declarations:, invoked_tolerance_sha256:, endorsement_position:)
|
|
201
|
+
pos = endorsement_position
|
|
202
|
+
raise ReproductionError, 'endorsement_position must be an Integer' unless pos.is_a?(Integer)
|
|
203
|
+
|
|
204
|
+
by_digest = {}
|
|
205
|
+
Array(targets).each do |t|
|
|
206
|
+
by_digest[Digest::SHA256.hexdigest(t.to_s)] = computation_id(t)
|
|
207
|
+
rescue ReproductionError
|
|
208
|
+
next # an unparsable target resolves nothing; bindings to it fall to residue
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
pooled = []
|
|
212
|
+
residue = []
|
|
213
|
+
seen = {}
|
|
214
|
+
Array(declarations).each do |d|
|
|
215
|
+
h = d.is_a?(Hash) ? d.transform_keys(&:to_s) : {}
|
|
216
|
+
decl = h['tolerance'].to_s
|
|
217
|
+
position = h['position']
|
|
218
|
+
digest = Digest::SHA256.hexdigest(decl)
|
|
219
|
+
begin
|
|
220
|
+
t = parse_tolerance!(decl)
|
|
221
|
+
rescue ReproductionError => e
|
|
222
|
+
residue << { tolerance_sha256: digest, reason: "malformed declaration: #{e.message}" }
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
unless position.is_a?(Integer)
|
|
226
|
+
residue << { tolerance_sha256: digest, reason: 'no committed position supplied (anteriority undecidable)' }
|
|
227
|
+
next
|
|
228
|
+
end
|
|
229
|
+
# A commitment is (digest, position); the same pair supplied twice is
|
|
230
|
+
# one commitment, not two (one declaration, one digest — rpr-1 §2).
|
|
231
|
+
next if seen[[digest, position]]
|
|
232
|
+
|
|
233
|
+
seen[[digest, position]] = true
|
|
234
|
+
comp = by_digest[t['target_sha256']]
|
|
235
|
+
if comp.nil?
|
|
236
|
+
residue << { tolerance_sha256: digest, target_sha256: t['target_sha256'],
|
|
237
|
+
reason: 'target unresolvable in supplied view (computation sharing undecidable)' }
|
|
238
|
+
next
|
|
239
|
+
end
|
|
240
|
+
pooled << { tolerance_sha256: digest, target_sha256: t['target_sha256'],
|
|
241
|
+
computation_id: comp, position: position, anterior: position < pos }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The invoked digest may be committed at several positions; anteriority
|
|
245
|
+
# is a property of the committed record, not of presentation order
|
|
246
|
+
# (RPR-3): conforming iff ANY anterior commitment exists, represented
|
|
247
|
+
# by the earliest one, with posterior commitments disclosed.
|
|
248
|
+
invoked_commitments = pooled.select { |e| e[:tolerance_sha256] == invoked_tolerance_sha256 }
|
|
249
|
+
invoked_anterior = invoked_commitments.select { |e| e[:anterior] }.min_by { |e| e[:position] }
|
|
250
|
+
invoked = invoked_anterior || invoked_commitments.min_by { |e| e[:position] }
|
|
251
|
+
invoked_posterior = invoked_commitments.reject { |e| e[:anterior] }
|
|
252
|
+
|
|
253
|
+
invoked_comp = invoked && invoked[:computation_id]
|
|
254
|
+
set = invoked_comp.nil? ? [] : pooled.select { |e| e[:computation_id] == invoked_comp && e[:anterior] }
|
|
255
|
+
# Deterministic report order (position, then digest): the report's
|
|
256
|
+
# bytes, not just its quantities, are presentation-order-free.
|
|
257
|
+
set = set.sort_by { |e| [e[:position], e[:tolerance_sha256]] }
|
|
258
|
+
# Multiplicity counts distinct declarations (digests), not commitments;
|
|
259
|
+
# rank is the invoked declaration's place in the pooled anterior set,
|
|
260
|
+
# ordered by each declaration's earliest anterior position (rpr-1 §2.1).
|
|
261
|
+
earliest = set.group_by { |e| e[:tolerance_sha256] }.transform_values { |es| es.map { |e| e[:position] }.min }
|
|
262
|
+
# Digest tiebreak: equal earliest positions must not fall back to hash
|
|
263
|
+
# insertion (= presentation) order — the rank, like the verdict, is a
|
|
264
|
+
# function of the committed record alone (rpr-1 §2.1).
|
|
265
|
+
ranked = earliest.sort_by { |d, p| [p, d] }.map(&:first)
|
|
266
|
+
rank = invoked_anterior && (ranked.index(invoked_tolerance_sha256) + 1)
|
|
267
|
+
{
|
|
268
|
+
invoked: invoked,
|
|
269
|
+
invoked_conforming: !invoked_anterior.nil?,
|
|
270
|
+
invoked_posterior: invoked_posterior,
|
|
271
|
+
invoked_rank: rank,
|
|
272
|
+
declaration_set: set,
|
|
273
|
+
multiplicity: ranked.size,
|
|
274
|
+
residue: residue,
|
|
275
|
+
note: if invoked.nil?
|
|
276
|
+
'invoked tolerance not found among supplied declarations (or unresolvable); nothing to assess'
|
|
277
|
+
elsif invoked_anterior.nil?
|
|
278
|
+
'no anterior commitment of the invoked tolerance (RPR-3: non-conforming)'
|
|
279
|
+
elsif ranked.size > 1
|
|
280
|
+
"#{ranked.size} anterior declaration(s) share the invoked computation identification; the menu is visible and priced by the reader (RPR-3)"
|
|
281
|
+
else
|
|
282
|
+
'single anterior declaration for this computation'
|
|
283
|
+
end
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# -- internal helpers --
|
|
288
|
+
|
|
289
|
+
def json_object!(string, label)
|
|
290
|
+
parsed = begin
|
|
291
|
+
JSON.parse(string.to_s)
|
|
292
|
+
rescue JSON::ParserError => e
|
|
293
|
+
raise ReproductionError, "#{label} is not valid JSON: #{e.message}"
|
|
294
|
+
end
|
|
295
|
+
raise ReproductionError, "#{label} must be a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
|
|
296
|
+
|
|
297
|
+
parsed
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def parse_record!(string, format, fields, label)
|
|
301
|
+
parsed = json_object!(string, label)
|
|
302
|
+
r = parsed.transform_keys(&:to_s)
|
|
303
|
+
keys = r.keys.sort
|
|
304
|
+
raise ReproductionError, "#{label} fields must be exactly #{fields.join(', ')}, got #{keys.join(', ')}" unless keys == fields
|
|
305
|
+
raise ReproductionError, "unknown format #{r['format'].inspect} (#{format} only)" unless r['format'] == format
|
|
306
|
+
|
|
307
|
+
yield r if block_given?
|
|
308
|
+
unless Entry.canonical_json(r) == string.to_s
|
|
309
|
+
raise ReproductionError, "#{label} is not in canonical serialization (one record, one digest)"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
r
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Design-constraint tests for the AUD-L3 reproduction-endorsement slice
|
|
4
|
+
# (aud_l3_reproducibility_design v0.4, freeze candidate; invariants RPR-1..5).
|
|
5
|
+
# Each block names the invariant whose implementable consequence it pins.
|
|
6
|
+
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'digest'
|
|
9
|
+
|
|
10
|
+
anchoring = File.expand_path('../lib/synoptis/anchoring', __dir__)
|
|
11
|
+
require File.join(anchoring, 'entry')
|
|
12
|
+
require File.join(anchoring, 'chain_credential')
|
|
13
|
+
require File.join(anchoring, 'attestation_types')
|
|
14
|
+
require File.join(anchoring, 'reproduction')
|
|
15
|
+
|
|
16
|
+
Entry = Synoptis::Anchoring::Entry
|
|
17
|
+
Cred = Synoptis::Anchoring::ChainCredential
|
|
18
|
+
AType = Synoptis::Anchoring::AttestationTypes
|
|
19
|
+
Repro = Synoptis::Anchoring::Reproduction
|
|
20
|
+
|
|
21
|
+
$pass = 0
|
|
22
|
+
$fail = 0
|
|
23
|
+
|
|
24
|
+
def assert(condition, message)
|
|
25
|
+
if condition
|
|
26
|
+
$pass += 1
|
|
27
|
+
puts " PASS: #{message}"
|
|
28
|
+
else
|
|
29
|
+
$fail += 1
|
|
30
|
+
puts " FAIL: #{message}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def assert_raises(klass, message)
|
|
35
|
+
yield
|
|
36
|
+
assert(false, "#{message} (no exception raised)")
|
|
37
|
+
rescue klass
|
|
38
|
+
assert(true, message)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
assert(false, "#{message} (raised #{e.class}: #{e.message})")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
HEX_A = 'a' * 64
|
|
44
|
+
HEX_B = 'b' * 64
|
|
45
|
+
HEX_C = 'c' * 64
|
|
46
|
+
HEX_D = 'd' * 64
|
|
47
|
+
HEX_E = 'e' * 64
|
|
48
|
+
|
|
49
|
+
OPERATOR_IDENTITY = "block1-sha256:#{'1' * 64}"
|
|
50
|
+
ENDORSER_IDENTITY = "block1-sha256:#{'2' * 64}"
|
|
51
|
+
OPERATOR_KEY = Cred.generate_key
|
|
52
|
+
ENDORSER_KEY = Cred.generate_key
|
|
53
|
+
OPERATOR_CRED = Cred.build(OPERATOR_IDENTITY, OPERATOR_KEY)
|
|
54
|
+
ENDORSER_CRED = Cred.build(ENDORSER_IDENTITY, ENDORSER_KEY)
|
|
55
|
+
|
|
56
|
+
puts '== rpr-1 convention is content-addressed =='
|
|
57
|
+
|
|
58
|
+
assert(Repro.convention_sha256.match?(/\A[a-f0-9]{64}\z/), 'rpr-1 convention digest is 64-hex (content-addressed)')
|
|
59
|
+
|
|
60
|
+
puts '== RPR-2: re-execution target, output included, closed schema =='
|
|
61
|
+
|
|
62
|
+
target = Repro.build_target(input_sha256: HEX_A, environment_sha256: HEX_B,
|
|
63
|
+
pipeline_sha256: HEX_C, output_sha256: HEX_D)
|
|
64
|
+
assert(Repro.parse_target!(target).is_a?(Hash), 'a built target parses (canonical, exact fields)')
|
|
65
|
+
assert(Repro.target_digest(target).match?(/\A[a-f0-9]{64}\z/), 'target digest is 64-hex of canonical JSON')
|
|
66
|
+
|
|
67
|
+
assert_raises(Repro::ReproductionError, 'extra field is refused, not coerced (closed schema)') do
|
|
68
|
+
extra = JSON.parse(target).merge('note' => 'x')
|
|
69
|
+
Repro.parse_target!(Entry.canonical_json(extra))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
assert_raises(Repro::ReproductionError, 'non-canonical serialization is refused (one record, one digest)') do
|
|
73
|
+
Repro.parse_target!(JSON.pretty_generate(JSON.parse(target)))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
assert_raises(Repro::ReproductionError, 'mutable/uncommitted referent (bad hex) is refused (RPR-2)') do
|
|
77
|
+
Repro.build_target(input_sha256: 'not-a-digest', environment_sha256: HEX_B,
|
|
78
|
+
pipeline_sha256: HEX_C, output_sha256: HEX_D)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
puts '== §3(b): computation identification excludes the output digest =='
|
|
82
|
+
|
|
83
|
+
sibling = Repro.build_target(input_sha256: HEX_A, environment_sha256: HEX_B,
|
|
84
|
+
pipeline_sha256: HEX_C, output_sha256: HEX_E)
|
|
85
|
+
other = Repro.build_target(input_sha256: HEX_E, environment_sha256: HEX_B,
|
|
86
|
+
pipeline_sha256: HEX_C, output_sha256: HEX_D)
|
|
87
|
+
assert(Repro.computation_id(target) == Repro.computation_id(sibling),
|
|
88
|
+
'targets differing only in output share the computation identification')
|
|
89
|
+
assert(Repro.computation_id(target) != Repro.computation_id(other),
|
|
90
|
+
'targets differing in input do not share the computation identification')
|
|
91
|
+
assert(Repro.target_digest(target) != Repro.target_digest(sibling),
|
|
92
|
+
'sibling targets remain distinct targets (identification is coarser than identity)')
|
|
93
|
+
|
|
94
|
+
puts '== RPR-3: tolerance is target-bound, result-free, closed schema =='
|
|
95
|
+
|
|
96
|
+
tol = Repro.build_tolerance(target_sha256: Repro.target_digest(target))
|
|
97
|
+
assert(Repro.parse_tolerance!(tol)['kind'] == 'bit-identity', 'rpr-1 tolerance kind is bit-identity (narrowness disclosed)')
|
|
98
|
+
assert(Repro.parse_tolerance!(tol)['target_sha256'] == Repro.target_digest(target), 'tolerance binds its target by digest')
|
|
99
|
+
|
|
100
|
+
assert_raises(Repro::ReproductionError, 'unknown tolerance kind is refused (closed schema = result-free by construction)') do
|
|
101
|
+
Repro.build_tolerance(target_sha256: Repro.target_digest(target), kind: 'whatever-fits')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
puts '== RPR-1: verdict both ways, nothing else =='
|
|
105
|
+
|
|
106
|
+
endo = Repro.build_endorsement(target_sha256: Repro.target_digest(target),
|
|
107
|
+
tolerance_sha256: Repro.tolerance_digest(tol),
|
|
108
|
+
verdict: 'reproduced', adjudication_mode: 'hand')
|
|
109
|
+
nendo = Repro.build_endorsement(target_sha256: Repro.target_digest(target),
|
|
110
|
+
tolerance_sha256: Repro.tolerance_digest(tol),
|
|
111
|
+
verdict: 'not-reproduced', adjudication_mode: 'hand')
|
|
112
|
+
assert(Repro.parse_endorsement!(endo)['verdict'] == 'reproduced', 'affirmative verdict is a conforming record')
|
|
113
|
+
assert(Repro.parse_endorsement!(nendo)['verdict'] == 'not-reproduced', 'negative verdict is the same kind of record (RPR-1: no publication bias by vocabulary)')
|
|
114
|
+
|
|
115
|
+
assert_raises(Repro::ReproductionError, 'a verdict outside reproduced/not-reproduced is refused') do
|
|
116
|
+
Repro.build_endorsement(target_sha256: Repro.target_digest(target),
|
|
117
|
+
tolerance_sha256: Repro.tolerance_digest(tol),
|
|
118
|
+
verdict: 'correct', adjudication_mode: 'hand')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
puts '== RPR-4: adjudication mode is named, per mode closed schema =='
|
|
122
|
+
|
|
123
|
+
pendo = Repro.build_endorsement(target_sha256: Repro.target_digest(target),
|
|
124
|
+
tolerance_sha256: Repro.tolerance_digest(tol),
|
|
125
|
+
verdict: 'reproduced', adjudication_mode: 'procedure',
|
|
126
|
+
procedure_sha256: HEX_E)
|
|
127
|
+
assert(Repro.parse_endorsement!(pendo)['procedure_sha256'] == HEX_E, 'procedure mode names the adopted procedure')
|
|
128
|
+
|
|
129
|
+
assert_raises(Repro::ReproductionError, 'procedure mode without procedure_sha256 is refused (gate must be readable)') do
|
|
130
|
+
Repro.build_endorsement(target_sha256: Repro.target_digest(target),
|
|
131
|
+
tolerance_sha256: Repro.tolerance_digest(tol),
|
|
132
|
+
verdict: 'reproduced', adjudication_mode: 'procedure')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
assert_raises(Repro::ReproductionError, 'hand mode carrying procedure_sha256 is refused (closed schema per mode)') do
|
|
136
|
+
Repro.build_endorsement(target_sha256: Repro.target_digest(target),
|
|
137
|
+
tolerance_sha256: Repro.tolerance_digest(tol),
|
|
138
|
+
verdict: 'reproduced', adjudication_mode: 'hand',
|
|
139
|
+
procedure_sha256: HEX_E)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
puts '== map-1 §1.1 signature reuse: sign/verify endorsement =='
|
|
143
|
+
|
|
144
|
+
sig = Repro.sign_endorsement(endo, ENDORSER_CRED, ENDORSER_KEY)
|
|
145
|
+
assert(Repro.verify_endorsement(endo, ENDORSER_CRED, sig), 'endorsement verifies with credential+record+signature only (MAP-2 self-authentication)')
|
|
146
|
+
assert(!Repro.verify_endorsement(nendo, ENDORSER_CRED, sig), 'signature does not transfer to a different verdict record')
|
|
147
|
+
assert(!Repro.verify_endorsement(endo, OPERATOR_CRED, sig), 'signature does not verify under a foreign credential')
|
|
148
|
+
|
|
149
|
+
puts '== RPR-4: foreignness is a conformance condition =='
|
|
150
|
+
|
|
151
|
+
assert(Repro.foreign?(ENDORSER_CRED, OPERATOR_CRED), 'distinct endorser credential is foreign (conforming)')
|
|
152
|
+
assert(!Repro.foreign?(OPERATOR_CRED, OPERATOR_CRED), 'operator self-endorsement is not foreign (non-conforming as rpr-1)')
|
|
153
|
+
|
|
154
|
+
puts '== RPR-3: declaration-set assessment (menu + siblings + residue) =='
|
|
155
|
+
|
|
156
|
+
tol_sib = Repro.build_tolerance(target_sha256: Repro.target_digest(sibling))
|
|
157
|
+
tol_other = Repro.build_tolerance(target_sha256: Repro.target_digest(other))
|
|
158
|
+
tol_orphan = Repro.build_tolerance(target_sha256: HEX_E) # target not in supplied view
|
|
159
|
+
|
|
160
|
+
report = Repro.assess_declarations(
|
|
161
|
+
targets: [target, sibling, other],
|
|
162
|
+
declarations: [
|
|
163
|
+
{ 'tolerance' => tol, 'position' => 10 },
|
|
164
|
+
{ 'tolerance' => tol_sib, 'position' => 11 },
|
|
165
|
+
{ 'tolerance' => tol_other, 'position' => 12 },
|
|
166
|
+
{ 'tolerance' => tol_orphan, 'position' => 13 }
|
|
167
|
+
],
|
|
168
|
+
invoked_tolerance_sha256: Repro.tolerance_digest(tol),
|
|
169
|
+
endorsement_position: 20
|
|
170
|
+
)
|
|
171
|
+
assert(report[:invoked_conforming], 'anterior invoked tolerance is conforming (RPR-3)')
|
|
172
|
+
assert(report[:multiplicity] == 2, 'sibling-target declaration pools into the same set (target-splitting exposed)')
|
|
173
|
+
assert(report[:declaration_set].none? { |e| e[:target_sha256] == Repro.target_digest(other) },
|
|
174
|
+
'a different computation does not pool into the set')
|
|
175
|
+
assert(report[:residue].size == 1 && report[:residue][0][:reason].include?('unresolvable'),
|
|
176
|
+
'unresolvable target binding is disclosed as residue, not silently dropped')
|
|
177
|
+
|
|
178
|
+
posterior = Repro.assess_declarations(
|
|
179
|
+
targets: [target],
|
|
180
|
+
declarations: [{ 'tolerance' => tol, 'position' => 30 }],
|
|
181
|
+
invoked_tolerance_sha256: Repro.tolerance_digest(tol),
|
|
182
|
+
endorsement_position: 20
|
|
183
|
+
)
|
|
184
|
+
assert(!posterior[:invoked_conforming], 'a tolerance committed after the endorsement is non-conforming (anteriority, MPR-8)')
|
|
185
|
+
|
|
186
|
+
puts '== RPR-3: assessment is order-independent over commitments =='
|
|
187
|
+
|
|
188
|
+
both_orders = [
|
|
189
|
+
[{ 'tolerance' => tol, 'position' => 5 }, { 'tolerance' => tol, 'position' => 30 }],
|
|
190
|
+
[{ 'tolerance' => tol, 'position' => 30 }, { 'tolerance' => tol, 'position' => 5 }]
|
|
191
|
+
].map do |decls|
|
|
192
|
+
Repro.assess_declarations(targets: [target], declarations: decls,
|
|
193
|
+
invoked_tolerance_sha256: Repro.tolerance_digest(tol),
|
|
194
|
+
endorsement_position: 20)
|
|
195
|
+
end
|
|
196
|
+
assert(both_orders.all? { |r| r[:invoked_conforming] },
|
|
197
|
+
'a digest committed anterior AND posterior conforms in both presentation orders (committed order, not array order)')
|
|
198
|
+
assert(both_orders.all? { |r| r[:invoked][:position] == 5 },
|
|
199
|
+
'the earliest anterior commitment represents the invoked declaration in both orders')
|
|
200
|
+
assert(both_orders.all? { |r| r[:invoked_posterior].size == 1 && r[:invoked_posterior][0][:position] == 30 },
|
|
201
|
+
'the posterior commitment of the invoked digest is disclosed, not dropped')
|
|
202
|
+
|
|
203
|
+
dup = Repro.assess_declarations(
|
|
204
|
+
targets: [target],
|
|
205
|
+
declarations: [{ 'tolerance' => tol, 'position' => 10 }, { 'tolerance' => tol, 'position' => 10 }],
|
|
206
|
+
invoked_tolerance_sha256: Repro.tolerance_digest(tol),
|
|
207
|
+
endorsement_position: 20
|
|
208
|
+
)
|
|
209
|
+
assert(dup[:multiplicity] == 1, 'the same (digest, position) supplied twice is one commitment; multiplicity counts distinct declarations')
|
|
210
|
+
assert(dup[:invoked_rank] == 1, 'the invoked declaration reports its rank in the pooled anterior set (rpr-1 §2.1)')
|
|
211
|
+
|
|
212
|
+
tol_a = Repro.build_tolerance(target_sha256: Repro.target_digest(target))
|
|
213
|
+
tol_b = Repro.build_tolerance(target_sha256: Repro.target_digest(sibling))
|
|
214
|
+
tie_orders = [
|
|
215
|
+
[{ 'tolerance' => tol_a, 'position' => 5 }, { 'tolerance' => tol_b, 'position' => 5 }],
|
|
216
|
+
[{ 'tolerance' => tol_b, 'position' => 5 }, { 'tolerance' => tol_a, 'position' => 5 }]
|
|
217
|
+
].map do |decls|
|
|
218
|
+
Repro.assess_declarations(targets: [target, sibling], declarations: decls,
|
|
219
|
+
invoked_tolerance_sha256: Repro.tolerance_digest(tol_a),
|
|
220
|
+
endorsement_position: 20)
|
|
221
|
+
end
|
|
222
|
+
assert(tie_orders[0][:invoked_rank] == tie_orders[1][:invoked_rank],
|
|
223
|
+
'equal earliest positions rank identically in both presentation orders (digest tiebreak, rpr-1 §2.1)')
|
|
224
|
+
|
|
225
|
+
puts '== RPR-5: retraction is the map-1 §3 act unchanged =='
|
|
226
|
+
|
|
227
|
+
retraction_ok = AType.validate_intake!('retraction', { 'target_entry_hash' => HEX_A })
|
|
228
|
+
assert(retraction_ok, 'a reproduction-endorsement retraction rides map-1 §3 intake unchanged')
|
|
229
|
+
assert(AType::VOCABULARY.include?('quality-endorsement'), 'endorsements are carried by the unchanged map-1 quality-endorsement type')
|
|
230
|
+
|
|
231
|
+
puts '== additive-only: rpr-1 leaves map-1 vocabulary untouched =='
|
|
232
|
+
|
|
233
|
+
assert(AType::VOCABULARY == %w[observation quality-endorsement succession-designation retraction],
|
|
234
|
+
'map-1 vocabulary unchanged by the rpr-1 slice (extension would be a new convention)')
|
|
235
|
+
|
|
236
|
+
puts
|
|
237
|
+
puts "#{$pass} passed, #{$fail} failed"
|
|
238
|
+
exit($fail.zero? ? 0 : 1)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kairos-chain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.46.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masaomi Hatakeyama
|
|
@@ -654,6 +654,7 @@ files:
|
|
|
654
654
|
- templates/skillsets/skillset_exchange/tools/skillset_withdraw.rb
|
|
655
655
|
- templates/skillsets/synoptis/bin/khab_verify.rb
|
|
656
656
|
- templates/skillsets/synoptis/bin/map_verify.rb
|
|
657
|
+
- templates/skillsets/synoptis/bin/rpr_verify.rb
|
|
657
658
|
- templates/skillsets/synoptis/config/synoptis.yml
|
|
658
659
|
- templates/skillsets/synoptis/knowledge/synoptis_protocol/synoptis_protocol.md
|
|
659
660
|
- templates/skillsets/synoptis/lib/synoptis.rb
|
|
@@ -663,6 +664,7 @@ files:
|
|
|
663
664
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/containment.rb
|
|
664
665
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/khab-1.md
|
|
665
666
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/map-1.md
|
|
667
|
+
- templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/rpr-1.md
|
|
666
668
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/cumulative_commitment.rb
|
|
667
669
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/deposit_board.rb
|
|
668
670
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/entry.rb
|
|
@@ -671,6 +673,7 @@ files:
|
|
|
671
673
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/public_read.rb
|
|
672
674
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/public_verifier.rb
|
|
673
675
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/read_path.rb
|
|
676
|
+
- templates/skillsets/synoptis/lib/synoptis/anchoring/reproduction.rb
|
|
674
677
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/succession.rb
|
|
675
678
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/write_budget.rb
|
|
676
679
|
- templates/skillsets/synoptis/lib/synoptis/anchoring/write_path.rb
|
|
@@ -699,6 +702,7 @@ files:
|
|
|
699
702
|
- templates/skillsets/synoptis/test/test_l2_constitutive_slice2.rb
|
|
700
703
|
- templates/skillsets/synoptis/test/test_l2_constitutive_slice3.rb
|
|
701
704
|
- templates/skillsets/synoptis/test/test_map_anchoring.rb
|
|
705
|
+
- templates/skillsets/synoptis/test/test_rpr_reproduction.rb
|
|
702
706
|
- templates/skillsets/synoptis/test/test_synoptis.rb
|
|
703
707
|
- templates/skillsets/synoptis/tools/attestation_issue.rb
|
|
704
708
|
- templates/skillsets/synoptis/tools/attestation_list.rb
|