crovia-tacet-operator 0.3.0__tar.gz
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.
- crovia_tacet_operator-0.3.0/PKG-INFO +81 -0
- crovia_tacet_operator-0.3.0/README.md +68 -0
- crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/PKG-INFO +81 -0
- crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/SOURCES.txt +24 -0
- crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/dependency_links.txt +1 -0
- crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/entry_points.txt +2 -0
- crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/requires.txt +5 -0
- crovia_tacet_operator-0.3.0/crovia_tacet_operator.egg-info/top_level.txt +1 -0
- crovia_tacet_operator-0.3.0/pyproject.toml +22 -0
- crovia_tacet_operator-0.3.0/setup.cfg +4 -0
- crovia_tacet_operator-0.3.0/tacet_operator/__init__.py +20 -0
- crovia_tacet_operator-0.3.0/tacet_operator/cli.py +134 -0
- crovia_tacet_operator-0.3.0/tacet_operator/config.py +113 -0
- crovia_tacet_operator-0.3.0/tacet_operator/drand.py +98 -0
- crovia_tacet_operator-0.3.0/tacet_operator/fetch.py +85 -0
- crovia_tacet_operator-0.3.0/tacet_operator/keys.py +32 -0
- crovia_tacet_operator-0.3.0/tacet_operator/ots.py +92 -0
- crovia_tacet_operator-0.3.0/tacet_operator/predicates/__init__.py +24 -0
- crovia_tacet_operator-0.3.0/tacet_operator/predicates/hf_card_training_data_v1.py +103 -0
- crovia_tacet_operator-0.3.0/tacet_operator/prove.py +131 -0
- crovia_tacet_operator-0.3.0/tacet_operator/publish.py +175 -0
- crovia_tacet_operator-0.3.0/tacet_operator/runner.py +260 -0
- crovia_tacet_operator-0.3.0/tacet_operator/state.py +127 -0
- crovia_tacet_operator-0.3.0/tacet_operator/targets.py +100 -0
- crovia_tacet_operator-0.3.0/tests/test_epoch_end_to_end.py +182 -0
- crovia_tacet_operator-0.3.0/tests/test_predicate.py +55 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crovia-tacet-operator
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Production operator for TACET: hourly epochs, signed surface snapshots, OTS anchoring, silence proofs as crovia.seal.v1
|
|
5
|
+
Author-email: Crovia Trust <info@croviatrust.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: crovia-tacet>=0.3.0
|
|
10
|
+
Requires-Dist: cryptography>=41.0.0
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
13
|
+
|
|
14
|
+
# TACET operator
|
|
15
|
+
|
|
16
|
+
Runs the TACET log in production. The [reference](../reference/python) defines
|
|
17
|
+
the objects; this package fetches, signs, anchors and publishes them.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
every hour tacet-operator run-epoch drand round → observe surfaces → update map → sign sheet → OTS stamp
|
|
21
|
+
every 6 h tacet-operator refresh-anchors upgrade OTS proofs; close sheets confirmed in Bitcoin
|
|
22
|
+
daily tacet-operator publish --proofs rebuild featured silence proofs (crovia.seal.v1)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What one epoch does
|
|
26
|
+
|
|
27
|
+
1. Fetches the current **drand** round (chain `8990e7…`, pinned in `trust_root.json`).
|
|
28
|
+
Nothing in the epoch could have existed before that round: lower time bound.
|
|
29
|
+
2. Fetches the surfaces of the featured targets and of a rotating slice of the
|
|
30
|
+
target list (default 120 per hour, 0.6 s apart, `crovia-tacet-observer` user
|
|
31
|
+
agent). Surface = the raw model card `…/raw/main/README.md`; for gated
|
|
32
|
+
repositories (401/403) the rendered model page.
|
|
33
|
+
3. Runs the public predicate `crovia.pred.hf-card-training-data 1.0.0`
|
|
34
|
+
([source](tacet_operator/predicates/hf_card_training_data_v1.py), hashed
|
|
35
|
+
byte-for-byte into every snapshot) on the bytes and signs a **snapshot**
|
|
36
|
+
with the observer key. Non-200 responses produce no snapshot: a fetch failure
|
|
37
|
+
is indeterminate, never absence.
|
|
38
|
+
4. Applies forward-only slot transitions: a positive snapshot for an empty slot
|
|
39
|
+
inserts a `disclosure` value; two consecutive negatives for a disclosed slot
|
|
40
|
+
insert a `retraction`. Every value is asserted by a `crovia.seal.v1` signed
|
|
41
|
+
with the Seal-issuer key (`values/<key>.json`).
|
|
42
|
+
5. Signs the **epoch sheet** (root, size, `snapshots_root`, `opened` beacon,
|
|
43
|
+
`prev_sheet_hash`) with the operator key and stamps `sheet_hash` with
|
|
44
|
+
OpenTimestamps. When the calendar reaches Bitcoin, `refresh-anchors` fills
|
|
45
|
+
`closed` (block height, block time, proof URL): upper time bound.
|
|
46
|
+
|
|
47
|
+
The operator is the only writer; all changes of an epoch are applied in one
|
|
48
|
+
batch, so the sheet's root is the root at `epoch_end`. Missed hours are
|
|
49
|
+
back-filled with empty sheets (historical drand round): the chain stays
|
|
50
|
+
contiguous and those hours contribute no silence.
|
|
51
|
+
|
|
52
|
+
## Silence proofs
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
tacet-operator prove mistralai/Mistral-7B-v0.1 --strength 2 -o proof.seal.json
|
|
56
|
+
tacet-operator verify proof.seal.json --operator-pubkey <key_hex from trust_root.json>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A proof carries every sheet in the range, the delta-encoded non-inclusion
|
|
60
|
+
path, the negative snapshots with their Merkle inclusion in each epoch's
|
|
61
|
+
`snapshots_root`, and the `silence` block recomputed by the verifier:
|
|
62
|
+
`silence_seconds` = sum of the durations of anchored epochs that hold a negative
|
|
63
|
+
snapshot for the target. Unobserved, unanchored or unreachable hours count
|
|
64
|
+
zero. The bundle is wrapped in an unmodified `crovia.seal.v1`.
|
|
65
|
+
|
|
66
|
+
## Keys
|
|
67
|
+
|
|
68
|
+
Three Ed25519 seeds in `state/keys/` (mode 0600), created on first run:
|
|
69
|
+
`operator` (signs sheets), `observer` (signs snapshots), `issuer`
|
|
70
|
+
(`urn:crovia:seal-issuer:tacet`, signs Seals: value assertions and proofs).
|
|
71
|
+
Public keys are published in `trust_root.json`.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ../reference/python -e <crovia-seal>/reference/python -e .
|
|
77
|
+
TACET_STATE=/opt/crovia/tacet/state TACET_PUBLIC=/var/www/registry/data/tacet tacet-operator keys
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Tests: `pytest -q tests` (predicate vectors are real model cards; the epoch
|
|
81
|
+
test uses a fake network, fake drand and fake OTS).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# TACET operator
|
|
2
|
+
|
|
3
|
+
Runs the TACET log in production. The [reference](../reference/python) defines
|
|
4
|
+
the objects; this package fetches, signs, anchors and publishes them.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
every hour tacet-operator run-epoch drand round → observe surfaces → update map → sign sheet → OTS stamp
|
|
8
|
+
every 6 h tacet-operator refresh-anchors upgrade OTS proofs; close sheets confirmed in Bitcoin
|
|
9
|
+
daily tacet-operator publish --proofs rebuild featured silence proofs (crovia.seal.v1)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What one epoch does
|
|
13
|
+
|
|
14
|
+
1. Fetches the current **drand** round (chain `8990e7…`, pinned in `trust_root.json`).
|
|
15
|
+
Nothing in the epoch could have existed before that round: lower time bound.
|
|
16
|
+
2. Fetches the surfaces of the featured targets and of a rotating slice of the
|
|
17
|
+
target list (default 120 per hour, 0.6 s apart, `crovia-tacet-observer` user
|
|
18
|
+
agent). Surface = the raw model card `…/raw/main/README.md`; for gated
|
|
19
|
+
repositories (401/403) the rendered model page.
|
|
20
|
+
3. Runs the public predicate `crovia.pred.hf-card-training-data 1.0.0`
|
|
21
|
+
([source](tacet_operator/predicates/hf_card_training_data_v1.py), hashed
|
|
22
|
+
byte-for-byte into every snapshot) on the bytes and signs a **snapshot**
|
|
23
|
+
with the observer key. Non-200 responses produce no snapshot: a fetch failure
|
|
24
|
+
is indeterminate, never absence.
|
|
25
|
+
4. Applies forward-only slot transitions: a positive snapshot for an empty slot
|
|
26
|
+
inserts a `disclosure` value; two consecutive negatives for a disclosed slot
|
|
27
|
+
insert a `retraction`. Every value is asserted by a `crovia.seal.v1` signed
|
|
28
|
+
with the Seal-issuer key (`values/<key>.json`).
|
|
29
|
+
5. Signs the **epoch sheet** (root, size, `snapshots_root`, `opened` beacon,
|
|
30
|
+
`prev_sheet_hash`) with the operator key and stamps `sheet_hash` with
|
|
31
|
+
OpenTimestamps. When the calendar reaches Bitcoin, `refresh-anchors` fills
|
|
32
|
+
`closed` (block height, block time, proof URL): upper time bound.
|
|
33
|
+
|
|
34
|
+
The operator is the only writer; all changes of an epoch are applied in one
|
|
35
|
+
batch, so the sheet's root is the root at `epoch_end`. Missed hours are
|
|
36
|
+
back-filled with empty sheets (historical drand round): the chain stays
|
|
37
|
+
contiguous and those hours contribute no silence.
|
|
38
|
+
|
|
39
|
+
## Silence proofs
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
tacet-operator prove mistralai/Mistral-7B-v0.1 --strength 2 -o proof.seal.json
|
|
43
|
+
tacet-operator verify proof.seal.json --operator-pubkey <key_hex from trust_root.json>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A proof carries every sheet in the range, the delta-encoded non-inclusion
|
|
47
|
+
path, the negative snapshots with their Merkle inclusion in each epoch's
|
|
48
|
+
`snapshots_root`, and the `silence` block recomputed by the verifier:
|
|
49
|
+
`silence_seconds` = sum of the durations of anchored epochs that hold a negative
|
|
50
|
+
snapshot for the target. Unobserved, unanchored or unreachable hours count
|
|
51
|
+
zero. The bundle is wrapped in an unmodified `crovia.seal.v1`.
|
|
52
|
+
|
|
53
|
+
## Keys
|
|
54
|
+
|
|
55
|
+
Three Ed25519 seeds in `state/keys/` (mode 0600), created on first run:
|
|
56
|
+
`operator` (signs sheets), `observer` (signs snapshots), `issuer`
|
|
57
|
+
(`urn:crovia:seal-issuer:tacet`, signs Seals: value assertions and proofs).
|
|
58
|
+
Public keys are published in `trust_root.json`.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install -e ../reference/python -e <crovia-seal>/reference/python -e .
|
|
64
|
+
TACET_STATE=/opt/crovia/tacet/state TACET_PUBLIC=/var/www/registry/data/tacet tacet-operator keys
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Tests: `pytest -q tests` (predicate vectors are real model cards; the epoch
|
|
68
|
+
test uses a fake network, fake drand and fake OTS).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crovia-tacet-operator
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Production operator for TACET: hourly epochs, signed surface snapshots, OTS anchoring, silence proofs as crovia.seal.v1
|
|
5
|
+
Author-email: Crovia Trust <info@croviatrust.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: crovia-tacet>=0.3.0
|
|
10
|
+
Requires-Dist: cryptography>=41.0.0
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
13
|
+
|
|
14
|
+
# TACET operator
|
|
15
|
+
|
|
16
|
+
Runs the TACET log in production. The [reference](../reference/python) defines
|
|
17
|
+
the objects; this package fetches, signs, anchors and publishes them.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
every hour tacet-operator run-epoch drand round → observe surfaces → update map → sign sheet → OTS stamp
|
|
21
|
+
every 6 h tacet-operator refresh-anchors upgrade OTS proofs; close sheets confirmed in Bitcoin
|
|
22
|
+
daily tacet-operator publish --proofs rebuild featured silence proofs (crovia.seal.v1)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What one epoch does
|
|
26
|
+
|
|
27
|
+
1. Fetches the current **drand** round (chain `8990e7…`, pinned in `trust_root.json`).
|
|
28
|
+
Nothing in the epoch could have existed before that round: lower time bound.
|
|
29
|
+
2. Fetches the surfaces of the featured targets and of a rotating slice of the
|
|
30
|
+
target list (default 120 per hour, 0.6 s apart, `crovia-tacet-observer` user
|
|
31
|
+
agent). Surface = the raw model card `…/raw/main/README.md`; for gated
|
|
32
|
+
repositories (401/403) the rendered model page.
|
|
33
|
+
3. Runs the public predicate `crovia.pred.hf-card-training-data 1.0.0`
|
|
34
|
+
([source](tacet_operator/predicates/hf_card_training_data_v1.py), hashed
|
|
35
|
+
byte-for-byte into every snapshot) on the bytes and signs a **snapshot**
|
|
36
|
+
with the observer key. Non-200 responses produce no snapshot: a fetch failure
|
|
37
|
+
is indeterminate, never absence.
|
|
38
|
+
4. Applies forward-only slot transitions: a positive snapshot for an empty slot
|
|
39
|
+
inserts a `disclosure` value; two consecutive negatives for a disclosed slot
|
|
40
|
+
insert a `retraction`. Every value is asserted by a `crovia.seal.v1` signed
|
|
41
|
+
with the Seal-issuer key (`values/<key>.json`).
|
|
42
|
+
5. Signs the **epoch sheet** (root, size, `snapshots_root`, `opened` beacon,
|
|
43
|
+
`prev_sheet_hash`) with the operator key and stamps `sheet_hash` with
|
|
44
|
+
OpenTimestamps. When the calendar reaches Bitcoin, `refresh-anchors` fills
|
|
45
|
+
`closed` (block height, block time, proof URL): upper time bound.
|
|
46
|
+
|
|
47
|
+
The operator is the only writer; all changes of an epoch are applied in one
|
|
48
|
+
batch, so the sheet's root is the root at `epoch_end`. Missed hours are
|
|
49
|
+
back-filled with empty sheets (historical drand round): the chain stays
|
|
50
|
+
contiguous and those hours contribute no silence.
|
|
51
|
+
|
|
52
|
+
## Silence proofs
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
tacet-operator prove mistralai/Mistral-7B-v0.1 --strength 2 -o proof.seal.json
|
|
56
|
+
tacet-operator verify proof.seal.json --operator-pubkey <key_hex from trust_root.json>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A proof carries every sheet in the range, the delta-encoded non-inclusion
|
|
60
|
+
path, the negative snapshots with their Merkle inclusion in each epoch's
|
|
61
|
+
`snapshots_root`, and the `silence` block recomputed by the verifier:
|
|
62
|
+
`silence_seconds` = sum of the durations of anchored epochs that hold a negative
|
|
63
|
+
snapshot for the target. Unobserved, unanchored or unreachable hours count
|
|
64
|
+
zero. The bundle is wrapped in an unmodified `crovia.seal.v1`.
|
|
65
|
+
|
|
66
|
+
## Keys
|
|
67
|
+
|
|
68
|
+
Three Ed25519 seeds in `state/keys/` (mode 0600), created on first run:
|
|
69
|
+
`operator` (signs sheets), `observer` (signs snapshots), `issuer`
|
|
70
|
+
(`urn:crovia:seal-issuer:tacet`, signs Seals: value assertions and proofs).
|
|
71
|
+
Public keys are published in `trust_root.json`.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ../reference/python -e <crovia-seal>/reference/python -e .
|
|
77
|
+
TACET_STATE=/opt/crovia/tacet/state TACET_PUBLIC=/var/www/registry/data/tacet tacet-operator keys
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Tests: `pytest -q tests` (predicate vectors are real model cards; the epoch
|
|
81
|
+
test uses a fake network, fake drand and fake OTS).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
crovia_tacet_operator.egg-info/PKG-INFO
|
|
4
|
+
crovia_tacet_operator.egg-info/SOURCES.txt
|
|
5
|
+
crovia_tacet_operator.egg-info/dependency_links.txt
|
|
6
|
+
crovia_tacet_operator.egg-info/entry_points.txt
|
|
7
|
+
crovia_tacet_operator.egg-info/requires.txt
|
|
8
|
+
crovia_tacet_operator.egg-info/top_level.txt
|
|
9
|
+
tacet_operator/__init__.py
|
|
10
|
+
tacet_operator/cli.py
|
|
11
|
+
tacet_operator/config.py
|
|
12
|
+
tacet_operator/drand.py
|
|
13
|
+
tacet_operator/fetch.py
|
|
14
|
+
tacet_operator/keys.py
|
|
15
|
+
tacet_operator/ots.py
|
|
16
|
+
tacet_operator/prove.py
|
|
17
|
+
tacet_operator/publish.py
|
|
18
|
+
tacet_operator/runner.py
|
|
19
|
+
tacet_operator/state.py
|
|
20
|
+
tacet_operator/targets.py
|
|
21
|
+
tacet_operator/predicates/__init__.py
|
|
22
|
+
tacet_operator/predicates/hf_card_training_data_v1.py
|
|
23
|
+
tests/test_epoch_end_to_end.py
|
|
24
|
+
tests/test_predicate.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tacet_operator
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crovia-tacet-operator"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Production operator for TACET: hourly epochs, signed surface snapshots, OTS anchoring, silence proofs as crovia.seal.v1"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
authors = [{ name = "Crovia Trust", email = "info@croviatrust.com" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["crovia-tacet>=0.3.0", "cryptography>=41.0.0"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
test = ["pytest>=7"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
tacet-operator = "tacet_operator.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
packages = ["tacet_operator", "tacet_operator.predicates"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""TACET operator: turns the reference primitives into a running log.
|
|
2
|
+
|
|
3
|
+
Layout (SPEC §12):
|
|
4
|
+
|
|
5
|
+
state/ private, on the operator host
|
|
6
|
+
keys/ 32-byte Ed25519 seeds (operator, observer, issuer)
|
|
7
|
+
map.json {key_hex: value_hash_hex} current map
|
|
8
|
+
values/<key_hex>.json slot value objects (kind disclosure/retraction)
|
|
9
|
+
cursor.json rotation cursor over the target list
|
|
10
|
+
public/ served at /registry/data/tacet/
|
|
11
|
+
trust_root.json map_id, genesis, keys, drand chain, predicates
|
|
12
|
+
sheets/<epoch>.json signed epoch sheets (closed once OTS confirms)
|
|
13
|
+
snapshots/<epoch>.jsonl signed surface snapshots taken in the epoch
|
|
14
|
+
changes/<epoch>.json [[key_hex, value_hash_hex], ...] applied in the epoch
|
|
15
|
+
ots/<epoch>.ots OpenTimestamps proof of the sheet hash
|
|
16
|
+
proofs/<slug>.seal.json featured silence proofs wrapped in crovia.seal.v1
|
|
17
|
+
latest.json, index.json discovery
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""tacet-operator: run-epoch | refresh-anchors | publish | prove | verify | build-targets | keys"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .config import Settings
|
|
11
|
+
from .keys import load_all
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _settings(args) -> Settings:
|
|
15
|
+
s = Settings.from_env()
|
|
16
|
+
if args.state:
|
|
17
|
+
s.paths.state = Path(args.state)
|
|
18
|
+
if args.public:
|
|
19
|
+
s.paths.public = Path(args.public)
|
|
20
|
+
if getattr(args, "targets", None):
|
|
21
|
+
s.targets_file = Path(args.targets)
|
|
22
|
+
if getattr(args, "budget", None):
|
|
23
|
+
s.per_epoch_budget = args.budget
|
|
24
|
+
s.paths.ensure()
|
|
25
|
+
return s
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(argv=None) -> int:
|
|
29
|
+
p = argparse.ArgumentParser(prog="tacet-operator")
|
|
30
|
+
p.add_argument("--state", help="state directory (default $TACET_STATE or /opt/crovia/tacet/state)")
|
|
31
|
+
p.add_argument("--public", help="public directory (default $TACET_PUBLIC or /var/www/registry/data/tacet)")
|
|
32
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
33
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
34
|
+
|
|
35
|
+
r = sub.add_parser("run-epoch", help="emit the current epoch (and back-fill missed ones)")
|
|
36
|
+
r.add_argument("--targets", help="newline-separated target ids")
|
|
37
|
+
r.add_argument("--budget", type=int)
|
|
38
|
+
r.add_argument("--no-ots", action="store_true", help="do not stamp (tests)")
|
|
39
|
+
|
|
40
|
+
sub.add_parser("refresh-anchors", help="stamp missing proofs; close sheets confirmed in Bitcoin")
|
|
41
|
+
|
|
42
|
+
pb = sub.add_parser("publish", help="rewrite trust_root/index/latest/targets (+ featured proofs)")
|
|
43
|
+
pb.add_argument("--proofs", action="store_true")
|
|
44
|
+
|
|
45
|
+
pr = sub.add_parser("prove", help="build a silence proof for one target, wrapped in crovia.seal.v1")
|
|
46
|
+
pr.add_argument("target")
|
|
47
|
+
pr.add_argument("--from-epoch", type=int, default=0)
|
|
48
|
+
pr.add_argument("--to-epoch", type=int)
|
|
49
|
+
pr.add_argument("--strength", type=int, default=2, choices=(1, 2))
|
|
50
|
+
pr.add_argument("-o", "--out")
|
|
51
|
+
|
|
52
|
+
vf = sub.add_parser("verify", help="verify a wrapped silence proof offline")
|
|
53
|
+
vf.add_argument("file")
|
|
54
|
+
vf.add_argument("--operator-pubkey", help="expected operator key_hex (from trust_root.json)")
|
|
55
|
+
vf.add_argument("--no-beacon", action="store_true")
|
|
56
|
+
vf.add_argument("--no-ots", action="store_true")
|
|
57
|
+
|
|
58
|
+
bt = sub.add_parser("build-targets", help="derive the target list from public registry files")
|
|
59
|
+
bt.add_argument("--candidates", required=True)
|
|
60
|
+
bt.add_argument("--silence")
|
|
61
|
+
bt.add_argument("-o", "--out", required=True)
|
|
62
|
+
|
|
63
|
+
sub.add_parser("keys", help="print public keys (creating seeds if absent)")
|
|
64
|
+
|
|
65
|
+
args = p.parse_args(argv)
|
|
66
|
+
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
|
|
67
|
+
format="%(asctime)s %(levelname)s %(message)s", stream=sys.stderr)
|
|
68
|
+
|
|
69
|
+
if args.cmd == "verify":
|
|
70
|
+
# Third parties run this on any machine: it must not touch operator state or keys.
|
|
71
|
+
from .prove import verify_file
|
|
72
|
+
res = verify_file(Path(args.file), expected_operator_pubkey_hex=args.operator_pubkey,
|
|
73
|
+
check_beacon=not args.no_beacon, check_ots=not args.no_ots)
|
|
74
|
+
print(json.dumps(res, indent=1))
|
|
75
|
+
return 0 if res["ok"] else 1
|
|
76
|
+
|
|
77
|
+
s = _settings(args)
|
|
78
|
+
|
|
79
|
+
if args.cmd == "keys":
|
|
80
|
+
keys = load_all(s.paths.keys)
|
|
81
|
+
print(json.dumps({r: {"id": k.id, "pubkey": k.pubkey_json()} for r, k in keys.items()}, indent=1))
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
if args.cmd == "run-epoch":
|
|
85
|
+
from .publish import publish_all
|
|
86
|
+
from .runner import EpochRunner
|
|
87
|
+
keys = load_all(s.paths.keys)
|
|
88
|
+
runner = EpochRunner(s, keys)
|
|
89
|
+
if args.no_ots:
|
|
90
|
+
runner.ots_stamp = None
|
|
91
|
+
sheet = runner.run()
|
|
92
|
+
publish_all(s, keys)
|
|
93
|
+
print(json.dumps({"emitted": sheet["epoch"] if sheet else None}))
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
if args.cmd == "refresh-anchors":
|
|
97
|
+
from .publish import publish_all
|
|
98
|
+
from .runner import refresh_anchors
|
|
99
|
+
counts = refresh_anchors(s)
|
|
100
|
+
# A newly closed sheet changes every silence figure; re-issue the featured proofs at once.
|
|
101
|
+
publish_all(s, load_all(s.paths.keys), with_proofs=counts.get("closed", 0) > 0)
|
|
102
|
+
print(json.dumps(counts))
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
if args.cmd == "publish":
|
|
106
|
+
from .publish import publish_all
|
|
107
|
+
latest = publish_all(s, load_all(s.paths.keys), with_proofs=args.proofs)
|
|
108
|
+
print(json.dumps(latest, indent=1))
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
if args.cmd == "prove":
|
|
112
|
+
from .prove import build
|
|
113
|
+
keys = load_all(s.paths.keys)
|
|
114
|
+
bundle = build(s, keys["issuer"], args.target, from_epoch=args.from_epoch, to_epoch=args.to_epoch, strength=args.strength)
|
|
115
|
+
text = json.dumps(bundle, indent=1, sort_keys=True)
|
|
116
|
+
if args.out:
|
|
117
|
+
Path(args.out).write_text(text + "\n")
|
|
118
|
+
print(json.dumps({"out": args.out, "silence": bundle["proof"]["silence"], "seal_id": bundle["seal"]["seal_id"]}))
|
|
119
|
+
else:
|
|
120
|
+
print(text)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
if args.cmd == "build-targets":
|
|
124
|
+
from .targets import build_from_public
|
|
125
|
+
ids = build_from_public(Path(args.candidates), Path(args.silence) if args.silence else None)
|
|
126
|
+
Path(args.out).write_text("\n".join(ids) + "\n")
|
|
127
|
+
print(json.dumps({"targets": len(ids), "out": args.out}))
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
return 2
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
sys.exit(main())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
MAP_ID = "urn:crovia:tacet:map:disclosure"
|
|
9
|
+
OPERATOR_ID = "urn:crovia:tacet:operator:crovia-trust"
|
|
10
|
+
OBSERVER_ID = "urn:crovia:observer:hetzner-1"
|
|
11
|
+
ISSUER_ID = "urn:crovia:seal-issuer:tacet" # Seal issuer ids are constrained by the Seal spec
|
|
12
|
+
|
|
13
|
+
# Epoch 0 starts here; epoch e covers [GENESIS + e*EPOCH_SECONDS, GENESIS + (e+1)*EPOCH_SECONDS).
|
|
14
|
+
GENESIS = datetime(2026, 9, 19, 18, 0, 0, tzinfo=timezone.utc)
|
|
15
|
+
EPOCH_SECONDS = 3600
|
|
16
|
+
|
|
17
|
+
DRAND_CHAIN_HASH = "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce"
|
|
18
|
+
DRAND_URLS = (
|
|
19
|
+
"https://api.drand.sh",
|
|
20
|
+
"https://drand.cloudflare.com",
|
|
21
|
+
"https://api2.drand.sh",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
PUBLIC_BASE_URL = "https://croviatrust.com/registry/data/tacet"
|
|
25
|
+
USER_AGENT = "crovia-tacet-observer/0.1 (+https://croviatrust.com/registry/tacet/)"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Paths:
|
|
30
|
+
state: Path
|
|
31
|
+
public: Path
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def keys(self) -> Path:
|
|
35
|
+
return self.state / "keys"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def map_file(self) -> Path:
|
|
39
|
+
return self.state / "map.json"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def values(self) -> Path:
|
|
43
|
+
return self.state / "values"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def cursor(self) -> Path:
|
|
47
|
+
return self.state / "cursor.json"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def fetch_log(self) -> Path:
|
|
51
|
+
return self.state / "fetch_log"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def sheets(self) -> Path:
|
|
55
|
+
return self.public / "sheets"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def snapshots(self) -> Path:
|
|
59
|
+
return self.public / "snapshots"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def changes(self) -> Path:
|
|
63
|
+
return self.public / "changes"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def ots(self) -> Path:
|
|
67
|
+
return self.public / "ots"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def proofs(self) -> Path:
|
|
71
|
+
return self.public / "proofs"
|
|
72
|
+
|
|
73
|
+
def ensure(self) -> None:
|
|
74
|
+
for d in (self.state, self.keys, self.values, self.fetch_log,
|
|
75
|
+
self.public, self.sheets, self.snapshots, self.changes, self.ots, self.proofs):
|
|
76
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Settings:
|
|
81
|
+
paths: Paths
|
|
82
|
+
per_epoch_budget: int = 120 # surfaces fetched per epoch (HF-friendly: 2/min)
|
|
83
|
+
featured_every_epoch: int = 40 # featured targets observed in every epoch
|
|
84
|
+
request_delay_s: float = 0.6
|
|
85
|
+
request_timeout_s: float = 25.0
|
|
86
|
+
featured: list = field(default_factory=list)
|
|
87
|
+
targets_file: Path | None = None # newline-separated org/model ids
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_env(cls) -> "Settings":
|
|
91
|
+
state = Path(os.environ.get("TACET_STATE", "/opt/crovia/tacet/state"))
|
|
92
|
+
public = Path(os.environ.get("TACET_PUBLIC", "/var/www/registry/data/tacet"))
|
|
93
|
+
s = cls(paths=Paths(state, public))
|
|
94
|
+
if os.environ.get("TACET_BUDGET"):
|
|
95
|
+
s.per_epoch_budget = int(os.environ["TACET_BUDGET"])
|
|
96
|
+
if os.environ.get("TACET_TARGETS"):
|
|
97
|
+
s.targets_file = Path(os.environ["TACET_TARGETS"])
|
|
98
|
+
return s
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def epoch_of(ts: datetime) -> int:
|
|
102
|
+
return int((ts - GENESIS).total_seconds() // EPOCH_SECONDS)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def epoch_bounds(epoch: int) -> tuple[str, str]:
|
|
106
|
+
start = GENESIS.timestamp() + epoch * EPOCH_SECONDS
|
|
107
|
+
end = start + EPOCH_SECONDS
|
|
108
|
+
f = lambda t: datetime.fromtimestamp(t, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # noqa: E731
|
|
109
|
+
return f(start), f(end)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def now_iso() -> str:
|
|
113
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""drand League of Entropy beacon: the epoch's lower time bound (SPEC §6).
|
|
2
|
+
|
|
3
|
+
The default chain (pedersen-bls-chained, 30 s period, genesis 1595431050) is
|
|
4
|
+
pinned by hash. `fetch_latest` tries the public relays in order and returns the
|
|
5
|
+
`opened` object for an epoch sheet. `round_time` lets verifiers check that the
|
|
6
|
+
round could not have existed before `epoch_start`.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import urllib.request
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
from .config import DRAND_CHAIN_HASH, DRAND_URLS, USER_AGENT
|
|
15
|
+
|
|
16
|
+
PERIOD = 30
|
|
17
|
+
GENESIS_TIME = 1595431050
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DrandError(RuntimeError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get(url: str, timeout: float) -> Dict[str, Any]:
|
|
25
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "application/json"})
|
|
26
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
27
|
+
return json.loads(r.read().decode("utf-8"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fetch_latest(timeout: float = 10.0) -> Dict[str, Any]:
|
|
31
|
+
last = None
|
|
32
|
+
for base in DRAND_URLS:
|
|
33
|
+
try:
|
|
34
|
+
d = _get(f"{base}/{DRAND_CHAIN_HASH}/public/latest", timeout)
|
|
35
|
+
for k in ("round", "randomness", "signature"):
|
|
36
|
+
if k not in d:
|
|
37
|
+
raise DrandError(f"{base}: missing {k}")
|
|
38
|
+
return {
|
|
39
|
+
"kind": "drand",
|
|
40
|
+
"chain_hash": DRAND_CHAIN_HASH,
|
|
41
|
+
"round": int(d["round"]),
|
|
42
|
+
"randomness": d["randomness"],
|
|
43
|
+
"signature": d["signature"],
|
|
44
|
+
}
|
|
45
|
+
except Exception as e: # noqa: BLE001 - try next relay
|
|
46
|
+
last = e
|
|
47
|
+
raise DrandError(f"all drand relays failed: {last}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fetch_info(timeout: float = 10.0) -> Dict[str, Any]:
|
|
51
|
+
last = None
|
|
52
|
+
for base in DRAND_URLS:
|
|
53
|
+
try:
|
|
54
|
+
d = _get(f"{base}/{DRAND_CHAIN_HASH}/info", timeout)
|
|
55
|
+
if d.get("hash") != DRAND_CHAIN_HASH:
|
|
56
|
+
raise DrandError("chain hash mismatch")
|
|
57
|
+
return {"chain_hash": d["hash"], "public_key": d["public_key"], "period": d["period"],
|
|
58
|
+
"genesis_time": d["genesis_time"], "scheme": d.get("schemeID")}
|
|
59
|
+
except Exception as e: # noqa: BLE001
|
|
60
|
+
last = e
|
|
61
|
+
raise DrandError(f"all drand relays failed: {last}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fetch_round(round_no: int, timeout: float = 10.0) -> Dict[str, Any]:
|
|
65
|
+
"""Historical round (used only to back-fill empty epochs after downtime)."""
|
|
66
|
+
last = None
|
|
67
|
+
for base in DRAND_URLS:
|
|
68
|
+
try:
|
|
69
|
+
d = _get(f"{base}/{DRAND_CHAIN_HASH}/public/{round_no}", timeout)
|
|
70
|
+
if int(d["round"]) != round_no:
|
|
71
|
+
raise DrandError("round mismatch")
|
|
72
|
+
return {"kind": "drand", "chain_hash": DRAND_CHAIN_HASH, "round": round_no,
|
|
73
|
+
"randomness": d["randomness"], "signature": d["signature"]}
|
|
74
|
+
except Exception as e: # noqa: BLE001
|
|
75
|
+
last = e
|
|
76
|
+
raise DrandError(f"all drand relays failed for round {round_no}: {last}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def round_time(round_no: int) -> int:
|
|
80
|
+
"""Unix time at which `round_no` became available."""
|
|
81
|
+
return GENESIS_TIME + (round_no - 1) * PERIOD
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def round_at(unix_time: int) -> int:
|
|
85
|
+
"""First round available at or after `unix_time`."""
|
|
86
|
+
if unix_time <= GENESIS_TIME:
|
|
87
|
+
return 1
|
|
88
|
+
return (unix_time - GENESIS_TIME + PERIOD - 1) // PERIOD + 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def beacon_check(opened: Dict[str, Any], epoch_start: str, tolerance_s: int = 1800) -> bool:
|
|
92
|
+
"""SPEC §8.5 step 2: the round must belong to the pinned chain and sit at the epoch start."""
|
|
93
|
+
from datetime import datetime, timezone
|
|
94
|
+
if opened.get("chain_hash") != DRAND_CHAIN_HASH:
|
|
95
|
+
return False
|
|
96
|
+
start = int(datetime.strptime(epoch_start, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).timestamp())
|
|
97
|
+
t = round_time(int(opened["round"]))
|
|
98
|
+
return start - PERIOD <= t <= start + tolerance_s
|