Skip to content

Commit 57a542b

Browse files
authored
Merge pull request #694 from serokell/krendelhoff/#633-specify-snapshot-provider
[#633] Allow users to specify snapshot provider
2 parents 1ffe733 + a760746 commit 57a542b

File tree

2 files changed

+177
-82
lines changed

2 files changed

+177
-82
lines changed

baking/src/tezos_baking/tezos_setup_wizard.py

+165-77
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,6 @@
2626
"node": "Only bootstrap and run the Tezos node.",
2727
}
2828

29-
snapshot_import_modes = {
30-
"download rolling": "Import rolling snapshot from xtz-shots.io (recommended)",
31-
"download full": "Import full snapshot from xtz-shots.io",
32-
"file": "Import snapshot from a file",
33-
"url": "Import snapshot from a url",
34-
"skip": "Skip snapshot import and synchronize with the network from scratch",
35-
}
36-
3729
systemd_enable = {
3830
"yes": "Enable the services, running them both now and on every boot",
3931
"no": "Start the services this time only",
@@ -51,6 +43,12 @@
5143
"on": "Request to continue or restart the subsidy",
5244
}
5345

46+
default_providers = {
47+
"xtz-shots.io": "https://xtz-shots.io/tezos-snapshots.json",
48+
"marigold.dev": "https://snapshots.tezos.marigold.dev/api/tezos-snapshots.json",
49+
}
50+
51+
recommended_provider = list(default_providers.keys())[0]
5452

5553
TMP_SNAPSHOT_LOCATION = "/tmp/octez_node.snapshot.d/"
5654

@@ -146,6 +144,10 @@ def __init__(self, actual_sha256=None, expected_sha256=None):
146144
self.expected_sha256 = expected_sha256
147145

148146

147+
class InterruptStep(Exception):
148+
"Raised when there is need to interrupt step handling flow."
149+
150+
149151
def check_file_contents_integrity(filename, sha256):
150152
import hashlib
151153

@@ -220,9 +222,32 @@ def get_node_version_hash():
220222
)
221223

222224
# We define this step as a function to better tailor snapshot options to the chosen history mode
223-
def get_snapshot_mode_query(modes):
225+
def get_snapshot_mode_query(config):
226+
227+
static_import_modes = {
228+
"file": "Import snapshot from a file",
229+
"direct url": "Import snapshot from a direct url",
230+
"provider url": "Import snapshot from a provider",
231+
"skip": "Skip snapshot import and synchronize with the network from scratch",
232+
}
233+
234+
history_mode = config["history_mode"]
235+
236+
mk_option = lambda pr, hm=history_mode: f"download {hm} ({pr})"
237+
mk_desc = lambda pr, hm=history_mode: f"Import {hm} snapshot from {pr}" + (
238+
" (recommended)" if pr == recommended_provider else ""
239+
)
240+
241+
dynamic_import_modes = {}
242+
243+
for name in default_providers.keys():
244+
if config["snapshots"].get(name, None):
245+
dynamic_import_modes[mk_option(name)] = mk_desc(name)
246+
247+
import_modes = {**dynamic_import_modes, **static_import_modes}
248+
224249
return Step(
225-
id="snapshot",
250+
id="snapshot_mode",
226251
prompt="The Tezos node can take a significant time to bootstrap from scratch.\n"
227252
"Bootstrapping from a snapshot is suggested instead.\n"
228253
"How would you like to proceed?",
@@ -231,13 +256,13 @@ def get_snapshot_mode_query(modes):
231256
"which will take a significant amount of time.\nIn order to avoid this, we suggest "
232257
"bootstrapping from a snapshot instead.\n\n"
233258
"Snapshots can be downloaded from the following websites:\n"
234-
"Tezos Giganode Snapshots - https://snapshots-tezos.giganode.io/ \n"
259+
"Marigold - https://snapshots.tezos.marigold.dev/ \n"
235260
"XTZ-Shots - https://xtz-shots.io/ \n\n"
236261
"We recommend to use rolling snapshots. This is the smallest and the fastest mode\n"
237262
"that is sufficient for baking. You can read more about other Tezos node history modes here:\n"
238263
"https://tezos.gitlab.io/user/history_modes.html#history-modes",
239-
options=modes,
240-
validator=Validator(enum_range_validator(modes)),
264+
options=import_modes,
265+
validator=Validator(enum_range_validator(import_modes)),
241266
)
242267

