robustrep 0.1.1__tar.gz → 0.2.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.
- {robustrep-0.1.1 → robustrep-0.2.0}/CHANGELOG.md +111 -0
- {robustrep-0.1.1/robustrep.egg-info → robustrep-0.2.0}/PKG-INFO +6 -2
- {robustrep-0.1.1 → robustrep-0.2.0}/README.md +4 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/constraints.txt +1 -1
- {robustrep-0.1.1 → robustrep-0.2.0}/pyproject.toml +5 -2
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/cli.py +7 -2
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/config.py +24 -2
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/provenance_keys.py +1 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/publish.py +20 -3
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/render.py +212 -115
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/base_erc8004.py +114 -16
- robustrep-0.2.0/robustrep/sources/log_fetcher.py +201 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/rpc.py +92 -7
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/store.py +61 -0
- {robustrep-0.1.1 → robustrep-0.2.0/robustrep.egg-info}/PKG-INFO +6 -2
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep.egg-info/SOURCES.txt +3 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep.egg-info/requires.txt +1 -1
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/e2e/test_live.py +73 -27
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/e2e/test_replay.py +4 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_base_erc8004.py +143 -1
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_cli.py +65 -2
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_live_guard.py +104 -12
- robustrep-0.2.0/tests/unit/test_log_fetcher.py +362 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_report.py +1 -13
- robustrep-0.2.0/tests/unit/test_report_log_sources.py +109 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_report_publish.py +51 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_rpc.py +167 -3
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_schema.py +28 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_store.py +76 -1
- {robustrep-0.1.1 → robustrep-0.2.0}/LICENSE +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/MANIFEST.in +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/SECURITY.md +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/__init__.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/aggregate.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/evidence.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/explain.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/normalize.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/pipeline.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/__init__.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/adversarial.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/export.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/figures.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/report/sensitivity.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/schema.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/__init__.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/blockscout_util.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/evidence_batch.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/evidence_fetch.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/http_util.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sources/rater_profile.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep/sybil.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep.egg-info/dependency_links.txt +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep.egg-info/entry_points.txt +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/robustrep.egg-info/top_level.txt +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/setup.cfg +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/adversarial/test_attacks.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/conftest.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/e2e/fixtures/base_logs_2000.json +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_aggregate.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_blockscout_util.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_conn_guard.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_evidence.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_evidence_batch.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_evidence_fetch.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_explain.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_http_util.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_normalize.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_pipeline.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_rater_profile.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_sybil.py +0 -0
- {robustrep-0.1.1 → robustrep-0.2.0}/tests/unit/test_version.py +0 -0
|
@@ -4,6 +4,115 @@ All notable changes to robustrep are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project is pre-1.0,
|
|
5
5
|
so a `0.x` bump may change published numbers without changing the API.
|
|
6
6
|
|
|
7
|
+
## [0.2.0] — 2026-10-12
|
|
8
|
+
|
|
9
|
+
Adaptive `eth_getLogs` fetching. On 2026-10-12 mainnet.base.org began answering
|
|
10
|
+
every `eth_getLogs` with HTTP 429 (`-32011 request limit reached`), even for a
|
|
11
|
+
100-block window on a first request. The remaining free endpoints each cap the
|
|
12
|
+
window differently (publicnode serves roughly the last 5,000 blocks, nodies
|
|
13
|
+
50 blocks per call but deep history, drpc about 100), so fixed 2,000-block
|
|
14
|
+
chunks could not back-fill the roughly 1.2M blocks since the last report. `fetch`
|
|
15
|
+
now learns each endpoint's window at runtime and records which host served
|
|
16
|
+
what. Scoring is unchanged.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`LogFetcher` (`robustrep/sources/log_fetcher.py`).** Sits between
|
|
21
|
+
`sync_feedback` and `RpcClient`: given a block range it returns all logs or
|
|
22
|
+
raises `RpcError`. It learns each endpoint's window cap (shrinks by at least
|
|
23
|
+
half on a range-too-large error, in one step to the cap when the server
|
|
24
|
+
names one), marks an endpoint dead for the run when it rejects the minimum
|
|
25
|
+
window (default 50 blocks), rotates past dead endpoints, and bounds invalid
|
|
26
|
+
rounds so it always terminates. `LogFetcher.from_config(rpc, cfg)` is the
|
|
27
|
+
construction path shared by the CLI and the live tests.
|
|
28
|
+
- **`sync_log` table and `Store.sync_sources()`.** One row per synced
|
|
29
|
+
sub-window of a chunk (`from_block, to_block, host, window, n_logs, fetched_at`), written by
|
|
30
|
+
delete-intersecting-then-insert so reruns are idempotent;
|
|
31
|
+
`sync_sources()` aggregates per host.
|
|
32
|
+
- **`rpc_sources` report provenance** (last in `PROVENANCE_KEYS`). `report.md`
|
|
33
|
+
gets a `Log sources:` line and a `Source recording starts at block ...`
|
|
34
|
+
line under Provenance (`Log sources: not recorded ...` for data fetched
|
|
35
|
+
before this version), and an always-present Limitations bullet, `Log sources
|
|
36
|
+
not cross-checked.` RPC endpoint hostnames appear in the public report and
|
|
37
|
+
`scores.json`; do not use an endpoint whose hostname embeds an account id or
|
|
38
|
+
key.
|
|
39
|
+
- **`Config` fields:** `rpc_window_hints` (tuple of `(host, cap)` pairs,
|
|
40
|
+
default empty), `rpc_min_window` (50) and `rpc_min_interval` (0.5 s between
|
|
41
|
+
call starts; this is the CLI default). The library default (`LogFetcher`'s
|
|
42
|
+
`min_interval`) is 0; `RpcClient` has no pacing.
|
|
43
|
+
- **`RpcRangeTooLargeError(endpoint, numbers)`**, raised immediately (no
|
|
44
|
+
rotation or backoff) on provider range-cap errors. Its message is fixed
|
|
45
|
+
text; only parsed integers leave the module. `RpcClient` also gains
|
|
46
|
+
`current_url` and `rotate()`.
|
|
47
|
+
- **`Store.failed_ranges()`**, a read-only view of the recorded failed
|
|
48
|
+
ranges, used by the live suite.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- **Default `rpc_urls` is now four endpoints, in rotation order:**
|
|
53
|
+
`https://mainnet.base.org`, `https://base-rpc.publicnode.com`,
|
|
54
|
+
`https://base-pokt.nodies.app`, `https://base.drpc.org`. `--rpc-url`
|
|
55
|
+
(repeatable) still replaces the whole list. Report provenance does not
|
|
56
|
+
record this list; it records `rpc_sources`, the hosts that actually served
|
|
57
|
+
each block range.
|
|
58
|
+
- **`sync_feedback(..., fetcher=None)`.** With a fetcher it records
|
|
59
|
+
`sync_log`; without one it behaves as before and records no sources. The
|
|
60
|
+
CLI always passes one.
|
|
61
|
+
- **Live suite: an unusable endpoint is a labelled failure, never a skip.**
|
|
62
|
+
`rpc_available()` and the new `check_failed_ranges()` report
|
|
63
|
+
`RPC unavailable: rate limited` / `transport failure: ...` with `pytest.fail`
|
|
64
|
+
instead of `pytest.skip`. A run that cannot reach the chain has checked
|
|
65
|
+
nothing, and a skip would have hidden the mainnet.base.org change for weeks;
|
|
66
|
+
the owner prefers the email. The label (never server-supplied text) says
|
|
67
|
+
whether to look at the endpoint list or at the data. Genuine data failures
|
|
68
|
+
propagate unchanged. Both live tests now go through `LogFetcher.from_config`.
|
|
69
|
+
- **`--chunk` smaller than the minimum window still works**; it caps the
|
|
70
|
+
window and is not raised to 50.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- **Live suite saw a throttled `eth_getLogs` as a bare checkpoint failure.**
|
|
75
|
+
`sync_feedback` records a chunk whose fetch failed in `failed_ranges` and
|
|
76
|
+
carries on rather than raising, so the availability guard never saw the 429
|
|
77
|
+
and the test failed on `last_block` with no hint why (weekly run of
|
|
78
|
+
2026-10-12). The test now reads the recorded errors through
|
|
79
|
+
`Store.failed_ranges()` and classifies them with the same markers; a
|
|
80
|
+
throttled sibling range can never hide a real one.
|
|
81
|
+
- **JSON-RPC error bodies carried by HTTP 400 are now classified.**
|
|
82
|
+
base.drpc.org answers an over-cap `eth_getLogs` with HTTP 400 and a JSON-RPC
|
|
83
|
+
error (`ranges over 10000 blocks are not supported on free plan`).
|
|
84
|
+
`RpcClient` raised on the status before reading the body, so the range error
|
|
85
|
+
was never recognised and drpc's window was never learned; every rotation to
|
|
86
|
+
it re-sent a full-width request that failed. A 400 whose body is a JSON
|
|
87
|
+
object with an `error` now goes through the same classification as a 200
|
|
88
|
+
with an error (range too large, non-retryable, or retry and rotate). A 400
|
|
89
|
+
without such a body, and every 403, 429 and 5xx, is still an HTTP error as
|
|
90
|
+
before.
|
|
91
|
+
- **Default CLI pacing is 0.5 s between `eth_getLogs` calls (was 0.2 s).** The
|
|
92
|
+
2026-10-12 back-fill showed nodies throttling at about 5 calls per second
|
|
93
|
+
sustained (104 HTTP 429s). `LogFetcher`'s library default stays 0.
|
|
94
|
+
|
|
95
|
+
### Rollback
|
|
96
|
+
|
|
97
|
+
To go back to 0.1.2 (no source recording, no automatic rotation), install the
|
|
98
|
+
old release first: `pip install 'robustrep==0.1.2'`, then
|
|
99
|
+
`robustrep fetch --db data/base.db --chunk 50 --rpc-url https://base-pokt.nodies.app`;
|
|
100
|
+
the database is compatible in both directions (0.1.2 ignores `sync_log`;
|
|
101
|
+
blocks fetched under 0.1.2 have no source rows).
|
|
102
|
+
|
|
103
|
+
## [0.1.2] — 2026-10-03
|
|
104
|
+
|
|
105
|
+
A dependency security release. No API, CLI or scoring changes.
|
|
106
|
+
|
|
107
|
+
### Security — Fixed
|
|
108
|
+
|
|
109
|
+
- **urllib3 floor raised to 2.8.** urllib3 2.6–2.7 can enter an infinite loop
|
|
110
|
+
on a chunked deflate stream (PYSEC-2026-4176) and buffer an unbounded
|
|
111
|
+
chunk-size line into memory (PYSEC-2026-4177). Both are reachable from
|
|
112
|
+
`fetch`, which reads responses from attacker-chosen evidence hosts, so a
|
|
113
|
+
hostile URI could hang or exhaust the fetcher. 2.8 also fixes HTTPS-proxy TLS
|
|
114
|
+
settings being ignored (PYSEC-2026-4175). The CI pin moves to 2.8.0 as well.
|
|
115
|
+
|
|
7
116
|
## [0.1.1] — 2026-09-15
|
|
8
117
|
|
|
9
118
|
A security release: it closes every finding of an external security review of
|
|
@@ -114,5 +223,7 @@ no longer rescales a group by its own extremes.
|
|
|
114
223
|
Initial release on PyPI: the scoring library, the Base ERC-8004 adapter, and the
|
|
115
224
|
reproducible report.
|
|
116
225
|
|
|
226
|
+
[0.2.0]: https://github.com/iamdongwang/robustrep/compare/v0.1.2...v0.2.0
|
|
227
|
+
[0.1.2]: https://github.com/iamdongwang/robustrep/compare/v0.1.1...v0.1.2
|
|
117
228
|
[0.1.1]: https://github.com/iamdongwang/robustrep/compare/v0.1.0...v0.1.1
|
|
118
229
|
[0.1.0]: https://github.com/iamdongwang/robustrep/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robustrep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Robust, transport-agnostic reputation scoring for AI agents
|
|
5
5
|
Author: Dong Wang
|
|
6
6
|
License: Apache-2.0
|
|
@@ -25,7 +25,7 @@ Requires-Dist: numpy>=1.24
|
|
|
25
25
|
Requires-Dist: eth-abi>=5.0
|
|
26
26
|
Requires-Dist: eth-hash[pycryptodome]>=0.5
|
|
27
27
|
Requires-Dist: requests>=2.31
|
|
28
|
-
Requires-Dist: urllib3>=2.
|
|
28
|
+
Requires-Dist: urllib3>=2.8
|
|
29
29
|
Requires-Dist: tabulate>=0.9
|
|
30
30
|
Requires-Dist: matplotlib>=3.7
|
|
31
31
|
Requires-Dist: typer>=0.12
|
|
@@ -68,6 +68,10 @@ result = score(records, Config())
|
|
|
68
68
|
robustrep score --db data/base.db --out scores.csv
|
|
69
69
|
robustrep report --db data/base.db --out-dir reports
|
|
70
70
|
|
|
71
|
+
The default endpoint list is rotated automatically and the `eth_getLogs` window adapts per
|
|
72
|
+
endpoint; `--rpc-url` (repeatable) replaces the list and `--chunk` caps the window.
|
|
73
|
+
RPC endpoint hostnames appear in the public report and `scores.json`; do not use an endpoint whose hostname embeds an account id or key.
|
|
74
|
+
|
|
71
75
|
Rater profiles: Blockscout (free, no key) by default; Etherscan V2 requires a paid plan for Base.
|
|
72
76
|
Pick a source explicitly with `fetch --profile-source {auto,blockscout,etherscan,none}`.
|
|
73
77
|
|
|
@@ -29,6 +29,10 @@ result = score(records, Config())
|
|
|
29
29
|
robustrep score --db data/base.db --out scores.csv
|
|
30
30
|
robustrep report --db data/base.db --out-dir reports
|
|
31
31
|
|
|
32
|
+
The default endpoint list is rotated automatically and the `eth_getLogs` window adapts per
|
|
33
|
+
endpoint; `--rpc-url` (repeatable) replaces the list and `--chunk` caps the window.
|
|
34
|
+
RPC endpoint hostnames appear in the public report and `scores.json`; do not use an endpoint whose hostname embeds an account id or key.
|
|
35
|
+
|
|
32
36
|
Rater profiles: Blockscout (free, no key) by default; Etherscan V2 requires a paid plan for Base.
|
|
33
37
|
Pick a source explicitly with `fetch --profile-source {auto,blockscout,etherscan,none}`.
|
|
34
38
|
|
|
@@ -98,7 +98,7 @@ typer==0.27.2
|
|
|
98
98
|
typing-inspection==0.4.4
|
|
99
99
|
typing_extensions==4.16.0
|
|
100
100
|
tzdata==2026.4
|
|
101
|
-
urllib3==2.
|
|
101
|
+
urllib3==2.8.0
|
|
102
102
|
zipp==4.1.0
|
|
103
103
|
# -- hand-added (not emitted by `pip freeze` on the dev box) -----------------
|
|
104
104
|
# pip itself: freeze omits it. CI installs this version explicitly before the
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "robustrep"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Robust, transport-agnostic reputation scoring for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -31,7 +31,10 @@ dependencies = [
|
|
|
31
31
|
# decompression bomb (`max_length` on urllib3's decoders) first exists in
|
|
32
32
|
# 2.6.0. On 2.0-2.5 the same call decompresses the whole body before
|
|
33
33
|
# truncating -- measured at a 455 MB peak against 0.8 MB on 2.7.
|
|
34
|
-
|
|
34
|
+
# >=2.8: 2.6-2.7 can loop forever on a chunked deflate stream and buffer an
|
|
35
|
+
# unbounded chunk-size line (PYSEC-2026-4176/4177), both reachable from an
|
|
36
|
+
# attacker-chosen evidence host.
|
|
37
|
+
"urllib3>=2.8",
|
|
35
38
|
"tabulate>=0.9",
|
|
36
39
|
"matplotlib>=3.7",
|
|
37
40
|
"typer>=0.12",
|
|
@@ -37,6 +37,7 @@ from .report.publish import (EVIDENCE_CAP_PER_URI_KEY, EVIDENCE_CAP_TOTAL_KEY, b
|
|
|
37
37
|
from .report.sensitivity import sensitivity_table
|
|
38
38
|
from .sources import base_erc8004 as base
|
|
39
39
|
from .sources.evidence_batch import DEFAULT_MAX_TOTAL_LOOKUPS, classify_all
|
|
40
|
+
from .sources.log_fetcher import LogFetcher
|
|
40
41
|
from .sources.rater_profile import DEFAULT_RPS, default_client, enrich_raters, estimate_seconds
|
|
41
42
|
from .sources.rpc import RpcClient, RpcError
|
|
42
43
|
from .store import Store
|
|
@@ -125,7 +126,8 @@ def _rpc_guard():
|
|
|
125
126
|
|
|
126
127
|
def _step_sync(store: Store, rpc: RpcClient, cfg: Config, to_block: Optional[int]) -> str:
|
|
127
128
|
"""Sync feedback/revocation/response events up to ``to_block`` (or head)."""
|
|
128
|
-
n = base.sync_feedback(store, rpc, chunk=cfg.chunk_blocks, end_block=to_block,
|
|
129
|
+
n = base.sync_feedback(store, rpc, chunk=cfg.chunk_blocks, end_block=to_block,
|
|
130
|
+
confirmations=cfg.confirmations, fetcher=LogFetcher.from_config(rpc, cfg))
|
|
129
131
|
return f"feedback rows added: {n}"
|
|
130
132
|
|
|
131
133
|
|
|
@@ -530,6 +532,8 @@ def report(
|
|
|
530
532
|
# Same reason: provenance publishes the caps the fetch run
|
|
531
533
|
# recorded in *this* store, not this build's constants.
|
|
532
534
|
evidence_caps = evidence_caps_for_provenance(store)
|
|
535
|
+
# And which RPC endpoints served the logs, from this store's sync_log.
|
|
536
|
+
sync_sources = store.sync_sources()
|
|
533
537
|
block = int(last_block) if last_block is not None else 0
|
|
534
538
|
# Inside the guard: sensitivity_table re-scores the whole dataset once
|
|
535
539
|
# per variant, so any ValueError scoring can raise, it can raise too.
|
|
@@ -541,7 +545,8 @@ def report(
|
|
|
541
545
|
adv = scenario_table()
|
|
542
546
|
provenance = build_provenance(mode, cfg, records, result,
|
|
543
547
|
n_lookup_budget_starved=n_lookup_budget_starved,
|
|
544
|
-
evidence_caps=evidence_caps
|
|
548
|
+
evidence_caps=evidence_caps,
|
|
549
|
+
sync_sources=sync_sources)
|
|
545
550
|
|
|
546
551
|
block_dir = out_dir / str(block)
|
|
547
552
|
# Guarded like the scoring above: `export_json` refuses an address-bearing
|
|
@@ -59,9 +59,25 @@ class Config:
|
|
|
59
59
|
bootstrap_seed: int = 0
|
|
60
60
|
ci_level: float = 0.95
|
|
61
61
|
# data source
|
|
62
|
-
|
|
62
|
+
# Measured 2026-10-12: mainnet.base.org answers every eth_getLogs with 429;
|
|
63
|
+
# publicnode serves only ~the last 5000 blocks; nodies caps at 50 blocks per
|
|
64
|
+
# call but serves deep history; drpc caps at ~100. RpcClient rotates on
|
|
65
|
+
# failure and sticks, LogFetcher learns each host's window.
|
|
66
|
+
rpc_urls: tuple[str, ...] = (
|
|
67
|
+
"https://mainnet.base.org",
|
|
68
|
+
"https://base-rpc.publicnode.com",
|
|
69
|
+
"https://base-pokt.nodies.app",
|
|
70
|
+
"https://base.drpc.org",
|
|
71
|
+
)
|
|
72
|
+
# (scheme://hostname, max block window) pairs; a tuple so Config stays hashable.
|
|
73
|
+
rpc_window_hints: tuple[tuple[str, int], ...] = ()
|
|
74
|
+
rpc_min_window: int = 50
|
|
75
|
+
# Seconds between eth_getLogs call starts (CLI default; LogFetcher's own is 0).
|
|
76
|
+
# nodies throttled at ~5 calls/s sustained during the 2026-10-12 back-fill
|
|
77
|
+
# (104 HTTP 429s), so 0.2 s was too tight.
|
|
78
|
+
rpc_min_interval: float = 0.5
|
|
63
79
|
chunk_blocks: int = 2000
|
|
64
|
-
user_agent: str = "robustrep/0.
|
|
80
|
+
user_agent: str = "robustrep/0.2 (+https://github.com/iamdongwang/robustrep)"
|
|
65
81
|
confirmations: int = DEFAULT_CONFIRMATIONS
|
|
66
82
|
|
|
67
83
|
def __post_init__(self) -> None:
|
|
@@ -90,5 +106,11 @@ class Config:
|
|
|
90
106
|
raise ValueError("chunk_blocks must be >= 1")
|
|
91
107
|
if len(self.rpc_urls) < 1:
|
|
92
108
|
raise ValueError("rpc_urls must have at least 1 entry")
|
|
109
|
+
if any(w < 1 for _, w in self.rpc_window_hints):
|
|
110
|
+
raise ValueError("rpc_window_hints values must be >= 1")
|
|
111
|
+
if self.rpc_min_window < 1:
|
|
112
|
+
raise ValueError("rpc_min_window must be >= 1")
|
|
113
|
+
if self.rpc_min_interval < 0:
|
|
114
|
+
raise ValueError("rpc_min_interval must be >= 0")
|
|
93
115
|
if self.confirmations < 0:
|
|
94
116
|
raise ValueError("confirmations must be >= 0")
|
|
@@ -22,19 +22,20 @@ says about the run that made it is part of the artifact, not part of the CLI.
|
|
|
22
22
|
"""
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
+
import dataclasses
|
|
25
26
|
import logging
|
|
26
27
|
import os
|
|
27
28
|
import shutil
|
|
28
29
|
import tempfile
|
|
29
30
|
from pathlib import Path
|
|
30
|
-
from typing import List, Optional
|
|
31
|
+
from typing import List, Optional, Union
|
|
31
32
|
|
|
32
33
|
import pandas
|
|
33
34
|
|
|
34
35
|
from ..config import Config
|
|
35
36
|
from ..evidence import MAX_TX_LOOKUPS_PER_URI
|
|
36
37
|
from ..sources.evidence_batch import DEFAULT_MAX_TOTAL_LOOKUPS
|
|
37
|
-
from ..store import Store
|
|
38
|
+
from ..store import Store, SyncSources
|
|
38
39
|
from .export import export_json, versions
|
|
39
40
|
from .figures import fig_evidence, fig_mean_vs_robust, fig_rank_shift, fig_sybil_clusters
|
|
40
41
|
from .provenance_keys import PROVENANCE_KEYS
|
|
@@ -173,10 +174,22 @@ def evidence_caps_for_provenance(store: Optional[Store]) -> dict:
|
|
|
173
174
|
return caps
|
|
174
175
|
|
|
175
176
|
|
|
177
|
+
def _rpc_sources_for_provenance(sync_sources: Optional[SyncSources]) -> Union[str, dict]:
|
|
178
|
+
"""`config["rpc_sources"]`: the store's per-host sync_log summary, or
|
|
179
|
+
`UNAVAILABLE` when there is no store or it recorded no source at all."""
|
|
180
|
+
if sync_sources is None or sync_sources.recorded_from_block is None:
|
|
181
|
+
return UNAVAILABLE
|
|
182
|
+
return {
|
|
183
|
+
"sources": [dataclasses.asdict(source) for source in sync_sources.sources],
|
|
184
|
+
"recorded_from_block": int(sync_sources.recorded_from_block),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
176
188
|
def build_provenance(mode: str, cfg: Config, records: pandas.DataFrame,
|
|
177
189
|
result: Optional[pandas.DataFrame] = None,
|
|
178
190
|
n_lookup_budget_starved: int = 0,
|
|
179
|
-
evidence_caps: Optional[dict] = None
|
|
191
|
+
evidence_caps: Optional[dict] = None,
|
|
192
|
+
sync_sources: Optional[SyncSources] = None) -> dict:
|
|
180
193
|
caps = evidence_caps if evidence_caps is not None else evidence_caps_for_provenance(None)
|
|
181
194
|
config = dict(
|
|
182
195
|
bootstrap_n=cfg.bootstrap_n,
|
|
@@ -204,6 +217,10 @@ def build_provenance(mode: str, cfg: Config, records: pandas.DataFrame,
|
|
|
204
217
|
# caller with no store to count from (e.g. `build_provenance` called
|
|
205
218
|
# directly on an in-memory `records` frame, as several tests do).
|
|
206
219
|
evidence_lookup_starved_uris=n_lookup_budget_starved,
|
|
220
|
+
# Which RPC endpoints served the logs, per Store.sync_sources. Last,
|
|
221
|
+
# and UNAVAILABLE (not an empty list) when this store never recorded
|
|
222
|
+
# a source: "not recorded" and "recorded, nothing" are different claims.
|
|
223
|
+
rpc_sources=_rpc_sources_for_provenance(sync_sources),
|
|
207
224
|
)
|
|
208
225
|
# PROVENANCE_KEYS is the published order, shared with `report.render` so the
|
|
209
226
|
# two artifacts cannot drift (a test pins them equal). Ordering through it
|