signafaide 0.2.1__py3-none-any.whl
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.
- signafaide/__init__.py +263 -0
- signafaide/__main__.py +5 -0
- signafaide/_plural.py +6 -0
- signafaide/_version.py +1 -0
- signafaide/bitcoin.py +102 -0
- signafaide/cli.py +449 -0
- signafaide/crypto.py +196 -0
- signafaide/decorator.py +318 -0
- signafaide/drand.py +75 -0
- signafaide/experiments.py +210 -0
- signafaide/http.py +148 -0
- signafaide/jcs.py +125 -0
- signafaide/ots.py +256 -0
- signafaide/records.py +379 -0
- signafaide/replay.py +302 -0
- signafaide/runtime.py +1047 -0
- signafaide/state.py +993 -0
- signafaide/telemetry.py +65 -0
- signafaide/verifier.py +1330 -0
- signafaide-0.2.1.dist-info/METADATA +238 -0
- signafaide-0.2.1.dist-info/RECORD +23 -0
- signafaide-0.2.1.dist-info/WHEEL +4 -0
- signafaide-0.2.1.dist-info/entry_points.txt +2 -0
signafaide/__init__.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""signafAIde: signed, chained records of what your AI agents decided.
|
|
2
|
+
|
|
3
|
+
import signafaide
|
|
4
|
+
|
|
5
|
+
@signafaide.record
|
|
6
|
+
def approve_payout(claim_id: str, amount: float): ...
|
|
7
|
+
|
|
8
|
+
Every call writes a ``decision`` record before the function runs and an
|
|
9
|
+
``outcome`` record after it. Records are signed with the agent's own key and
|
|
10
|
+
chained locally in ``.signafaide/``. Nothing is sent until a human opts in
|
|
11
|
+
(``signafaide init``, or SIGNAFAIDE_API_KEY); then records ship to the service
|
|
12
|
+
in the background. Only hashes leave the machine unless ``store_content=True``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Optional, Union
|
|
19
|
+
|
|
20
|
+
from ._plural import plural
|
|
21
|
+
|
|
22
|
+
from . import runtime
|
|
23
|
+
from ._version import __version__
|
|
24
|
+
from .drand import load_verifier as load_drand_verifier
|
|
25
|
+
from .decorator import NotRecorded, note, record, to_jsonable, wrap_tool, wrap_tools
|
|
26
|
+
from .experiments import ExperimentReport, VerificationFailed, compare
|
|
27
|
+
from .http import SignafaideError
|
|
28
|
+
from .replay import Replay, replay
|
|
29
|
+
from .runtime import Recorder, recording_disabled, test_mode
|
|
30
|
+
from .bitcoin import DEFAULT_URL as DEFAULT_TIMESTAMP_URL, lookup as lookup_timestamps
|
|
31
|
+
from .verifier import (
|
|
32
|
+
DRAND_NOT_CHECKED, KeysRequired, Report, bitcoin_heights, is_loopback_url, load_agent_keys, load_content, load_heads, load_service_keys,
|
|
33
|
+
loads as _loads, plain_http, verify_local, verify_pack,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"recording_disabled",
|
|
38
|
+
"test_mode",
|
|
39
|
+
"init",
|
|
40
|
+
"record",
|
|
41
|
+
"wrap_tools",
|
|
42
|
+
"wrap_tool",
|
|
43
|
+
"note",
|
|
44
|
+
"flush",
|
|
45
|
+
"stop",
|
|
46
|
+
"verify",
|
|
47
|
+
"status",
|
|
48
|
+
"replay",
|
|
49
|
+
"compare",
|
|
50
|
+
"Replay",
|
|
51
|
+
"ExperimentReport",
|
|
52
|
+
"VerificationFailed",
|
|
53
|
+
"Recorder",
|
|
54
|
+
"Report",
|
|
55
|
+
"SignafaideError",
|
|
56
|
+
"NotRecorded",
|
|
57
|
+
"KeysRequired",
|
|
58
|
+
"to_jsonable",
|
|
59
|
+
"__version__",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def not_yet(unsent: int, total: int, has_key: bool) -> list:
|
|
64
|
+
"""What the local verify could not check because the records have not reached a witness yet."""
|
|
65
|
+
if not unsent:
|
|
66
|
+
return []
|
|
67
|
+
what = f"all {total}" if unsent == total else f"{unsent} of {total}"
|
|
68
|
+
return [
|
|
69
|
+
f"checked here: signatures, chain links and content hashes of {what} record{'' if total == 1 else 's'}, using this machine's keys",
|
|
70
|
+
f"co-signature, public timestamp: not yet for {what} (`signafaide flush` sends them now, or says why it cannot)"
|
|
71
|
+
if has_key
|
|
72
|
+
else "co-signature, public timestamp: not yet. Run `signafaide init` to send hashes, or self-host (SIGNAFAIDE_URL)",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def init(
|
|
77
|
+
*,
|
|
78
|
+
url: Optional[str] = None,
|
|
79
|
+
api_key: Optional[str] = None,
|
|
80
|
+
home: Optional[str] = None,
|
|
81
|
+
store_content: bool = False,
|
|
82
|
+
quiet: bool = False,
|
|
83
|
+
key_provider: Optional[Callable[[str, str], Any]] = None,
|
|
84
|
+
exit_flush: Optional[float] = None,
|
|
85
|
+
head_file: Optional[str] = None,
|
|
86
|
+
on_head: Optional[Callable[[list], Any]] = None,
|
|
87
|
+
) -> Recorder:
|
|
88
|
+
"""Configure signafAIde (optional). Finds SIGNAFAIDE_API_KEY (env or .env).
|
|
89
|
+
Without one, records stay local (signed, chained, verifiable in ``.signafaide/``)
|
|
90
|
+
and nothing is sent: it never creates a sandbox. A human opts in with
|
|
91
|
+
``signafaide init`` or by setting the key. Never raises when the service is
|
|
92
|
+
offline: records are kept in the outbox."""
|
|
93
|
+
rec = runtime.configure(url=url, api_key=api_key, home=home, store_content=store_content, quiet=quiet, key_provider=key_provider,
|
|
94
|
+
exit_flush=exit_flush, head_file=head_file, on_head=on_head)
|
|
95
|
+
key = rec.api_key
|
|
96
|
+
if not key:
|
|
97
|
+
return rec
|
|
98
|
+
try:
|
|
99
|
+
rec.resolve_project(key)
|
|
100
|
+
except SignafaideError as e:
|
|
101
|
+
# A wrong key or an unreachable witness is a warning, never an exception in the
|
|
102
|
+
# caller's program (records are kept locally until it is fixed).
|
|
103
|
+
if not quiet:
|
|
104
|
+
import sys
|
|
105
|
+
|
|
106
|
+
print(f"signafaide: {e} Records are kept locally until then.", file=sys.stderr)
|
|
107
|
+
return rec
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def flush(timeout: float = 10.0) -> dict:
|
|
111
|
+
"""Ship the outbox now. Returns ``{shipped, left, errors}``."""
|
|
112
|
+
return runtime.get().flush(timeout)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def stop(timeout: float = 10.0) -> dict:
|
|
116
|
+
"""A clean stop of this instance (§8): ship everything, then write and ship a signed stop note
|
|
117
|
+
per agent, so the next instance's start is a clean restart. Call it in the shutdown handler of
|
|
118
|
+
an ephemeral container. Returns ``{shipped, left, errors, stopped}``."""
|
|
119
|
+
return runtime.get().stop(timeout)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def status() -> dict:
|
|
123
|
+
"""Key, env, agents, last receipts and outbox size."""
|
|
124
|
+
return runtime.get().status()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def check_held(rec: Recorder) -> dict:
|
|
128
|
+
"""Records ``signafaide repair`` set aside: their signatures (with the key that signed them),
|
|
129
|
+
chain links and content hashes still check; they are not co-signed. Failures name the check."""
|
|
130
|
+
from .crypto import check_content, verify_sig
|
|
131
|
+
from .records import record_hash
|
|
132
|
+
|
|
133
|
+
keys = {k["key_id"]: k for k in rec.state.agent_public_keys()}
|
|
134
|
+
failures, records = [], 0
|
|
135
|
+
segs = rec.state.held_segments()
|
|
136
|
+
for chain, path, recs in segs:
|
|
137
|
+
prev = None
|
|
138
|
+
for r in recs:
|
|
139
|
+
records += 1
|
|
140
|
+
item = f"{chain}#{r['envelope']['seq']} (held, {path.name})"
|
|
141
|
+
k = keys.get(r.get("key_id"))
|
|
142
|
+
if k is None:
|
|
143
|
+
failures.append({"check": "record_sig", "item": item, "message": f"no key {r.get('key_id')} for agent {r['envelope']['agent']} on this machine"})
|
|
144
|
+
elif not verify_sig(k["public_key"], "draft" if r.get("draft") else "record", record_hash(r), r.get("sig", "")):
|
|
145
|
+
failures.append({"check": "record_sig", "item": item, "message": "signature does not verify"})
|
|
146
|
+
if prev is not None and (r["envelope"]["seq"] != prev["envelope"]["seq"] + 1 or r["envelope"]["prev"] != record_hash(prev)):
|
|
147
|
+
failures.append({"check": "chain", "item": item, "message": "does not follow the held record before it"})
|
|
148
|
+
try:
|
|
149
|
+
h = check_content(r.get("content"), r.get("salt"), r["envelope"]["content_hash"])
|
|
150
|
+
if h:
|
|
151
|
+
failures.append({"check": "content", "item": item, "message": f"content hashes to {h}, envelope says {r['envelope']['content_hash']}"})
|
|
152
|
+
except ValueError as e:
|
|
153
|
+
failures.append({"check": "content", "item": item, "message": str(e)})
|
|
154
|
+
prev = r
|
|
155
|
+
return {"records": records, "segments": len(segs), "failures": failures}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def verify(
|
|
159
|
+
pack: Union[str, Path, dict, None] = None,
|
|
160
|
+
*,
|
|
161
|
+
keys: Any = None,
|
|
162
|
+
trust_embedded_keys: bool = False,
|
|
163
|
+
agent_keys: Any = None,
|
|
164
|
+
trust_embedded_agent_keys: bool = False,
|
|
165
|
+
head: Any = None,
|
|
166
|
+
timestamp_check: str = DEFAULT_TIMESTAMP_URL,
|
|
167
|
+
offline: bool = False,
|
|
168
|
+
content: Union[str, Path, None] = None,
|
|
169
|
+
fail_on_pending_send: bool = False,
|
|
170
|
+
) -> Report:
|
|
171
|
+
"""Run the §7 verifier on a pack (path or dict) or, with no pack, on the
|
|
172
|
+
local ``.signafaide`` state. ``print(report)`` shows one line per check and
|
|
173
|
+
the result line; ``report.ok`` is the result.
|
|
174
|
+
|
|
175
|
+
``keys`` is a file, URL or dict of service keys. Without it the keys are
|
|
176
|
+
fetched from the service (``SIGNAFAIDE_URL``), which is independent of the
|
|
177
|
+
pack. ``agent_keys`` is the customer's manifest (``signafaide keys export``);
|
|
178
|
+
for a pack it defaults to this project's own agent keys in ``.signafaide/agents``.
|
|
179
|
+
``head`` is a client's ``head.json``; for a pack it defaults to this project's own
|
|
180
|
+
``.signafaide/head.json``. Public timestamps are checked online against
|
|
181
|
+
``timestamp_check`` unless ``offline``. Keys over plain http are accepted only from this
|
|
182
|
+
machine and never count as independent. Raises :class:`KeysRequired` when no service
|
|
183
|
+
keys can be had, and OSError or ValueError when an input cannot be read.
|
|
184
|
+
|
|
185
|
+
For local state, a chain that cannot ship (refused by the witness, or wedged here) makes
|
|
186
|
+
the result BLOCKED, which always fails; ``fail_on_pending_send`` also fails while records
|
|
187
|
+
wait to be co-signed (for CI).
|
|
188
|
+
"""
|
|
189
|
+
rec = runtime.get()
|
|
190
|
+
notes = []
|
|
191
|
+
service_keys = None
|
|
192
|
+
keys_plain = False
|
|
193
|
+
if keys is not None:
|
|
194
|
+
service_keys = load_service_keys(keys)
|
|
195
|
+
keys_plain = plain_http(keys)
|
|
196
|
+
notes.append(f"service keys: {keys if isinstance(keys, (str, Path)) else 'given'}")
|
|
197
|
+
elif pack is None and not rec.state.chain_heads():
|
|
198
|
+
service_keys = [] # nothing has been shipped, so nothing needs a service key: no request is made
|
|
199
|
+
elif pack is None or not trust_embedded_keys:
|
|
200
|
+
url = f"{rec.url}/.well-known/signafaide-keys.json"
|
|
201
|
+
if url.startswith("http://") and not is_loopback_url(url):
|
|
202
|
+
raise KeysRequired(f"will not fetch service keys over plain http from {url}: anyone on the network path can alter them; pass --keys <file|https url>")
|
|
203
|
+
keys_plain = url.startswith("http://")
|
|
204
|
+
try:
|
|
205
|
+
service_keys = load_service_keys(rec.client().request("GET", "/.well-known/signafaide-keys.json", auth=False))
|
|
206
|
+
notes.append(f"service keys: {url}" + (" (plain http on this machine: not independent)" if keys_plain else ""))
|
|
207
|
+
except (SignafaideError, ValueError) as e:
|
|
208
|
+
if pack is not None or rec.state.chain_heads():
|
|
209
|
+
raise KeysRequired(f"could not load service keys from {url} ({e}); pass --keys <file|url>") from None
|
|
210
|
+
service_keys = [] # nothing has been shipped, so nothing needs a service key
|
|
211
|
+
drand = load_drand_verifier()
|
|
212
|
+
if pack is None:
|
|
213
|
+
report = verify_local(rec.state, service_keys, drand=drand, service_keys_plain_http=keys_plain)
|
|
214
|
+
notes.insert(0, f"local state: {rec.state.home}")
|
|
215
|
+
records = [r for c in rec.state.chains() for r in rec.state.read_all(c)]
|
|
216
|
+
unsent = sum(1 for r in records if "receipt" not in r)
|
|
217
|
+
report.after += not_yet(unsent, len(records), bool(rec.api_key))
|
|
218
|
+
held = check_held(rec)
|
|
219
|
+
if held["segments"]:
|
|
220
|
+
for f in held["failures"]:
|
|
221
|
+
c = report.check(f["check"])
|
|
222
|
+
c.failures.append({"item": f["item"], "message": f["message"]})
|
|
223
|
+
c.status = "fail"
|
|
224
|
+
report.after.append(f"held aside by `signafaide repair`: {plural(held['records'], 'record')} in {plural(held['segments'], 'segment')} "
|
|
225
|
+
"(signatures, links and content hashes checked here; not co-signed)")
|
|
226
|
+
for b in rec.diagnose():
|
|
227
|
+
report.blocked.append(f"{b['chain']}: {b['reason']}; {plural(b['records'], 'record')} cannot ship. Next: {b['next']}")
|
|
228
|
+
if fail_on_pending_send:
|
|
229
|
+
report.not_sent = unsent
|
|
230
|
+
else:
|
|
231
|
+
if isinstance(pack, (str, Path)):
|
|
232
|
+
pack = _loads(Path(pack).read_text())
|
|
233
|
+
if content is None and (rec.state.home / "chain").is_dir():
|
|
234
|
+
content = rec.state.home / "chain"
|
|
235
|
+
contents = load_content(content) if content is not None else None
|
|
236
|
+
independent_agents = None
|
|
237
|
+
if agent_keys is not None:
|
|
238
|
+
independent_agents = load_agent_keys(agent_keys)
|
|
239
|
+
notes.append(f"agent keys: {agent_keys if isinstance(agent_keys, (str, Path)) else 'given'}")
|
|
240
|
+
elif not trust_embedded_agent_keys:
|
|
241
|
+
local = [{"agent": k["agent"], "key_id": k["key_id"], "public_key": k["public_key"], "valid_from": k["created"], "valid_to": k["retired"]}
|
|
242
|
+
for k in rec.state.agent_public_keys() if k.get("created")]
|
|
243
|
+
if local:
|
|
244
|
+
independent_agents = local
|
|
245
|
+
notes.append(f"agent keys: this project's own ({rec.state.home / 'agents'})")
|
|
246
|
+
if head is not None:
|
|
247
|
+
heads = load_heads(head)
|
|
248
|
+
else:
|
|
249
|
+
# This project's own newest signed heads: a pack cut off before them fails.
|
|
250
|
+
heads = load_heads(rec.state.heads()) or None
|
|
251
|
+
if heads:
|
|
252
|
+
notes.append(f"head: this project's own ({rec.state.home / 'head.json'})")
|
|
253
|
+
heights = bitcoin_heights(pack)
|
|
254
|
+
stamps = None if offline or not heights else lookup_timestamps(timestamp_check, heights)
|
|
255
|
+
report = verify_pack(pack, service_keys, trust_embedded_keys=trust_embedded_keys, agent_keys=independent_agents,
|
|
256
|
+
trust_embedded_agent_keys=trust_embedded_agent_keys, heads=heads, bitcoin=stamps, contents=contents, drand=drand,
|
|
257
|
+
service_keys_plain_http=keys_plain, agent_keys_plain_http=plain_http(agent_keys))
|
|
258
|
+
if independent_agents is None and not trust_embedded_agent_keys:
|
|
259
|
+
notes.append("no agent keys: ask the customer for `signafaide keys export -o agent-keys.json` and pass --agent-keys agent-keys.json")
|
|
260
|
+
if drand is None and report.check("drand").checked:
|
|
261
|
+
notes.append(DRAND_NOT_CHECKED)
|
|
262
|
+
report.notes[:0] = notes
|
|
263
|
+
return report
|
signafaide/__main__.py
ADDED
signafaide/_plural.py
ADDED
signafaide/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
signafaide/bitcoin.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Bitcoin block headers for §7 check 11, from an Esplora-style explorer API.
|
|
2
|
+
|
|
3
|
+
``GET {base}/block-height/{h}`` gives the block hash; ``GET {base}/block/{hash}/header`` gives
|
|
4
|
+
the raw 80-byte header (hex). The header is trusted only when it hashes to that block hash and
|
|
5
|
+
meets its own proof of work (:func:`check_header`). mempool.space and blockstream.info serve
|
|
6
|
+
this API. An explorer over plain http is never asked: anyone on the path could answer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from typing import Any, Dict, Iterable, Optional, Tuple, Union
|
|
17
|
+
|
|
18
|
+
from ._version import __version__
|
|
19
|
+
|
|
20
|
+
DEFAULT_URL = "https://mempool.space/api"
|
|
21
|
+
_HEX64 = re.compile(r"\A[0-9a-fA-F]{64}\Z")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
25
|
+
def redirect_request(self, *a, **k): # an explorer answer is never a redirect
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get(url: str, timeout: float) -> str:
|
|
30
|
+
req = urllib.request.Request(url, headers={"User-Agent": f"signafaide-python/{__version__}", "Accept": "*/*"})
|
|
31
|
+
with urllib.request.build_opener(_NoRedirect).open(req, timeout=timeout) as r:
|
|
32
|
+
return r.read(1 << 20).decode()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#: Why a plain-http explorer is not asked.
|
|
36
|
+
PLAIN_HTTP_EXPLORER = "the explorer URL uses plain http, which anyone on the network path can alter; use https"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def target(bits: int) -> Optional[int]:
|
|
40
|
+
"""The target of a compact ``nBits``; None when it is negative, zero or overflows."""
|
|
41
|
+
exponent, mantissa = bits >> 24, bits & 0x7FFFFF
|
|
42
|
+
if bits & 0x800000 or mantissa == 0:
|
|
43
|
+
return None
|
|
44
|
+
t = mantissa << (8 * (exponent - 3)) if exponent >= 3 else mantissa >> (8 * (3 - exponent))
|
|
45
|
+
return t if 0 < t < 1 << 256 else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_header(block_hash: str, header_hex: str) -> bytes:
|
|
49
|
+
"""The Merkle root (header byte order) of a header that hashes to ``block_hash`` (display hex)
|
|
50
|
+
and meets its own proof of work. Raises ValueError saying why the header is not trusted."""
|
|
51
|
+
try:
|
|
52
|
+
header = bytes.fromhex(header_hex.strip())
|
|
53
|
+
except (ValueError, AttributeError):
|
|
54
|
+
raise ValueError("the block header is not hex") from None
|
|
55
|
+
if len(header) != 80:
|
|
56
|
+
raise ValueError(f"the block header is {len(header)} bytes, not 80")
|
|
57
|
+
block_id = hashlib.sha256(hashlib.sha256(header).digest()).digest()[::-1]
|
|
58
|
+
if block_id.hex() != str(block_hash).strip().lower():
|
|
59
|
+
raise ValueError(f"the block header does not hash to block {block_hash}")
|
|
60
|
+
bits = int.from_bytes(header[72:76], "little")
|
|
61
|
+
t = target(bits)
|
|
62
|
+
if t is None:
|
|
63
|
+
raise ValueError(f"the block header's nBits {bits:#010x} is not a valid target")
|
|
64
|
+
if int.from_bytes(block_id, "big") > t:
|
|
65
|
+
raise ValueError(f"block {block_hash} does not meet the proof of work its header claims")
|
|
66
|
+
return header[36:68]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def block_header(base: str, height: int, timeout: float = 15.0) -> Dict[str, str]:
|
|
70
|
+
"""``{hash, header}`` of block ``height`` as the explorer serves them. Raises ValueError."""
|
|
71
|
+
base = base.rstrip("/")
|
|
72
|
+
try:
|
|
73
|
+
h = _get(f"{base}/block-height/{height}", timeout).strip().lower()
|
|
74
|
+
if not _HEX64.match(h):
|
|
75
|
+
raise ValueError(f"no block at height {height}")
|
|
76
|
+
header = _get(f"{base}/block/{h}/header", timeout).strip().lower()
|
|
77
|
+
except (urllib.error.URLError, OSError, ValueError) as e:
|
|
78
|
+
raise ValueError(f"{base}: {getattr(e, 'reason', e)}") from None
|
|
79
|
+
return {"hash": h, "header": header}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def lookup(base: str, heights: Iterable[int]) -> Dict[int, Tuple[bool, Any]]:
|
|
83
|
+
"""height -> (True, {hash, header}) or (False, why it could not be fetched). A plain-http
|
|
84
|
+
explorer is not asked: every height then says why (the check shows NOT CHECKED)."""
|
|
85
|
+
out: Dict[int, Tuple[bool, Any]] = {}
|
|
86
|
+
plain = base.startswith("http://")
|
|
87
|
+
if plain:
|
|
88
|
+
print(f"WARNING: --timestamp-check {base} uses plain http://, which anyone on the network path can alter: public timestamps are not checked; use https://", file=sys.stderr)
|
|
89
|
+
for h in heights:
|
|
90
|
+
if plain:
|
|
91
|
+
out[h] = (False, PLAIN_HTTP_EXPLORER)
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
out[h] = (True, block_header(base, h))
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
out[h] = (False, str(e))
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def roots_from_vector(m: Dict[str, Union[str, dict]]) -> Dict[int, Tuple[bool, Any]]:
|
|
101
|
+
"""The ``bitcoin`` object of a test vector: height -> {hash, header}."""
|
|
102
|
+
return {int(h): (True, r) for h, r in m.items() if isinstance(r, dict)}
|