243268

@@ -250,11 +275,19 @@ def get_snapshot_mode_query(modes):
250275
validator=Validator([required_field_validator, filepath_validator]),
251276
)
252277

278+
provider_url_query = Step(
279+
id="provider_url",
280+
prompt="Provide the url of the snapshot provider.",
281+
help="You have indicated wanting to fetch the snapshot from a custom provider.\n",
282+
default=None,
283+
validator=Validator([required_field_validator, reachable_url_validator()]),
284+
)
285+
253286
snapshot_url_query = Step(
254287
id="snapshot_url",
255288
prompt="Provide the url of the node snapshot file.",
256289
help="You have indicated wanting to import the snapshot from a custom url.\n"
257-
"You can use e.g. links to XTZ-Shots or Tezos Giganode Snapshots resources.",
290+
"You can use e.g. links to XTZ-Shots or Marigold resources.",
258291
default=None,
259292
validator=Validator([required_field_validator, reachable_url_validator()]),
260293
)
@@ -368,13 +401,12 @@ def check_blockchain_data(self):
368401
return False
369402
return True
370403

371-
# Check https://xtz-shots.io/tezos-snapshots.json and collect the most recent snapshot
404+
# Check the provider url and collect the most recent snapshot
372405
# that is suited for the chosen history mode and network
373-
def get_snapshot_link(self):
406+
def get_snapshot_metadata(self, name, json_url):
374407
def hashes_comply(s1, s2):
375408
return s1.startswith(s2) or s2.startswith(s1)
376409

377-
json_url = "https://xtz-shots.io/tezos-snapshots.json"
378410
try:
379411
snapshot_array = None
380412
with urllib.request.urlopen(json_url) as url:
@@ -403,12 +435,23 @@ def hashes_comply(s1, s2):
403435
{"url": None, "block_hash": None},
404436
)
405437

406-
self.config["snapshot_metadata"] = snapshot_metadata
407-
408-
except (urllib.error.URLError, ValueError):
409-
print(f"Couldn't collect snapshot metadata from {json_url}")
438+
self.config["snapshots"][name] = snapshot_metadata
439+
except urllib.error.URLError:
440+
print(
441+
color(
442+
f"\nCouldn't collect snapshot metadata from {json_url} due to networking issues.\n",
443+
color_red,
444+
)
445+
)
446+
except ValueError:
447+
print(
448+
color(
449+
f"\nCouldn't collect snapshot metadata from {json_url} due to format mismatch.\n",
450+
color_red,
451+
)
452+
)
410453
except Exception as e:
411-
print(f"Unexpected error handling snapshot metadata:\n{e}\n")
454+
print(f"\nUnexpected error handling snapshot metadata:\n{e}\n")
412455

413456
def output_snapshot_metadata(self):
414457
from datetime import datetime
@@ -444,6 +487,70 @@ def output_snapshot_metadata(self):
444487
)
445488
)
446489

