Skip to content

Commit dcd1894

Browse files
committed
Update to resolve addresses
1 parent a6d1d26 commit dcd1894

2 files changed

Lines changed: 167 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,23 @@ Let's get the data:
188188
./download_parquet.py contract_deployments -n 48 # deployment_id -> chain_id, address (5.4 GB)
189189
```
190190

191-
which allows us to get a `repo.sourcify.dev/<chainId>/<address>` link.
191+
Then `resolve_address.py` walks
192+
`compiled_contracts_sources` → `verified_contracts` → `contract_deployments`
193+
and prints a `repo.sourcify.dev/<chainId>/<address>` link. It takes a hash or
194+
anything containing one, like `fetch_source.py`:
195+
196+
```bash
197+
./resolve_address.py <hash-or-uri>... --verify
198+
```
199+
200+
`--verify` re-fetches the sources from Sourcify and checks one of them hashes
201+
back to the input, so a link is proven rather than inferred — `source_hash` is
202+
plain sha256 of the content. `--chain N` restricts the output to one chain.
203+
204+
One source can back **many** deployments, so expect several addresses per hash:
205+
the same compilation redeployed. `ncomp` from `show_results.py -e` counts
206+
compilations, not deployments, and the two are not the same number. The chain id
207+
matters as much as the address — plenty of verified contracts are on testnets.
192208

193209
## Testing
194210

‎sourcify/resolve_address.py‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env python3
2+
"""Resolve a source_hash to the chain and address of contracts built from it.
3+
4+
Usage: ./resolve_address.py <hash|path>... [--verify] [--chain N]
5+
[--compiled DIR] [--verified DIR] [--deployments DIR]
6+
7+
Takes a bare 64-hex source_hash or anything containing one, like fetch_source.py,
8+
and walks compiled_contracts_sources -> verified_contracts -> contract_deployments.
9+
--verify re-fetches the sources from Sourcify and checks that one of them hashes
10+
back to the input, so a printed link is proven rather than inferred.
11+
"""
12+
import argparse
13+
import glob
14+
import hashlib
15+
import json
16+
import os
17+
import re
18+
import sys
19+
import urllib.error
20+
import urllib.request
21+
from collections import defaultdict
22+
23+
HASH = re.compile(r"([0-9a-f]{64})")
24+
HERE = os.path.dirname(os.path.abspath(__file__))
25+
API = "https://sourcify.dev/server/v2/contract/{chain}/{address}?fields=sources"
26+
REPO = "https://repo.sourcify.dev/{chain}/{address}/"
27+
28+
29+
def pick(patterns):
30+
out = []
31+
for p in patterns:
32+
if not os.path.isabs(p):
33+
p = os.path.join(HERE, p)
34+
out.extend(glob.glob(os.path.join(p, "*.parquet")))
35+
return sorted(set(out))
36+
37+
38+
def lookup(files, key, want, cols, binary_key=False):
39+
"""Rows of `files` whose `key` is in `want`, as a list of dicts.
40+
41+
The isin goes into the dataset scan so only matching rows are materialised;
42+
these tables are billions of rows and will not fit otherwise.
43+
"""
44+
import pyarrow.compute as pc
45+
import pyarrow.dataset as ds
46+
47+
keys = [bytes.fromhex(w) for w in want] if binary_key else list(want)
48+
tbl = ds.dataset(files, format="parquet").to_table(
49+
columns=cols, filter=pc.field(key).isin(keys))
50+
return tbl.to_pylist()
51+
52+
53+
def sourcify_sources(chain, address, timeout=30):
54+
"""The verified source contents Sourcify holds for a contract, or None."""
55+
url = API.format(chain=chain, address=address)
56+
try:
57+
with urllib.request.urlopen(url, timeout=timeout) as r:
58+
body = json.load(r)
59+
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as e:
60+
print(f" verify: cannot reach Sourcify ({e})", file=sys.stderr)
61+
return None
62+
srcs = body.get("sources") or {}
63+
return [v.get("content", "") for v in srcs.values() if isinstance(v, dict)]
64+
65+
66+
def main():
67+
ap = argparse.ArgumentParser(description=__doc__,
68+
formatter_class=argparse.RawDescriptionHelpFormatter)
69+
ap.add_argument("hashes", nargs="+", metavar="HASH|PATH")
70+
ap.add_argument("--verify", action="store_true",
71+
help="confirm each address really serves the input source")
72+
ap.add_argument("--chain", type=int, help="only report this chain id")
73+
ap.add_argument("--compiled", default="compiled_contracts_sources")
74+
ap.add_argument("--verified", default="verified_contracts")
75+
ap.add_argument("--deployments", default="contract_deployments")
76+
args = ap.parse_args()
77+
78+
wanted = []
79+
for a in args.hashes:
80+
m = HASH.search(a.lower())
81+
if not m:
82+
sys.exit(f"No 64-hex source hash in: {a}")
83+
if m.group(1) not in wanted:
84+
wanted.append(m.group(1))
85+
86+
for name, d in (("compiled_contracts_sources", args.compiled),
87+
("verified_contracts", args.verified),
88+
("contract_deployments", args.deployments)):
89+
if not pick([d]):
90+
sys.exit(f"No parquet in {d}\nDownload it with\n"
91+
f" ./download_parquet.py {name} -n 48")
92+
93+
print(f"compiled_contracts_sources: {len(wanted)} hash(es) ...", file=sys.stderr)
94+
rows = lookup(pick([args.compiled]), "source_hash", wanted,
95+
["source_hash", "compilation_id"], binary_key=True)
96+
comp_to_hash = defaultdict(set)
97+
for r in rows:
98+
comp_to_hash[r["compilation_id"]].add(bytes(r["source_hash"]).hex())
99+
if not comp_to_hash:
100+
sys.exit("no compilation uses any of those sources")
101+
102+
print(f"verified_contracts: {len(comp_to_hash)} compilation(s) ...", file=sys.stderr)
103+
rows = lookup(pick([args.verified]), "compilation_id", comp_to_hash.keys(),
104+
["compilation_id", "deployment_id", "runtime_match", "creation_match"])
105+
dep_to_meta = {}
106+
for r in rows:
107+
dep_to_meta[r["deployment_id"]] = (r["compilation_id"],
108+
r["runtime_match"], r["creation_match"])
109+
if not dep_to_meta:
110+
sys.exit("those compilations are not linked to any deployment")
111+
112+
print(f"contract_deployments: {len(dep_to_meta)} deployment(s) ...", file=sys.stderr)
113+
rows = lookup(pick([args.deployments]), "id", dep_to_meta.keys(),
114+
["id", "chain_id", "address"])
115+
116+
found = defaultdict(list)
117+
for r in rows:
118+
comp, rt, cr = dep_to_meta[r["id"]]
119+
for h in comp_to_hash[comp]:
120+
found[h].append((r["chain_id"], "0x" + bytes(r["address"]).hex(), rt, cr))
121+
122+
rc = 0
123+
for h in wanted:
124+
hits = sorted(set(found.get(h, [])))
125+
if args.chain is not None:
126+
hits = [x for x in hits if x[0] == args.chain]
127+
print(f"\n{h}")
128+
if not hits:
129+
print(" no deployment found")
130+
rc = 1
131+
continue
132+
for chain, address, rt, cr in hits:
133+
match = "runtime" if rt else ""
134+
match += ("+" if match and cr else "") + ("creation" if cr else "")
135+
print(f" chain {chain} {address} [{match or 'no match flags'}]")
136+
print(f" {REPO.format(chain=chain, address=address)}")
137+
if args.verify:
138+
srcs = sourcify_sources(chain, address)
139+
if srcs is None:
140+
rc = 1
141+
elif any(hashlib.sha256(s.encode()).hexdigest() == h for s in srcs):
142+
print(f" verified: Sourcify serves this exact source")
143+
else:
144+
print(f" MISMATCH: none of {len(srcs)} source(s) hash to {h[:16]}…")
145+
rc = 1
146+
return rc
147+
148+
149+
if __name__ == "__main__":
150+
sys.exit(main())

0 commit comments

Comments
 (0)