490+
def fetch_snapshot_from_provider(self, name):
491+
try:
492+
url = self.config["snapshots"][name]["url"]
493+
sha256 = self.config["snapshots"][name]["sha256"]
494+
return fetch_snapshot(url, sha256)
495+
except KeyError:
496+
raise InterruptStep
497+
except (ValueError, urllib.error.URLError):
498+
print()
499+
print("The snapshot download option you chose is unavailable,")
500+
print("which normally shouldn't happen. Please check your")
501+
print("internet connection or choose another option.")
502+
print()
503+
raise InterruptStep
504+
505+
def get_snapshot_from_provider(self, name, url):
506+
try:
507+
self.config["snapshots"][name]
508+
except KeyError:
509+
self.get_snapshot_metadata(name, url)
510+
snapshot_file = self.fetch_snapshot_from_provider(name)
511+
snapshot_block_hash = self.config["snapshots"][name]["block_hash"]
512+
return (snapshot_file, snapshot_block_hash)
513+
514+
def get_snapshot_from_direct_url(self, url):
515+
try:
516+
self.query_step(snapshot_sha256_query)
517+
sha256 = self.config["snapshot_sha256"]
518+
snapshot_file = fetch_snapshot(url, sha256)
519+
if sha256:
520+
print("Checking the snapshot integrity...")
521+
check_file_contents_integrity(snapshot_file, sha256)
522+
print("Integrity verified.")
523+
return (snapshot_file, None)
524+
except (ValueError, urllib.error.URLError):
525+
print()
526+
print("The snapshot URL you provided is unavailable.")
527+
print("Please check the URL again or choose another option.")
528+
print()
529+
raise InterruptStep
530+
except Sha256Mismatch as e:
531+
print()
532+
print("SHA256 mismatch.")
533+
print(f"Expected sha256: {e.expected_sha256}")
534+
print(f"Actual sha256: {e.actual_sha256}")
535+
print()
536+
self.query_step(ignore_hash_mismatch_query)
537+
if self.config["ignore_hash_mismatch"] == "no":
538+
raise InterruptStep
539+
else:
540+
return (snapshot_file, None)
541+
542+
def get_snapshot_from_provider_url(self, url):
543+
name = "custom"
544+
if os.path.basename(url) == "tezos-snapshots.json":
545+
return self.get_snapshot_from_provider(name, url)
546+
else:
547+
try:
548+
return self.get_snapshot_from_provider(name, url)
549+
except InterruptStep:
550+
return self.get_snapshot_from_provider(
551+
name, os.path.join(url, "tezos-snapshots.json")
552+
)
553+
447554
# Importing the snapshot for Node bootstrapping
448555
def import_snapshot(self):
449556
do_import = self.check_blockchain_data()
@@ -457,76 +564,57 @@ def import_snapshot(self):
457564
f"--history-mode {self.config['history_mode']}"
458565
)
459566

460-
self.get_snapshot_link()
567+
self.config["snapshots"] = {}
461568

462-
if self.config["snapshot_metadata"] is None:
463-
snapshot_import_modes.pop("download rolling", None)
464-
snapshot_import_modes.pop("download full", None)
465-
elif self.config["history_mode"] == "rolling":
466-
snapshot_import_modes.pop("download full", None)
467-
else:
468-
snapshot_import_modes.pop("download rolling", None)
569+
print("Getting snapshots' metadata from providers...")
570+
for name, url in default_providers.items():
571+
self.get_snapshot_metadata(name, url)
469572

470573
else:
471574
return
472575

473576
while not valid_choice:
474577

475-
self.query_step(get_snapshot_mode_query(snapshot_import_modes))
578+
self.query_step(get_snapshot_mode_query(self.config))
476579

477580
snapshot_file = TMP_SNAPSHOT_LOCATION
478581
snapshot_block_hash = None
479582

480-
if self.config["snapshot"] == "skip":
481-
return
482-
elif self.config["snapshot"] == "file":
483-
self.query_step(snapshot_file_query)
484-
snapshot_file = self.config["snapshot_file"]
485-
elif self.config["snapshot"] == "url":
486-
self.query_step(snapshot_url_query)
487-
try:
488-
self.query_step(snapshot_sha256_query)
489-
url = self.config["snapshot_metadata"]["url"]
490-
sha256 = self.config["snapshot_metadata"]["sha256"]
491-
snapshot_file = fetch_snapshot(url, sha256)
492-
if sha256:
493-
print("Checking the snapshot integrity...")
494-
check_file_contents_integrity(snapshot_file, sha256)
495-
print("Integrity verified.")
496-
except (ValueError, urllib.error.URLError):
497-
print()
498-
print("The snapshot URL you provided is unavailable.")
499-
print("Please check the URL again or choose another option.")
500-
print()
501-
continue
502-
except Sha256Mismatch as e:
503-
print()
504-
print("SHA256 mismatch.")
505-
print(f"Expected sha256: {e.expected_sha256}")
506-
print(f"Actual sha256: {e.actual_sha256}")
507-
print()
508-
self.query_step(ignore_hash_mismatch_query)
509-
if self.config["ignore_hash_mismatch"] == "no":
510-
continue
511-
else:
512-
url = self.config["snapshot_metadata"]["url"]
513-
sha256 = self.config["snapshot_metadata"]["sha256"]
514-
snapshot_block_hash = self.config["snapshot_metadata"]["block_hash"]
515-
try:
516-
self.output_snapshot_metadata()
517-
snapshot_file = fetch_snapshot(url, sha256)
518-
except (ValueError, urllib.error.URLError):
519-
print()
520-
print("The snapshot download option you chose is unavailable,")
521-
print("which normally shouldn't happen. Please check your")
522-
print("internet connection or choose another option.")
523-
print()
524-
continue
583+
try:
584+
if self.config["snapshot_mode"] == "skip":
585+
return
586+
elif self.config["snapshot_mode"] == "file":
587+
self.query_step(snapshot_file_query)
588+
snapshot_file = self.config["snapshot_file"]
589+
elif self.config["snapshot_mode"] == "direct url":
590+
self.query_step(snapshot_url_query)
591+
url = self.config["snapshot_url"]
592+
(
593+
snapshot_file,
594+
snapshot_block_hash,
595+
) = self.get_snapshot_from_direct_url(url)
596+
elif self.config["snapshot_mode"] == "provider url":
597+
self.query_step(provider_url_query)
598+
name, url = "custom", self.config["provider_url"]
599+
(
600+
snapshot_file,
601+
snapshot_block_hash,
602+
) = self.get_snapshot_from_provider_url(url)
603+
else:
604+
for name, url in default_providers.items():
605+
if name in self.config["snapshot_mode"]:
606+
(
607+
snapshot_file,
608+
snapshot_block_hash,
609+
) = self.get_snapshot_from_provider(name, url)
610+
except InterruptStep:
611+
print("Getting back to the snapshot import mode step.")
612+
continue
525613

526614
valid_choice = True
527615

528616
import_flag = ""
529-
if is_full_snapshot(snapshot_file, self.config["snapshot"]):
617+
if is_full_snapshot(snapshot_file, self.config["snapshot_mode"]):
530618
if self.config["history_mode"] == "archive":
531619
import_flag = "--reconstruct "
532620

baking/src/tezos_baking/wizard_structure.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def reachable_url_validator(suffix=None):
6565
def _validator(input):
6666
full_url = mk_full_url(input, suffix)
6767
if url_is_reachable(full_url):
68-
return input
68+
return full_url
6969
else:
7070
raise ValueError(f"{full_url} is unreachable. Please input a valid URL.")
7171

@@ -277,10 +277,17 @@ def yes_or_no(prompt, default=None):
277277

278278

279279
def mk_full_url(host_name, path):
280-
if path is None:
281-
return host_name.rstrip("/")
282-
else:
283-
return "/".join([host_name.rstrip("/"), path.lstrip("/")])
280+
from urllib.parse import urlparse
281+
282+
url = host_name
283+
if path is not None:
284+
url = os.path.join(host_name, path)
285+
286+
# urllib doesn't make an assumption which scheme to use by default in case of absence
287+
if urlparse(url).scheme == "":
288+
url = "https://" + url
289+
290+
return url
284291

285292

286293
def url_is_reachable(url):

0 commit comments

Comments
 (0)